diff --git a/compta/__pycache__/__init__.cpython-310.pyc b/compta/__pycache__/__init__.cpython-310.pyc deleted file mode 100644 index 97dd05d..0000000 Binary files a/compta/__pycache__/__init__.cpython-310.pyc and /dev/null differ diff --git a/compta/__pycache__/admin.cpython-310.pyc b/compta/__pycache__/admin.cpython-310.pyc deleted file mode 100644 index 6f05201..0000000 Binary files a/compta/__pycache__/admin.cpython-310.pyc and /dev/null differ diff --git a/compta/__pycache__/apps.cpython-310.pyc b/compta/__pycache__/apps.cpython-310.pyc deleted file mode 100644 index 871e9a1..0000000 Binary files a/compta/__pycache__/apps.cpython-310.pyc and /dev/null differ diff --git a/compta/__pycache__/models.cpython-310.pyc b/compta/__pycache__/models.cpython-310.pyc deleted file mode 100644 index 92e79de..0000000 Binary files a/compta/__pycache__/models.cpython-310.pyc and /dev/null differ diff --git a/compta/__pycache__/services.cpython-310.pyc b/compta/__pycache__/services.cpython-310.pyc deleted file mode 100644 index cbcd675..0000000 Binary files a/compta/__pycache__/services.cpython-310.pyc and /dev/null differ diff --git a/compta/__pycache__/urls.cpython-310.pyc b/compta/__pycache__/urls.cpython-310.pyc deleted file mode 100644 index 59866f7..0000000 Binary files a/compta/__pycache__/urls.cpython-310.pyc and /dev/null differ diff --git a/compta/__pycache__/views.cpython-310.pyc b/compta/__pycache__/views.cpython-310.pyc deleted file mode 100644 index 1ed4767..0000000 Binary files a/compta/__pycache__/views.cpython-310.pyc and /dev/null differ diff --git a/compta/__pycache__/views_saisie.cpython-310.pyc b/compta/__pycache__/views_saisie.cpython-310.pyc deleted file mode 100644 index 01ad69a..0000000 Binary files a/compta/__pycache__/views_saisie.cpython-310.pyc and /dev/null differ diff --git a/compta/management/commands/__pycache__/__init__.cpython-310.pyc b/compta/management/commands/__pycache__/__init__.cpython-310.pyc deleted file mode 100644 index bfc5c52..0000000 Binary files a/compta/management/commands/__pycache__/__init__.cpython-310.pyc and /dev/null differ diff --git a/compta/management/commands/__pycache__/seed_demo.cpython-310.pyc b/compta/management/commands/__pycache__/seed_demo.cpython-310.pyc deleted file mode 100644 index 03fcc92..0000000 Binary files a/compta/management/commands/__pycache__/seed_demo.cpython-310.pyc and /dev/null differ diff --git a/compta/management/commands/__pycache__/seed_reference.cpython-310.pyc b/compta/management/commands/__pycache__/seed_reference.cpython-310.pyc deleted file mode 100644 index bacd0e6..0000000 Binary files a/compta/management/commands/__pycache__/seed_reference.cpython-310.pyc and /dev/null differ diff --git a/compta/management/commands/seed_reference.py b/compta/management/commands/seed_reference.py index 60b3c4b..c5f6d1f 100644 --- a/compta/management/commands/seed_reference.py +++ b/compta/management/commands/seed_reference.py @@ -1,12 +1,13 @@ """ -Charge les DONNÉES DE RÉFÉRENCE (barèmes + catégories) — sans données de démo. -Idempotent : sûr à relancer en production après chaque mise à jour. +Données de RÉFÉRENCE (barèmes + catégories + groupe Gestionnaire) — sans démo. +Idempotent : sûr à relancer en production. Appelé par l'entrypoint au démarrage. Usage : python manage.py seed_reference """ from decimal import Decimal from django.core.management.base import BaseCommand +from django.contrib.auth.models import Group, Permission from compta.models import Bareme, Categorie, Activite @@ -19,7 +20,6 @@ BAREMES_2026 = [ Decimal("83600"), Decimal("37500"), Decimal("41250")), ] -# exclure_calculs=True => mouvement neutre (hors calculs fiscaux). CATEGORIES = [ ("Vente de produits finis", "vente", False, 1), ("Prestation de services", "vente", False, 2), @@ -35,6 +35,30 @@ CATEGORIES = [ ("Virement interne", "neutre", True, 5), ] +# Permissions du groupe "Gestionnaire entreprise" +GROUPE_NOM = "Gestionnaire entreprise" +PERMS_COMPLETES = ["client", "facture", "lignefacture", "depense", "paiement"] # add/change/delete/view +PERMS_CHANGE = ["entreprise"] # view + change (pas add/delete, ni gestion des accès) +PERMS_VUE = ["categorie", "bareme"] # view seulement + + +def charger_groupes(): + g, _ = Group.objects.get_or_create(name=GROUPE_NOM) + perms = [] + for model in PERMS_COMPLETES: + for action in ("add", "change", "delete", "view"): + perms += list(Permission.objects.filter( + content_type__app_label="compta", codename=f"{action}_{model}")) + for model in PERMS_CHANGE: + for action in ("view", "change"): + perms += list(Permission.objects.filter( + content_type__app_label="compta", codename=f"{action}_{model}")) + for model in PERMS_VUE: + perms += list(Permission.objects.filter( + content_type__app_label="compta", codename=f"view_{model}")) + g.permissions.set(perms) + return g + def charger_reference(stdout=None, style=None): for act, urssaf, ab, vl, sca, tb, tm in BAREMES_2026: @@ -47,14 +71,16 @@ def charger_reference(stdout=None, style=None): c, _ = Categorie.objects.update_or_create( nom=nom, defaults=dict(usage=usage, exclure_calculs=excl, ordre=ordre)) cat[nom] = c + g = charger_groupes() if stdout and style: stdout.write(style.SUCCESS( - f"Référence chargée : {len(BAREMES_2026)} barèmes, {len(cat)} catégories.")) + f"Référence : {len(BAREMES_2026)} barèmes, {len(cat)} catégories, " + f"groupe « {g.name} » ({g.permissions.count()} permissions).")) return cat class Command(BaseCommand): - help = "Charge les barèmes et catégories de référence (sans démo)." + help = "Charge barèmes, catégories et le groupe Gestionnaire (sans démo)." def handle(self, *args, **options): charger_reference(self.stdout, self.style) diff --git a/compta/templates/compta/base.html b/compta/templates/compta/base.html index 9bb265d..7f34b1c 100644 --- a/compta/templates/compta/base.html +++ b/compta/templates/compta/base.html @@ -54,6 +54,7 @@ Déclaration URSSAF Déclaration TVA + Saisir + Importer Admin diff --git a/compta/templates/compta/import_csv.html b/compta/templates/compta/import_csv.html new file mode 100644 index 0000000..6ba137e --- /dev/null +++ b/compta/templates/compta/import_csv.html @@ -0,0 +1,95 @@ + + + + + +JB Compta — Import CSV + + + +

Import d'un relevé (CSV)

+
← Tableau de bord · Saisie manuelle
+ +
+

Format attendu

+
Colonnes séparées par ; (en-tête facultatif) :
+ date ; libelle ; categorie ; activite ; taux_tva ; regime ; montant +
+ • montant négatif = dépense, positif = vente · regime : nationale / intracom / import (vide = nationale) + · activite : vente / service_bic / service_bnc · une catégorie « neutre » (TVA payée, Urssaf, remboursement) n'entre pas dans les calculs.
+ Exemple : 2026-06-25;ALTISSIMO;Vente de produits finis;vente;20;;168.40 +
+
+ + {% if erreurs %}{% for e in erreurs %}
{{ e }}
{% endfor %}{% endif %} + + {% if resultat %} +
Import terminé : + {% for k, v in resultat.items %}{{ v }} {{ k }}{% if not forloop.last %} · {% endif %}{% endfor %}. + Va voir dans les dépenses et + les factures. +
+ {% endif %} + +
+ {% csrf_token %} + + + + + + +
+ + +
+
+ + {% if lignes %} +
+

Aperçu — {{ lignes|length }} ligne(s)

+ + + + {% for l in lignes %} + + + + + + + {% endfor %} + +
#DateLibelléCatégorieTypeTauxRégimeMontant
{{ l.n }}{{ l.date }}{{ l.libelle }}{{ l.categorie_nom }}{% if l.categorie_nom and not l.categorie %} ⚠️{% endif %}{{ l.type }}{{ l.taux }} %{{ l.regime }}{{ l.montant }} €
+
⚠️ = catégorie non reconnue (sera laissée vide). Vérifie, puis clique « Importer ».
+
+ {% endif %} + + diff --git a/compta/urls.py b/compta/urls.py index 17229a3..cc0d9ff 100644 --- a/compta/urls.py +++ b/compta/urls.py @@ -1,6 +1,7 @@ from django.urls import path from . import views from . import views_saisie +from . import views_import urlpatterns = [ path("dashboard/", views.dashboard, name="dashboard"), @@ -8,6 +9,7 @@ urlpatterns = [ path("declaration/urssaf/", views.declaration_urssaf, name="declaration_urssaf"), path("declaration/tva/", views.declaration_tva, name="declaration_tva"), path("saisie/", views_saisie.saisie_paiement, name="saisie_paiement"), + path("import/", views_import.import_csv, name="import_csv"), path("export/ventes/", views.export_ventes, name="export_ventes"), path("export/tva/", views.export_tva, name="export_tva"), path("export/urssaf/", views.export_urssaf, name="export_urssaf"), diff --git a/compta/views_import.py b/compta/views_import.py new file mode 100644 index 0000000..ec83fe7 --- /dev/null +++ b/compta/views_import.py @@ -0,0 +1,168 @@ +""" +Import d'un relevé d'opérations au format CSV. +Colonnes : date ; libelle ; categorie ; activite ; taux_tva ; regime ; montant +- montant négatif = dépense, positif = vente (ou neutre si catégorie neutre). +Aperçu avant enregistrement. +""" +import csv +import io +from decimal import Decimal, InvalidOperation +from datetime import datetime + +from django.contrib.auth.decorators import login_required +from django.shortcuts import render + +from .models import (Entreprise, Client, Facture, LigneFacture, Paiement, + Depense, Categorie, Activite, RegimeTva) + +COLONNES = ["date", "libelle", "categorie", "activite", "taux_tva", "regime", "montant"] +CENT = Decimal("0.01") + + +def _dec(v, d="0"): + try: + return Decimal(str(v or d).replace(",", ".").replace(" ", "").strip() or d) + except (InvalidOperation, AttributeError): + return Decimal(d) + + +def _date(s): + s = (s or "").strip() + for fmt in ("%Y-%m-%d", "%d/%m/%Y", "%d-%m-%Y"): + try: + return datetime.strptime(s, fmt).date() + except ValueError: + continue + return None + + +def _activite(s): + s = (s or "").strip().lower() + table = {"vente": "vente", "service_bic": "service_bic", "service_bnc": "service_bnc", + "bic": "service_bic", "bnc": "service_bnc"} + return table.get(s, "") + + +def _regime(s): + s = (s or "").strip().lower() + if s in ("intracom", "intracommunautaire", "ue"): + return RegimeTva.INTRACOM + if s in ("import", "hors ue"): + return RegimeTva.IMPORT + return RegimeTva.NATIONALE + + +def _parse(text, cats): + """Retourne (lignes, erreurs). Chaque ligne = dict prêt à créer + aperçu.""" + text = text.lstrip("") + delim = ";" if text.count(";") >= text.count(",") else "," + reader = csv.reader(io.StringIO(text), delimiter=delim) + rows = list(reader) + if not rows: + return [], ["Fichier vide."] + # En-tête optionnel + start = 1 if rows and rows[0] and rows[0][0].strip().lower() in ("date", "date ") else 0 + lignes, erreurs = [], [] + for n, raw in enumerate(rows[start:], start=start + 1): + if not any(c.strip() for c in raw): + continue + c = (raw + [""] * 7)[:7] + dt = _date(c[0]) + montant = _dec(c[6]) + if dt is None: + erreurs.append(f"Ligne {n} : date invalide « {c[0]} ».") + continue + if montant == 0: + erreurs.append(f"Ligne {n} : montant nul.") + continue + cat = cats.get((c[2] or "").strip().lower()) + taux = _dec(c[4]) + regime = _regime(c[5]) + neutre = bool(cat and cat.exclure_calculs) + if montant > 0: + typ = "Mouvement neutre" if neutre else "Vente" + else: + typ = "Dépense neutre" if neutre else "Dépense" + lignes.append({ + "n": n, "date": dt, "libelle": (c[1] or "").strip() or "(sans libellé)", + "categorie": cat, "categorie_nom": (c[2] or "").strip(), + "activite": _activite(c[3]), "taux": taux, "regime": regime, + "montant": montant, "neutre": neutre, "type": typ, + }) + return lignes, erreurs + + +def _creer(entreprise, l): + montant = l["montant"] + if montant > 0: + if l["neutre"]: + Paiement.objects.create(entreprise=entreprise, date=l["date"], + libelle=l["libelle"], montant_total=montant, + sens="credit", categorie=l["categorie"]) + return "vente_neutre" + client, _ = Client.objects.get_or_create(entreprise=entreprise, nom=l["libelle"]) + ttc = montant + ht = (ttc / (Decimal("1") + l["taux"] / Decimal("100"))).quantize(CENT) + annee = l["date"].year + base = Facture.objects.filter(entreprise=entreprise, + numero__startswith=f"{annee}-").count() + 1 + numero = f"{annee}-{base:03d}" + while Facture.objects.filter(entreprise=entreprise, numero=numero).exists(): + base += 1 + numero = f"{annee}-{base:03d}" + f = Facture.objects.create( + entreprise=entreprise, client=client, numero=numero, + activite=l["activite"] or entreprise.activite, date_emission=l["date"], + date_encaissement=l["date"], statut="encaissee") + LigneFacture.objects.create(facture=f, designation=l["libelle"], + quantite=Decimal("1"), prix_unitaire_ht=ht, + taux_tva=l["taux"]) + f.recompute_totaux() + return "vente" + # Dépense + abs_m = -montant + if l["regime"] in (RegimeTva.INTRACOM, RegimeTva.IMPORT): + ttc, tva = abs_m, Decimal("0") + elif l["neutre"]: + ttc, tva = abs_m, Decimal("0") + else: + ttc = abs_m + tva = (abs_m * l["taux"] / (Decimal("100") + l["taux"])).quantize(CENT) if l["taux"] else Decimal("0") + Depense.objects.create( + entreprise=entreprise, date=l["date"], libelle=l["libelle"], + categorie=l["categorie"], activite=l["activite"], regime_tva=l["regime"], + taux_tva=l["taux"] if l["regime"] != RegimeTva.NATIONALE else Decimal("0"), + montant_ttc=ttc, montant_tva=tva) + return "depense" + + +@login_required +def import_csv(request): + entreprises = Entreprise.accessibles(request.user) + cats = {c.nom.strip().lower(): c for c in Categorie.objects.all()} + ctx = {"entreprises": entreprises, "colonnes": COLONNES} + + if request.method == "POST": + entreprise = entreprises.filter(pk=request.POST.get("entreprise")).first() + texte = request.POST.get("texte", "") + fichier = request.FILES.get("fichier") + if fichier: + texte = fichier.read().decode("utf-8-sig", "ignore") + lignes, erreurs = _parse(texte, cats) + ctx.update({"lignes": lignes, "erreurs": erreurs, "texte": texte, + "entreprise_id": int(request.POST.get("entreprise") or 0)}) + + if request.POST.get("action") == "import": + if not entreprise: + erreurs.append("Sélectionne une entreprise.") + if not lignes: + erreurs.append("Aucune ligne valide à importer.") + if not erreurs: + compteur = {} + for l in lignes: + k = _creer(entreprise, l) + compteur[k] = compteur.get(k, 0) + 1 + ctx["resultat"] = compteur + ctx["lignes"] = None # import fait + ctx["erreurs"] = erreurs + return render(request, "compta/import_csv.html", ctx) diff --git a/config/__pycache__/__init__.cpython-310.pyc b/config/__pycache__/__init__.cpython-310.pyc deleted file mode 100644 index a49be8e..0000000 Binary files a/config/__pycache__/__init__.cpython-310.pyc and /dev/null differ diff --git a/config/__pycache__/asgi.cpython-310.pyc b/config/__pycache__/asgi.cpython-310.pyc deleted file mode 100644 index 3dd4768..0000000 Binary files a/config/__pycache__/asgi.cpython-310.pyc and /dev/null differ diff --git a/config/__pycache__/settings.cpython-310.pyc b/config/__pycache__/settings.cpython-310.pyc deleted file mode 100644 index 918c024..0000000 Binary files a/config/__pycache__/settings.cpython-310.pyc and /dev/null differ diff --git a/config/__pycache__/urls.cpython-310.pyc b/config/__pycache__/urls.cpython-310.pyc deleted file mode 100644 index df50348..0000000 Binary files a/config/__pycache__/urls.cpython-310.pyc and /dev/null differ diff --git a/config/__pycache__/wsgi.cpython-310.pyc b/config/__pycache__/wsgi.cpython-310.pyc deleted file mode 100644 index 79d3c97..0000000 Binary files a/config/__pycache__/wsgi.cpython-310.pyc and /dev/null differ