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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Année
+
+ Toutes
+ 2023
+ 2024
+ 2025
+
+
+
+ Catégorie
+
+ Toutes
+
+
+
+
+ Type
+
+ Tous
+ Revenus
+ Dépenses
+
+
+
+
+ Appliquer les filtres
+
+
+
+
+
+
+
+
+
+
+ Transactions
+
+
+ Revenus par catégorie
+
+
+ Dépenses par catégorie
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Date
+ Description
+ Montant
+ Catégorie
+ Type
+ Actions
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Catégorie
+ Montant
+ Pourcentage
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Catégorie
+ Montant
+ Pourcentage
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Importer des données
+
+
+
+
+
+
+
+
+
Importer des données
+
+
Sélectionnez le type de données à importer.
+
+
+ Importer les revenus
+
+
+ Importer les dépenses
+
+
+
+
+
+ Fermer
+
+
+
+
+
+
+
+
+
+
+
\ 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 = `Toutes `;
+ for (let year = currentYear; year >= currentYear - 2; year--) {
+ yearFilter.innerHTML += `${year} `;
+ }
+
+ // 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 = 'Toutes ';
+
+ categories.forEach((category) => {
+ categoryFilter.innerHTML += `${category} `;
+ });
+ }
+
+ // 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"}
+
+
+
+
+ Éditer
+
+
+ Supprimer
+
+
+ `;
+
+ 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 @@
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 @@
CHF
+
+
+
+
Lignes de facturation
+
+
+
+
+ + Ajouter une ligne
+
+
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
+
+
+
+ Description
+ Montant (${formData.currency})
+
+
+
`;
+ // Ajouter chaque ligne
+ formData.items.forEach((item) => {
+ detailsHTML += `
+
+ ${item.description}
+ ${item.amount}
+
+ `;
+ });
+
+ detailsHTML += `
+
+
+
+ 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