diff --git a/compta/__pycache__/models.cpython-310.pyc b/compta/__pycache__/models.cpython-310.pyc index 0b5461e..42213d9 100644 Binary files a/compta/__pycache__/models.cpython-310.pyc and b/compta/__pycache__/models.cpython-310.pyc differ diff --git a/compta/__pycache__/services.cpython-310.pyc b/compta/__pycache__/services.cpython-310.pyc index 18f3e3e..cbcd675 100644 Binary files a/compta/__pycache__/services.cpython-310.pyc and b/compta/__pycache__/services.cpython-310.pyc differ diff --git a/compta/__pycache__/urls.cpython-310.pyc b/compta/__pycache__/urls.cpython-310.pyc index accb77d..59866f7 100644 Binary files a/compta/__pycache__/urls.cpython-310.pyc and b/compta/__pycache__/urls.cpython-310.pyc differ diff --git a/compta/__pycache__/views.cpython-310.pyc b/compta/__pycache__/views.cpython-310.pyc index 759d18e..329267b 100644 Binary files a/compta/__pycache__/views.cpython-310.pyc and b/compta/__pycache__/views.cpython-310.pyc differ diff --git a/compta/__pycache__/views_saisie.cpython-310.pyc b/compta/__pycache__/views_saisie.cpython-310.pyc index acb90aa..0c915b5 100644 Binary files a/compta/__pycache__/views_saisie.cpython-310.pyc and b/compta/__pycache__/views_saisie.cpython-310.pyc differ diff --git a/compta/migrations/0006_paiement_categorie_alter_depense_paiement_and_more.py b/compta/migrations/0006_paiement_categorie_alter_depense_paiement_and_more.py new file mode 100644 index 0000000..0813f64 --- /dev/null +++ b/compta/migrations/0006_paiement_categorie_alter_depense_paiement_and_more.py @@ -0,0 +1,34 @@ +# Generated by Django 5.2.15 on 2026-06-29 22:49 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('compta', '0005_categorie_entreprise_vue_tva_alter_depense_categorie'), + ] + + operations = [ + migrations.AddField( + model_name='paiement', + name='categorie', + field=models.ForeignKey(blank=True, help_text='Surtout pour les mouvements neutres (remboursement TVA, virement…).', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='paiements', to='compta.categorie'), + ), + migrations.AlterField( + model_name='depense', + name='paiement', + field=models.ForeignKey(blank=True, help_text='Opération bancaire dont cette dépense fait partie.', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='depenses', to='compta.paiement'), + ), + migrations.AlterField( + model_name='entreprise', + name='activite', + field=models.CharField(choices=[('vente', 'Vente de marchandises (BIC)'), ('service_bic', 'Prestation de services (BIC)'), ('service_bnc', 'Prestation de services / libéral (BNC)')], default='service_bnc', help_text='Activité par défaut des nouvelles factures.', max_length=20, verbose_name='Activité principale'), + ), + migrations.AlterField( + model_name='facture', + name='date_encaissement', + field=models.DateField(blank=True, help_text="Le CA micro est compté à l'encaissement.", null=True), + ), + ] diff --git a/compta/migrations/__pycache__/0001_initial.cpython-310.pyc b/compta/migrations/__pycache__/0001_initial.cpython-310.pyc deleted file mode 100644 index 665cab3..0000000 Binary files a/compta/migrations/__pycache__/0001_initial.cpython-310.pyc and /dev/null differ diff --git a/compta/migrations/__pycache__/0002_depense_activite_alter_entreprise_activite.cpython-310.pyc b/compta/migrations/__pycache__/0002_depense_activite_alter_entreprise_activite.cpython-310.pyc deleted file mode 100644 index 67c4a01..0000000 Binary files a/compta/migrations/__pycache__/0002_depense_activite_alter_entreprise_activite.cpython-310.pyc and /dev/null differ diff --git a/compta/migrations/__pycache__/0003_depense_regime_tva_depense_taux_tva_and_more.cpython-310.pyc b/compta/migrations/__pycache__/0003_depense_regime_tva_depense_taux_tva_and_more.cpython-310.pyc deleted file mode 100644 index afc3840..0000000 Binary files a/compta/migrations/__pycache__/0003_depense_regime_tva_depense_taux_tva_and_more.cpython-310.pyc and /dev/null differ diff --git a/compta/migrations/__pycache__/0004_paiement_depense_paiement.cpython-310.pyc b/compta/migrations/__pycache__/0004_paiement_depense_paiement.cpython-310.pyc deleted file mode 100644 index 3ce7cc6..0000000 Binary files a/compta/migrations/__pycache__/0004_paiement_depense_paiement.cpython-310.pyc and /dev/null differ diff --git a/compta/migrations/__pycache__/0005_categorie_entreprise_vue_tva_alter_depense_categorie.cpython-310.pyc b/compta/migrations/__pycache__/0005_categorie_entreprise_vue_tva_alter_depense_categorie.cpython-310.pyc deleted file mode 100644 index 3973bf4..0000000 Binary files a/compta/migrations/__pycache__/0005_categorie_entreprise_vue_tva_alter_depense_categorie.cpython-310.pyc and /dev/null differ diff --git a/compta/migrations/__pycache__/__init__.cpython-310.pyc b/compta/migrations/__pycache__/__init__.cpython-310.pyc deleted file mode 100644 index a5e4238..0000000 Binary files a/compta/migrations/__pycache__/__init__.cpython-310.pyc and /dev/null differ diff --git a/compta/models.py b/compta/models.py index 4a9e2c6..40535d7 100644 --- a/compta/models.py +++ b/compta/models.py @@ -1,9 +1,5 @@ """ Modèles de données — JB Compta. - -Conçus pour rester valables si le statut évolue : les taux et seuils -ne sont jamais codés en dur, ils vivent dans le modèle `Bareme` -(par activité et par année). """ from decimal import Decimal @@ -25,24 +21,14 @@ class StatutFacture(models.TextChoices): 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 Categorie(models.Model): - """Catégorie globale (partagée entre entreprises). - - Une catégorie « neutre » (exclure_calculs=True) sert au suivi de trésorerie - mais n'entre PAS dans les calculs fiscaux : paiement d'URSSAF, de TVA, - remboursement de TVA, prélèvement personnel, virement interne… - """ - USAGE = [ - ("depense", "Dépense"), - ("vente", "Vente"), - ("neutre", "Mouvement neutre"), - ] + """Catégorie globale. exclure_calculs=True => mouvement neutre (hors fiscal).""" + USAGE = [("depense", "Dépense"), ("vente", "Vente"), ("neutre", "Mouvement neutre")] nom = models.CharField(max_length=100, unique=True) usage = models.CharField(max_length=10, choices=USAGE, default="depense") exclure_calculs = models.BooleanField( @@ -62,21 +48,14 @@ class Categorie(models.Model): 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) annee = models.PositiveIntegerField() - taux_urssaf = models.DecimalField( - "Taux cotisations URSSAF (%)", max_digits=5, decimal_places=2) - abattement_fiscal = models.DecimalField( - "Abattement forfaitaire (%)", max_digits=5, decimal_places=2) - taux_vl = models.DecimalField( - "Taux versement libératoire (%)", max_digits=4, decimal_places=2) - seuil_ca = models.DecimalField( - "Plafond CA micro (€)", max_digits=12, decimal_places=2) - seuil_tva_base = models.DecimalField( - "Seuil franchise TVA (€)", max_digits=12, decimal_places=2) - seuil_tva_majore = models.DecimalField( - "Seuil franchise TVA majoré (€)", max_digits=12, decimal_places=2) + taux_urssaf = models.DecimalField("Taux cotisations URSSAF (%)", max_digits=5, decimal_places=2) + abattement_fiscal = models.DecimalField("Abattement forfaitaire (%)", max_digits=5, decimal_places=2) + taux_vl = models.DecimalField("Taux versement libératoire (%)", max_digits=4, decimal_places=2) + seuil_ca = models.DecimalField("Plafond CA micro (€)", max_digits=12, decimal_places=2) + seuil_tva_base = models.DecimalField("Seuil franchise TVA (€)", max_digits=12, decimal_places=2) + seuil_tva_majore = models.DecimalField("Seuil franchise TVA majoré (€)", max_digits=12, decimal_places=2) class Meta: verbose_name = "Barème" @@ -95,19 +74,14 @@ class Entreprise(models.Model): activite = models.CharField( "Activité principale", max_length=20, choices=Activite.choices, default=Activite.SERVICE_BNC, - help_text="Activité par défaut des nouvelles factures. " - "Une entreprise peut avoir plusieurs activités : " - "chaque facture/dépense porte la sienne.") + help_text="Activité par défaut des nouvelles factures.") franchise_tva = models.BooleanField( "En franchise de TVA", default=False, help_text="Décochez si vous facturez et déclarez la TVA.") taux_tva_defaut = models.DecimalField( - "Taux de TVA par défaut (%)", max_digits=4, decimal_places=2, - 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) + "Taux de TVA par défaut (%)", max_digits=4, decimal_places=2, 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) vue_tva = models.CharField( "Mode d'affichage de la déclaration TVA", max_length=10, choices=VUE_TVA, default="simplifie") @@ -128,8 +102,7 @@ class Entreprise(models.Model): class Client(models.Model): - entreprise = models.ForeignKey(Entreprise, on_delete=models.CASCADE, - related_name="clients") + entreprise = models.ForeignKey(Entreprise, on_delete=models.CASCADE, related_name="clients") nom = models.CharField(max_length=200) email = models.EmailField(blank=True) siret = models.CharField(max_length=20, blank=True) @@ -146,25 +119,18 @@ class Client(models.Model): class Facture(models.Model): - entreprise = models.ForeignKey(Entreprise, on_delete=models.CASCADE, - related_name="factures") - client = models.ForeignKey(Client, on_delete=models.PROTECT, - related_name="factures") + entreprise = models.ForeignKey(Entreprise, on_delete=models.CASCADE, related_name="factures") + client = models.ForeignKey(Client, on_delete=models.PROTECT, related_name="factures") numero = models.CharField(max_length=50) activite = models.CharField(max_length=20, choices=Activite.choices) date_emission = models.DateField() date_encaissement = models.DateField( null=True, blank=True, - help_text="À renseigner une fois la facture payée. " - "Le CA micro est compté à l'encaissement.") - statut = models.CharField(max_length=12, choices=StatutFacture.choices, - default=StatutFacture.BROUILLON) - montant_ht = models.DecimalField(max_digits=12, decimal_places=2, - default=Decimal("0")) - montant_tva = models.DecimalField(max_digits=12, decimal_places=2, - default=Decimal("0")) - montant_ttc = models.DecimalField(max_digits=12, decimal_places=2, - default=Decimal("0")) + help_text="Le CA micro est compté à l'encaissement.") + statut = models.CharField(max_length=12, choices=StatutFacture.choices, default=StatutFacture.BROUILLON) + montant_ht = models.DecimalField(max_digits=12, decimal_places=2, default=Decimal("0")) + montant_tva = models.DecimalField(max_digits=12, decimal_places=2, default=Decimal("0")) + montant_ttc = models.DecimalField(max_digits=12, decimal_places=2, default=Decimal("0")) notes = models.TextField(blank=True) cree_le = models.DateTimeField(auto_now_add=True) @@ -178,7 +144,6 @@ class Facture(models.Model): return f"{self.numero} — {self.client}" def recompute_totaux(self, save=True): - """Recalcule HT/TVA/TTC à partir des lignes.""" ht = Decimal("0") tva = Decimal("0") for ligne in self.lignes.all(): @@ -193,14 +158,11 @@ class Facture(models.Model): class LigneFacture(models.Model): - facture = models.ForeignKey(Facture, on_delete=models.CASCADE, - related_name="lignes") + facture = models.ForeignKey(Facture, on_delete=models.CASCADE, related_name="lignes") designation = models.CharField(max_length=255) - quantite = models.DecimalField(max_digits=12, decimal_places=3, - default=Decimal("1")) + 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")) class Meta: verbose_name = "Ligne de facture" @@ -215,15 +177,17 @@ class LigneFacture(models.Model): class Paiement(models.Model): - """Opération bancaire réelle, à éclater en une ou plusieurs dépenses - (ex. un débit Amazon regroupant plusieurs vendeurs/factures).""" + """Opération bancaire réelle, à éclater en une ou plusieurs dépenses.""" SENS = [("debit", "Débit (achat)"), ("credit", "Crédit (encaissement)")] - entreprise = models.ForeignKey(Entreprise, on_delete=models.CASCADE, - related_name="paiements") + entreprise = models.ForeignKey(Entreprise, on_delete=models.CASCADE, related_name="paiements") date = models.DateField() libelle = models.CharField("Libellé bancaire", max_length=255) montant_total = models.DecimalField(max_digits=12, decimal_places=2) sens = models.CharField(max_length=6, choices=SENS, default="debit") + categorie = models.ForeignKey(Categorie, on_delete=models.SET_NULL, + null=True, blank=True, related_name="paiements", + help_text="Surtout pour les mouvements neutres " + "(remboursement TVA, virement…).") cree_le = models.DateTimeField(auto_now_add=True) class Meta: @@ -249,32 +213,27 @@ class Paiement(models.Model): class Depense(models.Model): - entreprise = models.ForeignKey(Entreprise, on_delete=models.CASCADE, - related_name="depenses") - paiement = models.ForeignKey(Paiement, on_delete=models.SET_NULL, - null=True, blank=True, related_name="depenses", - help_text="Opération bancaire dont cette dépense " - "fait partie (achat groupé éclaté).") + entreprise = models.ForeignKey(Entreprise, on_delete=models.CASCADE, related_name="depenses") + paiement = models.ForeignKey(Paiement, on_delete=models.SET_NULL, null=True, blank=True, + related_name="depenses", + help_text="Opération bancaire dont cette dépense fait partie.") date = models.DateField() activite = models.CharField( max_length=20, choices=Activite.choices, blank=True, help_text="Activité concernée (facultatif, pour la ventilation).") - categorie = models.ForeignKey(Categorie, on_delete=models.SET_NULL, - null=True, blank=True, related_name="depenses") + categorie = models.ForeignKey(Categorie, on_delete=models.SET_NULL, null=True, blank=True, + related_name="depenses") libelle = models.CharField(max_length=255) 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 facturée (€)", 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) + "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"), + "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) @@ -288,33 +247,26 @@ class Depense(models.Model): @property def neutre(self): - """Mouvement neutre : exclu des calculs fiscaux.""" return bool(self.categorie and self.categorie.exclure_calculs) @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 and not self.neutre: - return (self.montant_ttc * self.taux_tva / Decimal("100")).quantize( - Decimal("0.01")) + 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). - Nulle pour un mouvement neutre.""" if self.neutre: return Decimal("0.00") if self.autoliquidee: diff --git a/compta/services.py b/compta/services.py index d5bfff7..ebbb7e1 100644 --- a/compta/services.py +++ b/compta/services.py @@ -1,96 +1,110 @@ """ Logique fiscale — JB Compta. -Bases de calcul : CA HORS TAXES (HT) encaissé. -- Base des cotisations URSSAF et du versement libératoire (assujetti TVA). -- En franchise de TVA, HT = TTC : le calcul reste correct. - -Multi-activité : une entreprise peut exercer plusieurs activités (ex. Vente BIC -+ Prestation BIC). Taux URSSAF, abattements, taux VL et seuils diffèrent par -activité, donc tout est ventilé par activité puis totalisé. - -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. +Bases : CA HORS TAXES (HT) encaissé. +TVA collectée = TVA ventes + TVA auto-liquidée (UE/import). +TVA déductible = TVA achats nationaux + TVA auto-liquidée. Neutres exclus. """ from decimal import Decimal from django.db.models import Sum from django.db.models.functions import TruncMonth -from .models import Facture, Depense, Bareme, StatutFacture, Activite +from .models import (Facture, Depense, LigneFacture, Bareme, StatutFacture, + Activite, RegimeTva) CENT = Decimal("0.01") +MOIS_FR = ["Janvier", "Février", "Mars", "Avril", "Mai", "Juin", "Juillet", + "Août", "Septembre", "Octobre", "Novembre", "Décembre"] -def _q(value): - return (value or Decimal("0")).quantize(CENT) +def _q(v): + return (v or Decimal("0")).quantize(CENT) -def factures_encaissees(entreprise, annee): - return Facture.objects.filter( - entreprise=entreprise, - statut=StatutFacture.ENCAISSEE, - date_encaissement__year=annee, - ) +# ---- Sélections de période --------------------------------------------- + +def factures_encaissees(entreprise, annee, mois=None): + qs = Facture.objects.filter(entreprise=entreprise, statut=StatutFacture.ENCAISSEE, + date_encaissement__year=annee) + if mois: + qs = qs.filter(date_encaissement__month=mois) + return qs -def depenses_annee(entreprise, annee): - return Depense.objects.filter(entreprise=entreprise, date__year=annee) +def depenses_periode(entreprise, annee, mois=None): + qs = Depense.objects.filter(entreprise=entreprise, date__year=annee) + if mois: + qs = qs.filter(date__month=mois) + return qs -def ca_ht_par_activite(entreprise, annee): - """Dict {activite: CA HT encaissé}.""" - rows = ( - factures_encaissees(entreprise, annee) - .values("activite") - .annotate(total=Sum("montant_ht")) - ) +# ---- CA ----------------------------------------------------------------- + +def ca_ht_par_activite(entreprise, annee, mois=None): + rows = factures_encaissees(entreprise, annee, mois).values("activite").annotate( + total=Sum("montant_ht")) return {r["activite"]: _q(r["total"]) for r in rows} -def ca_ht(entreprise, annee): - agg = factures_encaissees(entreprise, annee).aggregate(s=Sum("montant_ht")) - return _q(agg["s"]) +def ca_ht(entreprise, annee, mois=None): + return _q(factures_encaissees(entreprise, annee, mois).aggregate( + s=Sum("montant_ht"))["s"]) # ---- TVA ---------------------------------------------------------------- -def tva_sur_ventes(entreprise, annee): - agg = factures_encaissees(entreprise, annee).aggregate(s=Sum("montant_tva")) - return _q(agg["s"]) +def tva_sur_ventes(entreprise, annee, mois=None): + return _q(factures_encaissees(entreprise, annee, mois).aggregate( + s=Sum("montant_tva"))["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)), +def tva_autoliquidee(entreprise, annee, mois=None): + total = sum((d.tva_autoliquidee for d in depenses_periode(entreprise, annee, mois)), 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)), +def tva_deductible(entreprise, annee, mois=None): + total = sum((d.tva_deductible_value for d in depenses_periode(entreprise, annee, mois)), 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)) +def tva_collectee(entreprise, annee, mois=None): + return _q(tva_sur_ventes(entreprise, annee, mois) + + tva_autoliquidee(entreprise, annee, mois)) -# ---- CA mensuel --------------------------------------------------------- +def tva_par_taux(entreprise, annee, mois=None): + """Ventilation des ventes encaissées par taux de TVA : {taux: (base_ht, tva)}.""" + lignes = LigneFacture.objects.filter( + facture__in=factures_encaissees(entreprise, annee, mois)) + par_taux = {} + for l in lignes.select_related("facture"): + base = l.montant_ht + taux = l.taux_tva + b, t = par_taux.get(taux, (Decimal("0"), Decimal("0"))) + par_taux[taux] = (b + base, t + base * taux / Decimal("100")) + return {taux: (_q(b), _q(t)) for taux, (b, t) in sorted(par_taux.items())} + + +def intracom_periode(entreprise, annee, mois=None): + """Acquisitions UE/import auto-liquidées : (base_ht, tva).""" + base = Decimal("0") + tva = Decimal("0") + for d in depenses_periode(entreprise, annee, mois): + if d.autoliquidee and not d.neutre: + base += d.montant_ht + tva += d.tva_autoliquidee + return _q(base), _q(tva) + + +# ---- CA mensuel (graphe) ------------------------------------------------ def ca_ht_mensuel(entreprise, annee): - """Liste de 12 valeurs (index 0 = janvier) du CA HT encaissé (toutes activités).""" - rows = ( - factures_encaissees(entreprise, annee) - .annotate(mois=TruncMonth("date_encaissement")) - .values("mois") - .annotate(total=Sum("montant_ht")) - ) + rows = (factures_encaissees(entreprise, annee) + .annotate(mois=TruncMonth("date_encaissement")) + .values("mois").annotate(total=Sum("montant_ht"))) mensuel = [Decimal("0")] * 12 for r in rows: if r["mois"]: @@ -101,7 +115,6 @@ def ca_ht_mensuel(entreprise, annee): # ---- Détail par activité ------------------------------------------------ def detail_activite(activite, ca, bareme): - """Calculs fiscaux pour une activité donnée.""" label = dict(Activite.choices).get(activite, activite) d = {"activite": activite, "label": label, "ca_ht": ca, "bareme": bareme} if bareme: @@ -111,72 +124,147 @@ def detail_activite(activite, ca, bareme): "taux_vl": bareme.taux_vl, "versement_liberatoire": _q(ca * bareme.taux_vl / Decimal("100")), "abattement_fiscal": bareme.abattement_fiscal, - "revenu_imposable": _q( - ca * (Decimal("100") - bareme.abattement_fiscal) / Decimal("100")), + "revenu_imposable": _q(ca * (Decimal("100") - bareme.abattement_fiscal) / Decimal("100")), "seuil_ca": bareme.seuil_ca, - "pct_seuil_ca": (_q(ca / bareme.seuil_ca * Decimal("100")) - if bareme.seuil_ca else Decimal("0")), + "pct_seuil_ca": (_q(ca / bareme.seuil_ca * Decimal("100")) if bareme.seuil_ca else Decimal("0")), "seuil_tva_base": bareme.seuil_tva_base, "depasse_seuil_tva": ca > bareme.seuil_tva_base, "depasse_seuil_ca": ca > bareme.seuil_ca, }) else: - d.update({ - "taux_urssaf": None, "cotisations": None, - "taux_vl": None, "versement_liberatoire": None, - "abattement_fiscal": None, "revenu_imposable": None, - "seuil_ca": None, "pct_seuil_ca": None, - "seuil_tva_base": None, "depasse_seuil_tva": False, - "depasse_seuil_ca": False, - }) + for k in ("taux_urssaf", "cotisations", "taux_vl", "versement_liberatoire", + "abattement_fiscal", "revenu_imposable", "seuil_ca", "pct_seuil_ca", + "seuil_tva_base"): + d[k] = None + d["depasse_seuil_tva"] = d["depasse_seuil_ca"] = False return d -def synthese(entreprise, annee): - """Tableau de bord chiffré, ventilé par activité puis totalisé.""" - ca_par_act = ca_ht_par_activite(entreprise, annee) - - activites = set(ca_par_act) | {entreprise.activite} +def _lignes_activite(entreprise, annee, mois=None): + ca_par = ca_ht_par_activite(entreprise, annee, mois) + activites = set(ca_par) | {entreprise.activite} baremes = {b.activite: b for b in Bareme.objects.filter(annee=annee)} + return [detail_activite(a, ca_par.get(a, Decimal("0")), baremes.get(a)) + for a in sorted(activites)] - lignes = [] - for act in sorted(activites): - ca = ca_par_act.get(act, Decimal("0")) - lignes.append(detail_activite(act, ca, baremes.get(act))) - 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 +def _somme(lignes, 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 + +# ---- Vue globale (annuelle) -------------------------------------------- + +def synthese(entreprise, annee): + lignes = _lignes_activite(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, - "annee": annee, - "lignes_activite": lignes, + "entreprise": entreprise, "annee": annee, "lignes_activite": lignes, "bareme_manquant": any(l["bareme"] is None and l["ca_ht"] for l in lignes), "ca_ht": ca_ht(entreprise, annee), - "cotisations_urssaf": somme("cotisations"), - "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, + "cotisations_urssaf": _somme(lignes, "cotisations"), + "versement_liberatoire": (_somme(lignes, "versement_liberatoire") + if entreprise.versement_liberatoire else None), + "revenu_imposable": _somme(lignes, "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), "ca_mensuel": ca_ht_mensuel(entreprise, annee), } +# ---- Vue mensuelle ------------------------------------------------------ + +def tableau_mensuel(entreprise, annee): + baremes = {b.activite: b for b in Bareme.objects.filter(annee=annee)} + rows = [] + for m in range(1, 13): + ca_par = ca_ht_par_activite(entreprise, annee, m) + cot = Decimal("0") + vl = Decimal("0") + for act, ca in ca_par.items(): + b = baremes.get(act) + if b: + cot += ca * b.taux_urssaf / Decimal("100") + vl += ca * b.taux_vl / Decimal("100") + tva_col = tva_collectee(entreprise, annee, m) + tva_ded = tva_deductible(entreprise, annee, m) + rows.append({ + "mois": m, "nom": MOIS_FR[m - 1], + "ca_ht": ca_ht(entreprise, annee, m), + "cotisations": _q(cot), + "versement_liberatoire": _q(vl) if entreprise.versement_liberatoire else None, + "tva_collectee": tva_col, "tva_deductible": tva_ded, + "tva_a_reverser": _q(tva_col - tva_ded), + }) + return rows + + +# ---- Déclaration URSSAF (période) -------------------------------------- + +def declaration_urssaf(entreprise, annee, mois=None): + lignes = _lignes_activite(entreprise, annee, mois) + return { + "entreprise": entreprise, "annee": annee, "mois": mois, + "mois_nom": MOIS_FR[mois - 1] if mois else "Année entière", + "lignes": lignes, + "ca_ht": ca_ht(entreprise, annee, mois), + "cotisations": _somme(lignes, "cotisations"), + "versement_liberatoire": (_somme(lignes, "versement_liberatoire") + if entreprise.versement_liberatoire else None), + } + + +# ---- Déclaration TVA (période) ----------------------------------------- + +def declaration_tva(entreprise, annee, mois=None): + par_taux = tva_par_taux(entreprise, annee, mois) + intra_base, intra_tva = intracom_periode(entreprise, annee, mois) + tva_ventes = sum((t for _, t in par_taux.values()), Decimal("0")) + tva_ded = tva_deductible(entreprise, annee, mois) + tva_brute = _q(tva_ventes + intra_tva) + a_payer = _q(tva_brute - tva_ded) + + def couple(taux): + return par_taux.get(Decimal(taux), (Decimal("0.00"), Decimal("0.00"))) + + b20, t20 = couple("20.00") + b10, t10 = couple("10.00") + b55, t55 = couple("5.50") + base_taxable = _q(sum((b for b, _ in par_taux.values()), Decimal("0"))) + + return { + "entreprise": entreprise, "annee": annee, "mois": mois, + "mois_nom": MOIS_FR[mois - 1] if mois else "Année entière", + "par_taux": [{"taux": k, "base": v[0], "tva": v[1]} for k, v in par_taux.items()], + # Résumé simplifié + "tva_sur_ventes": _q(tva_ventes), + "tva_autoliquidee": intra_tva, + "tva_collectee": tva_brute, + "tva_deductible": tva_ded, + "tva_a_payer": a_payer, + "credit_tva": _q(-a_payer) if a_payer < 0 else Decimal("0.00"), + # Champs CA3 + "ca3": { + "l01_base_taxable": base_taxable, + "l08_base": b20, "l08_tva": t20, + "l9B_base": b10, "l9B_tva": t10, + "l09_base": b55, "l09_tva": t55, + "l03_intra_base": intra_base, + "l17_intra_tva": intra_tva, + "l16_tva_brute": tva_brute, + "l20_tva_deductible": tva_ded, + "l28_total_deductible": tva_ded, + "l32_a_payer": a_payer if a_payer > 0 else Decimal("0.00"), + "l25_credit": _q(-a_payer) if a_payer < 0 else Decimal("0.00"), + }, + } + + def annees_disponibles(entreprise): - qs = ( - Facture.objects.filter(entreprise=entreprise, - statut=StatutFacture.ENCAISSEE, - date_encaissement__isnull=False) - .dates("date_encaissement", "year") - ) + qs = Facture.objects.filter(entreprise=entreprise, statut=StatutFacture.ENCAISSEE, + date_encaissement__isnull=False).dates("date_encaissement", "year") return sorted({d.year for d in qs}, reverse=True) diff --git a/compta/templates/compta/base.html b/compta/templates/compta/base.html new file mode 100644 index 0000000..9bb265d --- /dev/null +++ b/compta/templates/compta/base.html @@ -0,0 +1,81 @@ + + + + + +JB Compta — {% block titre %}Tableau de bord{% endblock %} + + + + +{% if not entreprise %} +

Aucune entreprise

+

Crée une entreprise dans l'administration, + ou saisis une opération.

+{% else %} + + +

{{ entreprise.nom }}

+
{{ entreprise.get_activite_display }} (principale) · + {% if entreprise.franchise_tva %}Franchise de TVA{% else %}Assujetti TVA{% endif %} + {% if entreprise.versement_liberatoire %}· Versement libératoire{% endif %} + · Exercice {{ annee }}
+ +
+ + + {% block extrafilters %}{% endblock %} +
+ + {% block content %}{% endblock %} +{% endif %} + +{% block scripts %}{% endblock %} + + diff --git a/compta/templates/compta/dashboard.html b/compta/templates/compta/dashboard.html index 6694271..f076149 100644 --- a/compta/templates/compta/dashboard.html +++ b/compta/templates/compta/dashboard.html @@ -1,86 +1,21 @@ - - - - - -JB Compta — Tableau de bord - - - - -{% if not entreprise %} -

Aucune entreprise

-

Crée une entreprise et des factures dans - l'administration, puis reviens ici.

-{% else %} -

{{ entreprise.nom }}

-
{{ entreprise.get_activite_display }} (principale) · - {% if entreprise.franchise_tva %}Franchise de TVA{% else %}Assujetti TVA{% endif %} - {% if entreprise.versement_liberatoire %}· Versement libératoire{% endif %} - · Exercice {{ annee }}
- -
- - - - -
+{% extends "compta/base.html" %} +{% block titre %}Global{% endblock %} +{% block content %} {% if bareme_manquant %}
- Une activité avec du CA n'a pas de barème {{ annee }}. Ajoute-le dans - Barèmes. + Une activité avec du CA n'a pas de barème {{ annee }}. + Ajoute-le dans Barèmes.
{% endif %} -
CA HT encaissé (total)
{{ ca_ht }} €
-
Cotisations URSSAF (total)
+
Cotisations URSSAF
{{ cotisations_urssaf|default:"—" }} €
@@ -91,93 +26,55 @@
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 %} + {% if tva_autoliquidee %}
dont {{ tva_autoliquidee }} € auto-liquidée
{% endif %}
-

Ventilation par activité

- - - - - - + {% for l in lignes_activite %} - + {% endfor %} - - - - - - - - +
ActivitéCA HT% plafondURSSAFImpôt (VL)Revenu imposable
ActivitéCA HT% plafondURSSAFImpôt (VL)Revenu imposable
{{ l.label }} {{ l.ca_ht }} € - {% if l.pct_seuil_ca is not None %}{{ l.pct_seuil_ca }} %{% else %}—{% endif %} - {% if l.pct_seuil_ca is not None %}{{ l.pct_seuil_ca }} %{% else %}—{% endif %} {% if l.cotisations is not None %}{{ l.cotisations }} €{% else %}—{% endif %} {% if l.versement_liberatoire is not None %}{{ l.versement_liberatoire }} €{% else %}—{% endif %} {% if l.revenu_imposable is not None %}{{ l.revenu_imposable }} €{% else %}—{% endif %}
Total{{ ca_ht }} €{{ cotisations_urssaf|default:"—" }} €{{ versement_liberatoire|default:"—" }} €{{ revenu_imposable|default:"—" }} €
Total{{ ca_ht }} €{{ cotisations_urssaf|default:"—" }} €{{ versement_liberatoire|default:"—" }} €{{ revenu_imposable|default:"—" }} €
- -

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)
+
CA HT mensuel
-
À provisionner (année)
- - - - - -
PosteMontant
URSSAF{{ cotisations_urssaf|default:"—" }} €
Impôt (VL){{ versement_liberatoire|default:"—" }} €
TVA{{ tva_a_reverser }} €
-
-
Exports CSV
+
Exports CSV
+
+{% endblock %} +{% block scripts %} -{% endif %} - - +{% endblock %} diff --git a/compta/templates/compta/decl_tva.html b/compta/templates/compta/decl_tva.html new file mode 100644 index 0000000..96189d2 --- /dev/null +++ b/compta/templates/compta/decl_tva.html @@ -0,0 +1,67 @@ +{% extends "compta/base.html" %} +{% block titre %}Déclaration TVA{% endblock %} + +{% block extrafilters %} + +{% endblock %} + +{% block content %} +

Déclaration TVA — {{ decl.mois_nom }} {{ annee }}

+
+ Mode : + Résumé · + Champs CA3 +  (réglage par défaut de l'entreprise : {{ entreprise.get_vue_tva_display }}) +
+ + {% if mode == 'ca3' %} +
+ + + + + + + + + + + + + + {% if decl.ca3.l25_credit %}{% endif %} + +
Ligne CA3LibelléBase HTTVA
01Ventes / prestations taxables{{ decl.ca3.l01_base_taxable }} €
08Taux normal 20 %{{ decl.ca3.l08_base }} €{{ decl.ca3.l08_tva }} €
9BTaux réduit 10 %{{ decl.ca3.l9B_base }} €{{ decl.ca3.l9B_tva }} €
09Taux réduit 5,5 %{{ decl.ca3.l09_base }} €{{ decl.ca3.l09_tva }} €
03Acquisitions intracommunautaires{{ decl.ca3.l03_intra_base }} €
17dont TVA sur acquisitions intracom{{ decl.ca3.l17_intra_tva }} €
16TVA brute due{{ decl.ca3.l16_tva_brute }} €
20TVA déductible (biens et services){{ decl.ca3.l20_tva_deductible }} €
28Total TVA déductible{{ decl.ca3.l28_total_deductible }} €
32TVA à payer{{ decl.ca3.l32_a_payer }} €
25Crédit de TVA{{ decl.ca3.l25_credit }} €
+
+
Numéros de lignes du formulaire 3310-CA3 (régime réel normal). À recouper + avec ta déclaration officielle — vérifie notamment la ventilation par taux.
+ {% else %} +
+
TVA collectée sur ventes
{{ decl.tva_sur_ventes }} €
+
TVA auto-liquidée (UE/import)
{{ decl.tva_autoliquidee }} €
+
TVA collectée totale
{{ decl.tva_collectee }} €
+
TVA déductible
{{ decl.tva_deductible }} €
+
+
{% if decl.credit_tva %}Crédit de TVA{% else %}TVA à payer{% endif %}
+
{% if decl.credit_tva %}{{ decl.credit_tva }}{% else %}{{ decl.tva_a_payer }}{% endif %} €
+
+
+ {% if decl.par_taux %} +

Détail par taux (ventes)

+
+ + + + {% for t in decl.par_taux %}{% endfor %} + +
TauxBase HTTVA
{{ t.taux }} %{{ t.base }} €{{ t.tva }} €
+
+ {% endif %} + {% endif %} +{% endblock %} diff --git a/compta/templates/compta/decl_urssaf.html b/compta/templates/compta/decl_urssaf.html new file mode 100644 index 0000000..ef53586 --- /dev/null +++ b/compta/templates/compta/decl_urssaf.html @@ -0,0 +1,43 @@ +{% extends "compta/base.html" %} +{% block titre %}Déclaration URSSAF{% endblock %} + +{% block extrafilters %} + +{% endblock %} + +{% block content %} +

Déclaration URSSAF — {{ decl.mois_nom }} {{ annee }}

+
À déclarer sur autoentrepreneur.urssaf.fr : le CA HT encaissé de la période, + par type d'activité. Les cotisations (et l'impôt si versement libératoire) en découlent.
+ +
+ + + + {% if decl.versement_liberatoire is not None %}{% endif %} + + + {% for l in decl.lignes %} + + + + + + {% if decl.versement_liberatoire is not None %} + + + {% endif %} + + {% endfor %} + + + {% if decl.versement_liberatoire is not None %}{% endif %} + + +
ActivitéCA HT à déclarerTaux URSSAFCotisationsTaux VLImpôt (VL)
{{ l.label }}{{ l.ca_ht }} €{% if l.taux_urssaf is not None %}{{ l.taux_urssaf }} %{% else %}—{% endif %}{% if l.cotisations is not None %}{{ l.cotisations }} €{% else %}—{% endif %}{% if l.taux_vl is not None %}{{ l.taux_vl }} %{% else %}—{% endif %}{% if l.versement_liberatoire is not None %}{{ l.versement_liberatoire }} €{% else %}—{% endif %}
Total{{ decl.ca_ht }} €{{ decl.cotisations|default:"—" }} €{{ decl.versement_liberatoire }} €
+
+{% endblock %} diff --git a/compta/templates/compta/mensuel.html b/compta/templates/compta/mensuel.html new file mode 100644 index 0000000..62df8c4 --- /dev/null +++ b/compta/templates/compta/mensuel.html @@ -0,0 +1,29 @@ +{% extends "compta/base.html" %} +{% block titre %}Mensuel{% endblock %} + +{% block content %} +

Vue mensuelle {{ annee }}

+
+ + + + + + + + + {% for r in rows %} + + + + + + + + + + {% endfor %} + +
MoisCA HTCotisations URSSAFImpôt (VL)TVA collectéeTVA déductibleTVA à reverser
{{ r.nom }}{{ r.ca_ht }} €{{ r.cotisations }} €{% if r.versement_liberatoire is not None %}{{ r.versement_liberatoire }} €{% else %}—{% endif %}{{ r.tva_collectee }} €{{ r.tva_deductible }} €{{ r.tva_a_reverser }} €
+
+{% endblock %} diff --git a/compta/templates/compta/saisie_paiement.html b/compta/templates/compta/saisie_paiement.html index de3ba61..47659db 100644 --- a/compta/templates/compta/saisie_paiement.html +++ b/compta/templates/compta/saisie_paiement.html @@ -3,7 +3,7 @@ -JB Compta — Saisie d'un paiement +JB Compta — Saisie -

Saisie d'un paiement

-
Éclate une opération bancaire (ex. un débit Amazon groupé) en - plusieurs dépenses. · ← Tableau de bord · - Voir les dépenses
+

Saisie d'une opération

+
Montant négatif = dépense · positif = vente/encaissement. + · ← Tableau de bord
- {% if messages %} - {% for m in messages %} -
{{ m }}
- {% endfor %} - {% endif %} + {% if messages %}{% for m in messages %}
{{ m }}
{% endfor %}{% endif %}
{% csrf_token %} @@ -66,45 +67,85 @@
- +
- + + placeholder="-150 ou 168.40" oninput="toggle()">
-
+ + + + +
- +
@@ -114,7 +155,7 @@ @@ -137,20 +178,32 @@ diff --git a/compta/urls.py b/compta/urls.py index 7353247..17229a3 100644 --- a/compta/urls.py +++ b/compta/urls.py @@ -4,6 +4,9 @@ from . import views_saisie urlpatterns = [ path("dashboard/", views.dashboard, name="dashboard"), + path("dashboard/mensuel/", views.vue_mensuelle, name="vue_mensuelle"), + 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("export/ventes/", views.export_ventes, name="export_ventes"), path("export/tva/", views.export_tva, name="export_tva"), diff --git a/compta/views.py b/compta/views.py index 217d0bc..47b03d2 100644 --- a/compta/views.py +++ b/compta/views.py @@ -8,26 +8,40 @@ from django.shortcuts import render, get_object_or_404 from .models import Entreprise from . import services +MOIS = services.MOIS_FR -def _entreprise_courante(request): + +def _entreprise(request): eid = request.GET.get("entreprise") - qs = Entreprise.objects.all() if eid: return get_object_or_404(Entreprise, pk=eid) - return qs.first() + return Entreprise.objects.first() + + +def _annee(request): + return int(request.GET.get("annee", date.today().year)) + + +def _base_ctx(entreprise, annee, vue): + return { + "entreprise": entreprise, + "entreprises": Entreprise.objects.all(), + "annee": annee, + "annees": services.annees_disponibles(entreprise) or [annee], + "vue": vue, + "mois_liste": list(enumerate(MOIS, start=1)), + } @login_required def dashboard(request): - entreprise = _entreprise_courante(request) + entreprise = _entreprise(request) if entreprise is None: return render(request, "compta/dashboard.html", {"entreprise": None}) - - annee = int(request.GET.get("annee", date.today().year)) + annee = _annee(request) ctx = services.synthese(entreprise, annee) + ctx.update(_base_ctx(entreprise, annee, "global")) ctx.update({ - "entreprises": Entreprise.objects.all(), - "annees": services.annees_disponibles(entreprise) or [annee], "mois_labels": ["Jan", "Fév", "Mar", "Avr", "Mai", "Juin", "Juil", "Août", "Sep", "Oct", "Nov", "Déc"], "ca_mensuel_json": [float(v) for v in ctx["ca_mensuel"]], @@ -35,18 +49,59 @@ def dashboard(request): return render(request, "compta/dashboard.html", ctx) -def _csv_response(filename): +@login_required +def vue_mensuelle(request): + entreprise = _entreprise(request) + if entreprise is None: + return render(request, "compta/mensuel.html", {"entreprise": None}) + annee = _annee(request) + ctx = _base_ctx(entreprise, annee, "mensuel") + ctx["rows"] = services.tableau_mensuel(entreprise, annee) + return render(request, "compta/mensuel.html", ctx) + + +@login_required +def declaration_urssaf(request): + entreprise = _entreprise(request) + if entreprise is None: + return render(request, "compta/decl_urssaf.html", {"entreprise": None}) + annee = _annee(request) + mois = int(request.GET.get("mois", date.today().month)) + ctx = _base_ctx(entreprise, annee, "urssaf") + ctx["mois_sel"] = mois + ctx["decl"] = services.declaration_urssaf(entreprise, annee, mois) + return render(request, "compta/decl_urssaf.html", ctx) + + +@login_required +def declaration_tva(request): + entreprise = _entreprise(request) + if entreprise is None: + return render(request, "compta/decl_tva.html", {"entreprise": None}) + annee = _annee(request) + mois = int(request.GET.get("mois", date.today().month)) + mode = request.GET.get("mode") or entreprise.vue_tva + ctx = _base_ctx(entreprise, annee, "tva") + ctx["mois_sel"] = mois + ctx["mode"] = mode + ctx["decl"] = services.declaration_tva(entreprise, annee, mois) + return render(request, "compta/decl_tva.html", ctx) + + +# ---- Exports CSV -------------------------------------------------------- + +def _csv(filename): resp = HttpResponse(content_type="text/csv; charset=utf-8") resp["Content-Disposition"] = f'attachment; filename="{filename}"' - resp.write("") # BOM pour Excel + resp.write("") return resp @login_required def export_ventes(request): - entreprise = _entreprise_courante(request) - annee = int(request.GET.get("annee", date.today().year)) - resp = _csv_response(f"ventes_{annee}.csv") + entreprise = _entreprise(request) + annee = _annee(request) + resp = _csv(f"ventes_{annee}.csv") w = csv.writer(resp, delimiter=";") w.writerow(["Numero", "Client", "Activite", "Date emission", "Date encaissement", "Statut", "HT", "TVA", "TTC"]) @@ -59,13 +114,13 @@ def export_ventes(request): @login_required def export_tva(request): - entreprise = _entreprise_courante(request) - annee = int(request.GET.get("annee", date.today().year)) + entreprise = _entreprise(request) + annee = _annee(request) s = services.synthese(entreprise, annee) - resp = _csv_response(f"tva_{annee}.csv") + resp = _csv(f"tva_{annee}.csv") w = csv.writer(resp, delimiter=";") - w.writerow(["Annee", "TVA sur ventes", "TVA autoliquidee (UE/import)", - "TVA collectee totale", "TVA deductible", "TVA a reverser"]) + w.writerow(["Annee", "TVA sur ventes", "TVA autoliquidee", "TVA collectee", + "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 @@ -73,18 +128,17 @@ def export_tva(request): @login_required def export_urssaf(request): - entreprise = _entreprise_courante(request) - annee = int(request.GET.get("annee", date.today().year)) + entreprise = _entreprise(request) + annee = _annee(request) s = services.synthese(entreprise, annee) - resp = _csv_response(f"urssaf_{annee}.csv") + resp = _csv(f"urssaf_{annee}.csv") w = csv.writer(resp, delimiter=";") w.writerow(["Annee", "Activite", "CA HT", "Taux URSSAF", "Cotisations", - "Taux VL", "Versement liberatoire", "Abattement", - "Revenu imposable"]) + "Taux VL", "Versement liberatoire", "Abattement", "Revenu imposable"]) for l in s["lignes_activite"]: - w.writerow([annee, l["label"], l["ca_ht"], l["taux_urssaf"], - l["cotisations"], l["taux_vl"], l["versement_liberatoire"], - l["abattement_fiscal"], l["revenu_imposable"]]) - w.writerow([annee, "TOTAL", s["ca_ht"], "", s["cotisations_urssaf"], - "", s["versement_liberatoire"], "", s["revenu_imposable"]]) + w.writerow([annee, l["label"], l["ca_ht"], l["taux_urssaf"], l["cotisations"], + l["taux_vl"], l["versement_liberatoire"], l["abattement_fiscal"], + l["revenu_imposable"]]) + w.writerow([annee, "TOTAL", s["ca_ht"], "", s["cotisations_urssaf"], "", + s["versement_liberatoire"], "", s["revenu_imposable"]]) return resp diff --git a/compta/views_saisie.py b/compta/views_saisie.py index 272f0db..1b73ca7 100644 --- a/compta/views_saisie.py +++ b/compta/views_saisie.py @@ -1,5 +1,8 @@ """ -Page de saisie / import : éclater un paiement bancaire en plusieurs dépenses. +Saisie unifiée : une opération bancaire signée. +- Montant négatif -> dépense(s) (éclatement possible). +- Montant positif -> facture (vente) avec création de client à la volée, + OU mouvement neutre (ex. remboursement de TVA). """ from decimal import Decimal, InvalidOperation from datetime import date @@ -8,7 +11,10 @@ from django.contrib import messages from django.contrib.auth.decorators import login_required from django.shortcuts import render, redirect -from .models import Entreprise, Paiement, Depense, Activite, RegimeTva, Categorie +from .models import ( + Entreprise, Client, Facture, LigneFacture, Paiement, Depense, + Activite, RegimeTva, Categorie, +) def _dec(val, defaut="0"): @@ -18,85 +24,149 @@ def _dec(val, defaut="0"): return Decimal(defaut) -@login_required -def saisie_paiement(request): - entreprises = Entreprise.objects.all() - - if request.method == "POST": - entreprise = entreprises.filter(pk=request.POST.get("entreprise")).first() - d = request.POST.get("date") or date.today().isoformat() - libelle = (request.POST.get("libelle") or "").strip() - montant_total = _dec(request.POST.get("montant_total")) - - libelles = request.POST.getlist("l_libelle") - categories = request.POST.getlist("l_categorie") - activites = request.POST.getlist("l_activite") - 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)): - m = _dec(montants[i] if i < len(montants) else "0") - lib = (libelles[i] or "").strip() - if m == 0 and not lib: - continue - lignes.append({ - "libelle": lib or "(sans libellé)", - "categorie_id": categories[i] if i < len(categories) else "", - "activite": activites[i] if i < len(activites) else "", - "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")) - erreurs = [] - if not entreprise: - erreurs.append("Sélectionne une entreprise.") - if not libelle: - erreurs.append("Renseigne le libellé du paiement.") - if not lignes: - erreurs.append("Ajoute au moins une ligne de dépense.") - if lignes and abs(somme - montant_total) >= Decimal("0.01"): - erreurs.append( - f"La somme des lignes ({somme} €) ne correspond pas au " - f"montant total ({montant_total} €). Écart : {somme - montant_total} €.") - - if erreurs: - for e in erreurs: - messages.error(request, e) - return render(request, "compta/saisie_paiement.html", - _context(entreprises, request.POST)) - - cats = {str(c.pk): c for c in Categorie.objects.all()} - paiement = Paiement.objects.create( - entreprise=entreprise, date=d, libelle=libelle, - montant_total=montant_total, sens="debit") - for l in lignes: - Depense.objects.create( - entreprise=entreprise, paiement=paiement, date=d, - libelle=l["libelle"], categorie=cats.get(l["categorie_id"]), - activite=l["activite"] or "", regime_tva=l["regime"], - taux_tva=l["taux"], montant_ttc=l["montant"], montant_tva=l["tva"]) - - messages.success( - request, - f"Paiement « {libelle} » enregistré et éclaté en {len(lignes)} dépense(s).") - return redirect("saisie_paiement") - - return render(request, "compta/saisie_paiement.html", _context(entreprises)) +def _numero_unique(entreprise, annee): + base = Facture.objects.filter( + entreprise=entreprise, numero__startswith=f"{annee}-").count() + 1 + while True: + numero = f"{annee}-{base:03d}" + if not Facture.objects.filter(entreprise=entreprise, numero=numero).exists(): + return numero + base += 1 def _context(entreprises, post=None): return { "entreprises": entreprises, + "clients": Client.objects.select_related("entreprise"), "activites": Activite.choices, "regimes": RegimeTva.choices, - "categories": Categorie.objects.filter( - actif=True).exclude(usage="vente"), + "categories_depense": Categorie.objects.filter(actif=True).exclude(usage="vente"), + "categories_vente": Categorie.objects.filter(actif=True, usage__in=["vente", "neutre"]), "today": date.today().isoformat(), "post": post, } + + +@login_required +def saisie_paiement(request): + entreprises = Entreprise.objects.all() + if request.method != "POST": + return render(request, "compta/saisie_paiement.html", _context(entreprises)) + + entreprise = entreprises.filter(pk=request.POST.get("entreprise")).first() + d = request.POST.get("date") or date.today().isoformat() + libelle = (request.POST.get("libelle") or "").strip() + montant_total = _dec(request.POST.get("montant_total")) + annee = int(d[:4]) + + erreurs = [] + if not entreprise: + erreurs.append("Sélectionne une entreprise.") + if not libelle: + erreurs.append("Renseigne le libellé du paiement.") + if montant_total == 0: + erreurs.append("Le montant ne peut pas être nul.") + + if erreurs: + for e in erreurs: + messages.error(request, e) + return render(request, "compta/saisie_paiement.html", + _context(entreprises, request.POST)) + + cats = {str(c.pk): c for c in Categorie.objects.all()} + + # --------------------------------------------------------------- + # CRÉDIT (montant positif) -> facture (vente) ou mouvement neutre + # --------------------------------------------------------------- + if montant_total > 0: + cat = cats.get(request.POST.get("v_categorie")) + if cat and cat.exclure_calculs: + # Mouvement neutre (ex. remboursement de TVA) : pas de CA. + Paiement.objects.create( + entreprise=entreprise, date=d, libelle=libelle, + montant_total=montant_total, sens="credit", categorie=cat) + messages.success(request, f"Mouvement neutre « {libelle} » enregistré.") + return redirect("saisie_paiement") + + # Vente -> facture encaissée + client = None + nouveau = (request.POST.get("v_client_nouveau") or "").strip() + if nouveau: + client, _ = Client.objects.get_or_create(entreprise=entreprise, nom=nouveau) + elif request.POST.get("v_client"): + client = Client.objects.filter( + entreprise=entreprise, pk=request.POST.get("v_client")).first() + if not client: + messages.error(request, "Choisis un client existant ou saisis-en un nouveau.") + return render(request, "compta/saisie_paiement.html", + _context(entreprises, request.POST)) + + activite = request.POST.get("v_activite") or entreprise.activite + taux = _dec(request.POST.get("v_taux"), "0") + designation = (request.POST.get("v_designation") or libelle).strip() + ttc = montant_total + ht = (ttc / (Decimal("1") + taux / Decimal("100"))).quantize(Decimal("0.01")) + + f = Facture.objects.create( + entreprise=entreprise, client=client, + numero=_numero_unique(entreprise, annee), activite=activite, + date_emission=d, date_encaissement=d, statut="encaissee") + LigneFacture.objects.create( + facture=f, designation=designation, quantite=Decimal("1"), + prix_unitaire_ht=ht, taux_tva=taux) + f.recompute_totaux() + messages.success(request, f"Vente « {libelle} » enregistrée (facture {f.numero}).") + return redirect("saisie_paiement") + + # --------------------------------------------------------------- + # DÉBIT (montant négatif) -> dépense(s), éclatement possible + # --------------------------------------------------------------- + total_abs = -montant_total + libelles = request.POST.getlist("l_libelle") + categories = request.POST.getlist("l_categorie") + activites = request.POST.getlist("l_activite") + 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)): + m = _dec(montants[i] if i < len(montants) else "0") + lib = (libelles[i] or "").strip() + if m == 0 and not lib: + continue + lignes.append({ + "libelle": lib or "(sans libellé)", + "categorie": cats.get(categories[i] if i < len(categories) else ""), + "activite": activites[i] if i < len(activites) else "", + "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")) + if not lignes: + messages.error(request, "Ajoute au moins une ligne de dépense.") + return render(request, "compta/saisie_paiement.html", + _context(entreprises, request.POST)) + if abs(somme - total_abs) >= Decimal("0.01"): + messages.error(request, + f"La somme des lignes ({somme} €) ne correspond pas au montant " + f"({total_abs} €). Écart : {somme - total_abs} €.") + return render(request, "compta/saisie_paiement.html", + _context(entreprises, request.POST)) + + paiement = Paiement.objects.create( + entreprise=entreprise, date=d, libelle=libelle, + montant_total=total_abs, sens="debit") + for l in lignes: + 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"]) + messages.success(request, + f"Paiement « {libelle} » enregistré ({len(lignes)} dépense(s)).") + return redirect("saisie_paiement") diff --git a/config/__pycache__/__init__.cpython-310.pyc b/config/__pycache__/__init__.cpython-310.pyc index 7b12e9c..a49be8e 100644 Binary files a/config/__pycache__/__init__.cpython-310.pyc and b/config/__pycache__/__init__.cpython-310.pyc differ diff --git a/config/__pycache__/asgi.cpython-310.pyc b/config/__pycache__/asgi.cpython-310.pyc new file mode 100644 index 0000000..3dd4768 Binary files /dev/null and b/config/__pycache__/asgi.cpython-310.pyc differ diff --git a/config/__pycache__/settings.cpython-310.pyc b/config/__pycache__/settings.cpython-310.pyc index fc79426..918c024 100644 Binary files a/config/__pycache__/settings.cpython-310.pyc and b/config/__pycache__/settings.cpython-310.pyc differ diff --git a/config/__pycache__/urls.cpython-310.pyc b/config/__pycache__/urls.cpython-310.pyc index a0eb1fc..df50348 100644 Binary files a/config/__pycache__/urls.cpython-310.pyc and b/config/__pycache__/urls.cpython-310.pyc differ diff --git a/config/__pycache__/wsgi.cpython-310.pyc b/config/__pycache__/wsgi.cpython-310.pyc new file mode 100644 index 0000000..79d3c97 Binary files /dev/null and b/config/__pycache__/wsgi.cpython-310.pyc differ diff --git a/db.sqlite3 b/db.sqlite3 deleted file mode 100644 index e69de29..0000000