monregistraire/tests/test_api.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

636 lines
26 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Tests de régression — API du gestionnaire d'immobilisations.
Couvre : CRUD actifs, rattachement/détachement, porteur d'étiquette,
mécanique DPA (demi-année, perte finale, récupération), export CSV,
rapport fiscal, sauvegardes, factures jointes, seed-demo.
Exécuter : .venv/bin/pytest tests/ -v
"""
import io
import pytest
from fastapi.testclient import TestClient
from app.main import app
client = TestClient(app)
@pytest.fixture(autouse=True)
def reset_db():
"""Base et registre vides avant chaque test."""
client.post("/api/reset")
yield
client.post("/api/reset")
# ─── Helpers ────────────────────────────────────────────────────────────────
def _newest(assets: list) -> dict:
"""Retourne l'actif avec le plus grand id (le dernier créé)."""
return max(assets, key=lambda a: a["id"])
def _make_parent(name="Parent", cca=50) -> dict:
r = client.post("/api/assets", json={"type": "parent", "name": name, "cca_class_id": cca})
return _newest(r.json()["assets"])
def _make_piece(name="Pièce", cost=500.0, acq=None, cca=50) -> dict:
body = {"type": "piece", "name": name, "cost_cad": cost, "cca_class_id": cca}
if acq:
body["acquisition_date"] = acq
r = client.post("/api/assets", json=body)
return _newest(r.json()["assets"])
# ═══════════════════════════════════════════════════════════
# CRUD — actifs
# ═══════════════════════════════════════════════════════════
def test_create_parent():
r = client.post("/api/assets", json={"type": "parent", "name": "Srv Test", "cca_class_id": 50})
assert r.status_code == 200
d = _newest(r.json()["assets"])
assert d["type"] == "parent"
assert d["name"] == "Srv Test"
assert d["view_cpa"]["cca_class"]["id"] == 50
def test_create_piece():
r = client.post("/api/assets", json={"type": "piece", "name": "RAM Test", "cost_cad": 420.0})
assert r.status_code == 200
d = _newest(r.json()["assets"])
assert d["type"] == "piece"
assert float(d["view_cpa"]["capitalizable_base_cad"]) == pytest.approx(420.0)
def test_list_assets():
client.post("/api/assets", json={"type": "parent", "name": "P1"})
client.post("/api/assets", json={"type": "piece", "name": "PC1", "cost_cad": 100.0})
r = client.get("/api/assets")
assert r.status_code == 200
assert len(r.json()) == 2
def test_get_asset():
aid = _make_piece()["id"]
r = client.get(f"/api/assets/{aid}")
assert r.status_code == 200
assert r.json()["id"] == aid
def test_get_asset_not_found():
r = client.get("/api/assets/99999")
assert r.status_code == 404
def test_patch_name():
aid = _make_piece(name="Ancien")["id"]
r = client.patch(f"/api/assets/{aid}", json={"changes": {"name": "Nouveau"}})
assert r.status_code == 200
updated = next(a for a in r.json()["assets"] if a["id"] == aid)
assert updated["name"] == "Nouveau"
def test_patch_cost():
aid = _make_piece(cost=100.0)["id"]
r = client.patch(f"/api/assets/{aid}", json={"changes": {"view_cpa.capitalizable_base_cad": 999.0}})
assert r.status_code == 200
updated = next(a for a in r.json()["assets"] if a["id"] == aid)
assert float(updated["view_cpa"]["capitalizable_base_cad"]) == pytest.approx(999.0)
def test_delete_piece():
aid = _make_piece()["id"]
r = client.delete(f"/api/assets/{aid}")
assert r.status_code == 200
assert not any(a["id"] == aid for a in r.json()["assets"])
def test_delete_parent_frees_pieces():
"""Supprimer un parent libère ses pièces (elles deviennent libres)."""
p = _make_parent()
pc = _make_piece(cost=100.0)
client.post(f"/api/assets/{pc['id']}/attach", json={"parent_id": p["id"]})
r = client.delete(f"/api/assets/{p['id']}")
assert r.status_code == 200
assets = r.json()["assets"]
child = next(a for a in assets if a["id"] == pc["id"])
assert child["parent_id"] is None
# ═══════════════════════════════════════════════════════════
# Rattachement / détachement
# ═══════════════════════════════════════════════════════════
def test_attach_updates_parent_base():
"""La base amortissable du parent = somme exacte des coûts de ses pièces."""
p = _make_parent()
pc1 = _make_piece(cost=1000.0)
pc2 = _make_piece(cost=500.0)
client.post(f"/api/assets/{pc1['id']}/attach", json={"parent_id": p["id"]})
client.post(f"/api/assets/{pc2['id']}/attach", json={"parent_id": p["id"]})
r = client.get(f"/api/assets/{p['id']}")
assert float(r.json()["view_cpa"]["capitalizable_base_cad"]) == pytest.approx(1500.0)
def test_detach_free():
p = _make_parent()
pc = _make_piece()
client.post(f"/api/assets/{pc['id']}/attach", json={"parent_id": p["id"]})
r = client.post(f"/api/assets/{pc['id']}/detach", json={"disposition": "free"})
assert r.status_code == 200
detached = next(a for a in r.json()["assets"] if a["id"] == pc["id"])
assert detached["parent_id"] is None
def test_detach_scrap():
p = _make_parent()
pc = _make_piece()
client.post(f"/api/assets/{pc['id']}/attach", json={"parent_id": p["id"]})
r = client.post(f"/api/assets/{pc['id']}/detach", json={"disposition": "scrap"})
assert r.status_code == 200
scrapped = next(a for a in r.json()["assets"] if a["id"] == pc["id"])
assert scrapped["view_admin"]["operational_status"] == "Rebut"
assert scrapped["parent_id"] is None
def test_detach_reduces_parent_base():
p = _make_parent()
pc1 = _make_piece(cost=1000.0)
pc2 = _make_piece(cost=400.0)
client.post(f"/api/assets/{pc1['id']}/attach", json={"parent_id": p["id"]})
client.post(f"/api/assets/{pc2['id']}/attach", json={"parent_id": p["id"]})
client.post(f"/api/assets/{pc2['id']}/detach", json={"disposition": "free"})
r = client.get(f"/api/assets/{p['id']}")
assert float(r.json()["view_cpa"]["capitalizable_base_cad"]) == pytest.approx(1000.0)
def test_set_tag_bearer():
p = _make_parent()
pc = _newest(client.post("/api/assets", json={
"type": "piece", "name": "Porteur", "cost_cad": 500.0, "inventory_tag": "TAG-001"
}).json()["assets"])
client.post(f"/api/assets/{pc['id']}/attach", json={"parent_id": p["id"]})
r = client.post(f"/api/assets/{pc['id']}/set-tag-bearer")
assert r.status_code == 200
bearer = next(a for a in r.json()["assets"] if a["id"] == pc["id"])
assert bearer["view_admin"]["is_tag_bearer"] is True
def test_tag_bearer_unique_per_parent():
"""Désigner un nouveau porteur retire le statut à l'ancien."""
p = _make_parent()
pc1 = _newest(client.post("/api/assets", json={
"type": "piece", "name": "A", "cost_cad": 100.0, "inventory_tag": "T1"
}).json()["assets"])
pc2 = _newest(client.post("/api/assets", json={
"type": "piece", "name": "B", "cost_cad": 200.0, "inventory_tag": "T2"
}).json()["assets"])
client.post(f"/api/assets/{pc1['id']}/attach", json={"parent_id": p["id"]})
client.post(f"/api/assets/{pc2['id']}/attach", json={"parent_id": p["id"]})
client.post(f"/api/assets/{pc1['id']}/set-tag-bearer")
r = client.post(f"/api/assets/{pc2['id']}/set-tag-bearer")
assets = r.json()["assets"]
assert next(a for a in assets if a["id"] == pc1["id"])["view_admin"]["is_tag_bearer"] is False
assert next(a for a in assets if a["id"] == pc2["id"])["view_admin"]["is_tag_bearer"] is True
# ═══════════════════════════════════════════════════════════
# Catégories DPA
# ═══════════════════════════════════════════════════════════
def test_cca_categories():
r = client.get("/api/cca-categories")
assert r.status_code == 200
ids = [c["id"] for c in r.json()]
for expected in (50, 8, 10, 12, 46):
assert expected in ids
def test_cca_categories_have_rate():
cats = client.get("/api/cca-categories").json()
for c in cats:
assert 0 < c["dpa_rate"] <= 1.0
# ═══════════════════════════════════════════════════════════
# Registre pool DPA — mécanique fiscale
# ═══════════════════════════════════════════════════════════
def test_pool_demi_annee_standard():
"""Première année, ajouts = 10 000 $, coeff 0,5 → DPA = 2 750 $ (Cat. 50)."""
r = client.post("/api/cca-registry/year", json={
"year": 2024, "cca_class_id": 50,
"additions_override": 10_000.0, "dispositions": 0,
"adjustment_coeff": 0.5, "opening_ucc_override": 0,
})
assert r.status_code == 200
y = next(yr for yr in r.json()["years"] if yr["year"] == 2024)
assert float(y["additions"]) == pytest.approx(10_000.0)
assert float(y["adjustment"]) == pytest.approx(5_000.0)
assert float(y["base_for_cca"]) == pytest.approx(5_000.0)
assert float(y["cca_claimed"]) == pytest.approx(2_750.0) # 5 000 × 0.55
assert float(y["closing_ucc"]) == pytest.approx(7_250.0)
def test_pool_annee_pleine():
"""Deuxième année sans acquisitions → DPA pleine sur le solde résiduel."""
client.post("/api/cca-registry/year", json={
"year": 2023, "cca_class_id": 50,
"additions_override": 10_000.0, "adjustment_coeff": 0.5,
"opening_ucc_override": 0,
})
r = client.post("/api/cca-registry/year", json={
"year": 2024, "cca_class_id": 50,
"additions_override": 0, "adjustment_coeff": 1.0,
})
y = next(yr for yr in r.json()["years"] if yr["year"] == 2024)
# Ouverture 2024 = 7 250 $ (clôture 2023) → DPA = 7 250 × 0.55 = 3 987.50
assert float(y["opening_ucc"]) == pytest.approx(7_250.0)
assert float(y["cca_claimed"]) == pytest.approx(3_987.50)
def test_pool_recapture():
"""Produit de cession > FNACC résiduelle → récupération d'amortissement."""
client.post("/api/cca-registry/year", json={
"year": 2022, "cca_class_id": 10,
"additions_override": 0.0, "dispositions": 0,
"adjustment_coeff": 0.5, "opening_ucc_override": 3_000.0,
})
client.post("/api/cca-registry/year", json={
"year": 2023, "cca_class_id": 10,
"additions_override": 0, "dispositions": 0, "adjustment_coeff": 1.0,
})
# Vente à 2 500 $, FNACC < 2 500 → récupération
r = client.post("/api/cca-registry/year", json={
"year": 2024, "cca_class_id": 10,
"additions_override": 0, "dispositions": 2_500.0,
"adjustment_coeff": 1.0, "pool_physically_empty": True,
})
y = next(yr for yr in r.json()["years"] if yr["year"] == 2024)
assert float(y["recapture"]) > 0, "Récupération attendue"
assert float(y["terminal_loss"]) == 0
assert float(y["closing_ucc"]) == 0
def test_pool_perte_finale():
"""Catégorie physiquement vide avec FNACC positive → perte finale."""
client.post("/api/cca-registry/year", json={
"year": 2024, "cca_class_id": 12,
"additions_override": 3_000.0, "dispositions": 0,
"adjustment_coeff": 0.5, "opening_ucc_override": 0,
})
r = client.post("/api/cca-registry/year", json={
"year": 2025, "cca_class_id": 12,
"additions_override": 0, "dispositions": 0,
"adjustment_coeff": 1.0, "pool_physically_empty": True,
})
y = next(yr for yr in r.json()["years"] if yr["year"] == 2025)
assert float(y["terminal_loss"]) > 0, "Perte finale attendue"
assert float(y["cca_claimed"]) == 0
assert float(y["closing_ucc"]) == 0
def test_pool_progression_continue():
"""La FNACC de clôture d'une année est l'ouverture de la suivante."""
for yr, coeff in [(2022, 0.5), (2023, 1.0), (2024, 1.0)]:
client.post("/api/cca-registry/year", json={
"year": yr, "cca_class_id": 50,
"additions_override": 0, "dispositions": 0,
"adjustment_coeff": coeff,
**({"opening_ucc_override": 20_000.0} if yr == 2022 else {}),
})
r = client.get("/api/cca-registry?cca_class_id=50")
years = {y["year"]: y for y in r.json()["years"]}
assert float(years[2022]["closing_ucc"]) == pytest.approx(float(years[2023]["opening_ucc"]))
assert float(years[2023]["closing_ucc"]) == pytest.approx(float(years[2024]["opening_ucc"]))
def test_delete_registry_year():
client.post("/api/cca-registry/year", json={"year": 2024, "cca_class_id": 50})
r = client.delete("/api/cca-registry/year/2024?cca_class_id=50")
assert r.status_code == 200
assert not any(y["year"] == 2024 for y in r.json()["years"])
# ═══════════════════════════════════════════════════════════
# Annexe 8 / Schedule 8
# ═══════════════════════════════════════════════════════════
def test_annexe8_empty():
r = client.get("/api/annexe8?year=2024")
assert r.status_code == 200
assert r.json()["classes"] == []
def test_annexe8_calcul_dpa_cat8():
"""Annexe 8 utilise le bon taux de la catégorie (Cat. 8 = 20 %, pas 55 %)."""
p = _make_parent(cca=8)
pc = _make_piece(cost=10_000.0, acq="2024-01-01", cca=8)
client.post(f"/api/assets/{pc['id']}/attach", json={"parent_id": p["id"]})
client.post("/api/cca-registry/year", json={
"year": 2024, "cca_class_id": 8, "dispositions": 0, "adjustment_coeff": 0.5,
})
r = client.get("/api/annexe8?year=2024")
cls8 = next(c for c in r.json()["classes"] if c["cca_class_id"] == 8)
y = cls8["year_data"]
# adj = 0.5 × 10 000 = 5 000 ; base = 5 000 ; DPA = 5 000 × 0.20 = 1 000
assert float(y["cca_claimed"]) == pytest.approx(1_000.0)
assert float(y["closing_ucc"]) == pytest.approx(9_000.0)
def test_annexe8_cat50_calcul():
p = _make_parent(cca=50)
pc = _make_piece(cost=20_000.0, acq="2023-03-01", cca=50)
client.post(f"/api/assets/{pc['id']}/attach", json={"parent_id": p["id"]})
client.post("/api/cca-registry/year", json={
"year": 2023, "cca_class_id": 50, "dispositions": 0, "adjustment_coeff": 0.5,
})
r = client.get("/api/annexe8?year=2023")
cls50 = next(c for c in r.json()["classes"] if c["cca_class_id"] == 50)
y = cls50["year_data"]
assert float(y["additions"]) == pytest.approx(20_000.0)
assert float(y["cca_claimed"]) == pytest.approx(5_500.0) # 10 000 × 0.55
# ═══════════════════════════════════════════════════════════
# Export fiscal
# ═══════════════════════════════════════════════════════════
def test_fiscal_export_base_correcte():
p = _make_parent(cca=50)
for cost in (5_000.0, 1_500.0, 300.0):
pc = _make_piece(cost=cost)
client.post(f"/api/assets/{pc['id']}/attach", json={"parent_id": p["id"]})
r = client.get("/api/fiscal-export")
assert r.status_code == 200
cls = next(c for c in r.json()["classes"] if c["cca_class_id"] == 50)
assert float(cls["total_base_cad"]) == pytest.approx(6_800.0)
def test_fiscal_export_plusieurs_categories():
for cca in (50, 8, 46):
p = _make_parent(cca=cca)
pc = _make_piece(cost=1_000.0, cca=cca)
client.post(f"/api/assets/{pc['id']}/attach", json={"parent_id": p["id"]})
r = client.get("/api/fiscal-export")
ids = [c["cca_class_id"] for c in r.json()["classes"]]
assert 50 in ids and 8 in ids and 46 in ids
def test_fiscal_export_csv():
r = client.get("/api/fiscal-export.csv")
assert r.status_code == 200
assert "text/csv" in r.headers["content-type"]
def test_annexe8_csv():
r = client.get("/api/annexe8.csv?year=2024")
assert r.status_code == 200
assert "text/csv" in r.headers["content-type"]
def test_cca_registry_csv():
r = client.get("/api/cca-registry.csv?cca_class_id=50")
assert r.status_code == 200
assert "text/csv" in r.headers["content-type"]
# ═══════════════════════════════════════════════════════════
# Rapport fiscal (Schedule 1 / Annexe 1)
# ═══════════════════════════════════════════════════════════
def test_rapport_fiscal_structure():
r = client.get("/api/rapport-fiscal?year=2024")
assert r.status_code == 200
d = r.json()
assert d["year"] == 2024
s1 = d["schedule1"]
for key in ("book_deprec_cad", "cca_claimed_cad", "recapture_cad", "terminal_loss_cad"):
assert key in s1
def test_rapport_fiscal_html():
r = client.get("/api/rapport-fiscal.html?year=2024")
assert r.status_code == 200
assert "text/html" in r.headers["content-type"]
assert "Chezlepro" in r.text
def test_rapport_schedule1_cca_coherent():
"""DPA du Schedule 1 = DPA totale de l'Annexe 8."""
p = _make_parent(cca=50)
pc = _make_piece(cost=10_000.0, acq="2024-01-01", cca=50)
client.post(f"/api/assets/{pc['id']}/attach", json={"parent_id": p["id"]})
client.post("/api/cca-registry/year", json={
"year": 2024, "cca_class_id": 50, "dispositions": 0, "adjustment_coeff": 0.5,
})
rapport = client.get("/api/rapport-fiscal?year=2024").json()
annexe8 = client.get("/api/annexe8?year=2024").json()
total_dpa_a8 = sum(
float(c["year_data"]["cca_claimed"])
for c in annexe8["classes"] if c["year_data"]
)
assert rapport["schedule1"]["cca_claimed_cad"] == pytest.approx(total_dpa_a8)
# ═══════════════════════════════════════════════════════════
# Sauvegardes et restauration
# ═══════════════════════════════════════════════════════════
def test_create_and_list_backup():
_make_piece(name="Avant backup", cost=999.0)
r = client.post("/api/backup")
assert r.status_code == 200
filename = r.json()["filename"]
assert filename.startswith("immos_") and filename.endswith(".db")
r2 = client.get("/api/backups")
assert any(b["filename"] == filename for b in r2.json())
def test_restore_backup():
_make_piece(name="Avant backup", cost=999.0)
filename = client.post("/api/backup").json()["filename"]
client.post("/api/reset")
assert client.get("/api/assets").json() == []
r = client.post("/api/restore", json={"filename": filename})
assert r.status_code == 200
restored = client.get("/api/assets").json()
assert any(a["name"] == "Avant backup" for a in restored)
def test_restore_invalide():
r = client.post("/api/restore", json={"filename": "../../etc/passwd"})
assert r.status_code == 400
r2 = client.post("/api/restore", json={"filename": "immos_1234-99-99_99-99-99.db"})
assert r2.status_code == 404
# ═══════════════════════════════════════════════════════════
# Reset — vide actifs ET registre
# ═══════════════════════════════════════════════════════════
def test_reset_vide_actifs():
_make_piece()
client.post("/api/reset")
assert client.get("/api/assets").json() == []
def test_reset_vide_registre():
"""Après reset, le registre ne conserve aucune année."""
client.post("/api/cca-registry/year", json={
"year": 2024, "cca_class_id": 50, "additions_override": 1_000.0
})
client.post("/api/reset")
r = client.get("/api/cca-registry?cca_class_id=50")
assert r.json()["years"] == []
# ═══════════════════════════════════════════════════════════
# Factures jointes
# ═══════════════════════════════════════════════════════════
def test_invoice_upload_et_suppression():
contenu = b"%PDF-1.4 fichier test"
files = {"file": ("facture.pdf", io.BytesIO(contenu), "application/pdf")}
r = client.post("/api/invoices", files=files)
assert r.status_code == 200
file_id = r.json()["file_id"]
assert file_id.endswith(".pdf")
assert len(file_id) == 36 # 32 hex + ".pdf"
r2 = client.get(f"/api/invoices/{file_id}")
assert r2.status_code == 200
assert r2.content == contenu
client.delete(f"/api/invoices/{file_id}")
assert client.get(f"/api/invoices/{file_id}").status_code == 404
def test_invoice_upload_image():
contenu = b"\xff\xd8\xff\xe0" # en-tête JPEG minimal
files = {"file": ("photo.jpg", io.BytesIO(contenu), "image/jpeg")}
r = client.post("/api/invoices", files=files)
assert r.status_code == 200
assert r.json()["file_id"].endswith(".jpg")
def test_invoice_extension_invalide():
files = {"file": ("script.exe", io.BytesIO(b"MZ"), "application/octet-stream")}
r = client.post("/api/invoices", files=files)
assert r.status_code == 400
def test_invoice_path_traversal():
# file_id avec des caractères invalides (non-hex) → rejeté par la validation ou le routage
r = client.get("/api/invoices/INVALID_FILE_ID_WITH_UPPER_CASE.pdf")
assert r.status_code in (400, 404, 422)
def test_invoice_inexistant():
r = client.get("/api/invoices/" + "a" * 32 + ".pdf")
assert r.status_code == 404
# ═══════════════════════════════════════════════════════════
# Données de démonstration — seed-demo
# ═══════════════════════════════════════════════════════════
def test_seed_demo_actifs():
r = client.post("/api/seed-demo")
assert r.status_code == 200
assets = r.json()["assets"]
parents = [a for a in assets if a["type"] == "parent"]
pieces = [a for a in assets if a["type"] == "piece"]
assert len(parents) >= 6, "Au moins 6 immobilisations attendues"
assert len(pieces) >= 10, "Au moins 10 pièces attendues"
# Plusieurs catégories DPA
cat_ids = {a["view_cpa"]["cca_class"]["id"] for a in parents}
assert len(cat_ids) >= 4, f"Au moins 4 catégories attendues, obtenu : {cat_ids}"
# Pièces au rebut
rebuts = [a for a in pieces if a["view_admin"]["operational_status"] == "Rebut"]
assert len(rebuts) >= 2, "Au moins 2 pièces au rebut (Veeam + Broadcom)"
# Porteurs d'étiquette
bearers = [a for a in pieces if a["view_admin"].get("is_tag_bearer")]
assert len(bearers) >= 4, "Un porteur par immobilisation active"
# Pièces libres (stock)
libres = [a for a in pieces if a["parent_id"] is None
and a["view_admin"]["operational_status"] != "Rebut"]
assert len(libres) >= 2, "Des pièces de rechange en stock"
def test_seed_demo_registre_cat50():
client.post("/api/seed-demo")
r = client.get("/api/cca-registry?cca_class_id=50")
assert r.status_code == 200
assert len(r.json()["years"]) >= 3, "3 exercices pour Cat. 50 (2023-2025)"
def test_seed_demo_perte_finale_cat12():
"""Cat. 12 — perte finale en 2025 : licence Veeam non renouvelée."""
client.post("/api/seed-demo")
r = client.get("/api/annexe8?year=2025")
cls12 = next((c for c in r.json()["classes"] if c["cca_class_id"] == 12), None)
assert cls12 is not None, "Cat. 12 doit apparaître dans l'Annexe 8"
y = cls12["year_data"]
assert y is not None
assert float(y["terminal_loss"]) > 0, "Perte finale attendue en 2025 (Cat. 12)"
def test_seed_demo_recuperation_cat10():
"""Cat. 10 — récupération en 2024 : IBM vendu pour plus que la FNACC."""
client.post("/api/seed-demo")
r = client.get("/api/annexe8?year=2024")
cls10 = next((c for c in r.json()["classes"] if c["cca_class_id"] == 10), None)
assert cls10 is not None, "Cat. 10 doit apparaître dans l'Annexe 8"
y = cls10["year_data"]
assert y is not None
assert float(y["recapture"]) > 0, "Récupération attendue en 2024 (Cat. 10)"
def test_seed_demo_bases_coherentes():
"""La base de chaque parent = somme des coûts de ses pièces rattachées."""
client.post("/api/seed-demo")
assets = client.get("/api/assets").json()
pieces_by_parent: dict[int, list[float]] = {}
for a in assets:
if a["type"] == "piece" and a["parent_id"] is not None:
pieces_by_parent.setdefault(a["parent_id"], []).append(
float(a["view_cpa"]["capitalizable_base_cad"])
)
for a in assets:
if a["type"] != "parent" or a["id"] not in pieces_by_parent:
continue
expected = sum(pieces_by_parent[a["id"]])
actual = float(a["view_cpa"]["capitalizable_base_cad"])
assert actual == pytest.approx(expected, rel=1e-4), (
f"Base incohérente pour « {a['name']} » : {actual}{expected}"
)
def test_seed_double_idempotent():
"""Deux appels seed-demo successifs ne provoquent pas d'erreur."""
assert client.post("/api/seed-demo").status_code == 200
assert client.post("/api/seed-demo").status_code == 200