352 lines
14 KiB
Python
352 lines
14 KiB
Python
"""
|
||
Logique fiscale — JB Compta.
|
||
|
||
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, ROUND_HALF_UP
|
||
from django.db.models import Sum
|
||
from django.db.models.functions import TruncMonth
|
||
|
||
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(v):
|
||
return (v or Decimal("0")).quantize(CENT)
|
||
|
||
|
||
def _euro(v):
|
||
"""Arrondi à l'euro le plus proche (règle URSSAF / TVA : 0,50 → euro sup.)."""
|
||
return (v or Decimal("0")).quantize(Decimal("1"), rounding=ROUND_HALF_UP)
|
||
|
||
|
||
def _somme_euro(lignes, cle):
|
||
vals = [l[cle] for l in lignes if l.get(cle) is not None]
|
||
return _euro(sum(vals, Decimal("0"))) if vals else None
|
||
|
||
|
||
# ---- Sélections de période ---------------------------------------------
|
||
|
||
def _filtre_mois(qs, field, mois):
|
||
"""mois peut être un entier (1 mois), une liste de mois (trimestre) ou None (année)."""
|
||
if not mois:
|
||
return qs
|
||
if isinstance(mois, (list, tuple, set)):
|
||
return qs.filter(**{f"{field}__month__in": list(mois)})
|
||
return qs.filter(**{f"{field}__month": mois})
|
||
|
||
|
||
def factures_encaissees(entreprise, annee, mois=None):
|
||
qs = Facture.objects.filter(entreprise=entreprise, statut=StatutFacture.ENCAISSEE,
|
||
date_encaissement__year=annee)
|
||
return _filtre_mois(qs, "date_encaissement", mois)
|
||
|
||
|
||
def depenses_periode(entreprise, annee, mois=None):
|
||
qs = Depense.objects.filter(entreprise=entreprise, date__year=annee)
|
||
return _filtre_mois(qs, "date", mois)
|
||
|
||
|
||
# ---- 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, mois=None):
|
||
return _q(factures_encaissees(entreprise, annee, mois).aggregate(
|
||
s=Sum("montant_ht"))["s"])
|
||
|
||
|
||
# ---- TVA ----------------------------------------------------------------
|
||
|
||
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, 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, mois=None):
|
||
total = sum((d.tva_deductible_value for d in depenses_periode(entreprise, annee, mois)),
|
||
Decimal("0"))
|
||
return _q(total)
|
||
|
||
|
||
def tva_deductible_split(entreprise, annee, mois=None):
|
||
"""TVA déductible ventilée : (immobilisations [ligne 19], autres biens/services [ligne 20])."""
|
||
immo = Decimal("0")
|
||
autres = Decimal("0")
|
||
for d in depenses_periode(entreprise, annee, mois):
|
||
v = d.tva_deductible_value
|
||
if not v:
|
||
continue
|
||
compte = (d.categorie.compte_pcg if d.categorie else "") or ""
|
||
if compte.startswith("2"): # comptes de classe 2 = immobilisations
|
||
immo += v
|
||
else:
|
||
autres += v
|
||
return _q(immo), _q(autres)
|
||
|
||
|
||
def tva_collectee(entreprise, annee, mois=None):
|
||
return _q(tva_sur_ventes(entreprise, annee, mois)
|
||
+ tva_autoliquidee(entreprise, annee, mois))
|
||
|
||
|
||
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):
|
||
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"]:
|
||
mensuel[r["mois"].month - 1] = _q(r["total"])
|
||
return mensuel
|
||
|
||
|
||
# ---- Détail par activité ------------------------------------------------
|
||
|
||
def detail_activite(activite, ca, bareme):
|
||
label = dict(Activite.choices).get(activite, activite)
|
||
d = {"activite": activite, "label": label, "ca_ht": ca, "bareme": bareme}
|
||
if bareme:
|
||
d.update({
|
||
"taux_urssaf": bareme.taux_urssaf,
|
||
"cotisations": _euro(ca * bareme.taux_urssaf / Decimal("100")),
|
||
"taux_vl": bareme.taux_vl,
|
||
"versement_liberatoire": _euro(ca * bareme.taux_vl / Decimal("100")),
|
||
"taux_cfp": bareme.taux_cfp,
|
||
"cfp": _euro(ca * bareme.taux_cfp / Decimal("100")),
|
||
"abattement_fiscal": bareme.abattement_fiscal,
|
||
"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")),
|
||
"seuil_tva_base": bareme.seuil_tva_base,
|
||
"depasse_seuil_tva": ca > bareme.seuil_tva_base,
|
||
"depasse_seuil_ca": ca > bareme.seuil_ca,
|
||
})
|
||
else:
|
||
for k in ("taux_urssaf", "cotisations", "taux_vl", "versement_liberatoire",
|
||
"taux_cfp", "cfp", "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 _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)]
|
||
|
||
|
||
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,
|
||
"bareme_manquant": any(l["bareme"] is None and l["ca_ht"] for l in lignes),
|
||
"ca_ht": ca_ht(entreprise, annee),
|
||
"cotisations_urssaf": _somme_euro(lignes, "cotisations"),
|
||
"versement_liberatoire": (_somme_euro(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": _euro(_euro(tva_col) - _euro(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": _euro(cot),
|
||
"versement_liberatoire": _euro(vl) if entreprise.versement_liberatoire else None,
|
||
"tva_collectee": tva_col, "tva_deductible": tva_ded,
|
||
"tva_a_reverser": _euro(_euro(tva_col) - _euro(tva_ded)),
|
||
})
|
||
return rows
|
||
|
||
|
||
# ---- Déclaration URSSAF (période) --------------------------------------
|
||
|
||
def declaration_urssaf(entreprise, annee, mois=None, label=None):
|
||
lignes = _lignes_activite(entreprise, annee, mois)
|
||
# URSSAF : le CA déclaré et les cotisations sont arrondis à l'euro le plus proche.
|
||
for l in lignes:
|
||
l["ca_ht"] = _euro(l["ca_ht"])
|
||
return {
|
||
"entreprise": entreprise, "annee": annee, "mois": mois,
|
||
"mois_nom": label or (MOIS_FR[mois - 1] if isinstance(mois, int) else "Année entière"),
|
||
"lignes": lignes,
|
||
"ca_ht": _euro(ca_ht(entreprise, annee, mois)),
|
||
"cotisations": _somme_euro(lignes, "cotisations"),
|
||
"cfp": _somme_euro(lignes, "cfp"),
|
||
"versement_liberatoire": (_somme_euro(lignes, "versement_liberatoire")
|
||
if entreprise.versement_liberatoire else None),
|
||
"total_regle": _euro(
|
||
(_somme_euro(lignes, "cotisations") or Decimal("0"))
|
||
+ (_somme_euro(lignes, "cfp") or Decimal("0"))
|
||
+ ((_somme_euro(lignes, "versement_liberatoire") or Decimal("0"))
|
||
if entreprise.versement_liberatoire else Decimal("0"))),
|
||
}
|
||
|
||
|
||
# ---- Déclaration TVA (période) -----------------------------------------
|
||
|
||
def _ca3_ligne(ref, libelle, base=None, tva=None, total=False):
|
||
return {"ref": ref, "libelle": libelle, "base": base, "tva": tva, "total": total}
|
||
|
||
|
||
def declaration_tva(entreprise, annee, mois=None, label=None):
|
||
par_taux = tva_par_taux(entreprise, annee, mois)
|
||
intra_base, intra_tva = intracom_periode(entreprise, annee, mois)
|
||
# CA3 : chaque ligne (base ET TVA) est arrondie à l'euro le plus proche ;
|
||
# les totaux sont la somme des lignes arrondies (comme le formulaire officiel).
|
||
def couple(taux):
|
||
b, t = par_taux.get(Decimal(taux), (Decimal("0.00"), Decimal("0.00")))
|
||
return _euro(b), _euro(t)
|
||
|
||
b20, t20 = couple("20.00")
|
||
b10, t10 = couple("10.00")
|
||
b55, t55 = couple("5.50")
|
||
intra_base = _euro(intra_base)
|
||
intra_tva = _euro(intra_tva)
|
||
ded_immo, ded_autres = tva_deductible_split(entreprise, annee, mois)
|
||
ded_immo, ded_autres = _euro(ded_immo), _euro(ded_autres)
|
||
tva_ventes = t20 + t10 + t55
|
||
tva_brute = tva_ventes + intra_tva
|
||
tva_ded = ded_immo + ded_autres
|
||
a_payer = tva_brute - tva_ded
|
||
base_taxable = b20 + b10 + b55
|
||
|
||
Z = Decimal("0")
|
||
# --- Cadre A : montant des opérations réalisées (bases HT) ---
|
||
a_taxees = []
|
||
if base_taxable:
|
||
a_taxees.append(_ca3_ligne("A1", "Ventes, prestations de services", base=base_taxable))
|
||
if intra_base:
|
||
a_taxees.append(_ca3_ligne("B2", "Acquisitions intracommunautaires", base=intra_base))
|
||
# --- Cadre B : décompte de la TVA à payer ---
|
||
brute = []
|
||
if b20 or t20:
|
||
brute.append(_ca3_ligne("08", "Taux normal 20 %", base=b20, tva=t20))
|
||
if b10 or t10:
|
||
brute.append(_ca3_ligne("9B", "Taux réduit 10 %", base=b10, tva=t10))
|
||
if b55 or t55:
|
||
brute.append(_ca3_ligne("09", "Taux réduit 5,5 %", base=b55, tva=t55))
|
||
if intra_tva:
|
||
brute.append(_ca3_ligne("17", "Dont TVA sur acquisitions intracommunautaires", tva=intra_tva))
|
||
brute.append(_ca3_ligne("16", "Total de la TVA brute due", tva=tva_brute, total=True))
|
||
|
||
deductible = []
|
||
if ded_immo:
|
||
deductible.append(_ca3_ligne("19", "Biens constituant des immobilisations", tva=ded_immo))
|
||
deductible.append(_ca3_ligne("20", "Autres biens et services", tva=ded_autres))
|
||
deductible.append(_ca3_ligne("23", "Total de la TVA déductible", tva=tva_ded, total=True))
|
||
|
||
if a_payer >= 0:
|
||
solde = {"titre": "Taxe due",
|
||
"lignes": [_ca3_ligne("TD", "TVA due (ligne 16 − ligne 23)", tva=a_payer, total=True)]}
|
||
else:
|
||
solde = {"titre": "Crédit",
|
||
"lignes": [_ca3_ligne("25", "Crédit de TVA (ligne 23 − ligne 16)",
|
||
tva=_euro(-a_payer), total=True)]}
|
||
|
||
cadres = [
|
||
{"titre": "A — Montant des opérations réalisées",
|
||
"sous": [{"titre": "Opérations taxées (HT)", "lignes": a_taxees}] if a_taxees else []},
|
||
{"titre": "B — Décompte de la TVA à payer",
|
||
"sous": [{"titre": "TVA brute", "lignes": brute},
|
||
{"titre": "TVA déductible", "lignes": deductible},
|
||
solde]},
|
||
]
|
||
|
||
return {
|
||
"entreprise": entreprise, "annee": annee, "mois": mois,
|
||
"mois_nom": label or (MOIS_FR[mois - 1] if isinstance(mois, int) else "Année entière"),
|
||
"par_taux": [{"taux": k, "base": _euro(v[0]), "tva": _euro(v[1])}
|
||
for k, v in par_taux.items()],
|
||
"tva_sur_ventes": tva_ventes,
|
||
"tva_autoliquidee": intra_tva,
|
||
"tva_collectee": tva_brute,
|
||
"tva_deductible": tva_ded,
|
||
"tva_a_payer": a_payer if a_payer > 0 else Z,
|
||
"credit_tva": _euro(-a_payer) if a_payer < 0 else Z,
|
||
"cadres": cadres,
|
||
}
|
||
|
||
|
||
def annees_disponibles(entreprise):
|
||
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)
|