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 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)
+
+
+
+
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 %}
+
+
+
+ {% if lignes %}
+
+
Aperçu — {{ lignes|length }} ligne(s)
+
+ | # | Date | Libellé | Catégorie | Type | Taux | Régime | Montant |
+
+ {% for l in lignes %}
+
+ | {{ 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 }} € |
+
+ {% endfor %}
+
+
+
⚠️ = 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