diff --git a/accounting_data/accounting_manager.py b/accounting_data/accounting_manager.py new file mode 100644 index 0000000..75d829f --- /dev/null +++ b/accounting_data/accounting_manager.py @@ -0,0 +1,312 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import os +import sys +import json +import sqlite3 +import pyexcel_ods +from datetime import datetime +from pathlib import Path + +class AccountingManager: + """Gestionnaire des fichiers de comptabilité""" + + def __init__(self, db_path="invoices.db"): + """Initialise le gestionnaire de comptabilité + + Args: + db_path: Chemin vers la base de données SQLite + """ + self.db_path = db_path + self.accounting_files = { + "revenue": "compta.ods", + "expenses": "spending.ods" + } + self.ensure_tables() + + def ensure_tables(self): + """Assure que les tables nécessaires existent dans la base de données""" + conn = sqlite3.connect(self.db_path) + cursor = conn.cursor() + + # Table pour les revenus + cursor.execute(''' + CREATE TABLE IF NOT EXISTS accounting_revenue ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + date TEXT NOT NULL, + description TEXT NOT NULL, + amount REAL NOT NULL, + category TEXT, + invoice_id INTEGER, + notes TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (invoice_id) REFERENCES invoices(id) + ) + ''') + + # Table pour les dépenses + cursor.execute(''' + CREATE TABLE IF NOT EXISTS accounting_expenses ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + date TEXT NOT NULL, + description TEXT NOT NULL, + amount REAL NOT NULL, + category TEXT, + payment_method TEXT, + receipt_path TEXT, + notes TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) + ''') + + conn.commit() + conn.close() + + def import_revenue_data(self, file_path=None): + """Importe les données de revenus depuis le fichier compta.ods + + Args: + file_path: Chemin vers le fichier ODS (optionnel) + + Returns: + int: Nombre d'entrées importées + """ + if file_path is None: + file_path = self.accounting_files["revenue"] + + if not os.path.exists(file_path): + print(f"Erreur: Fichier {file_path} introuvable") + return 0 + + try: + data = pyexcel_ods.get_data(file_path) + # Supposons que la première feuille contient les données + sheet_name = list(data.keys())[0] + sheet_data = data[sheet_name] + + # Supposons que la première ligne contient les en-têtes + headers = sheet_data[0] + rows = sheet_data[1:] + + conn = sqlite3.connect(self.db_path) + cursor = conn.cursor() + + count = 0 + for row in rows: + # Validation de base + if len(row) < 3: # Au moins date, description, montant + continue + + # Mappage des colonnes selon l'en-tête + row_data = dict(zip(headers, row)) + + # Conversion des formats de date si nécessaire + date_str = row_data.get('Date', '') + if isinstance(date_str, str) and date_str: + try: + # Supposons le format JJ/MM/AAAA + date_obj = datetime.strptime(date_str, '%d/%m/%Y') + date_str = date_obj.strftime('%Y-%m-%d') + except ValueError: + # Garder la chaîne d'origine si la conversion échoue + pass + + cursor.execute(''' + INSERT INTO accounting_revenue + (date, description, amount, category, notes) + VALUES (?, ?, ?, ?, ?) + ''', ( + date_str, + row_data.get('Description', ''), + float(row_data.get('Montant', 0)), + row_data.get('Catégorie', ''), + row_data.get('Notes', '') + )) + count += 1 + + conn.commit() + conn.close() + return count + + except Exception as e: + print(f"Erreur lors de l'importation des données: {e}") + return 0 + + def import_expenses_data(self, file_path=None): + """Importe les données de dépenses depuis le fichier spending.ods + + Args: + file_path: Chemin vers le fichier ODS (optionnel) + + Returns: + int: Nombre d'entrées importées + """ + if file_path is None: + file_path = self.accounting_files["expenses"] + + if not os.path.exists(file_path): + print(f"Erreur: Fichier {file_path} introuvable") + return 0 + + try: + data = pyexcel_ods.get_data(file_path) + # Supposons que la première feuille contient les données + sheet_name = list(data.keys())[0] + sheet_data = data[sheet_name] + + # Supposons que la première ligne contient les en-têtes + headers = sheet_data[0] + rows = sheet_data[1:] + + conn = sqlite3.connect(self.db_path) + cursor = conn.cursor() + + count = 0 + for row in rows: + # Validation de base + if len(row) < 3: # Au moins date, description, montant + continue + + # Mappage des colonnes selon l'en-tête + row_data = dict(zip(headers, row)) + + # Conversion des formats de date si nécessaire + date_str = row_data.get('Date', '') + if isinstance(date_str, str) and date_str: + try: + # Supposons le format JJ/MM/AAAA + date_obj = datetime.strptime(date_str, '%d/%m/%Y') + date_str = date_obj.strftime('%Y-%m-%d') + except ValueError: + # Garder la chaîne d'origine si la conversion échoue + pass + + cursor.execute(''' + INSERT INTO accounting_expenses + (date, description, amount, category, payment_method, notes) + VALUES (?, ?, ?, ?, ?, ?) + ''', ( + date_str, + row_data.get('Description', ''), + float(row_data.get('Montant', 0)), + row_data.get('Catégorie', ''), + row_data.get('Méthode de Paiement', ''), + row_data.get('Notes', '') + )) + count += 1 + + conn.commit() + conn.close() + return count + + except Exception as e: + print(f"Erreur lors de l'importation des données: {e}") + return 0 + + def get_balance_sheet(self, year=None): + """Génère un bilan comptable + + Args: + year: Année pour filtrer les résultats (optionnel) + + Returns: + dict: Bilan avec revenus, dépenses et solde + """ + conn = sqlite3.connect(self.db_path) + cursor = conn.cursor() + + date_filter = "" + params = [] + + if year: + date_filter = "WHERE date LIKE ?" + params = [f"{year}%"] + + # Calculer le total des revenus + cursor.execute(f''' + SELECT SUM(amount) FROM accounting_revenue + {date_filter} + ''', params) + total_revenue = cursor.fetchone()[0] or 0 + + # Calculer le total des dépenses + cursor.execute(f''' + SELECT SUM(amount) FROM accounting_expenses + {date_filter} + ''', params) + total_expenses = cursor.fetchone()[0] or 0 + + # Obtenir la répartition par catégorie pour les revenus + cursor.execute(f''' + SELECT category, SUM(amount) as total + FROM accounting_revenue + {date_filter} + GROUP BY category + ORDER BY total DESC + ''', params) + revenue_by_category = {row[0] or 'Non catégorisé': row[1] for row in cursor.fetchall()} + + # Obtenir la répartition par catégorie pour les dépenses + cursor.execute(f''' + SELECT category, SUM(amount) as total + FROM accounting_expenses + {date_filter} + GROUP BY category + ORDER BY total DESC + ''', params) + expenses_by_category = {row[0] or 'Non catégorisé': row[1] for row in cursor.fetchall()} + + conn.close() + + return { + 'total_revenue': total_revenue, + 'total_expenses': total_expenses, + 'balance': total_revenue - total_expenses, + 'revenue_by_category': revenue_by_category, + 'expenses_by_category': expenses_by_category + } + +if __name__ == "__main__": + # Usage comme script indépendant + manager = AccountingManager() + + if len(sys.argv) > 1: + command = sys.argv[1] + + if command == "import": + print("Importation des données comptables...") + + rev_count = manager.import_revenue_data() + exp_count = manager.import_expenses_data() + + print(f"Importation terminée: {rev_count} revenus et {exp_count} dépenses importés.") + + elif command == "balance": + year = sys.argv[2] if len(sys.argv) > 2 else None + balance = manager.get_balance_sheet(year) + + print("\n=== BILAN COMPTABLE ===") + if year: + print(f"Année: {year}") + print(f"Total des revenus: {balance['total_revenue']:.2f} €") + print(f"Total des dépenses: {balance['total_expenses']:.2f} €") + print(f"Solde: {balance['balance']:.2f} €") + + print("\nRépartition des revenus par catégorie:") + for cat, amount in balance['revenue_by_category'].items(): + print(f" - {cat}: {amount:.2f} €") + + print("\nRépartition des dépenses par catégorie:") + for cat, amount in balance['expenses_by_category'].items(): + print(f" - {cat}: {amount:.2f} €") + + else: + print(f"Commande inconnue: {command}") + print("Utilisations possibles:") + print(" - import: Importer les données des fichiers ODS") + print(" - balance [année]: Afficher le bilan comptable") + else: + print("Veuillez spécifier une commande:") + print(" - import: Importer les données des fichiers ODS") + print(" - balance [année]: Afficher le bilan comptable") \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 0a94e57..22f9b3c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ flask==3.0.2 python-dotenv==1.0.1 -typst==0.1.0 \ No newline at end of file +typst==0.1.0 +pyexcel-ods==0.6.0 \ No newline at end of file diff --git a/server.py b/server.py index e823277..f7f204d 100644 --- a/server.py +++ b/server.py @@ -23,6 +23,10 @@ def dashboard(): def preview(): return app.send_static_file('preview.html') +@app.route('/accounting') +def accounting(): + return app.send_static_file('accounting.html') + @app.route('/api/invoices', methods=['GET']) def get_invoices(): filters = {} @@ -117,6 +121,17 @@ def create_invoice(): vat_number=data.get('recipient_vat_number') ) + # Traiter les lignes de facturation + invoice_items = data.get('items', []) + if not invoice_items: + # Utiliser une ligne par défaut si aucun élément n'est fourni + invoice_items = [{"description": "Poong rental", "amount": data['amount']}] + + # Générer les lignes pour le tableau Typst + invoice_rows = "" + for item in invoice_items: + invoice_rows += f" [{item['description']}], [{item['amount']}],\n" + # Créer le contenu du template Typst typst_content = f'''#let language = "{data['language']}" #let invoice_number = "{invoice_number}" @@ -262,8 +277,7 @@ def create_invoice(): [*#t.description*], [*#t.amount (#currency)* ], ), - [Poong rental], [#amount], - [*#t.total*], [*#amount #currency*], +{invoice_rows} [*#t.total*], [*#amount #currency*], ) #v(.5cm) @@ -278,8 +292,12 @@ def create_invoice(): #v(0.5cm) // Coordonnées bancaires avec notification spéciale - #text(weight: "bold", size: 13pt)[#t.banking_info] - #v(0.2cm) +#text(weight: "bold", size: 13pt)[#t.banking_info] +#v(0.2cm) + +// Afficher le compte bancaire en fonction de la devise +#if currency == "EUR" [ + // Compte pour les transactions en euros #t.bank Wise, Rue du Trône 100, 3rd floor, Brussels, 1050, Belgium #v(0.1cm) IBAN: BE22905094540247 @@ -287,9 +305,28 @@ def create_invoice(): BIC/SWIFT: TRWIBEB1XXX #v(0.1cm) #t.account_holder Robin Szymczak - #v(0.2cm) - #t.account_notice +] else if currency == "CHF" [ + // Compte pour les transactions en francs suisses + #t.bank PostFinance SA, Mingerstrasse 20, 3030 Bern, Switzerland + #v(0.1cm) + IBAN: CH56 0900 0000 1527 2120 9 + #v(0.1cm) + BIC/SWIFT: POFICHBEXXX + #v(0.1cm) + #t.account_holder Robin Szymczak +] else [ + // Compte par défaut (identique à EUR pour la compatibilité) + #t.bank Wise, Rue du Trône 100, 3rd floor, Brussels, 1050, Belgium + #v(0.1cm) + IBAN: BE22905094540247 + #v(0.1cm) + BIC/SWIFT: TRWIBEB1XXX + #v(0.1cm) + #t.account_holder Robin Szymczak +] +#v(0.2cm) +#t.account_notice // Pied de page avec contact #align(center)[ @@ -316,4 +353,4 @@ def create_invoice(): }) if __name__ == '__main__': - app.run(debug=True) \ No newline at end of file + app.run(debug=True, port=5001) \ No newline at end of file diff --git a/static/accounting.html b/static/accounting.html new file mode 100644 index 0000000..aefdc37 --- /dev/null +++ b/static/accounting.html @@ -0,0 +1,199 @@ + + + + + + Gestion Comptable + + + + +
+
+

Gestion Comptable

+ +
+ + +
+
+

Revenus

+

0 €

+
+
+

Dépenses

+

0 €

+
+
+

Solde

+

0 €

+
+
+ + +
+
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+
+ + +
+
+ +
+
+ + +
+ +
+
+ + + + + + + + + + + + + + +
DateDescriptionMontantCatégorieTypeActions
+
+
+ + + + + + +
+ + +
+ +
+
+ + + + + + + + + \ No newline at end of file diff --git a/static/accounting.js b/static/accounting.js new file mode 100644 index 0000000..b35239d --- /dev/null +++ b/static/accounting.js @@ -0,0 +1,452 @@ +document.addEventListener("DOMContentLoaded", () => { + // Éléments du DOM + const yearFilter = document.getElementById("yearFilter"); + const categoryFilter = document.getElementById("categoryFilter"); + const typeFilter = document.getElementById("typeFilter"); + const applyFilters = document.getElementById("applyFilters"); + const transactionsList = document.getElementById("transactionsList"); + const totalRevenue = document.getElementById("totalRevenue"); + const totalExpenses = document.getElementById("totalExpenses"); + const balanceAmount = document.getElementById("balanceAmount"); + const revenuesByCategoryList = document.getElementById( + "revenuesByCategoryList" + ); + const expensesByCategoryList = document.getElementById( + "expensesByCategoryList" + ); + + // Boutons d'onglets + const tabButtons = document.querySelectorAll(".tab-button"); + const tabContents = document.querySelectorAll(".tab-content"); + + // Modal d'importation + const importButton = document.getElementById("importButton"); + const importModal = document.getElementById("importModal"); + const closeImportModal = document.getElementById("closeImportModal"); + const importRevenues = document.getElementById("importRevenues"); + const importExpenses = document.getElementById("importExpenses"); + + // Variables pour les graphiques + let revenuesChart = null; + let expensesChart = null; + + // Initialiser la page + initPage(); + + // Gestionnaire d'événements pour les filtres + applyFilters.addEventListener("click", () => { + loadData({ + year: yearFilter.value, + category: categoryFilter.value, + type: typeFilter.value, + }); + }); + + // Gestionnaires d'événements pour les onglets + tabButtons.forEach((button) => { + button.addEventListener("click", () => { + const tabId = button.id.replace("tab", "").toLowerCase(); + switchTab(tabId); + }); + }); + + // Gestionnaires d'événements pour le modal d'importation + importButton.addEventListener("click", () => { + importModal.classList.remove("hidden"); + }); + + closeImportModal.addEventListener("click", () => { + importModal.classList.add("hidden"); + }); + + importRevenues.addEventListener("click", () => { + importData("revenue"); + }); + + importExpenses.addEventListener("click", () => { + importData("expense"); + }); + + // Fonction d'initialisation + function initPage() { + // Années pour le filtre (année courante et les deux précédentes) + const currentYear = new Date().getFullYear(); + yearFilter.innerHTML = ``; + for (let year = currentYear; year >= currentYear - 2; year--) { + yearFilter.innerHTML += ``; + } + + // Charger les données initiales + loadData(); + + // Générer les couleurs pour les graphiques + generateChartColors(); + } + + // Fonction pour charger les données + async function loadData(filters = {}) { + try { + const response = await fetch( + "/api/accounting?" + new URLSearchParams(filters) + ); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const data = await response.json(); + + // Mettre à jour les statistiques + updateStatistics(data.statistics); + + // Mettre à jour les transactions + displayTransactions(data.transactions); + + // Mettre à jour les catégories de filtres + updateCategoryFilters(data.categories); + + // Mettre à jour les graphiques + updateCharts(data.statistics); + } catch (error) { + console.error("Erreur lors du chargement des données:", error); + showMessage("Erreur lors du chargement des données", "error"); + } + } + + // Fonction pour importer des données + async function importData(type) { + try { + const response = await fetch(`/api/accounting/import/${type}`, { + method: "POST", + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const result = await response.json(); + + showMessage(result.message || "Importation réussie"); + importModal.classList.add("hidden"); + + // Recharger les données + loadData(); + } catch (error) { + console.error("Erreur lors de l'importation:", error); + showMessage("Erreur lors de l'importation", "error"); + } + } + + // Fonction pour mettre à jour les statistiques + function updateStatistics(stats) { + totalRevenue.textContent = `${stats.total_revenue.toFixed(2)} €`; + totalExpenses.textContent = `${stats.total_expenses.toFixed(2)} €`; + + const balance = stats.balance; + balanceAmount.textContent = `${Math.abs(balance).toFixed(2)} €`; + + if (balance >= 0) { + balanceAmount.classList.remove("text-red-500"); + balanceAmount.classList.add("text-green-500"); + } else { + balanceAmount.classList.remove("text-green-500"); + balanceAmount.classList.add("text-red-500"); + } + } + + // Fonction pour mettre à jour les catégories du filtre + function updateCategoryFilters(categories) { + categoryFilter.innerHTML = ''; + + categories.forEach((category) => { + categoryFilter.innerHTML += ``; + }); + } + + // Fonction pour afficher les transactions + function displayTransactions(transactions) { + transactionsList.innerHTML = ""; + + transactions.forEach((transaction) => { + const row = document.createElement("tr"); + + const date = new Date(transaction.date); + const formattedDate = date.toLocaleDateString(); + + const isRevenue = transaction.type === "revenue"; + const amount = `${isRevenue ? "+" : "-"} ${Math.abs( + transaction.amount + ).toFixed(2)} €`; + const amountClass = isRevenue ? "text-green-500" : "text-red-500"; + + row.innerHTML = ` + + ${formattedDate} + + + ${transaction.description} + + + ${amount} + + + ${transaction.category || "Non catégorisé"} + + + + ${isRevenue ? "Revenu" : "Dépense"} + + + + + + + `; + + transactionsList.appendChild(row); + }); + + if (transactions.length === 0) { + transactionsList.innerHTML = ` + + + Aucune transaction trouvée + + + `; + } + } + + // Fonction pour changer d'onglet + function switchTab(tabId) { + // Mettre à jour les classes des boutons + tabButtons.forEach((button) => { + const buttonTabId = button.id.replace("tab", "").toLowerCase(); + + if (buttonTabId === tabId) { + button.classList.add("active", "border-blue-500", "text-blue-600"); + button.classList.remove("border-transparent", "text-gray-500"); + } else { + button.classList.remove("active", "border-blue-500", "text-blue-600"); + button.classList.add("border-transparent", "text-gray-500"); + } + }); + + // Afficher le contenu de l'onglet sélectionné + tabContents.forEach((content) => { + const contentId = content.id; + + if (contentId === `${tabId}Content`) { + content.classList.remove("hidden"); + } else { + content.classList.add("hidden"); + } + }); + } + + // Génération de couleurs pour les graphiques + function generateChartColors(count = 10) { + const colors = [ + "#4F46E5", + "#10B981", + "#F59E0B", + "#EF4444", + "#EC4899", + "#8B5CF6", + "#06B6D4", + "#84CC16", + "#F97316", + "#6366F1", + ]; + + // Si on a besoin de plus de couleurs, on les génère aléatoirement + if (count > colors.length) { + for (let i = colors.length; i < count; i++) { + const r = Math.floor(Math.random() * 200); + const g = Math.floor(Math.random() * 200); + const b = Math.floor(Math.random() * 200); + colors.push(`rgb(${r}, ${g}, ${b})`); + } + } + + return colors; + } + + // Mise à jour des graphiques + function updateCharts(statistics) { + updateRevenuesChart(statistics.revenue_by_category); + updateExpensesChart(statistics.expenses_by_category); + } + + // Mise à jour du graphique des revenus + function updateRevenuesChart(revenuesByCategory) { + const ctx = document.getElementById("revenuesByCategoryChart"); + const colors = generateChartColors(Object.keys(revenuesByCategory).length); + + // Détruire le graphique précédent s'il existe + if (revenuesChart) { + revenuesChart.destroy(); + } + + // Créer le nouveau graphique + revenuesChart = new Chart(ctx, { + type: "pie", + data: { + labels: Object.keys(revenuesByCategory), + datasets: [ + { + data: Object.values(revenuesByCategory), + backgroundColor: colors, + borderWidth: 1, + }, + ], + }, + options: { + responsive: true, + maintainAspectRatio: false, + plugins: { + legend: { + position: "right", + }, + tooltip: { + callbacks: { + label: function (context) { + const value = context.raw; + const total = context.dataset.data.reduce((a, b) => a + b, 0); + const percentage = ((value / total) * 100).toFixed(1); + return `${value.toFixed(2)} € (${percentage}%)`; + }, + }, + }, + }, + }, + }); + + // Mettre à jour la liste des revenus par catégorie + updateCategoryList(revenuesByCategoryList, revenuesByCategory); + } + + // Mise à jour du graphique des dépenses + function updateExpensesChart(expensesByCategory) { + const ctx = document.getElementById("expensesByCategoryChart"); + const colors = generateChartColors(Object.keys(expensesByCategory).length); + + // Détruire le graphique précédent s'il existe + if (expensesChart) { + expensesChart.destroy(); + } + + // Créer le nouveau graphique + expensesChart = new Chart(ctx, { + type: "pie", + data: { + labels: Object.keys(expensesByCategory), + datasets: [ + { + data: Object.values(expensesByCategory), + backgroundColor: colors, + borderWidth: 1, + }, + ], + }, + options: { + responsive: true, + maintainAspectRatio: false, + plugins: { + legend: { + position: "right", + }, + tooltip: { + callbacks: { + label: function (context) { + const value = context.raw; + const total = context.dataset.data.reduce((a, b) => a + b, 0); + const percentage = ((value / total) * 100).toFixed(1); + return `${value.toFixed(2)} € (${percentage}%)`; + }, + }, + }, + }, + }, + }); + + // Mettre à jour la liste des dépenses par catégorie + updateCategoryList(expensesByCategoryList, expensesByCategory); + } + + // Mise à jour des listes de catégories + function updateCategoryList(listElement, categoryData) { + listElement.innerHTML = ""; + + const total = Object.values(categoryData).reduce((a, b) => a + b, 0); + + Object.entries(categoryData) + .sort((a, b) => b[1] - a[1]) // Trier par montant décroissant + .forEach(([category, amount]) => { + const percentage = ((amount / total) * 100).toFixed(1); + + const row = document.createElement("tr"); + row.innerHTML = ` + + ${category} + + + ${amount.toFixed(2)} € + + + ${percentage}% + + `; + + listElement.appendChild(row); + }); + } + + // Fonctions globales pour l'édition et la suppression + window.editTransaction = async (transactionId) => { + alert("Fonctionnalité d'édition à implémenter"); + // TODO: Implémenter l'édition des transactions + }; + + window.deleteTransaction = async (transactionId) => { + if (confirm("Êtes-vous sûr de vouloir supprimer cette transaction ?")) { + try { + const response = await fetch( + `/api/accounting/transaction/${transactionId}`, + { + method: "DELETE", + } + ); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + showMessage("Transaction supprimée avec succès"); + loadData(); + } catch (error) { + console.error("Erreur lors de la suppression:", error); + showMessage("Erreur lors de la suppression", "error"); + } + } + }; + + // Fonction pour afficher des messages à l'utilisateur + function showMessage(message, type = "success") { + // On pourrait implémenter un système de notification plus élaboré ici + alert(message); + } +}); diff --git a/static/dashboard.html b/static/dashboard.html index 9562edd..6fffd7c 100644 --- a/static/dashboard.html +++ b/static/dashboard.html @@ -11,9 +11,14 @@

Gestion des Factures

- - Nouvelle Facture - +
diff --git a/static/generator.html b/static/generator.html index dcbb169..de8919f 100644 --- a/static/generator.html +++ b/static/generator.html @@ -48,6 +48,17 @@
+ + +
+

Lignes de facturation

+
+ +
+ +
diff --git a/static/preview.js b/static/preview.js index fca18a3..66bb987 100644 --- a/static/preview.js +++ b/static/preview.js @@ -19,29 +19,91 @@ document.addEventListener("DOMContentLoaded", function () { // Afficher les détails de la facture const invoiceDetails = document.getElementById("invoiceDetails"); - invoiceDetails.innerHTML = ` -
-
-

Numéro de facture

-

${formData.invoice_number}

-
-
-

Date

-

${new Date().toLocaleDateString()}

-
-
-

Langue

-

${formData.language}

-
-
-

Montant

-

${formData.amount} ${ - formData.currency - }

-
+ + // Créer le contenu de base des détails + let detailsHTML = ` +
+
+

Numéro de facture

+

${formData.invoice_number}

+
+

Date

+

${new Date().toLocaleDateString()}

+
+
+

Langue

+

${formData.language}

+
+
+

Montant Total

+

${formData.amount} ${formData.currency}

+
+
+ `; + + // Ajouter le tableau des lignes de facturation si présentes + if (formData.items && formData.items.length > 0) { + detailsHTML += ` +
+

Lignes de facturation

+ + + + + + + + `; + // Ajouter chaque ligne + formData.items.forEach((item) => { + detailsHTML += ` + + + + + `; + }); + + detailsHTML += ` + + + + + + + +
DescriptionMontant (${formData.currency})
${item.description}${item.amount}
Total${formData.amount}
+
+ `; + } + + invoiceDetails.innerHTML = detailsHTML; + + // Mettre à jour les informations de paiement en fonction de la devise + const paymentInfoDiv = document.querySelector( + ".mt-8.border-t.pt-8 .space-y-2" + ); + if (paymentInfoDiv) { + if (formData.currency === "CHF") { + paymentInfoDiv.innerHTML = ` +

Banque: PostFinance SA, Berne, Suisse

+

IBAN: CH56 0900 0000 1527 2120 9

+

BIC/SWIFT: POFICHBEXXX

+

Titulaire: Robin Szymczak

+ `; + } else { + paymentInfoDiv.innerHTML = ` +

Banque: Wise, Bruxelles, Belgique

+

IBAN: BE22905094540247

+

BIC/SWIFT: TRWIBEB1XXX

+

Titulaire: Robin Szymczak

+ `; + } + } + // Gérer la validation de la facture document .getElementById("validateInvoice") diff --git a/static/script.js b/static/script.js index 9015dcf..6e18271 100644 --- a/static/script.js +++ b/static/script.js @@ -1,54 +1,140 @@ document.addEventListener("DOMContentLoaded", () => { const form = document.getElementById("invoiceForm"); + const invoiceItems = document.getElementById("invoiceItems"); + const addItemButton = document.getElementById("addItemButton"); - // Fonction pour afficher les messages - function showMessage(message, type = "success") { - const messageDiv = document.createElement("div"); - messageDiv.className = `${type}-message`; - messageDiv.textContent = message; - document.body.appendChild(messageDiv); + // Initialiser avec une ligne de facturation par défaut + addInvoiceItem(); - // Afficher le message - setTimeout(() => messageDiv.classList.add("show"), 100); + // Ajouter une ligne de facturation lorsqu'on clique sur le bouton + addItemButton.addEventListener("click", addInvoiceItem); - // Supprimer le message après 3 secondes - setTimeout(() => { - messageDiv.classList.remove("show"); - setTimeout(() => messageDiv.remove(), 300); - }, 3000); + // Fonction pour créer une nouvelle ligne de facturation + function addInvoiceItem(description = "", amount = "") { + const itemId = Date.now(); // ID unique pour l'élément + const itemDiv = document.createElement("div"); + itemDiv.className = "invoice-item grid grid-cols-5 gap-2"; + itemDiv.dataset.id = itemId; + + itemDiv.innerHTML = ` +
+ +
+
+ +
+
+ +
+ `; + + // Ajouter un gestionnaire d'événements pour supprimer la ligne + const deleteButton = itemDiv.querySelector(".delete-item"); + deleteButton.addEventListener("click", function () { + const id = this.getAttribute("data-id"); + deleteInvoiceItem(id); + }); + + invoiceItems.appendChild(itemDiv); + } + + // Fonction pour supprimer une ligne de facturation + function deleteInvoiceItem(id) { + const item = document.querySelector(`.invoice-item[data-id="${id}"]`); + if (item) { + item.remove(); + } + + // S'assurer qu'il reste au moins une ligne + if (invoiceItems.children.length === 0) { + addInvoiceItem(); + } + } + + // Fonction pour collecter toutes les lignes de facturation + function collectInvoiceItems() { + const items = []; + document.querySelectorAll(".invoice-item").forEach((item) => { + const id = item.dataset.id; + const description = item.querySelector( + `[name="item_description_${id}"]` + ).value; + const amount = item.querySelector(`[name="item_amount_${id}"]`).value; + + if (description && amount) { + items.push({ description, amount }); + } + }); + return items; } // Fonction pour valider le formulaire - function validateForm(formData) { + function validateForm() { const requiredFields = [ - "invoice_number", - "amount", - "recipient_name", - "recipient_address", - "recipient_postal_code", - "recipient_town", - "recipient_country", + form.invoice_number, + form.amount, + form.recipient_name, + form.recipient_address, + form.recipient_postal_code, + form.recipient_town, + form.recipient_country, ]; for (const field of requiredFields) { - if (!formData.get(field)) { - showMessage(`Le champ ${field} est requis`, "error"); + if (!field.value) { + showMessage(`Le champ ${field.name} est requis`, "error"); + field.focus(); return false; } } + const items = collectInvoiceItems(); + if (items.length === 0) { + showMessage("Ajoutez au moins une ligne de facturation", "error"); + return false; + } + return true; } + // Fonction pour afficher les messages + function showMessage(message, type = "success") { + const messageDiv = document.createElement("div"); + messageDiv.className = `${type}-message fixed top-4 right-4 p-4 rounded-md shadow-lg ${ + type === "success" ? "bg-green-500" : "bg-red-500" + } text-white`; + messageDiv.textContent = message; + document.body.appendChild(messageDiv); + + // Afficher le message + setTimeout(() => messageDiv.classList.add("opacity-100"), 100); + + // Supprimer le message après 3 secondes + setTimeout(() => { + messageDiv.classList.remove("opacity-100"); + setTimeout(() => messageDiv.remove(), 300); + }, 3000); + } + // Gestionnaire de soumission du formulaire form.addEventListener("submit", function (e) { e.preventDefault(); + if (!validateForm()) { + return; + } + // Récupérer les données du formulaire const formData = { language: form.language.value, invoice_number: form.invoice_number.value, - amount: form.amount.value, + amount: form.amount.value, // Montant total currency: form.currency.value, recipient_name: form.recipient_name.value, recipient_address: form.recipient_address.value, @@ -56,6 +142,7 @@ document.addEventListener("DOMContentLoaded", () => { recipient_town: form.recipient_town.value, recipient_country: form.recipient_country.value, recipient_vat_number: form.recipient_vat_number.value || null, + items: collectInvoiceItems(), // Ajouter les lignes de facturation }; // Rediriger vers la page de prévisualisation avec les données