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>
163 lines
6.4 KiB
Python
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
|