""" Algorithme comptable déterministe (Québec/Canada) — Étape 4 validée. process_asset_detachment : impact fiscal du détachement d'un composant enfant de son parent. Aucune décision heuristique : entrées identiques => sortie identique. """ from __future__ import annotations from dataclasses import dataclass from datetime import date from decimal import Decimal, ROUND_HALF_UP from typing import Literal DetachmentReason = Literal["stock", "scrap"] TWOPLACES = Decimal("0.01") def _q(x: Decimal) -> Decimal: """Arrondi monétaire déterministe à 2 décimales (demi vers le haut).""" return x.quantize(TWOPLACES, rounding=ROUND_HALF_UP) @dataclass(frozen=True) class JournalLine: account: str label: str debit: Decimal = Decimal("0.00") credit: Decimal = Decimal("0.00") def to_dict(self) -> dict: return { "account": self.account, "label": self.label, "debit": str(self.debit), "credit": str(self.credit), } @dataclass(frozen=True) class DetachmentResult: child_id: int reason: DetachmentReason detachment_date: str capitalizable_base_cad: Decimal days_in_service: int useful_life_days: int accumulated_depreciation_cad: Decimal net_book_value_cad: Decimal fiscal_cca_taken_indicative_cad: Decimal fiscal_undepreciated_balance_cad: Decimal fiscal_note: str journal_entries: list[JournalLine] parent_capitalizable_base_reduction_cad: Decimal def to_dict(self) -> dict: return { "child_id": self.child_id, "reason": self.reason, "detachment_date": self.detachment_date, "capitalizable_base_cad": str(self.capitalizable_base_cad), "days_in_service": self.days_in_service, "useful_life_days": self.useful_life_days, "accumulated_depreciation_cad": str(self.accumulated_depreciation_cad), "net_book_value_cad": str(self.net_book_value_cad), "fiscal_cca_taken_indicative_cad": str(self.fiscal_cca_taken_indicative_cad), "fiscal_undepreciated_balance_cad": str(self.fiscal_undepreciated_balance_cad), "fiscal_note": self.fiscal_note, "journal_entries": [l.to_dict() for l in self.journal_entries], "parent_capitalizable_base_reduction_cad": str( self.parent_capitalizable_base_reduction_cad ), } # Durée de vie comptable utilisée pour le prorata linéaire (paramétrable). # Catégorie 50 = matériel informatique : 4 ans par défaut. DEFAULT_USEFUL_LIFE_DAYS = 365 * 4 # Comptes (plan comptable interne déterministe) ACCT_ASSET_COST = "1700-Immobilisations-Materiel-Informatique" ACCT_ACCUM_DEPREC = "1709-Amortissement-Cumule-Materiel" ACCT_PARTS_INVENTORY = "1340-Inventaire-Pieces-Detachees" ACCT_LOSS_ON_DISPOSAL = "7200-Perte-sur-Disposition-Actif" def process_asset_detachment( child: dict, detachment_date: date, reason: DetachmentReason, useful_life_days: int = DEFAULT_USEFUL_LIFE_DAYS, ) -> DetachmentResult: """Impact comptable et fiscal (Québec/Canada) du détachement d'un enfant.""" cpa = child["view_cpa"] child_id = int(child["id"]) base = Decimal(str(cpa["capitalizable_base_cad"])) dpa_rate = Decimal(str(cpa["cca_class"]["dpa_rate"])) # 0.55 if base < 0: raise ValueError("Base amortissable négative.") in_service_str = cpa.get("in_service_date") or "" if not in_service_str: # Pièce jamais mise en service : zéro amortissement, VNC = coût intégral. days_in_service = 0 accum_deprec = Decimal("0.00") nbv = base fiscal_cca = Decimal("0.00") fiscal_ucc = base fiscal_note = ( "Pièce retirée sans avoir été mise en service (aucune date de mise en " "service enregistrée). Aucun amortissement comptable ni fiscal appliqué : " "VNC = coût d'acquisition intégral." ) else: in_service = date.fromisoformat(in_service_str) if detachment_date < in_service: raise ValueError("La date de détachement précède la mise en service.") # --- 1. Amortissement comptable au prorata temporis exact (en jours) --- days_in_service = (detachment_date - in_service).days capped_days = min(days_in_service, useful_life_days) accum_deprec = _q(base * Decimal(capped_days) / Decimal(useful_life_days)) nbv = _q(base - accum_deprec) # --- 2. Suivi fiscal indicatif (déclin 55% + règle demi-année) --- fiscal_year_1_base = base / Decimal(2) full_years = capped_days // 365 ucc = base cca_total = Decimal("0") if full_years >= 1: y1 = _q(fiscal_year_1_base * dpa_rate) cca_total += y1 ucc -= y1 for _ in range(1, full_years): yn = _q(ucc * dpa_rate) cca_total += yn ucc -= yn else: y1 = _q(fiscal_year_1_base * dpa_rate) cca_total += y1 ucc -= y1 fiscal_cca = _q(cca_total) fiscal_ucc = _q(ucc) fiscal_note = ( "DPA fiscale calculée à titre indicatif au niveau de la pièce. " "La disposition réelle (produit vs FNACC de la Catégorie 50) se traite " "au niveau du solde de catégorie : récupération d'amortissement ou perte " "finale uniquement si la catégorie est vidée. Aucune perte en capital " "sur biens amortissables." ) # --- 3. Écriture de journal selon le motif --- journal: list[JournalLine] = [] if reason == "stock": journal.append(JournalLine(ACCT_PARTS_INVENTORY, "Transfert pièce détachée (VNC)", debit=nbv)) journal.append(JournalLine(ACCT_ACCUM_DEPREC, "Reprise amortissement cumulé", debit=accum_deprec)) journal.append(JournalLine(ACCT_ASSET_COST, "Sortie coût d'origine immobilisation", credit=base)) elif reason == "scrap": journal.append(JournalLine(ACCT_ACCUM_DEPREC, "Reprise amortissement cumulé", debit=accum_deprec)) journal.append(JournalLine(ACCT_LOSS_ON_DISPOSAL, "Perte sèche sur disposition (VNC)", debit=nbv)) journal.append(JournalLine(ACCT_ASSET_COST, "Sortie coût d'origine immobilisation", credit=base)) else: raise ValueError(f"Motif de détachement invalide: {reason}") total_debit = sum((l.debit for l in journal), Decimal("0")) total_credit = sum((l.credit for l in journal), Decimal("0")) if _q(total_debit) != _q(total_credit): raise ArithmeticError(f"Écriture déséquilibrée: D={total_debit} C={total_credit}") # --- 4. Impact sur le parent --- parent_reduction = base return DetachmentResult( child_id=child_id, reason=reason, detachment_date=detachment_date.isoformat(), capitalizable_base_cad=base, days_in_service=days_in_service, useful_life_days=useful_life_days, accumulated_depreciation_cad=accum_deprec, net_book_value_cad=nbv, fiscal_cca_taken_indicative_cad=fiscal_cca, fiscal_undepreciated_balance_cad=fiscal_ucc, fiscal_note=fiscal_note, journal_entries=journal, parent_capitalizable_base_reduction_cad=parent_reduction, )