monregistraire/app/cca_pool.py
Daniel Allaire be7a64a6cc Lancement v1.0 — Gestionnaire d'immobilisations fiscales (Chezlepro Inc.)
Application web locale (FastAPI + SQLite + React SPA) pour la gestion des
immobilisations fiscales d'une PME québécoise (T2 / CO-17).

─── Modèle de données ───────────────────────────────────────────────────────
• Deux niveaux : PARENT (immobilisation fiscale) + PIÈCE (inventaire physique)
• Trois vues par actif : Propriétaire, CPA, Auditeur
• Porteur d'étiquette d'assemblage (unique par immobilisation)
• Piste d'audit journalisée sur chaque modification

─── Mécanique fiscale ───────────────────────────────────────────────────────
• Pool DPA par catégorie (Cat. 8, 10, 12, 46, 50) — amortissement dégressif
• Règle de la demi-année paramétrable (coeff. 0..1 par exercice)
• Perte finale et récupération d'amortissement calculées automatiquement
• Annexe 8 / Schedule 8 multi-catégories (JSON + CSV téléchargeable)
• Rapport fiscal HTML imprimable — lignes Schedule 1 (T2 : 104/107/410/414 ;
  CO-17 : 130/135/150/155)
• Export immobilisations par catégorie (JSON + CSV)

─── Fonctionnalités ─────────────────────────────────────────────────────────
• CRUD complet : parents et pièces, rattachement/détachement (libre/stock/rebut)
• Écriture comptable générée au détachement
• Registre de pool DPA multi-exercices avec override manuel
• Sauvegarde/restauration (SQLite native backup, horodatée)
• Factures jointes aux pièces (PDF/JPG/PNG, max 20 Mo, UUID + regex)
• UI mobile-first : bottom nav, FAB, bottom sheet, safe-area iPhone
• Guide utilisateur complet in-app (aide.html, 11 sections)
• Données de démonstration riches (6 immos, 5 catégories, perte finale +
  récupération incluses)

─── Qualité ─────────────────────────────────────────────────────────────────
• 50 tests pytest — 100 % verts (calculs DPA, export, backup, factures, seed)
• Lancement en 1 commande : ./lancer.sh (Mac/Linux) ou lancer.bat (Windows)

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

163 lines
6.4 KiB
Python

"""
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