#!/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()