From 6856734de7ee26e35fb52374f804c624dbdb528e Mon Sep 17 00:00:00 2001 From: admin Date: Tue, 30 Jun 2026 00:10:47 +0200 Subject: [PATCH] Autoliquidation TVA (achats UE/import) --- compta/admin.py | 15 +++--- compta/management/commands/seed_demo.py | 14 +++-- ...se_regime_tva_depense_taux_tva_and_more.py | 39 ++++++++++++++ compta/models.py | 48 +++++++++++++++-- compta/services.py | 52 ++++++++++++++----- compta/templates/compta/dashboard.html | 13 +++++ compta/views.py | 7 +-- 7 files changed, 161 insertions(+), 27 deletions(-) create mode 100644 compta/migrations/0003_depense_regime_tva_depense_taux_tva_and_more.py diff --git a/compta/admin.py b/compta/admin.py index a91e638..b0d724a 100644 --- a/compta/admin.py +++ b/compta/admin.py @@ -16,9 +16,9 @@ class BaremeAdmin(admin.ModelAdmin): @admin.register(Entreprise) class EntrepriseAdmin(admin.ModelAdmin): list_display = ("nom", "activite", "franchise_tva", - "versement_liberatoire", "email") + "versement_liberatoire", "num_tva_intracom", "email") list_filter = ("activite", "franchise_tva", "versement_liberatoire") - search_fields = ("nom", "siret") + search_fields = ("nom", "siret", "num_tva_intracom") @admin.register(Client) @@ -52,14 +52,17 @@ class FactureAdmin(admin.ModelAdmin): def save_related(self, request, form, formsets, change): super().save_related(request, form, formsets, change) - # Recalcule les totaux à partir des lignes après sauvegarde form.instance.recompute_totaux() @admin.register(Depense) class DepenseAdmin(admin.ModelAdmin): - list_display = ("date", "entreprise", "libelle", "activite", "categorie", - "montant_ttc", "montant_tva") - list_filter = ("entreprise", "activite", "categorie", "date") + list_display = ("date", "entreprise", "libelle", "activite", "regime_tva", + "montant_ttc", "montant_tva", "tva_autoliq") + list_filter = ("entreprise", "regime_tva", "activite", "categorie", "date") search_fields = ("libelle", "categorie") date_hierarchy = "date" + + @admin.display(description="TVA auto-liquidée") + def tva_autoliq(self, obj): + return obj.tva_autoliquidee diff --git a/compta/management/commands/seed_demo.py b/compta/management/commands/seed_demo.py index 85ae3ca..db22e50 100644 --- a/compta/management/commands/seed_demo.py +++ b/compta/management/commands/seed_demo.py @@ -1,5 +1,5 @@ """ -Charge les barèmes 2026 et un jeu de données d'exemple (multi-activité). +Charge les barèmes 2026 et un jeu de données d'exemple (multi-activité + autoliq). Usage : python manage.py seed_demo """ from decimal import Decimal @@ -9,7 +9,7 @@ from django.core.management.base import BaseCommand from compta.models import ( Bareme, Entreprise, Client, Facture, LigneFacture, Depense, Activite, - StatutFacture, + StatutFacture, RegimeTva, ) # Barèmes officiels 2026 (voir docs/regles-fiscales-2026.md) @@ -42,7 +42,8 @@ class Command(BaseCommand): 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"), + versement_liberatoire=True, email="jb@example.fr", + num_tva_intracom="FR00123456789"), ) clients = {} @@ -89,5 +90,12 @@ class Command(BaseCommand): entreprise=ent, libelle="Abonnement outils", date=date(2026, 2, 10), defaults=dict(categorie="Logiciel", activite=Activite.SERVICE_BIC, montant_ttc=Decimal("34.80"), montant_tva=Decimal("5.80"))) + # Achat intracommunautaire (ex. imprimante depuis l'Allemagne) : + # le fournisseur facture HT, on auto-liquide la TVA à 20 %. + Depense.objects.get_or_create( + entreprise=ent, libelle="Imprimante 3D (DE)", date=date(2026, 2, 20), + defaults=dict(categorie="Materiel", activite=Activite.VENTE, + regime_tva=RegimeTva.INTRACOM, taux_tva=Decimal("20"), + montant_ttc=Decimal("500.00"), montant_tva=Decimal("0"))) self.stdout.write(self.style.SUCCESS("Données d'exemple chargées.")) diff --git a/compta/migrations/0003_depense_regime_tva_depense_taux_tva_and_more.py b/compta/migrations/0003_depense_regime_tva_depense_taux_tva_and_more.py new file mode 100644 index 0000000..0960090 --- /dev/null +++ b/compta/migrations/0003_depense_regime_tva_depense_taux_tva_and_more.py @@ -0,0 +1,39 @@ +# Generated by Django 5.2.15 on 2026-06-29 22:08 + +from decimal import Decimal +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('compta', '0002_depense_activite_alter_entreprise_activite'), + ] + + operations = [ + migrations.AddField( + model_name='depense', + name='regime_tva', + field=models.CharField(choices=[('nationale', 'Nationale (TVA française facturée)'), ('intracom', 'Intracommunautaire (autoliquidation)'), ('import', 'Import hors UE (autoliquidation)')], default='nationale', max_length=12, verbose_name='Régime de TVA'), + ), + migrations.AddField( + model_name='depense', + name='taux_tva', + field=models.DecimalField(decimal_places=2, default=Decimal('0'), help_text='Taux français à auto-liquider pour les achats UE/import (ex. 20).', max_digits=4, verbose_name='Taux TVA autoliquidation (%)'), + ), + migrations.AddField( + model_name='entreprise', + name='num_tva_intracom', + field=models.CharField(blank=True, max_length=20, verbose_name='N° TVA intracommunautaire'), + ), + migrations.AlterField( + model_name='depense', + name='montant_ttc', + field=models.DecimalField(decimal_places=2, help_text="Pour un achat UE/import, c'est le montant HT (sans TVA facturée).", max_digits=12, verbose_name='Montant payé (€)'), + ), + migrations.AlterField( + model_name='depense', + name='montant_tva', + field=models.DecimalField(decimal_places=2, default=Decimal('0'), help_text='TVA payée au fournisseur (achats nationaux uniquement).', max_digits=12, verbose_name='TVA déductible facturée (€)'), + ), + ] diff --git a/compta/models.py b/compta/models.py index c05e291..109f9c8 100644 --- a/compta/models.py +++ b/compta/models.py @@ -24,6 +24,13 @@ class StatutFacture(models.TextChoices): ANNULEE = "annulee", "Annulée" +class RegimeTva(models.TextChoices): + """Régime de TVA d'une dépense (achat).""" + NATIONALE = "nationale", "Nationale (TVA française facturée)" + INTRACOM = "intracom", "Intracommunautaire (autoliquidation)" + IMPORT = "import", "Import hors UE (autoliquidation)" + + class Bareme(models.Model): """Taux et seuils officiels, par activité et par année (paramétrable).""" activite = models.CharField(max_length=20, choices=Activite.choices) @@ -68,6 +75,8 @@ class Entreprise(models.Model): default=Decimal("20.00")) versement_liberatoire = models.BooleanField( "Option versement libératoire", default=False) + num_tva_intracom = models.CharField( + "N° TVA intracommunautaire", max_length=20, blank=True) email = models.EmailField(blank=True) adresse = models.TextField(blank=True) cree_le = models.DateTimeField(auto_now_add=True) @@ -180,10 +189,20 @@ class Depense(models.Model): help_text="Activité concernée (facultatif, pour la ventilation).") categorie = models.CharField(max_length=100, blank=True) libelle = models.CharField(max_length=255) - montant_ttc = models.DecimalField(max_digits=12, decimal_places=2) + montant_ttc = models.DecimalField( + "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 (€)", max_digits=12, decimal_places=2, - default=Decimal("0")) + "TVA déductible facturée (€)", max_digits=12, decimal_places=2, + default=Decimal("0"), + help_text="TVA payée au fournisseur (achats nationaux uniquement).") + 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).") cree_le = models.DateTimeField(auto_now_add=True) class Meta: @@ -194,6 +213,29 @@ class Depense(models.Model): def __str__(self): return f"{self.date} — {self.libelle}" + @property + def autoliquidee(self): + """True si la TVA est auto-liquidée (achat UE ou import).""" + return self.regime_tva in (RegimeTva.INTRACOM, RegimeTva.IMPORT) + @property def montant_ht(self): + # En autoliquidation, le fournisseur ne facture pas de TVA : HT = montant payé. + if self.autoliquidee: + return self.montant_ttc.quantize(Decimal("0.01")) return (self.montant_ttc - self.montant_tva).quantize(Decimal("0.01")) + + @property + def tva_autoliquidee(self): + """TVA française auto-liquidée (comptée en collectée ET en déductible).""" + if self.autoliquidee: + return (self.montant_ttc * self.taux_tva / Decimal("100")).quantize( + Decimal("0.01")) + return Decimal("0.00") + + @property + def tva_deductible_value(self): + """TVA récupérable : facturée (national) ou auto-liquidée (UE/import).""" + if self.autoliquidee: + return self.tva_autoliquidee + return self.montant_tva diff --git a/compta/services.py b/compta/services.py index 4b728e3..d5bfff7 100644 --- a/compta/services.py +++ b/compta/services.py @@ -6,10 +6,14 @@ Bases de calcul : CA HORS TAXES (HT) encaissé. - En franchise de TVA, HT = TTC : le calcul reste correct. Multi-activité : une entreprise peut exercer plusieurs activités (ex. Vente BIC -+ Prestation BIC). Les taux URSSAF, abattements, taux de versement libératoire et -seuils diffèrent par activité, donc tout est ventilé par activité puis totalisé. ++ Prestation BIC). Taux URSSAF, abattements, taux VL et seuils diffèrent par +activité, donc tout est ventilé par activité puis totalisé. -TVA à reverser = TVA collectée (ventes encaissées) − TVA déductible (dépenses). +TVA : +- TVA collectée = TVA sur ventes encaissées + TVA auto-liquidée (achats UE/import). +- TVA déductible = TVA facturée sur achats nationaux + TVA auto-liquidée. +- L'autoliquidation s'ajoute aux deux côtés (effet net souvent nul), mais doit + apparaître sur la CA3 : on l'expose donc séparément. """ from decimal import Decimal from django.db.models import Sum @@ -32,6 +36,10 @@ def factures_encaissees(entreprise, annee): ) +def depenses_annee(entreprise, annee): + return Depense.objects.filter(entreprise=entreprise, date__year=annee) + + def ca_ht_par_activite(entreprise, annee): """Dict {activite: CA HT encaissé}.""" rows = ( @@ -47,18 +55,34 @@ def ca_ht(entreprise, annee): return _q(agg["s"]) -def tva_collectee(entreprise, annee): +# ---- TVA ---------------------------------------------------------------- + +def tva_sur_ventes(entreprise, annee): agg = factures_encaissees(entreprise, annee).aggregate(s=Sum("montant_tva")) return _q(agg["s"]) -def tva_deductible(entreprise, annee): - agg = Depense.objects.filter( - entreprise=entreprise, date__year=annee - ).aggregate(s=Sum("montant_tva")) - return _q(agg["s"]) +def tva_autoliquidee(entreprise, annee): + """TVA auto-liquidée sur les achats UE/import (intracom + import).""" + total = sum((d.tva_autoliquidee for d in depenses_annee(entreprise, annee)), + Decimal("0")) + return _q(total) +def tva_deductible(entreprise, annee): + """TVA récupérable : facturée (national) + auto-liquidée (UE/import).""" + total = sum((d.tva_deductible_value for d in depenses_annee(entreprise, annee)), + Decimal("0")) + return _q(total) + + +def tva_collectee(entreprise, annee): + """TVA due : sur ventes + auto-liquidée sur achats UE/import.""" + return _q(tva_sur_ventes(entreprise, annee) + tva_autoliquidee(entreprise, annee)) + + +# ---- CA mensuel --------------------------------------------------------- + def ca_ht_mensuel(entreprise, annee): """Liste de 12 valeurs (index 0 = janvier) du CA HT encaissé (toutes activités).""" rows = ( @@ -74,6 +98,8 @@ def ca_ht_mensuel(entreprise, annee): return mensuel +# ---- Détail par activité ------------------------------------------------ + def detail_activite(activite, ca, bareme): """Calculs fiscaux pour une activité donnée.""" label = dict(Activite.choices).get(activite, activite) @@ -110,7 +136,6 @@ def synthese(entreprise, annee): """Tableau de bord chiffré, ventilé par activité puis totalisé.""" ca_par_act = ca_ht_par_activite(entreprise, annee) - # On inclut toujours l'activité principale, même sans CA encore. activites = set(ca_par_act) | {entreprise.activite} baremes = {b.activite: b for b in Bareme.objects.filter(annee=annee)} @@ -119,13 +144,14 @@ def synthese(entreprise, annee): ca = ca_par_act.get(act, Decimal("0")) lignes.append(detail_activite(act, ca, baremes.get(act))) - # Totaux (on additionne ce qui est calculable) def somme(cle): vals = [l[cle] for l in lignes if l.get(cle) is not None] return _q(sum(vals, Decimal("0"))) if vals else None - tva_col = tva_collectee(entreprise, annee) + tva_ventes = tva_sur_ventes(entreprise, annee) + tva_autoliq = tva_autoliquidee(entreprise, annee) tva_ded = tva_deductible(entreprise, annee) + tva_col = _q(tva_ventes + tva_autoliq) return { "entreprise": entreprise, @@ -137,6 +163,8 @@ def synthese(entreprise, annee): "versement_liberatoire": ( somme("versement_liberatoire") if entreprise.versement_liberatoire else None), "revenu_imposable": somme("revenu_imposable"), + "tva_sur_ventes": tva_ventes, + "tva_autoliquidee": tva_autoliq, "tva_collectee": tva_col, "tva_deductible": tva_ded, "tva_a_reverser": _q(tva_col - tva_ded), diff --git a/compta/templates/compta/dashboard.html b/compta/templates/compta/dashboard.html index 408f900..fa0ef85 100644 --- a/compta/templates/compta/dashboard.html +++ b/compta/templates/compta/dashboard.html @@ -90,6 +90,7 @@
TVA à reverser
{{ tva_a_reverser }} €
{{ tva_collectee }} collectée − {{ tva_deductible }} déductible
+ {% if tva_autoliquidee %}
dont {{ tva_autoliquidee }} € auto-liquidée (UE/import)
{% endif %} @@ -128,6 +129,18 @@ + +

TVA

+
+ + + + + + +
TVA collectée sur ventes{{ tva_sur_ventes }} €
TVA auto-liquidée (UE / import){{ tva_autoliquidee }} €
TVA collectée totale (due){{ tva_collectee }} €
TVA déductible{{ tva_deductible }} €
TVA à reverser{{ tva_a_reverser }} €
+
+
CA HT mensuel (toutes activités)
diff --git a/compta/views.py b/compta/views.py index 38dfb0a..217d0bc 100644 --- a/compta/views.py +++ b/compta/views.py @@ -64,9 +64,10 @@ def export_tva(request): s = services.synthese(entreprise, annee) resp = _csv_response(f"tva_{annee}.csv") w = csv.writer(resp, delimiter=";") - w.writerow(["Annee", "TVA collectee", "TVA deductible", "TVA a reverser"]) - w.writerow([annee, s["tva_collectee"], s["tva_deductible"], - s["tva_a_reverser"]]) + w.writerow(["Annee", "TVA sur ventes", "TVA autoliquidee (UE/import)", + "TVA collectee totale", "TVA deductible", "TVA a reverser"]) + w.writerow([annee, s["tva_sur_ventes"], s["tva_autoliquidee"], + s["tva_collectee"], s["tva_deductible"], s["tva_a_reverser"]]) return resp