feat: améliorations du système de facturation (multi-lignes, comptes bancaires EUR/CHF, interface comptable)
This commit is contained in:
312
accounting_data/accounting_manager.py
Normal file
312
accounting_data/accounting_manager.py
Normal file
@@ -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")
|
||||
Reference in New Issue
Block a user