Saisie unifiée + 4 vues dashboard (mensuel, déclarations URSSAF/TVA)

This commit is contained in:
2026-06-30 08:21:14 +02:00
parent 54f9fc56f4
commit 80042faf0f
29 changed files with 808 additions and 437 deletions
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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),
),
]
+39 -87
View File
@@ -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:
+185 -97
View File
@@ -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)
+81
View File
@@ -0,0 +1,81 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>JB Compta — {% block titre %}Tableau de bord{% endblock %}</title>
<style>
:root { --bg:#0f172a; --card:#1e293b; --txt:#e2e8f0; --muted:#94a3b8;
--accent:#38bdf8; --ok:#34d399; --warn:#fbbf24; --bad:#f87171; }
* { box-sizing:border-box; }
body { margin:0; font-family:system-ui,Segoe UI,Roboto,sans-serif;
background:var(--bg); color:var(--txt); padding:24px; }
h1 { margin:0 0 4px; font-size:22px; }
h2 { font-size:15px; color:var(--muted); margin:24px 0 10px; font-weight:600; }
a { color:var(--accent); text-decoration:none; }
.sub { color:var(--muted); margin-bottom:16px; }
.nav { display:flex; gap:8px; flex-wrap:wrap; margin-bottom:14px; }
.nav a { padding:7px 13px; border-radius:8px; border:1px solid #334155;
background:var(--card); font-size:14px; }
.nav a.on { background:var(--accent); color:#04263a; border-color:var(--accent); font-weight:600; }
.nav a.saisie { margin-left:auto; }
form.filters { display:flex; gap:12px; flex-wrap:wrap; margin-bottom:20px; }
select, button, input { background:var(--card); color:var(--txt); border:1px solid #334155;
border-radius:8px; padding:8px 12px; font-size:14px; }
button { cursor:pointer; }
.grid { display:grid; grid-template-columns:repeat(auto-fit,minmax(190px,1fr)); gap:16px; margin-bottom:8px; }
.card { background:var(--card); border:1px solid #334155; border-radius:12px; padding:16px; }
.card .label { color:var(--muted); font-size:13px; }
.card .value { font-size:24px; font-weight:600; margin-top:6px; }
.card .hint { color:var(--muted); font-size:12px; margin-top:4px; }
.ok{color:var(--ok)} .warn{color:var(--warn)} .bad{color:var(--bad)}
.bar { height:8px; background:#334155; border-radius:99px; overflow:hidden; margin-top:8px; }
.bar > span { display:block; height:100%; background:var(--accent); }
.row { display:grid; grid-template-columns:2fr 1fr; gap:16px; margin-top:8px; }
@media(max-width:800px){ .row{grid-template-columns:1fr;} }
canvas { max-width:100%; }
table { width:100%; border-collapse:collapse; font-size:14px; }
td,th { padding:8px; border-bottom:1px solid #334155; text-align:right; }
th:first-child, td:first-child { text-align:left; }
tr.total td { font-weight:600; border-top:2px solid #475569; }
.exports a { display:inline-block; margin-right:12px; }
</style>
</head>
<body>
{% if not entreprise %}
<h1>Aucune entreprise</h1>
<p class="sub">Crée une entreprise dans <a href="/admin/">l'administration</a>,
ou <a href="/saisie/">saisis une opération</a>.</p>
{% else %}
<div class="nav">
<a class="{% if vue == 'global' %}on{% endif %}" href="/dashboard/?entreprise={{ entreprise.pk }}&annee={{ annee }}">Global</a>
<a class="{% if vue == 'mensuel' %}on{% endif %}" href="/dashboard/mensuel/?entreprise={{ entreprise.pk }}&annee={{ annee }}">Mensuel</a>
<a class="{% if vue == 'urssaf' %}on{% endif %}" href="/declaration/urssaf/?entreprise={{ entreprise.pk }}&annee={{ annee }}">Déclaration URSSAF</a>
<a class="{% if vue == 'tva' %}on{% endif %}" href="/declaration/tva/?entreprise={{ entreprise.pk }}&annee={{ annee }}">Déclaration TVA</a>
<a class="saisie" href="/saisie/">+ Saisir</a>
<a href="/admin/">Admin</a>
</div>
<h1>{{ entreprise.nom }}</h1>
<div class="sub">{{ 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 }}</div>
<form class="filters" method="get">
<select name="entreprise" onchange="this.form.submit()">
{% for e in entreprises %}<option value="{{ e.pk }}" {% if e.pk == entreprise.pk %}selected{% endif %}>{{ e.nom }}</option>{% endfor %}
</select>
<select name="annee" onchange="this.form.submit()">
{% for a in annees %}<option value="{{ a }}" {% if a == annee %}selected{% endif %}>{{ a }}</option>{% endfor %}
</select>
{% block extrafilters %}{% endblock %}
</form>
{% block content %}{% endblock %}
{% endif %}
{% block scripts %}{% endblock %}
</body>
</html>
+19 -122
View File
@@ -1,86 +1,21 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>JB Compta — Tableau de bord</title>
<style>
:root { --bg:#0f172a; --card:#1e293b; --txt:#e2e8f0; --muted:#94a3b8;
--accent:#38bdf8; --ok:#34d399; --warn:#fbbf24; --bad:#f87171; }
* { box-sizing:border-box; }
body { margin:0; font-family:system-ui,Segoe UI,Roboto,sans-serif;
background:var(--bg); color:var(--txt); padding:24px; }
h1 { margin:0 0 4px; font-size:22px; }
h2 { font-size:15px; color:var(--muted); margin:24px 0 10px; font-weight:600; }
a { color:var(--accent); }
.sub { color:var(--muted); margin-bottom:20px; }
form.filters { display:flex; gap:12px; flex-wrap:wrap; margin-bottom:24px; }
select, button { background:var(--card); color:var(--txt); border:1px solid #334155;
border-radius:8px; padding:8px 12px; font-size:14px; }
button { cursor:pointer; }
.grid { display:grid; grid-template-columns:repeat(auto-fit,minmax(200px,1fr));
gap:16px; margin-bottom:8px; }
.card { background:var(--card); border:1px solid #334155; border-radius:12px;
padding:16px; }
.card .label { color:var(--muted); font-size:13px; }
.card .value { font-size:26px; font-weight:600; margin-top:6px; }
.card .hint { color:var(--muted); font-size:12px; margin-top:4px; }
.ok{color:var(--ok)} .warn{color:var(--warn)} .bad{color:var(--bad)}
.bar { height:8px; background:#334155; border-radius:99px; overflow:hidden; margin-top:8px;}
.bar > span { display:block; height:100%; background:var(--accent); }
.row { display:grid; grid-template-columns:2fr 1fr; gap:16px; margin-top:8px; }
@media(max-width:800px){ .row{grid-template-columns:1fr;} }
.exports a { display:inline-block; margin-right:12px; }
canvas { max-width:100%; }
table { width:100%; border-collapse:collapse; font-size:14px; }
td,th { padding:8px; border-bottom:1px solid #334155; text-align:right; }
th:first-child, td:first-child { text-align:left; }
tr.total td { font-weight:600; border-top:2px solid #475569; }
</style>
</head>
<body>
{% if not entreprise %}
<h1>Aucune entreprise</h1>
<p class="sub">Crée une entreprise et des factures dans
<a href="/admin/">l'administration</a>, puis reviens ici.</p>
{% else %}
<h1>{{ entreprise.nom }}</h1>
<div class="sub">{{ 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 }}</div>
<form class="filters" method="get">
<select name="entreprise" onchange="this.form.submit()">
{% for e in entreprises %}
<option value="{{ e.pk }}" {% if e.pk == entreprise.pk %}selected{% endif %}>{{ e.nom }}</option>
{% endfor %}
</select>
<select name="annee" onchange="this.form.submit()">
{% for a in annees %}
<option value="{{ a }}" {% if a == annee %}selected{% endif %}>{{ a }}</option>
{% endfor %}
</select>
<a href="/saisie/"><button type="button">+ Saisir un paiement</button></a>
<a href="/admin/"><button type="button">Admin</button></a>
</form>
{% extends "compta/base.html" %}
{% block titre %}Global{% endblock %}
{% block content %}
{% if bareme_manquant %}
<div class="card bad" style="margin-bottom:16px;">
Une activité avec du CA n'a pas de barème {{ annee }}. Ajoute-le dans
<a href="/admin/compta/bareme/">Barèmes</a>.
Une activité avec du CA n'a pas de barème {{ annee }}.
Ajoute-le dans <a href="/admin/compta/bareme/">Barèmes</a>.
</div>
{% endif %}
<!-- Totaux entreprise -->
<div class="grid">
<div class="card">
<div class="label">CA HT encaissé (total)</div>
<div class="value">{{ ca_ht }} €</div>
</div>
<div class="card">
<div class="label">Cotisations URSSAF (total)</div>
<div class="label">Cotisations URSSAF</div>
<div class="value">{{ cotisations_urssaf|default:"—" }} €</div>
</div>
<div class="card">
@@ -91,93 +26,55 @@
<div class="label">TVA à reverser</div>
<div class="value">{{ tva_a_reverser }} €</div>
<div class="hint">{{ tva_collectee }} collectée {{ tva_deductible }} déductible</div>
{% if tva_autoliquidee %}<div class="hint">dont {{ tva_autoliquidee }} € auto-liquidée (UE/import)</div>{% endif %}
{% if tva_autoliquidee %}<div class="hint">dont {{ tva_autoliquidee }} € auto-liquidée</div>{% endif %}
</div>
</div>
<!-- Ventilation par activité -->
<h2>Ventilation par activité</h2>
<div class="card">
<table>
<thead>
<tr>
<th>Activité</th><th>CA HT</th><th>% plafond</th>
<th>URSSAF</th><th>Impôt (VL)</th><th>Revenu imposable</th>
</tr>
</thead>
<thead><tr><th>Activité</th><th>CA HT</th><th>% plafond</th><th>URSSAF</th><th>Impôt (VL)</th><th>Revenu imposable</th></tr></thead>
<tbody>
{% for l in lignes_activite %}
<tr>
<td>{{ l.label }}</td>
<td>{{ l.ca_ht }} €</td>
<td class="{% if l.depasse_seuil_ca %}bad{% endif %}">
{% if l.pct_seuil_ca is not None %}{{ l.pct_seuil_ca }} %{% else %}—{% endif %}
</td>
<td class="{% if l.depasse_seuil_ca %}bad{% endif %}">{% if l.pct_seuil_ca is not None %}{{ l.pct_seuil_ca }} %{% else %}—{% endif %}</td>
<td>{% if l.cotisations is not None %}{{ l.cotisations }} €{% else %}—{% endif %}</td>
<td>{% if l.versement_liberatoire is not None %}{{ l.versement_liberatoire }} €{% else %}—{% endif %}</td>
<td>{% if l.revenu_imposable is not None %}{{ l.revenu_imposable }} €{% else %}—{% endif %}</td>
</tr>
{% endfor %}
<tr class="total">
<td>Total</td>
<td>{{ ca_ht }} €</td>
<td></td>
<td>{{ cotisations_urssaf|default:"—" }} €</td>
<td>{{ versement_liberatoire|default:"—" }} €</td>
<td>{{ revenu_imposable|default:"—" }} €</td>
</tr>
<tr class="total"><td>Total</td><td>{{ ca_ht }} €</td><td></td><td>{{ cotisations_urssaf|default:"—" }} €</td><td>{{ versement_liberatoire|default:"—" }} €</td><td>{{ revenu_imposable|default:"—" }} €</td></tr>
</tbody>
</table>
</div>
<!-- Détail TVA -->
<h2>TVA</h2>
<div class="card">
<table>
<tr><td>TVA collectée sur ventes</td><td>{{ tva_sur_ventes }} €</td></tr>
<tr><td>TVA auto-liquidée (UE / import)</td><td>{{ tva_autoliquidee }} €</td></tr>
<tr><td>TVA collectée totale (due)</td><td>{{ tva_collectee }} €</td></tr>
<tr><td>TVA déductible</td><td>{{ tva_deductible }} €</td></tr>
<tr class="total"><td>TVA à reverser</td><td>{{ tva_a_reverser }} €</td></tr>
</table>
</div>
<div class="row">
<div class="card">
<div class="label" style="margin-bottom:8px;">CA HT mensuel (toutes activités)</div>
<div class="label" style="margin-bottom:8px;">CA HT mensuel</div>
<canvas id="caChart" height="120"></canvas>
</div>
<div class="card">
<div class="label" style="margin-bottom:8px;">À provisionner (année)</div>
<table>
<tr><th>Poste</th><th>Montant</th></tr>
<tr><td>URSSAF</td><td>{{ cotisations_urssaf|default:"—" }} €</td></tr>
<tr><td>Impôt (VL)</td><td>{{ versement_liberatoire|default:"—" }} €</td></tr>
<tr><td>TVA</td><td>{{ tva_a_reverser }} €</td></tr>
</table>
<div class="exports" style="margin-top:16px;">
<div class="label" style="margin-bottom:6px;">Exports CSV</div>
<div class="label" style="margin-bottom:8px;">Exports CSV</div>
<div class="exports">
<a href="{% url 'export_ventes' %}?entreprise={{ entreprise.pk }}&annee={{ annee }}">Ventes</a>
<a href="{% url 'export_tva' %}?entreprise={{ entreprise.pk }}&annee={{ annee }}">TVA</a>
<a href="{% url 'export_urssaf' %}?entreprise={{ entreprise.pk }}&annee={{ annee }}">URSSAF</a>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
<script>
const ctx = document.getElementById('caChart');
new Chart(ctx, {
new Chart(document.getElementById('caChart'), {
type: 'bar',
data: {
labels: {{ mois_labels|safe }},
datasets: [{ label: 'CA HT (€)', data: {{ ca_mensuel_json|safe }},
backgroundColor: '#38bdf8' }]
},
data: { labels: {{ mois_labels|safe }},
datasets: [{ label:'CA HT (€)', data: {{ ca_mensuel_json|safe }}, backgroundColor:'#38bdf8' }] },
options: { plugins:{legend:{display:false}},
scales:{ x:{ticks:{color:'#94a3b8'}}, y:{ticks:{color:'#94a3b8'}} } }
});
</script>
{% endif %}
</body>
</html>
{% endblock %}
+67
View File
@@ -0,0 +1,67 @@
{% extends "compta/base.html" %}
{% block titre %}Déclaration TVA{% endblock %}
{% block extrafilters %}
<select name="mois" onchange="this.form.submit()">
{% for num, nom in mois_liste %}
<option value="{{ num }}" {% if num == mois_sel %}selected{% endif %}>{{ nom }}</option>
{% endfor %}
</select>
{% endblock %}
{% block content %}
<h2>Déclaration TVA — {{ decl.mois_nom }} {{ annee }}</h2>
<div class="sub">
Mode :
<a href="/declaration/tva/?entreprise={{ entreprise.pk }}&annee={{ annee }}&mois={{ mois_sel }}&mode=simplifie"
{% if mode != 'ca3' %}class="on" style="padding:2px 8px;border-radius:6px;"{% endif %}>Résumé</a> ·
<a href="/declaration/tva/?entreprise={{ entreprise.pk }}&annee={{ annee }}&mois={{ mois_sel }}&mode=ca3"
{% if mode == 'ca3' %}class="on" style="padding:2px 8px;border-radius:6px;"{% endif %}>Champs CA3</a>
&nbsp;(réglage par défaut de l'entreprise : {{ entreprise.get_vue_tva_display }})
</div>
{% if mode == 'ca3' %}
<div class="card">
<table>
<thead><tr><th>Ligne CA3</th><th>Libellé</th><th>Base HT</th><th>TVA</th></tr></thead>
<tbody>
<tr><td>01</td><td>Ventes / prestations taxables</td><td>{{ decl.ca3.l01_base_taxable }} €</td><td></td></tr>
<tr><td>08</td><td>Taux normal 20 %</td><td>{{ decl.ca3.l08_base }} €</td><td>{{ decl.ca3.l08_tva }} €</td></tr>
<tr><td>9B</td><td>Taux réduit 10 %</td><td>{{ decl.ca3.l9B_base }} €</td><td>{{ decl.ca3.l9B_tva }} €</td></tr>
<tr><td>09</td><td>Taux réduit 5,5 %</td><td>{{ decl.ca3.l09_base }} €</td><td>{{ decl.ca3.l09_tva }} €</td></tr>
<tr><td>03</td><td>Acquisitions intracommunautaires</td><td>{{ decl.ca3.l03_intra_base }} €</td><td></td></tr>
<tr><td>17</td><td>dont TVA sur acquisitions intracom</td><td></td><td>{{ decl.ca3.l17_intra_tva }} €</td></tr>
<tr class="total"><td>16</td><td>TVA brute due</td><td></td><td>{{ decl.ca3.l16_tva_brute }} €</td></tr>
<tr><td>20</td><td>TVA déductible (biens et services)</td><td></td><td>{{ decl.ca3.l20_tva_deductible }} €</td></tr>
<tr class="total"><td>28</td><td>Total TVA déductible</td><td></td><td>{{ decl.ca3.l28_total_deductible }} €</td></tr>
<tr class="total"><td>32</td><td>TVA à payer</td><td></td><td>{{ decl.ca3.l32_a_payer }} €</td></tr>
{% if decl.ca3.l25_credit %}<tr class="total"><td>25</td><td>Crédit de TVA</td><td></td><td>{{ decl.ca3.l25_credit }} €</td></tr>{% endif %}
</tbody>
</table>
</div>
<div class="sub">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.</div>
{% else %}
<div class="grid">
<div class="card"><div class="label">TVA collectée sur ventes</div><div class="value">{{ decl.tva_sur_ventes }} €</div></div>
<div class="card"><div class="label">TVA auto-liquidée (UE/import)</div><div class="value">{{ decl.tva_autoliquidee }} €</div></div>
<div class="card"><div class="label">TVA collectée totale</div><div class="value">{{ decl.tva_collectee }} €</div></div>
<div class="card"><div class="label">TVA déductible</div><div class="value">{{ decl.tva_deductible }} €</div></div>
<div class="card">
<div class="label">{% if decl.credit_tva %}Crédit de TVA{% else %}TVA à payer{% endif %}</div>
<div class="value {% if decl.credit_tva %}ok{% endif %}">{% if decl.credit_tva %}{{ decl.credit_tva }}{% else %}{{ decl.tva_a_payer }}{% endif %} €</div>
</div>
</div>
{% if decl.par_taux %}
<h2>Détail par taux (ventes)</h2>
<div class="card">
<table>
<thead><tr><th>Taux</th><th>Base HT</th><th>TVA</th></tr></thead>
<tbody>
{% for t in decl.par_taux %}<tr><td>{{ t.taux }} %</td><td>{{ t.base }} €</td><td>{{ t.tva }} €</td></tr>{% endfor %}
</tbody>
</table>
</div>
{% endif %}
{% endif %}
{% endblock %}
+43
View File
@@ -0,0 +1,43 @@
{% extends "compta/base.html" %}
{% block titre %}Déclaration URSSAF{% endblock %}
{% block extrafilters %}
<select name="mois" onchange="this.form.submit()">
{% for num, nom in mois_liste %}
<option value="{{ num }}" {% if num == mois_sel %}selected{% endif %}>{{ nom }}</option>
{% endfor %}
</select>
{% endblock %}
{% block content %}
<h2>Déclaration URSSAF — {{ decl.mois_nom }} {{ annee }}</h2>
<div class="sub">À 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.</div>
<div class="card">
<table>
<thead>
<tr><th>Activité</th><th>CA HT à déclarer</th><th>Taux URSSAF</th><th>Cotisations</th>
{% if decl.versement_liberatoire is not None %}<th>Taux VL</th><th>Impôt (VL)</th>{% endif %}</tr>
</thead>
<tbody>
{% for l in decl.lignes %}
<tr>
<td>{{ l.label }}</td>
<td>{{ l.ca_ht }} €</td>
<td>{% if l.taux_urssaf is not None %}{{ l.taux_urssaf }} %{% else %}—{% endif %}</td>
<td>{% if l.cotisations is not None %}{{ l.cotisations }} €{% else %}—{% endif %}</td>
{% if decl.versement_liberatoire is not None %}
<td>{% if l.taux_vl is not None %}{{ l.taux_vl }} %{% else %}—{% endif %}</td>
<td>{% if l.versement_liberatoire is not None %}{{ l.versement_liberatoire }} €{% else %}—{% endif %}</td>
{% endif %}
</tr>
{% endfor %}
<tr class="total">
<td>Total</td><td>{{ decl.ca_ht }} €</td><td></td><td>{{ decl.cotisations|default:"—" }} €</td>
{% if decl.versement_liberatoire is not None %}<td></td><td>{{ decl.versement_liberatoire }} €</td>{% endif %}
</tr>
</tbody>
</table>
</div>
{% endblock %}
+29
View File
@@ -0,0 +1,29 @@
{% extends "compta/base.html" %}
{% block titre %}Mensuel{% endblock %}
{% block content %}
<h2>Vue mensuelle {{ annee }}</h2>
<div class="card">
<table>
<thead>
<tr>
<th>Mois</th><th>CA HT</th><th>Cotisations URSSAF</th>
<th>Impôt (VL)</th><th>TVA collectée</th><th>TVA déductible</th><th>TVA à reverser</th>
</tr>
</thead>
<tbody>
{% for r in rows %}
<tr>
<td>{{ r.nom }}</td>
<td>{{ r.ca_ht }} €</td>
<td>{{ r.cotisations }} €</td>
<td>{% if r.versement_liberatoire is not None %}{{ r.versement_liberatoire }} €{% else %}—{% endif %}</td>
<td>{{ r.tva_collectee }} €</td>
<td>{{ r.tva_deductible }} €</td>
<td>{{ r.tva_a_reverser }} €</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endblock %}
+82 -29
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>JB Compta — Saisie d'un paiement</title>
<title>JB Compta — Saisie</title>
<style>
:root { --bg:#0f172a; --card:#1e293b; --txt:#e2e8f0; --muted:#94a3b8;
--accent:#38bdf8; --ok:#34d399; --warn:#fbbf24; --bad:#f87171; }
@@ -19,7 +19,8 @@
input, select { background:#0f172a; color:var(--txt); border:1px solid #334155;
border-radius:8px; padding:8px 10px; font-size:14px; width:100%; }
.head { display:grid; grid-template-columns:1fr 1fr 2fr 1fr; gap:12px; }
@media(max-width:700px){ .head{grid-template-columns:1fr 1fr;} }
.grid2 { display:grid; grid-template-columns:1fr 1fr; gap:12px; }
@media(max-width:700px){ .head,.grid2{grid-template-columns:1fr;} }
table { width:100%; border-collapse:collapse; font-size:13px; margin-top:8px; }
th,td { padding:6px; border-bottom:1px solid #334155; text-align:left; vertical-align:top; }
th { color:var(--muted); font-weight:600; }
@@ -36,19 +37,19 @@
.msg.error { background:#7f1d1d; }
.msg.success { background:#064e3b; }
.actions { display:flex; gap:12px; margin-top:16px; }
.badge { display:inline-block; padding:2px 10px; border-radius:99px; font-size:12px; }
.badge.dep { background:#7f1d1d; }
.badge.ven { background:#064e3b; }
.hidden { display:none; }
.hint { color:var(--muted); font-size:12px; margin-top:6px; }
</style>
</head>
<body>
<h1>Saisie d'un paiement</h1>
<div class="sub">Éclate une opération bancaire (ex. un débit Amazon groupé) en
plusieurs dépenses. · <a href="/dashboard/">← Tableau de bord</a> ·
<a href="/admin/compta/depense/">Voir les dépenses</a></div>
<h1>Saisie d'une opération</h1>
<div class="sub">Montant <b>négatif</b> = dépense · <b>positif</b> = vente/encaissement.
· <a href="/dashboard/">← Tableau de bord</a></div>
{% if messages %}
{% for m in messages %}
<div class="msg {{ m.tags }}">{{ m }}</div>
{% endfor %}
{% endif %}
{% if messages %}{% for m in messages %}<div class="msg {{ m.tags }}">{{ m }}</div>{% endfor %}{% endif %}
<form method="post" id="frm">
{% csrf_token %}
@@ -66,45 +67,85 @@
</div>
<div>
<label>Libellé bancaire</label>
<input type="text" name="libelle" placeholder="ex. AMAZON EU SARL" required>
<input type="text" name="libelle" placeholder="ex. AMAZON / ALTISSIMO" required>
</div>
<div>
<label>Montant total payé (€)</label>
<label>Montant signé (€) <span id="badge"></span></label>
<input type="number" step="0.01" name="montant_total" id="total"
placeholder="150.00" oninput="recalc()">
placeholder="-150 ou 168.40" oninput="toggle()">
</div>
</div>
</div>
<div class="card">
<!-- ===== PANEL DÉPENSE (montant négatif) ===== -->
<div class="card hidden" id="panel-depense">
<div class="sub" style="margin:0 0 8px">Dépense — éclate en une ou plusieurs lignes.</div>
<table>
<thead>
<tr>
<th style="width:22%">Libellé / vendeur</th>
<th style="width:16%">Catégorie</th>
<th style="width:15%">Activité</th>
<th style="width:15%">Régime TVA</th>
<th style="width:14%">Activité</th>
<th style="width:16%">Régime TVA</th>
<th style="width:7%">Taux %</th>
<th style="width:11%">Montant €</th>
<th style="width:11%">TVA déduct.</th>
<th style="width:11%">TVA déduct.</th>
<th></th>
</tr>
</thead>
<tbody id="lignes"></tbody>
</table>
<div class="actions">
<button type="button" onclick="addRow()">+ Ajouter une ligne</button>
</div>
<div class="actions"><button type="button" onclick="addRow()">+ Ajouter une ligne</button></div>
<div class="totaux">
<span>Réparti : <b id="repartis">0.00</b></span>
<span>Reste : <b id="reste" class="bad">0.00</b></span>
</div>
</div>
<!-- ===== PANEL VENTE (montant positif) ===== -->
<div class="card hidden" id="panel-vente">
<div class="sub" style="margin:0 0 8px">Vente — crée une facture encaissée (ou un mouvement neutre).</div>
<div class="grid2">
<div>
<label>Client existant</label>
<select name="v_client">
<option value=""></option>
{% for c in clients %}<option value="{{ c.pk }}">{{ c.nom }} ({{ c.entreprise.nom }})</option>{% endfor %}
</select>
</div>
<div>
<label>…ou nouveau client</label>
<input type="text" name="v_client_nouveau" placeholder="Nom du nouveau client">
</div>
<div>
<label>Activité</label>
<select name="v_activite">
{% for val,lab in activites %}<option value="{{ val }}">{{ lab }}</option>{% endfor %}
</select>
</div>
<div>
<label>Taux TVA (%)</label>
<input type="number" step="0.01" name="v_taux" value="20">
</div>
<div>
<label>Désignation</label>
<input type="text" name="v_designation" placeholder="ex. Vente de produits finis">
</div>
<div>
<label>Ou catégorie neutre (ex. remboursement TVA)</label>
<select name="v_categorie">
<option value="">— (vente normale)</option>
{% for c in categories_vente %}<option value="{{ c.pk }}">{{ c.nom }}{% if c.exclure_calculs %} (neutre){% endif %}</option>{% endfor %}
</select>
</div>
</div>
<div class="hint">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 <b>neutre</b>, aucune facture
n'est créée (simple mouvement de trésorerie).</div>
</div>
<div class="actions">
<button type="submit" class="primary">Enregistrer le paiement</button>
<button type="submit" class="primary">Enregistrer</button>
</div>
</form>
@@ -114,7 +155,7 @@
<td>
<select name="l_categorie">
<option value=""></option>
{% for c in categories %}<option value="{{ c.pk }}">{{ c.nom }}</option>{% endfor %}
{% for c in categories_depense %}<option value="{{ c.pk }}">{{ c.nom }}</option>{% endfor %}
</select>
</td>
<td>
@@ -137,20 +178,32 @@
<script>
function addRow(){
const tpl = document.getElementById('rowtpl');
document.getElementById('lignes').appendChild(tpl.content.cloneNode(true));
document.getElementById('lignes').appendChild(
document.getElementById('rowtpl').content.cloneNode(true));
}
function recalc(){
let s = 0;
document.querySelectorAll('.montant').forEach(i => s += parseFloat(i.value||0));
const total = parseFloat(document.getElementById('total').value||0);
const total = Math.abs(parseFloat(document.getElementById('total').value||0));
document.getElementById('repartis').textContent = s.toFixed(2);
const reste = (total - s);
const el = document.getElementById('reste');
el.textContent = reste.toFixed(2);
el.className = Math.abs(reste) < 0.01 ? 'ok' : 'bad';
}
addRow(); addRow(); addRow();
function toggle(){
const v = parseFloat(document.getElementById('total').value||0);
const dep = document.getElementById('panel-depense');
const ven = document.getElementById('panel-vente');
const badge = document.getElementById('badge');
if (v < 0){ dep.classList.remove('hidden'); ven.classList.add('hidden');
badge.className='badge dep'; badge.textContent='Dépense';
if(!document.querySelector('#lignes tr')){addRow();addRow();}
recalc(); }
else if (v > 0){ ven.classList.remove('hidden'); dep.classList.add('hidden');
badge.className='badge ven'; badge.textContent='Vente'; }
else { dep.classList.add('hidden'); ven.classList.add('hidden'); badge.textContent=''; }
}
</script>
</body>
</html>
+3
View File
@@ -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"),
+82 -28
View File
@@ -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
+144 -74
View File
@@ -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")
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
View File