""" 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 intracom_par_taux(entreprise, annee, mois=None): """Acquisitions auto-liquidées (UE/import) ventilées par taux : {taux: (base_ht, tva)}.""" par = {} for d in depenses_periode(entreprise, annee, mois): if d.autoliquidee and not d.neutre: b, t = par.get(d.taux_tva, (Decimal("0"), Decimal("0"))) par[d.taux_tva] = (b + d.montant_ht, t + d.tva_autoliquidee) return {taux: (_q(b), _q(t)) for taux, (b, t) in par.items()} 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): dom = tva_par_taux(entreprise, annee, mois) # ventes {taux: (base, tva)} intra = intracom_par_taux(entreprise, annee, mois) # acquisitions auto-liquidées # CA3 : chaque ligne (base ET TVA) est arrondie à l'euro le plus proche. # Les acquisitions intracom/import auto-liquidées sont taxées sur les lignes de # taux (08/9B/09) : leur base s'ajoute à celle des ventes, sinon le total des # bases du cadre B ne correspond pas au cadre A (contrôle bloquant du CA3). def rate(taux): db, dt = dom.get(Decimal(taux), (Decimal("0"), Decimal("0"))) ib, it = intra.get(Decimal(taux), (Decimal("0"), Decimal("0"))) return _euro(db + ib), _euro(dt + it) b20, t20 = rate("20.00") b10, t10 = rate("10.00") b55, t55 = rate("5.50") ventes_ht = _euro(sum((b for b, _ in dom.values()), Decimal("0"))) ventes_tva = _euro(sum((t for _, t in dom.values()), Decimal("0"))) intra_base = _euro(sum((b for b, _ in intra.values()), Decimal("0"))) intra_tva = _euro(sum((t for _, t in intra.values()), Decimal("0"))) ded_immo, ded_autres = tva_deductible_split(entreprise, annee, mois) ded_immo, ded_autres = _euro(ded_immo), _euro(ded_autres) tva_brute = t20 + t10 + t55 # inclut déjà l'auto-liquidation tva_ded = ded_immo + ded_autres a_payer = tva_brute - tva_ded Z = Decimal("0") # --- Cadre A : montant des opérations réalisées (bases HT) --- a_taxees = [] if ventes_ht: a_taxees.append(_ca3_ligne("A1", "Ventes, prestations de services", base=ventes_ht)) 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 dom.items()], "tva_sur_ventes": ventes_tva, "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)