From 22d6330205860435e6e886fa6b9fbaf395e09459 Mon Sep 17 00:00:00 2001 From: robin Date: Sat, 31 May 2025 17:41:17 +0200 Subject: [PATCH] feat: add database and latest updates for deployment preparation - Add invoices.db temporarily for deployment - Update README with comprehensive documentation - Latest improvements to dashboard, preview, and static files - Ready for cloud deployment --- README.md | 83 +++++++++ database.py | 29 ++-- invoices.db | Bin 0 -> 57344 bytes server.py | 6 +- static/dashboard.html | 7 +- static/dashboard.js | 56 ++++-- static/preview.html | 4 +- static/preview.js | 41 ++++- static/script.js | 390 +++++++++++++++++++++++++++++++++++++++++- static/styles.css | 23 +++ 10 files changed, 601 insertions(+), 38 deletions(-) create mode 100644 README.md create mode 100644 invoices.db diff --git a/README.md b/README.md new file mode 100644 index 0000000..9ed608e --- /dev/null +++ b/README.md @@ -0,0 +1,83 @@ +# Gestionnaire de Factures + +Application interne pour la gestion et la génération de factures. + +## Installation + +1. Installer les dépendances : + +```bash +pip install -r requirements.txt +``` + +2. Installer Typst : + +```bash +brew install typst # macOS +``` + +## Utilisation + +1. Démarrer le serveur : + +```bash +python3 server.py +``` + +2. Accéder à l'application : + +- http://localhost:5000 : Page d'accueil +- http://localhost:5000/dashboard : Tableau de bord +- http://localhost:5000/generator : Création de factures + +## Fonctionnalités + +- Création de factures (FR/DE/EN) +- Génération de PDF avec Typst +- Tableau de bord des factures +- Support EUR/CHF +- Numérotation automatique + +## Structure + +``` +accounting/ +├── server.py # Application Flask +├── database.py # Base de données SQLite +├── requirements.txt # Dépendances +├── static/ # Fichiers statiques +└── generated/ # PDF générés +``` + +## Fonctionnalités Techniques + +- **Gestion de la Base de Données** : + + - Système de migration pour les mises à jour du schéma + - Gestion des connexions avec timeout et retry + - Mode WAL pour de meilleures performances + +- **Génération de PDF** : + + - Templates Typst personnalisables + - Support multilingue + - Mise en page professionnelle + +- **Interface Utilisateur** : + - Design responsive avec Tailwind CSS + - Prévisualisation en temps réel + - Validation des formulaires + +## Contribution + +Les contributions sont les bienvenues ! N'hésitez pas à : + +1. Fork le projet +2. Créer une branche pour votre fonctionnalité +3. Commiter vos changements +4. Pousser vers la branche +5. Ouvrir une Pull Request + +## Licence + +Ce projet est sous licence MIT. Voir le fichier `LICENSE` pour plus de détails. diff --git a/database.py b/database.py index 1b4bdcf..d77a602 100644 --- a/database.py +++ b/database.py @@ -182,25 +182,34 @@ class Database: conn.commit() def get_statistics(self): + """Récupérer les statistiques des factures""" with self.get_connection() as conn: c = conn.cursor() - stats = {} - - # Total des factures + # Nombre total de factures c.execute('SELECT COUNT(*) FROM invoices') - stats['total_invoices'] = c.fetchone()[0] + total_invoices = c.fetchone()[0] or 0 # Montant total des factures c.execute('SELECT SUM(amount) FROM invoices') - stats['total_amount'] = c.fetchone()[0] or 0 + total_amount = c.fetchone()[0] or 0 - # Montant total des factures payées + # Montant des factures payées c.execute('SELECT SUM(amount) FROM invoices WHERE status = "paid"') - stats['total_paid'] = c.fetchone()[0] or 0 + total_paid = c.fetchone()[0] or 0 - # Montant total des factures en retard + # Montant des factures en retard c.execute('SELECT SUM(amount) FROM invoices WHERE status = "overdue"') - stats['total_overdue'] = c.fetchone()[0] or 0 + total_overdue = c.fetchone()[0] or 0 - return stats \ No newline at end of file + # Montant des factures annulées + c.execute('SELECT SUM(amount) FROM invoices WHERE status = "cancelled"') + total_cancelled = c.fetchone()[0] or 0 + + return { + 'total_invoices': total_invoices, + 'total_amount': total_amount, + 'total_paid': total_paid, + 'total_overdue': total_overdue, + 'total_cancelled': total_cancelled + } \ No newline at end of file diff --git a/invoices.db b/invoices.db new file mode 100644 index 0000000000000000000000000000000000000000..2f4ba03d46e822813e08f63bf956848f44cc2497 GIT binary patch literal 57344 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 literal 0 HcmV?d00001 diff --git a/server.py b/server.py index f7f204d..9cda882 100644 --- a/server.py +++ b/server.py @@ -307,11 +307,11 @@ def create_invoice(): #t.account_holder Robin Szymczak ] else if currency == "CHF" [ // Compte pour les transactions en francs suisses - #t.bank PostFinance SA, Mingerstrasse 20, 3030 Bern, Switzerland + #t.bank Wise Payments Limited, 1st Floor, Worship Square, 65 Clifton Street, London, EC2A 4JE, United Kingdom #v(0.1cm) - IBAN: CH56 0900 0000 1527 2120 9 + IBAN: GB51 TRWI 2308 0140 8766 98 #v(0.1cm) - BIC/SWIFT: POFICHBEXXX + BIC/SWIFT: TRWIGB2LXXX #v(0.1cm) #t.account_holder Robin Szymczak ] else [ diff --git a/static/dashboard.html b/static/dashboard.html index 6fffd7c..995dbb8 100644 --- a/static/dashboard.html +++ b/static/dashboard.html @@ -22,7 +22,7 @@ -
+

Total Factures

0

@@ -39,6 +39,10 @@

Factures en Retard

0 €

+
+

Factures Annulées

+

0 €

+
@@ -51,6 +55,7 @@ +
diff --git a/static/dashboard.js b/static/dashboard.js index 67ec2a3..041b1d4 100644 --- a/static/dashboard.js +++ b/static/dashboard.js @@ -58,6 +58,9 @@ document.addEventListener("DOMContentLoaded", () => { document.getElementById( "totalOverdue" ).textContent = `${stats.total_overdue.toFixed(2)} €`; + document.getElementById("totalCancelled").textContent = `${( + stats.total_cancelled || 0 + ).toFixed(2)} €`; } // Fonction pour afficher les factures @@ -66,6 +69,35 @@ document.addEventListener("DOMContentLoaded", () => { invoices.forEach((invoice) => { const row = document.createElement("tr"); + + // Préparer les boutons d'action en fonction du statut + let actionButtons = ` + `; + + // N'afficher le bouton "Marquer comme payée" que si la facture n'est pas déjà payée ou annulée + if (invoice.status !== "paid" && invoice.status !== "cancelled") { + actionButtons += ` + `; + } + + // N'afficher le bouton "Annuler" que si la facture n'est pas déjà annulée + if (invoice.status !== "cancelled") { + actionButtons += ` + `; + } + + // Toujours afficher le bouton de téléchargement + actionButtons += ` + `; + row.innerHTML = ` ${invoice.invoice_number} @@ -86,21 +118,7 @@ document.addEventListener("DOMContentLoaded", () => { - - - + ${actionButtons} `; invoicesList.appendChild(row); @@ -114,6 +132,8 @@ document.addEventListener("DOMContentLoaded", () => { return "bg-green-100 text-green-800"; case "overdue": return "bg-red-100 text-red-800"; + case "cancelled": + return "bg-gray-100 text-gray-800"; default: return "bg-yellow-100 text-yellow-800"; } @@ -126,6 +146,8 @@ document.addEventListener("DOMContentLoaded", () => { return "Payée"; case "overdue": return "En retard"; + case "cancelled": + return "Annulée"; default: return "Émise"; } @@ -164,14 +186,14 @@ document.addEventListener("DOMContentLoaded", () => { }; // Fonction pour mettre à jour le statut d'une facture - window.updateStatus = async (invoiceId) => { + window.updateStatus = async (invoiceId, status) => { try { await fetch(`/api/invoices/${invoiceId}/status`, { method: "PUT", headers: { "Content-Type": "application/json", }, - body: JSON.stringify({ status: "paid" }), + body: JSON.stringify({ status: status }), }); loadInvoices(); diff --git a/static/preview.html b/static/preview.html index c12dad4..c34c002 100644 --- a/static/preview.html +++ b/static/preview.html @@ -12,9 +12,9 @@

Prévisualisation de la Facture

- + diff --git a/static/preview.js b/static/preview.js index 66bb987..86ddf55 100644 --- a/static/preview.js +++ b/static/preview.js @@ -89,9 +89,9 @@ document.addEventListener("DOMContentLoaded", function () { if (paymentInfoDiv) { if (formData.currency === "CHF") { paymentInfoDiv.innerHTML = ` -

Banque: PostFinance SA, Berne, Suisse

-

IBAN: CH56 0900 0000 1527 2120 9

-

BIC/SWIFT: POFICHBEXXX

+

Banque: Wise Payments Limited, London, United Kingdom

+

IBAN: GB51 TRWI 2308 0140 8766 98

+

BIC/SWIFT: TRWIGB2LXXX

Titulaire: Robin Szymczak

`; } else { @@ -104,6 +104,14 @@ document.addEventListener("DOMContentLoaded", function () { } } + // Gérer le bouton "Retour" + document.getElementById("backToForm").addEventListener("click", function () { + // Sauvegarder les données du formulaire dans localStorage + localStorage.setItem("invoiceData", JSON.stringify(formData)); + // Rediriger vers la page du générateur + window.location.href = "/generator"; + }); + // Gérer la validation de la facture document .getElementById("validateInvoice") @@ -125,6 +133,13 @@ document.addEventListener("DOMContentLoaded", function () { const result = await response.json(); console.log("Facture créée avec succès:", result); alert("Facture créée avec succès !"); + + // Effacer les données du formulaire du localStorage après validation réussie + localStorage.removeItem("invoiceData"); + + // Supprimer également de l'historique des factures récentes + removeFromRecentInvoices(formData.recipient_name); + window.location.href = "/dashboard"; } else { const error = await response.json(); @@ -139,4 +154,24 @@ document.addEventListener("DOMContentLoaded", function () { alert("Erreur lors de la création de la facture : " + error.message); } }); + + // Fonction pour supprimer une facture de l'historique récent + function removeFromRecentInvoices(recipientName) { + if (!recipientName) return; + + try { + let recentInvoices = JSON.parse( + localStorage.getItem("recentInvoices") || "[]" + ); + + // Filtrer pour supprimer la facture avec le même destinataire + recentInvoices = recentInvoices.filter( + (invoice) => invoice.recipient_name !== recipientName + ); + + localStorage.setItem("recentInvoices", JSON.stringify(recentInvoices)); + } catch (error) { + console.error("Erreur lors de la suppression de l'historique:", error); + } + } }); diff --git a/static/script.js b/static/script.js index 6e18271..2c81566 100644 --- a/static/script.js +++ b/static/script.js @@ -2,13 +2,393 @@ document.addEventListener("DOMContentLoaded", () => { const form = document.getElementById("invoiceForm"); const invoiceItems = document.getElementById("invoiceItems"); const addItemButton = document.getElementById("addItemButton"); + const clearFormButton = document.createElement("button"); + const recipientNameInput = form.recipient_name; + const clientSuggestions = document.createElement("div"); - // Initialiser avec une ligne de facturation par défaut - addInvoiceItem(); + // Configuration de la boîte de suggestions + clientSuggestions.className = + "client-suggestions absolute z-10 bg-white shadow-lg rounded-md w-full max-h-60 overflow-y-auto hidden"; + recipientNameInput.parentNode.style.position = "relative"; + recipientNameInput.parentNode.appendChild(clientSuggestions); + + // Charger les clients précédents + let clientsData = JSON.parse(localStorage.getItem("clientsData") || "[]"); + + // Ajouter un bouton pour effacer le formulaire + clearFormButton.type = "button"; + clearFormButton.className = + "bg-red-500 text-white px-4 py-2 rounded-md hover:bg-red-600 ml-4"; + clearFormButton.textContent = "Effacer le formulaire"; + clearFormButton.id = "clearFormButton"; + + // Ajouter le bouton après le bouton de soumission + const submitButton = form.querySelector('button[type="submit"]'); + submitButton.parentNode.insertBefore( + clearFormButton, + submitButton.nextSibling + ); + + // Gestionnaire d'événement pour effacer le formulaire + clearFormButton.addEventListener("click", () => { + if (confirm("Êtes-vous sûr de vouloir effacer le formulaire ?")) { + localStorage.removeItem("invoiceData"); + form.reset(); + // Réinitialiser les lignes de facturation + invoiceItems.innerHTML = ""; + addInvoiceItem(); + showMessage("Formulaire effacé", "success"); + } + }); + + // Restaurer les données du formulaire depuis localStorage si elles existent + restoreFormData(); + + // Initialiser avec une ligne de facturation par défaut ou restaurer les lignes sauvegardées + if (invoiceItems.children.length === 0) { + addInvoiceItem(); + } // Ajouter une ligne de facturation lorsqu'on clique sur le bouton addItemButton.addEventListener("click", addInvoiceItem); + // Sauvegarder les données pendant la saisie (debounced) + let saveTimeout; + form.addEventListener("input", () => { + clearTimeout(saveTimeout); + saveTimeout = setTimeout(() => { + saveFormData(); + }, 500); // Attendre 500ms après la dernière saisie + }); + + // Gérer l'auto-complétion des clients + recipientNameInput.addEventListener("input", function () { + const query = this.value.toLowerCase(); + if (query.length < 2) { + clientSuggestions.classList.add("hidden"); + return; + } + + // Filtrer les clients qui correspondent à la recherche + const matches = clientsData.filter((client) => + client.name.toLowerCase().includes(query) + ); + + if (matches.length === 0) { + clientSuggestions.classList.add("hidden"); + return; + } + + // Afficher les suggestions + clientSuggestions.innerHTML = matches + .map( + (client) => ` +
+
${client.name}
+
${client.address}, ${client.postal_code} ${client.town}
+
+ ` + ) + .join(""); + + clientSuggestions.classList.remove("hidden"); + + // Ajouter des gestionnaires d'événements pour les suggestions + document.querySelectorAll(".client-suggestion").forEach((suggestion) => { + suggestion.addEventListener("click", function () { + const clientId = this.getAttribute("data-client-id"); + const client = clientsData.find((c) => c.id === clientId); + if (client) { + fillClientData(client); + clientSuggestions.classList.add("hidden"); + } + }); + }); + }); + + // Masquer les suggestions lorsqu'on clique ailleurs + document.addEventListener("click", function (e) { + if ( + !recipientNameInput.contains(e.target) && + !clientSuggestions.contains(e.target) + ) { + clientSuggestions.classList.add("hidden"); + } + }); + + // Fonction pour remplir les données du client + function fillClientData(client) { + form.recipient_name.value = client.name; + form.recipient_address.value = client.address; + form.recipient_postal_code.value = client.postal_code; + form.recipient_town.value = client.town; + form.recipient_country.value = client.country; + if (client.vat_number) { + form.recipient_vat_number.value = client.vat_number; + } + } + + // Fonction pour sauvegarder un nouveau client + function saveClient() { + // Ne sauvegarder que si les champs essentiels sont remplis + if ( + !form.recipient_name.value || + !form.recipient_address.value || + !form.recipient_postal_code.value || + !form.recipient_town.value + ) { + return; + } + + const clientData = { + id: Date.now().toString(), // ID unique + name: form.recipient_name.value, + address: form.recipient_address.value, + postal_code: form.recipient_postal_code.value, + town: form.recipient_town.value, + country: form.recipient_country.value, + vat_number: form.recipient_vat_number.value || null, + lastUsed: new Date().toISOString(), + }; + + // Vérifier si le client existe déjà (par nom) + const existingClientIndex = clientsData.findIndex( + (client) => client.name.toLowerCase() === clientData.name.toLowerCase() + ); + + if (existingClientIndex !== -1) { + // Mettre à jour le client existant + clientsData[existingClientIndex] = { + ...clientsData[existingClientIndex], + ...clientData, + id: clientsData[existingClientIndex].id, // Conserver l'ID d'origine + }; + } else { + // Ajouter le nouveau client + clientsData.push(clientData); + } + + // Trier par date d'utilisation (plus récent en premier) + clientsData.sort((a, b) => new Date(b.lastUsed) - new Date(a.lastUsed)); + + // Limiter à 50 clients + clientsData = clientsData.slice(0, 50); + + // Sauvegarder dans localStorage + localStorage.setItem("clientsData", JSON.stringify(clientsData)); + } + + // Fonction pour sauvegarder les données du formulaire + function saveFormData() { + const formData = { + language: form.language.value, + invoice_number: form.invoice_number.value, + amount: form.amount.value, + currency: form.currency.value, + recipient_name: form.recipient_name.value, + recipient_address: form.recipient_address.value, + recipient_postal_code: form.recipient_postal_code.value, + recipient_town: form.recipient_town.value, + recipient_country: form.recipient_country.value, + recipient_vat_number: form.recipient_vat_number.value || null, + items: collectInvoiceItems(), + }; + + localStorage.setItem("invoiceData", JSON.stringify(formData)); + + // Sauvegarder aussi dans l'historique des factures récentes + saveToRecentInvoices(formData); + + // Sauvegarder les informations du client si tous les champs sont remplis + saveClient(); + } + + // Fonction pour sauvegarder dans l'historique des factures récentes + function saveToRecentInvoices(formData) { + if (!formData.recipient_name) return; // Ne pas sauvegarder si pas de nom de destinataire + + let recentInvoices = JSON.parse( + localStorage.getItem("recentInvoices") || "[]" + ); + + // Créer un identifiant unique basé sur le nom et la date + const now = new Date(); + const id = `${formData.recipient_name}_${now.toISOString()}`; + + // Ajouter la facture actuelle à l'historique avec un timestamp + const invoiceWithMeta = { + ...formData, + id, + lastEdited: now.toISOString(), + }; + + // Vérifier si une facture similaire existe déjà (même destinataire) + const existingIndex = recentInvoices.findIndex( + (invoice) => invoice.recipient_name === formData.recipient_name + ); + + if (existingIndex !== -1) { + // Remplacer la facture existante + recentInvoices[existingIndex] = invoiceWithMeta; + } else { + // Ajouter la nouvelle facture + recentInvoices.unshift(invoiceWithMeta); + } + + // Limiter à 10 factures récentes + recentInvoices = recentInvoices.slice(0, 10); + + localStorage.setItem("recentInvoices", JSON.stringify(recentInvoices)); + + // Mettre à jour la liste des factures récentes dans l'interface + updateRecentInvoicesList(); + } + + // Fonction pour mettre à jour la liste des factures récentes dans l'interface + function updateRecentInvoicesList() { + const recentInvoicesContainer = document.getElementById("recentInvoices"); + if (!recentInvoicesContainer) { + // Créer le conteneur s'il n'existe pas + const container = document.createElement("div"); + container.id = "recentInvoices"; + container.className = "mt-8 bg-white rounded-lg shadow p-4"; + container.innerHTML = ` +

Factures récentes

+
+ `; + form.parentNode.insertBefore(container, form.nextSibling); + } + + const recentInvoicesList = document.getElementById("recentInvoicesList"); + const recentInvoices = JSON.parse( + localStorage.getItem("recentInvoices") || "[]" + ); + + if (recentInvoices.length === 0) { + recentInvoicesList.innerHTML = `

Aucune facture récente

`; + return; + } + + recentInvoicesList.innerHTML = recentInvoices + .map( + (invoice) => ` +
+
+ ${invoice.recipient_name} + ${invoice.amount} ${ + invoice.currency + } + ${new Date( + invoice.lastEdited + ).toLocaleDateString()} +
+ +
+ ` + ) + .join(""); + + // Ajouter les gestionnaires d'événements pour charger les factures + document.querySelectorAll(".load-invoice").forEach((button) => { + button.addEventListener("click", (e) => { + const id = e.target.getAttribute("data-id"); + const invoice = recentInvoices.find((inv) => inv.id === id); + if (invoice) { + if ( + confirm( + "Charger cette facture ? Les données actuelles seront remplacées." + ) + ) { + loadInvoice(invoice); + } + } + }); + }); + } + + // Fonction pour charger une facture depuis l'historique + function loadInvoice(invoice) { + // Remplir le formulaire avec les données de la facture + form.language.value = invoice.language || ""; + form.invoice_number.value = invoice.invoice_number || ""; + form.amount.value = invoice.amount || ""; + form.currency.value = invoice.currency || "EUR"; + form.recipient_name.value = invoice.recipient_name || ""; + form.recipient_address.value = invoice.recipient_address || ""; + form.recipient_postal_code.value = invoice.recipient_postal_code || ""; + form.recipient_town.value = invoice.recipient_town || ""; + form.recipient_country.value = invoice.recipient_country || ""; + form.recipient_vat_number.value = invoice.recipient_vat_number || ""; + + // Restaurer les lignes de facturation + invoiceItems.innerHTML = ""; + if (invoice.items && invoice.items.length > 0) { + invoice.items.forEach((item) => { + addInvoiceItem(item.description, item.amount); + }); + } else { + addInvoiceItem(); + } + + // Sauvegarder dans le localStorage + localStorage.setItem("invoiceData", JSON.stringify(invoice)); + + showMessage("Facture chargée avec succès", "success"); + } + + // Fonction pour restaurer les données du formulaire depuis localStorage + function restoreFormData() { + const savedData = localStorage.getItem("invoiceData"); + if (!savedData) { + // Mettre à jour la liste des factures récentes même s'il n'y a pas de données sauvegardées + updateRecentInvoicesList(); + return; + } + + try { + const formData = JSON.parse(savedData); + + // Remplir les champs du formulaire + if (formData.language) form.language.value = formData.language; + if (formData.invoice_number) + form.invoice_number.value = formData.invoice_number; + if (formData.amount) form.amount.value = formData.amount; + if (formData.currency) form.currency.value = formData.currency; + if (formData.recipient_name) + form.recipient_name.value = formData.recipient_name; + if (formData.recipient_address) + form.recipient_address.value = formData.recipient_address; + if (formData.recipient_postal_code) + form.recipient_postal_code.value = formData.recipient_postal_code; + if (formData.recipient_town) + form.recipient_town.value = formData.recipient_town; + if (formData.recipient_country) + form.recipient_country.value = formData.recipient_country; + if (formData.recipient_vat_number) + form.recipient_vat_number.value = formData.recipient_vat_number; + + // Restaurer les lignes de facturation + if (formData.items && formData.items.length > 0) { + // Vider les lignes existantes + invoiceItems.innerHTML = ""; + + // Ajouter chaque ligne sauvegardée + formData.items.forEach((item) => { + addInvoiceItem(item.description, item.amount); + }); + } + + // Mettre à jour la liste des factures récentes + updateRecentInvoicesList(); + } catch (error) { + console.error("Erreur lors de la restauration des données:", error); + } + } + // Fonction pour créer une nouvelle ligne de facturation function addInvoiceItem(description = "", amount = "") { const itemId = Date.now(); // ID unique pour l'élément @@ -145,6 +525,12 @@ document.addEventListener("DOMContentLoaded", () => { items: collectInvoiceItems(), // Ajouter les lignes de facturation }; + // Sauvegarder les données du formulaire dans localStorage + localStorage.setItem("invoiceData", JSON.stringify(formData)); + + // Sauvegarder les informations du client + saveClient(); + // Rediriger vers la page de prévisualisation avec les données const queryString = encodeURIComponent(JSON.stringify(formData)); window.location.href = `/preview?data=${queryString}`; diff --git a/static/styles.css b/static/styles.css index 48ea30f..7a8c3d8 100644 --- a/static/styles.css +++ b/static/styles.css @@ -49,4 +49,27 @@ select { .error-message.show { transform: translateX(0); +} + +/* Styles pour l'auto-complétion des clients */ +.client-suggestions { + border: 1px solid #e2e8f0; + box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); + max-height: 200px; + overflow-y: auto; + z-index: 50; +} + +.client-suggestion { + padding: 0.75rem 1rem; + cursor: pointer; + transition: background-color 0.2s; +} + +.client-suggestion:hover { + background-color: #f3f4f6; +} + +.client-suggestion:not(:last-child) { + border-bottom: 1px solid #e2e8f0; } \ No newline at end of file