""" Module fiscal — Registre de catégorie (pool global), ex. Catégorie 50. L'actif fiscal est un FLUX, pas un objet. La DPA se calcule sur le solde global de la catégorie (FNACC), pas pièce par pièce. Mécanique annuelle (déterministe) : FNACC_ouverture + ajouts_annee - dispositions_annee (produit de disposition, souvent 0 $) = solde_avant_ajustement - ajustement (coeff * ajouts_nets) ; demi-année => coeff 0.5 = base_calcul * taux (0.55) = DPA_annee (bornée >= 0) FNACC_cloture = solde_avant_ajustement - DPA_annee Perte finale : si l'inventaire PHYSIQUE de la catégorie est vide (plus aucun actif) mais que la FNACC reste positive, ce solde devient une perte finale déductible à 100 % (DPA = solde, FNACC clôture = 0). Récupération d'amortissement : si la FNACC devient négative (dispositions > solde), ce négatif est un revenu (récupération) ; la FNACC clôture est ramenée à 0 et le montant est signalé. Le coefficient d'ajustement de l'année d'acquisition est PARAMÉTRABLE (demi-année standard = 0.5 ; incitatif accéléré éventuel saisi par l'usager sur avis de son CPA). Aucun taux d'accélération n'est câblé en dur. """ from __future__ import annotations from dataclasses import dataclass, field from decimal import Decimal, ROUND_HALF_UP TWOPLACES = Decimal("0.01") def _q(x: Decimal) -> Decimal: return x.quantize(TWOPLACES, rounding=ROUND_HALF_UP) @dataclass class YearResult: year: int opening_ucc: Decimal # FNACC d'ouverture additions: Decimal # ajouts de l'année dispositions: Decimal # produit des dispositions de l'année adjustment_coeff: Decimal # coeff sur ajouts nets (0.5 = demi-année) adjustment: Decimal # montant retiré de la base de calcul cca_rate: Decimal # 0.55 base_for_cca: Decimal # base après ajustement cca_claimed: Decimal # DPA de l'année closing_ucc: Decimal # FNACC de clôture recapture: Decimal # récupération (revenu) si > 0 terminal_loss: Decimal # perte finale (déductible) si > 0 note: str = "" def to_dict(self) -> dict: return { "year": self.year, "opening_ucc": str(self.opening_ucc), "additions": str(self.additions), "dispositions": str(self.dispositions), "adjustment_coeff": str(self.adjustment_coeff), "adjustment": str(self.adjustment), "cca_rate": str(self.cca_rate), "base_for_cca": str(self.base_for_cca), "cca_claimed": str(self.cca_claimed), "closing_ucc": str(self.closing_ucc), "recapture": str(self.recapture), "terminal_loss": str(self.terminal_loss), "note": self.note, } @dataclass class YearInput: year: int additions: Decimal = Decimal("0") dispositions: Decimal = Decimal("0") adjustment_coeff: Decimal = Decimal("0.5") # demi-année par défaut pool_physically_empty: bool = False # plus aucun actif physique claim_full_cca: bool = True # demander la DPA max cette année def compute_pool_year(opening_ucc: Decimal, yi: YearInput, cca_rate: Decimal = Decimal("0.55")) -> YearResult: """Calcule une année du pool de façon déterministe.""" opening = _q(opening_ucc) additions = _q(yi.additions) dispositions = _q(yi.dispositions) solde_avant = opening + additions - dispositions recapture = Decimal("0") terminal_loss = Decimal("0") note = "" # Récupération : solde négatif et il reste (ou non) des actifs. if solde_avant < 0: recapture = -solde_avant result = YearResult( year=yi.year, opening_ucc=opening, additions=additions, dispositions=dispositions, adjustment_coeff=yi.adjustment_coeff, adjustment=Decimal("0.00"), cca_rate=cca_rate, base_for_cca=Decimal("0.00"), cca_claimed=Decimal("0.00"), closing_ucc=Decimal("0.00"), recapture=_q(recapture), terminal_loss=Decimal("0.00"), note="Solde négatif : récupération d'amortissement (revenu imposable). FNACC ramenée à 0.", ) return result # Perte finale : plus d'actif physique mais solde positif. if yi.pool_physically_empty and solde_avant > 0: terminal_loss = solde_avant return YearResult( year=yi.year, opening_ucc=opening, additions=additions, dispositions=dispositions, adjustment_coeff=yi.adjustment_coeff, adjustment=Decimal("0.00"), cca_rate=cca_rate, base_for_cca=Decimal("0.00"), cca_claimed=Decimal("0.00"), closing_ucc=Decimal("0.00"), recapture=Decimal("0.00"), terminal_loss=_q(terminal_loss), note="Catégorie vidée physiquement avec solde positif : perte finale déductible à 100 %.", ) # Ajustement de l'année d'acquisition sur les ajouts NETS. net_additions = additions - dispositions if net_additions < 0: net_additions = Decimal("0") # pas d'ajustement négatif adjustment = _q(yi.adjustment_coeff * net_additions) base = solde_avant - adjustment if base < 0: base = Decimal("0") cca = _q(base * cca_rate) if yi.claim_full_cca else Decimal("0.00") # La DPA ne peut excéder le solde disponible. if cca > solde_avant: cca = solde_avant closing = _q(solde_avant - cca) return YearResult( year=yi.year, opening_ucc=opening, additions=additions, dispositions=dispositions, adjustment_coeff=yi.adjustment_coeff, adjustment=adjustment, cca_rate=cca_rate, base_for_cca=_q(base), cca_claimed=cca, closing_ucc=closing, recapture=Decimal("0.00"), terminal_loss=Decimal("0.00"), note=note or "Calcul standard : DPA dégressive sur le solde global.", ) def compute_pool_timeline(years: list[YearInput], opening_ucc: Decimal = Decimal("0"), cca_rate: Decimal = Decimal("0.55")) -> list[YearResult]: """Enchaîne plusieurs années : la clôture d'une année ouvre la suivante.""" results: list[YearResult] = [] ucc = opening_ucc for yi in sorted(years, key=lambda y: y.year): r = compute_pool_year(ucc, yi, cca_rate) results.append(r) ucc = r.closing_ucc return results