monregistraire/app/cca_pool.py
Daniel Allaire b150b08c6e Produit v1.1 — Mon Registraire : page de bienvenue, À propos, citations législatives
UX :
- Page de bienvenue au lancement (WelcomeModal) : trois personas (dirigeant,
  CPA, auditeur) avec codes couleur, liste des capacités et avertissements
- Case "Ne plus afficher" persistée dans localStorage (clé immos_welcome_v1)
- Bouton "En savoir plus →" ouvre static/apropos.html dans un nouvel onglet
- Nom de produit : Mon Registraire (titre, onglets, toutes les pages statiques)

Documentation :
- Nouvelle page static/apropos.html : intention, pour qui, pas pour qui,
  capacités détaillées (3 tableaux), limitations (6 cartes) — même style
  visuel que aide.html, navigation latérale avec scroll actif

Conformité fiscale :
- cca_pool.py : bloc de références législatives dans le docstring
  (LIR 20(1)(a), 13(21), 13(1), 20(16) ; Règl. 1100(1)/(2) ; Règl. Annexe II)
  + 3 commentaires ciblés sur la récupération, la perte finale et la règle
  de la demi-année
- cca_registry.py : bloc de références (LIR 13(21), Règl. Partie XI, Guide T2 T4012)
- store.py : références Règl. Annexe II sur le tableau CCA_CATEGORIES et
  Règl. 1100(1)/(2) sur le calcul FNACC indicatif par pièce

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-10 15:26:38 -04:00

178 lines
7.4 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.

"""
Module fiscal — Mécanique du pool de catégorie (DPA dégressive).
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.
Références législatives :
- LIR 20(1)(a) — Déduction pour amortissement du revenu d'entreprise ou de bien
- LIR 13(21) — Définition de la FNACC (fraction non amortie du coût en capital)
- LIR 13(1) — Récupération d'amortissement (inclusion dans le revenu)
- LIR 20(16) — Perte finale (déduction du revenu)
- Règl. 1100(1) — Méthode dégressive ; taux par catégorie (Règl. Annexe II)
- Règl. 1100(2) — Règle de la demi-année : ajustement sur ajouts nets de l'année
d'acquisition (coefficient 0,5 par défaut)
Loi de l'impôt sur le revenu (LIR), L.R.C. 1985, ch. 1 (5e suppl.)
Règlement de l'impôt sur le revenu (Règl.), C.R.C., ch. 945
"""
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 = ""
# LIR 13(1) — récupération : le produit de disposition excède la FNACC ;
# le solde négatif est inclus dans le revenu imposable de l'année.
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
# LIR 20(16) — perte finale : catégorie vide physiquement avec FNACC positive ;
# le solde résiduel est déductible en totalité (pas de plafond de taux).
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 %.",
)
# Règl. 1100(2) — règle de la demi-année : l'ajustement (coeff × ajouts nets)
# réduit la base de calcul pour l'année d'acquisition (coeff 0,5 par défaut).
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