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>
241 lines
11 KiB
Python
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()
|