Files
jb-compta/compta/services.py
T

352 lines
14 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
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)