From 3cc6ede5721baf035710d3805dbf98b02485da6d Mon Sep 17 00:00:00 2001 From: admin Date: Tue, 30 Jun 2026 09:09:47 +0200 Subject: [PATCH] Taux TVA auto + seed enrichi + import photo collee --- compta/admin.py | 11 +-- compta/management/commands/seed_demo.py | 78 +++++++++++-------- ...ant_tva_alter_depense_taux_tva_and_more.py | 29 +++++++ compta/models.py | 30 +++++-- compta/templates/compta/import_csv.html | 26 ++++++- compta/templates/compta/saisie_paiement.html | 29 ++++--- compta/views_import.py | 14 +--- compta/views_saisie.py | 5 +- 8 files changed, 145 insertions(+), 77 deletions(-) create mode 100644 compta/migrations/0008_alter_depense_montant_tva_alter_depense_taux_tva_and_more.py diff --git a/compta/admin.py b/compta/admin.py index 5beb84a..d62025c 100644 --- a/compta/admin.py +++ b/compta/admin.py @@ -28,8 +28,6 @@ class ScopedAdmin(admin.ModelAdmin): return super().formfield_for_foreignkey(db_field, request, **kwargs) -# ----- Référentiels mutualisés (visibles par tous) ----- - @admin.register(Categorie) class CategorieAdmin(admin.ModelAdmin): list_display = ("nom", "usage", "exclure_calculs", "actif", "ordre") @@ -45,8 +43,6 @@ class BaremeAdmin(admin.ModelAdmin): list_filter = ("annee", "activite") -# ----- Entreprise (accès) ----- - @admin.register(Entreprise) class EntrepriseAdmin(admin.ModelAdmin): list_display = ("nom", "activite", "franchise_tva", "versement_liberatoire", @@ -62,14 +58,11 @@ class EntrepriseAdmin(admin.ModelAdmin): return Entreprise.accessibles(request.user) def get_readonly_fields(self, request, obj=None): - # Seul un superadmin gère les droits d'accès. if not request.user.is_superuser: return ("utilisateurs", "groupes") return () -# ----- Données cloisonnées par entreprise ----- - @admin.register(Client) class ClientAdmin(ScopedAdmin): list_display = ("nom", "entreprise", "email", "siret") @@ -108,6 +101,7 @@ class DepenseInline(admin.TabularInline): extra = 0 fields = ("date", "libelle", "categorie", "activite", "regime_tva", "taux_tva", "montant_ttc", "montant_tva") + readonly_fields = ("montant_tva",) @admin.register(Paiement) @@ -135,10 +129,11 @@ class PaiementAdmin(ScopedAdmin): @admin.register(Depense) class DepenseAdmin(ScopedAdmin): list_display = ("date", "entreprise", "libelle", "categorie", "activite", - "regime_tva", "montant_ttc", "montant_tva", "tva_autoliq", "paiement") + "regime_tva", "taux_tva", "montant_ttc", "montant_tva", "tva_autoliq", "paiement") list_filter = ("entreprise", "categorie", "regime_tva", "activite", "date") search_fields = ("libelle",) date_hierarchy = "date" + readonly_fields = ("montant_tva",) @admin.display(description="TVA auto-liquidée") def tva_autoliq(self, obj): diff --git a/compta/management/commands/seed_demo.py b/compta/management/commands/seed_demo.py index 8e4fa5c..4a475cc 100644 --- a/compta/management/commands/seed_demo.py +++ b/compta/management/commands/seed_demo.py @@ -1,7 +1,7 @@ """ -Charge les données de RÉFÉRENCE (barèmes + catégories) puis un jeu de -données d'EXEMPLE (entreprise, clients, factures, dépenses). -Pour la prod, préférez `seed_reference` (référence seule, sans démo). +Référence (barèmes + catégories + groupe) PUIS un jeu de données d'exemple +couvrant tous les modèles : entreprise, clients, factures + lignes, dépenses, +et opérations bancaires (débit éclaté, crédit neutre). Usage : python manage.py seed_demo """ @@ -11,14 +11,14 @@ from datetime import date from django.core.management.base import BaseCommand from compta.models import ( - Entreprise, Client, Facture, LigneFacture, Depense, Activite, + Entreprise, Client, Facture, LigneFacture, Depense, Paiement, Activite, StatutFacture, RegimeTva, ) from compta.management.commands.seed_reference import charger_reference class Command(BaseCommand): - help = "Charge la référence + des données d'exemple." + help = "Charge la référence + des données d'exemple (tous les modèles)." def handle(self, *args, **options): cat = charger_reference(self.stdout, self.style) @@ -26,16 +26,14 @@ class Command(BaseCommand): ent, _ = Entreprise.objects.get_or_create( nom="JB Conseil", defaults=dict(activite=Activite.SERVICE_BIC, franchise_tva=False, - taux_tva_defaut=Decimal("20.00"), - versement_liberatoire=True, email="jb@example.fr", - num_tva_intracom="FR00123456789")) + taux_tva_defaut=Decimal("20.00"), versement_liberatoire=True, + email="jb@example.fr", num_tva_intracom="FR00123456789")) clients = {} for nom, mail in [("Acme SARL", "contact@acme.fr"), ("Studio Lumiere", "hello@studiolumiere.fr"), ("Mairie de Ville", "compta@ville.fr")]: - c, _ = Client.objects.get_or_create(entreprise=ent, nom=nom, - defaults=dict(email=mail)) + c, _ = Client.objects.get_or_create(entreprise=ent, nom=nom, defaults=dict(email=mail)) clients[nom] = c V, S = Activite.VENTE, Activite.SERVICE_BIC @@ -55,32 +53,44 @@ class Command(BaseCommand): f, created = Facture.objects.get_or_create( entreprise=ent, numero=numero, defaults=dict(client=clients[client_nom], activite=activite, - date_emission=emis, date_encaissement=enc, - statut=statut)) + date_emission=emis, date_encaissement=enc, statut=statut)) if created: for des, qte, pu in lignes: - LigneFacture.objects.create( - facture=f, designation=des, quantite=Decimal(qte), - prix_unitaire_ht=Decimal(pu), taux_tva=Decimal("20")) + LigneFacture.objects.create(facture=f, designation=des, quantite=Decimal(qte), + prix_unitaire_ht=Decimal(pu), taux_tva=Decimal("20")) f.recompute_totaux() - Depense.objects.get_or_create( - entreprise=ent, libelle="Bobines PLA/PETG", date=date(2026, 1, 15), - defaults=dict(categorie=cat["Matière première"], activite=Activite.VENTE, - montant_ttc=Decimal("96.00"), montant_tva=Decimal("16.00"))) - Depense.objects.get_or_create( - entreprise=ent, libelle="Abonnement outils", date=date(2026, 2, 10), - defaults=dict(categorie=cat["Abonnement logiciel"], - activite=Activite.SERVICE_BIC, - montant_ttc=Decimal("34.80"), montant_tva=Decimal("5.80"))) - Depense.objects.get_or_create( - entreprise=ent, libelle="Imprimante 3D (DE)", date=date(2026, 2, 20), - defaults=dict(categorie=cat["Matériel et outillage"], activite=Activite.VENTE, - regime_tva=RegimeTva.INTRACOM, taux_tva=Decimal("20"), - montant_ttc=Decimal("500.00"), montant_tva=Decimal("0"))) - Depense.objects.get_or_create( - entreprise=ent, libelle="URSSAF DE LORRAINE", date=date(2026, 3, 3), - defaults=dict(categorie=cat["Cotisation sociale Urssaf"], - montant_ttc=Decimal("73.00"), montant_tva=Decimal("0"))) + def depense(libelle, jour, montant_ttc, cat_nom, taux="20", + regime=RegimeTva.NATIONALE, activite="", paiement=None): + # montant_tva est calculé automatiquement dans Depense.save(). + Depense.objects.get_or_create( + entreprise=ent, libelle=libelle, date=date(2026, *jour), + defaults=dict(categorie=cat[cat_nom], activite=activite, paiement=paiement, + regime_tva=regime, taux_tva=Decimal(taux), + montant_ttc=Decimal(montant_ttc))) - self.stdout.write(self.style.SUCCESS("Données d'exemple chargées.")) + # Opération bancaire DÉBIT éclatée : un paiement Amazon = 2 dépenses + amazon, _ = Paiement.objects.get_or_create( + entreprise=ent, libelle="AMAZON PAYMENTS", date=date(2026, 6, 22), + defaults=dict(montant_total=Decimal("24.52"), sens="debit")) + depense("Amazon - visserie", (6, 22), "15.82", "Matériel et outillage", + activite=Activite.VENTE, paiement=amazon) + depense("Amazon - filament", (6, 18), "8.70", "Matériel et outillage", + activite=Activite.VENTE, paiement=amazon) + + # Dépenses variées (taux 20 / 10 / intracom / neutre) + depense("Bobines PLA/PETG", (1, 15), "96.00", "Matière première", activite=Activite.VENTE) + depense("Burger King", (6, 11), "16.15", "Frais de repas hors domicile", taux="10") + depense("Abonnement outils", (2, 10), "34.80", "Abonnement logiciel", + activite=Activite.SERVICE_BIC) + depense("Imprimante 3D (DE)", (2, 20), "500.00", "Matériel et outillage", + regime=RegimeTva.INTRACOM, activite=Activite.VENTE) + depense("URSSAF DE LORRAINE", (6, 3), "73.00", "Cotisation sociale Urssaf", taux="0") + + # Opération bancaire CRÉDIT neutre : remboursement de TVA + Paiement.objects.get_or_create( + entreprise=ent, libelle="SIE EPINAL", date=date(2026, 6, 15), + defaults=dict(montant_total=Decimal("103.00"), sens="credit", + categorie=cat["Remboursement de TVA"])) + + self.stdout.write(self.style.SUCCESS("Données d'exemple chargées (tous les modèles).")) diff --git a/compta/migrations/0008_alter_depense_montant_tva_alter_depense_taux_tva_and_more.py b/compta/migrations/0008_alter_depense_montant_tva_alter_depense_taux_tva_and_more.py new file mode 100644 index 0000000..68e9fee --- /dev/null +++ b/compta/migrations/0008_alter_depense_montant_tva_alter_depense_taux_tva_and_more.py @@ -0,0 +1,29 @@ +# Generated by Django 5.2.15 on 2026-06-30 07:05 + +from decimal import Decimal +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('compta', '0007_alter_paiement_options_entreprise_groupes_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='depense', + name='montant_tva', + field=models.DecimalField(decimal_places=2, default=Decimal('0'), help_text='Calculée automatiquement depuis le montant payé et le taux.', max_digits=12, verbose_name='TVA déductible (€)'), + ), + migrations.AlterField( + model_name='depense', + name='taux_tva', + field=models.DecimalField(choices=[(Decimal('0'), '0 %'), (Decimal('5.5'), '5,5 %'), (Decimal('10'), '10 %'), (Decimal('20'), '20 %')], decimal_places=2, default=Decimal('20'), help_text='Achat national : la TVA déductible est calculée. UE/import : taux auto-liquidé.', max_digits=4, verbose_name='Taux de TVA (%)'), + ), + migrations.AlterField( + model_name='lignefacture', + name='taux_tva', + field=models.DecimalField(choices=[(Decimal('0'), '0 %'), (Decimal('5.5'), '5,5 %'), (Decimal('10'), '10 %'), (Decimal('20'), '20 %')], decimal_places=2, default=Decimal('0'), max_digits=4), + ), + ] diff --git a/compta/models.py b/compta/models.py index 93ec728..4d3bbe6 100644 --- a/compta/models.py +++ b/compta/models.py @@ -27,6 +27,10 @@ class RegimeTva(models.TextChoices): IMPORT = "import", "Import hors UE (autoliquidation)" +TAUX_TVA_CHOICES = [(Decimal("0"), "0 %"), (Decimal("5.5"), "5,5 %"), + (Decimal("10"), "10 %"), (Decimal("20"), "20 %")] + + class Categorie(models.Model): """Catégorie globale (mutualisée). exclure_calculs=True => mouvement neutre.""" USAGE = [("depense", "Dépense"), ("vente", "Vente"), ("neutre", "Mouvement neutre")] @@ -84,7 +88,6 @@ class Entreprise(models.Model): num_tva_intracom = models.CharField("N° TVA intracommunautaire", max_length=20, blank=True) vue_tva = models.CharField( "Mode d'affichage de la déclaration TVA", max_length=10, choices=VUE_TVA, default="simplifie") - # --- Droits d'accès (multi-utilisateur) --- utilisateurs = models.ManyToManyField( settings.AUTH_USER_MODEL, blank=True, related_name="entreprises", verbose_name="Utilisateurs autorisés", @@ -110,7 +113,6 @@ class Entreprise(models.Model): @classmethod def accessibles(cls, user): - """Entreprises visibles par un utilisateur (toutes si superadmin).""" if not user or not user.is_authenticated: return cls.objects.none() if user.is_superuser: @@ -179,7 +181,8 @@ class LigneFacture(models.Model): designation = models.CharField(max_length=255) quantite = models.DecimalField(max_digits=12, decimal_places=3, default=Decimal("1")) prix_unitaire_ht = models.DecimalField(max_digits=12, decimal_places=2) - taux_tva = models.DecimalField(max_digits=4, decimal_places=2, default=Decimal("0")) + taux_tva = models.DecimalField(max_digits=4, decimal_places=2, default=Decimal("0"), + choices=TAUX_TVA_CHOICES) class Meta: verbose_name = "Ligne de facture" @@ -244,13 +247,14 @@ class Depense(models.Model): "Montant payé (€)", max_digits=12, decimal_places=2, help_text="Pour un achat UE/import, c'est le montant HT (sans TVA facturée).") montant_tva = models.DecimalField( - "TVA déductible facturée (€)", max_digits=12, decimal_places=2, default=Decimal("0"), - help_text="TVA payée au fournisseur (achats nationaux uniquement).") + "TVA déductible (€)", max_digits=12, decimal_places=2, default=Decimal("0"), + help_text="Calculée automatiquement depuis le montant payé et le taux.") regime_tva = models.CharField( "Régime de TVA", max_length=12, choices=RegimeTva.choices, default=RegimeTva.NATIONALE) taux_tva = models.DecimalField( - "Taux TVA autoliquidation (%)", max_digits=4, decimal_places=2, default=Decimal("0"), - help_text="Taux français à auto-liquider pour les achats UE/import (ex. 20).") + "Taux de TVA (%)", max_digits=4, decimal_places=2, default=Decimal("20"), + choices=TAUX_TVA_CHOICES, + help_text="Achat national : la TVA déductible est calculée. UE/import : taux auto-liquidé.") cree_le = models.DateTimeField(auto_now_add=True) class Meta: @@ -261,6 +265,18 @@ class Depense(models.Model): def __str__(self): return f"{self.date} — {self.libelle}" + def save(self, *args, **kwargs): + # TVA déductible calculée : incluse dans le TTC (national), nulle en autoliq. + t = self.taux_tva or Decimal("0") + if self.autoliquidee: + self.montant_tva = Decimal("0.00") + elif t: + self.montant_tva = ((self.montant_ttc or Decimal("0")) * t + / (Decimal("100") + t)).quantize(Decimal("0.01")) + else: + self.montant_tva = Decimal("0.00") + super().save(*args, **kwargs) + @property def neutre(self): return bool(self.categorie and self.categorie.exclure_calculs) diff --git a/compta/templates/compta/import_csv.html b/compta/templates/compta/import_csv.html index b4bc22c..276f4ab 100644 --- a/compta/templates/compta/import_csv.html +++ b/compta/templates/compta/import_csv.html @@ -45,7 +45,7 @@ {% endif %} {% if not lignes %} -
+ {% csrf_token %}
@@ -59,8 +59,9 @@
- - + + +
@@ -110,5 +111,24 @@
{% endif %} + + diff --git a/compta/templates/compta/saisie_paiement.html b/compta/templates/compta/saisie_paiement.html index 47659db..cd6526f 100644 --- a/compta/templates/compta/saisie_paiement.html +++ b/compta/templates/compta/saisie_paiement.html @@ -47,7 +47,7 @@

Saisie d'une opération

Montant négatif = dépense · positif = vente/encaissement. - · ← Tableau de bord
+ · ← Tableau de bord · Importer un relevé {% if messages %}{% for m in messages %}
{{ m }}
{% endfor %}{% endif %} @@ -83,13 +83,12 @@ - + - + - - - + + @@ -125,7 +124,10 @@
- +
@@ -139,9 +141,8 @@
-
Le montant TTC encaissé = le montant signé saisi plus haut. HT et TVA - sont calculés depuis le taux. Si tu choisis une catégorie neutre, aucune facture - n'est créée (simple mouvement de trésorerie).
+
Le montant TTC encaissé = le montant signé saisi plus haut. + Une catégorie neutre n'enregistre pas de vente (mouvement de trésorerie).
@@ -169,9 +170,13 @@ {% for val,lab in regimes %}{% endfor %} -
+ - diff --git a/compta/views_import.py b/compta/views_import.py index 1f638d3..ac8a991 100644 --- a/compta/views_import.py +++ b/compta/views_import.py @@ -19,13 +19,13 @@ CENT = Decimal("0.01") MOIS = {"janvier": 1, "février": 2, "fevrier": 2, "mars": 3, "avril": 4, "mai": 5, "juin": 6, "juillet": 7, "août": 8, "aout": 8, "septembre": 9, "octobre": 10, "novembre": 11, "décembre": 12, "decembre": 12} -AMOUNT_RE = re.compile(r"^-?\d[\d   .]*[.,]\d{2}$") +AMOUNT_RE = re.compile(r"^-?\d[\d .]*[.,]\d{2}$") def _dec(v, d="0"): try: s = str(v or d) - for ch in (" ", " ", " "): + for ch in (" ", " ", " "): s = s.replace(ch, "") s = s.replace(",", ".").strip() return Decimal(s or d) @@ -224,16 +224,10 @@ def _creer(entreprise, dt, libelle, montant, taux, regime, categorie): prix_unitaire_ht=ht, taux_tva=taux) f.recompute_totaux() return "vente" - abs_m = -montant - if regime in (RegimeTva.INTRACOM, RegimeTva.IMPORT) or neutre: - ttc, tva = abs_m, Decimal("0") - else: - ttc = abs_m - tva = (abs_m * taux / (Decimal("100") + taux)).quantize(CENT) if taux else Decimal("0") + # montant_tva est calculé automatiquement dans Depense.save() depuis le taux. Depense.objects.create(entreprise=entreprise, date=dt, libelle=libelle, categorie=categorie, regime_tva=regime, - taux_tva=taux if regime != RegimeTva.NATIONALE else Decimal("0"), - montant_ttc=ttc, montant_tva=tva) + taux_tva=taux, montant_ttc=-montant) return "depense" diff --git a/compta/views_saisie.py b/compta/views_saisie.py index 9756d14..d4d31a5 100644 --- a/compta/views_saisie.py +++ b/compta/views_saisie.py @@ -117,7 +117,6 @@ def saisie_paiement(request): regimes = request.POST.getlist("l_regime") taux = request.POST.getlist("l_taux") montants = request.POST.getlist("l_montant") - tvas = request.POST.getlist("l_tva") lignes = [] for i in range(len(libelles)): @@ -132,7 +131,6 @@ def saisie_paiement(request): "regime": regimes[i] if i < len(regimes) else RegimeTva.NATIONALE, "taux": _dec(taux[i] if i < len(taux) else "0"), "montant": m, - "tva": _dec(tvas[i] if i < len(tvas) else "0"), }) somme = sum((l["montant"] for l in lignes), Decimal("0")) @@ -148,9 +146,10 @@ def saisie_paiement(request): paiement = Paiement.objects.create( entreprise=entreprise, date=d, libelle=libelle, montant_total=total_abs, sens="debit") for l in lignes: + # montant_tva est calculé automatiquement dans Depense.save() depuis le taux. Depense.objects.create( entreprise=entreprise, paiement=paiement, date=d, libelle=l["libelle"], categorie=l["categorie"], activite=l["activite"] or "", regime_tva=l["regime"], - taux_tva=l["taux"], montant_ttc=l["montant"], montant_tva=l["tva"]) + taux_tva=l["taux"], montant_ttc=l["montant"]) messages.success(request, f"Opération « {libelle} » enregistrée ({len(lignes)} dépense(s)).") return redirect("saisie_paiement")
Libellé / vendeurLibellé / vendeur CatégorieActivitéActivité Régime TVATaux %Montant €TVA déduct.Taux TVAMontant €
+ +