monregistraire/scripts/import_config.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

241 lines
11 KiB
Python

#!/usr/bin/env python3
"""
Script d'import de configuration d'immobilisations fiscales.
Usage (depuis la racine du projet) :
python3 scripts/import_config.py
Par défaut le script écrit dans immos_test.db.
Après validation, changer DB_FILE en "immos.db".
"""
import sys
from pathlib import Path
# Garantit que la racine du projet est dans sys.path, peu importe d'où le
# script est appelé.
ROOT = Path(__file__).resolve().parent.parent
if str(ROOT) not in sys.path:
sys.path.insert(0, str(ROOT))
from app.store import AssetStore # noqa: E402
# ── BASE DE TEST ─────────────────────────────────────────────────────────────
# Changer en ROOT / "immos.db" uniquement après validation avec la base test.
DB_FILE = ROOT / "immos_test.db"
store = AssetStore(db_path=DB_FILE)
# ── CONFIGURATION DES IMMOBILISATIONS ────────────────────────────────────────
# Structure :
# "asset" : champs du PARENT (immobilisation fiscale)
# "pieces" : liste des pièces à créer et rattacher
#
# Champs reconnus par create_asset :
# Parent : name, cca_class_id, in_service_date, assembler, inventory_tag
# Pièce : name, serial_number, inventory_tag, manufacturer, model,
# form_factor, cost_cad, acquisition_date, supplier, invoice_number,
# replacement_cost_cad, fnacc_override_cad (None = calcul auto)
#
# NOTE : ne saisissez pas de valeur directement sur le parent — sa base fiscale
# est calculée automatiquement comme la somme des coûts des pièces.
# ─────────────────────────────────────────────────────────────────────────────
IMMOBILISATIONS = [
{
"asset": {
"name": "Serveur de calcul DEMO-SRV-01",
"cca_class_id": 50, # Cat. 50 : matériel informatique, 55 %
"in_service_date": "2025-01-15",
"assembler": "Acme Integrators Inc.",
"inventory_tag": "TAG-SRV-001",
},
"pieces": [
{
"name": "CPU Intel Xeon Gold 6338",
"serial_number": "SN-CPU-001",
"manufacturer": "Intel",
"model": "Xeon Gold 6338",
"form_factor": "CPU",
"cost_cad": 4200.00,
"acquisition_date": "2025-01-10",
"supplier": "MicroAge Canada",
"invoice_number": "INV-2025-0042",
"replacement_cost_cad": 4500.00, # prix marché actuel pour ce CPU
},
{
"name": "RAM DDR4 ECC 256 Go (8x32 Go)",
"serial_number": "SN-RAM-001",
"manufacturer": "Samsung",
"model": "M393A4K40DB3",
"form_factor": "DIMM",
"cost_cad": 2800.00,
"acquisition_date": "2025-01-10",
"supplier": "MicroAge Canada",
"invoice_number": "INV-2025-0042",
"replacement_cost_cad": 2600.00, # prix actuel légèrement inférieur
},
{
"name": "SSD NVMe 4 To Samsung 980 Pro",
"serial_number": "SN-SSD-001",
"manufacturer": "Samsung",
"model": "980 Pro 4TB",
"form_factor": "M.2",
"cost_cad": 1100.00,
"acquisition_date": "2025-01-10",
"supplier": "MicroAge Canada",
"invoice_number": "INV-2025-0042",
"replacement_cost_cad": 950.00, # prix en baisse depuis l'achat
},
],
},
{
"asset": {
"name": "Commutateur reseau DEMO-SW-01",
"cca_class_id": 46, # Cat. 46 : infra reseau de donnees, 30 %
"in_service_date": "2025-02-01",
"assembler": "Cisco Reseller Quebec",
"inventory_tag": "TAG-SW-001",
},
"pieces": [
{
"name": "Cisco Catalyst 9300-48P",
"serial_number": "SN-SW-001",
"manufacturer": "Cisco",
"model": "Catalyst 9300-48P",
"form_factor": "1U",
"cost_cad": 8500.00,
"acquisition_date": "2025-01-25",
"supplier": "Cisco Quebec",
"invoice_number": "INV-2025-0055",
"replacement_cost_cad": 9200.00,
},
{
"name": "Alim. redondante Cisco PWR-C1-1100WAC",
"serial_number": "SN-PSU-001",
"manufacturer": "Cisco",
"model": "PWR-C1-1100WAC",
"form_factor": "PSU",
"cost_cad": 750.00,
"acquisition_date": "2025-01-25",
"supplier": "Cisco Quebec",
"invoice_number": "INV-2025-0055",
"replacement_cost_cad": 820.00,
},
],
},
]
# ─────────────────────────────────────────────────────────────────────────────
def _check_duplicates(config: list[dict]) -> list[str]:
"""Retourne les avertissements de doublons potentiels dans la base cible."""
warnings = []
existing = store.all()
existing_names = {a["name"] for a in existing if a["type"] == "parent"}
existing_serials = {
a["view_admin"]["serial_number"]
for a in existing
if a["type"] == "piece" and a["view_admin"].get("serial_number")
}
for entry in config:
aname = entry["asset"]["name"]
if aname in existing_names:
warnings.append(f" PARENT deja present : << {aname} >>")
for piece in entry.get("pieces", []):
sn = piece.get("serial_number", "")
if sn and sn in existing_serials:
warnings.append(
f" PIECE deja presente (S/N {sn}) : {piece['name']}"
)
return warnings
def _import_all(config: list[dict]) -> None:
for entry in config:
# ── Creer le parent ──────────────────────────────────────────────────
a_payload = dict(entry["asset"])
a_payload["type"] = "parent"
parent = store.create_asset(a_payload)
parent_id = parent["id"]
# ── Creer et rattacher chaque piece ──────────────────────────────────
pieces_created = []
for p in entry.get("pieces", []):
p_payload = dict(p)
p_payload["type"] = "piece"
# La piece herite de la categorie DPA du parent.
p_payload["cca_class_id"] = entry["asset"].get("cca_class_id", 50)
piece = store.create_asset(p_payload)
store.attach(piece["id"], parent_id)
pieces_created.append(piece)
# Relit le parent et les pieces pour avoir toutes les valeurs calculees.
parent_updated = store.get(parent_id)
cpa = parent_updated["view_cpa"]
cat = cpa["cca_class"]
adm = parent_updated["view_admin"]
print(f"\n [OK] {parent_updated['name']} (id={parent_id})")
print(f" Categorie DPA : {cat['id']} -- {cat['description']} "
f"({cat['dpa_rate'] * 100:.0f} %)")
print(f" Mise en service : {cpa['in_service_date']}")
print(f" Assembleur : {adm['assembler']}")
print(f" Pieces rattachees : {len(pieces_created)}")
print(f" {'Piece':<42} {'Cout hist.':>11} {'FNACC':>10} {'Rempl.':>10} Override")
print(f" {'-'*42} {'-'*11} {'-'*10} {'-'*10} {'-'*8}")
for pc in pieces_created:
# Relit la piece pour avoir fnacc_cad calcule
pc_full = store.get(pc["id"])
cost = pc_full["view_cpa"]["capitalizable_base_cad"]
fnacc = pc_full["view_cpa"].get("fnacc_cad", 0)
ref_y = pc_full["view_cpa"].get("fnacc_reference_year", "?")
is_ovr = pc_full["view_cpa"].get("fnacc_is_override", False)
repl = pc_full["view_admin"].get("replacement_cost_cad", 0)
sn = pc_full["view_admin"]["serial_number"] or ""
nm = f"{pc_full['name']} [{sn}]"
ovr_flag = f"oui ({ref_y})" if is_ovr else f"auto ({ref_y})"
print(f" {nm:<40} {cost:>11,.2f} {fnacc:>10,.2f} {repl:>10,.2f} {ovr_flag}")
print(f" {'':42} {'-'*11} {'-'*10} {'-'*10}")
print(f" {'TOTAUX':42} {cpa['capitalizable_base_cad']:>11,.2f} "
f"{cpa['net_book_value_cad']:>10,.2f} "
f"{adm['total_replacement_cost_cad']:>10,.2f}")
from datetime import date as _date
ref_label = _date.today().year - 1
print(f" (base fiscale historique / FNACC theorique au 31-12-{ref_label} / cout remplacement)")
def main() -> None:
print()
print("=" * 62)
print(f" Import d'immobilisations -- BASE : {DB_FILE.name}")
print("=" * 62)
# ── Verification des doublons ────────────────────────────────────────────
warnings = _check_duplicates(IMMOBILISATIONS)
if warnings:
print("\n[AVERTISSEMENT -- DOUBLONS POTENTIELS]")
for w in warnings:
print(w)
resp = input("\nContinuer quand meme ? (oui/non) : ").strip().lower()
if resp != "oui":
print("Import annule.")
sys.exit(0)
# ── Import ───────────────────────────────────────────────────────────────
print("\n[IMPORT EN COURS]")
_import_all(IMMOBILISATIONS)
# ── Recapitulatif global ─────────────────────────────────────────────────
all_parents = [a for a in store.all() if a["type"] == "parent"]
total_base = sum(a["view_cpa"]["capitalizable_base_cad"] for a in all_parents)
print()
print("-" * 62)
print(f" Recapitulatif global ({DB_FILE.name})")
print(f" Immobilisations : {len(all_parents)}")
print(f" Base fiscale totale : {total_base:,.2f} $ CAD")
print("=" * 62)
print()
if __name__ == "__main__":
main()