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
<!DOCTYPE html>
< html lang = "fr" >
< head >
< meta charset = "UTF-8" / >
< meta name = "viewport" content = "width=device-width, initial-scale=1.0, viewport-fit=cover" / >
Produit v1.1 — Mon Registraire : page de bienvenue, À propos, citations législatives
UX :
- Page de bienvenue au lancement (WelcomeModal) : trois personas (dirigeant,
CPA, auditeur) avec codes couleur, liste des capacités et avertissements
- Case "Ne plus afficher" persistée dans localStorage (clé immos_welcome_v1)
- Bouton "En savoir plus →" ouvre static/apropos.html dans un nouvel onglet
- Nom de produit : Mon Registraire (titre, onglets, toutes les pages statiques)
Documentation :
- Nouvelle page static/apropos.html : intention, pour qui, pas pour qui,
capacités détaillées (3 tableaux), limitations (6 cartes) — même style
visuel que aide.html, navigation latérale avec scroll actif
Conformité fiscale :
- cca_pool.py : bloc de références législatives dans le docstring
(LIR 20(1)(a), 13(21), 13(1), 20(16) ; Règl. 1100(1)/(2) ; Règl. Annexe II)
+ 3 commentaires ciblés sur la récupération, la perte finale et la règle
de la demi-année
- cca_registry.py : bloc de références (LIR 13(21), Règl. Partie XI, Guide T2 T4012)
- store.py : références Règl. Annexe II sur le tableau CCA_CATEGORIES et
Règl. 1100(1)/(2) sur le calcul FNACC indicatif par pièce
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-10 15:26:38 -04:00
< title > Mon Registraire — Gestionnaire d'immobilisations fiscales< / title >
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
< script src = "https://unpkg.com/react@18/umd/react.production.min.js" crossorigin > < / script >
< script src = "https://unpkg.com/react-dom@18/umd/react-dom.production.min.js" crossorigin > < / script >
< script src = "https://unpkg.com/@babel/standalone/babel.min.js" > < / script >
< style >
* { box-sizing: border-box; }
body { margin: 0; font-family: -apple-system, Segoe UI, Roboto, sans-serif; background: #0f172a; color: #e2e8f0; }
#root { display: flex; height: 100vh; height: 100dvh; }
/* ── Main column ── */
.main { flex: 1; display: flex; flex-direction: column; overflow: hidden; min-width: 0; }
.topbar { padding: 12px 16px; border-bottom: 1px solid #334155; display: flex; align-items: center; gap: 8px; flex-wrap: wrap; background: #0b1220; }
.crumb { color: #60a5fa; cursor: pointer; font-size: 14px; }
.crumb:hover { text-decoration: underline; }
.crumb.current { color: #e2e8f0; cursor: default; }
.crumb-sep { color: #475569; }
.spacer { flex: 1; }
button.bar { border: none; padding: 8px 12px; border-radius: 6px; cursor: pointer; font-size: 13px; color: white; white-space: nowrap; }
.btn-new { background: #16a34a; }
.btn-export { background: #7c3aed; }
.btn-ghost { background: #334155; }
/* ── Grid ── */
.grid-wrap { flex: 1; overflow-y: auto; padding: 20px 16px; }
.section-title { font-size: 12px; text-transform: uppercase; letter-spacing: .08em; color: #64748b; margin: 0 0 10px; }
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(230px, 1fr)); gap: 14px; margin-bottom: 28px; }
.card { background: #1e293b; border: 1px solid #334155; border-radius: 10px; padding: 14px 16px; cursor: pointer; transition: border-color .15s, transform .1s; position: relative; }
.card:hover { border-color: #2563eb; }
.card.selected { border-color: #facc15; }
.card-type { font-size: 10px; text-transform: uppercase; letter-spacing: .08em; padding: 2px 8px; border-radius: 999px; display: inline-block; margin-bottom: 8px; }
.t-parent { background: #1d4ed8; }
.t-piece { background: #15803d; }
.t-piece-free { background: #b45309; }
.card-name { font-weight: 600; font-size: 15px; margin-bottom: 3px; }
.card-sub { font-size: 12px; color: #94a3b8; }
.card-foot { margin-top: 10px; font-size: 11px; color: #64748b; display: flex; justify-content: space-between; align-items: center; }
.badge-status { font-size: 10px; padding: 2px 6px; border-radius: 4px; background: #334155; }
.drill { position: absolute; bottom: 12px; right: 14px; font-size: 11px; color: #60a5fa; }
.empty { color: #64748b; padding: 14px 0; }
/* ── Side panel (desktop) ── */
.panel { width: 410px; background: #0b1220; border-left: 1px solid #334155; display: flex; flex-direction: column; overflow: hidden; }
.panel-head { padding: 14px 16px; border-bottom: 1px solid #334155; display: flex; justify-content: space-between; align-items: flex-start; gap: 8px; }
.panel-head h3 { margin: 0 0 3px; font-size: 16px; }
.panel-head .sub { font-size: 12px; color: #64748b; }
.icon-btn { background: #1e293b; border: 1px solid #334155; color: #e2e8f0; border-radius: 6px; padding: 7px 11px; cursor: pointer; font-size: 12px; white-space: nowrap; }
.tabs { display: flex; gap: 4px; padding: 10px 14px 0; }
.tab { flex: 1; padding: 9px; text-align: center; background: #1e293b; border: 1px solid #334155; cursor: pointer; font-size: 12px; border-radius: 6px 6px 0 0; }
.tab.active { background: #2563eb; border-color: #2563eb; }
.panel-body { flex: 1; overflow-y: auto; padding: 14px; }
.field { margin-bottom: 10px; font-size: 13px; }
.field b { color: #94a3b8; display: block; font-size: 11px; text-transform: uppercase; margin-bottom: 2px; }
.kpi { display: flex; gap: 8px; margin-bottom: 12px; }
.kpi > div { flex: 1; background: #1e293b; border: 1px solid #334155; border-radius: 8px; padding: 10px; }
.kpi .v { font-size: 17px; font-weight: 700; font-variant-numeric: tabular-nums; }
.kpi .l { font-size: 10px; color: #94a3b8; text-transform: uppercase; }
.actions { display: flex; gap: 8px; margin-top: 10px; flex-wrap: wrap; }
button.act { flex: 1; min-width: 90px; padding: 10px 8px; border: none; border-radius: 6px; cursor: pointer; font-size: 13px; color: white; }
.btn-free { background: #475569; } .btn-stock { background: #0891b2; } .btn-scrap { background: #dc2626; }
.btn-attach { background: #2563eb; } .btn-save { background: #16a34a; } .btn-cancel { background: #475569; }
.btn-delete { background: #991b1b; }
select, input { width: 100%; padding: 8px; background: #1e293b; border: 1px solid #334155; color: #e2e8f0; border-radius: 6px; font-size: 13px; margin-bottom: 8px; }
label.fl { font-size: 11px; color: #94a3b8; text-transform: uppercase; display: block; margin-bottom: 2px; }
.locked { font-size: 11px; color: #64748b; font-style: italic; }
.journal { margin-top: 14px; background: #1e293b; border: 1px solid #334155; border-radius: 8px; padding: 12px; font-size: 12px; }
.journal table { width: 100%; border-collapse: collapse; }
.journal td { padding: 4px; border-bottom: 1px solid #334155; vertical-align: top; }
.num { text-align: right; font-variant-numeric: tabular-nums; }
.note { font-size: 11px; color: #64748b; margin-top: 8px; line-height: 1.4; }
.note.cpa { color: #f59e0b; border-left: 2px solid #f59e0b; padding-left: 6px; margin-top: 10px; }
.audit-item { font-size: 11px; border-left: 2px solid #334155; padding-left: 8px; margin-bottom: 6px; }
2026-06-10 14:04:34 -04:00
/* ── Welcome modal ── */
.welcome-modal { width: 620px; }
.welcome-logo { font-size: 36px; margin-bottom: 4px; }
.welcome-title { font-size: 20px; font-weight: 700; margin: 0 0 2px; }
.welcome-sub { font-size: 13px; color: #64748b; margin-bottom: 20px; }
.welcome-section { margin-bottom: 16px; }
.welcome-section h4 { font-size: 11px; text-transform: uppercase; letter-spacing: .08em; color: #60a5fa; margin: 0 0 6px; }
.welcome-section p, .welcome-section li { font-size: 13px; color: #cbd5e1; line-height: 1.6; margin: 0; }
.welcome-section ul { margin: 4px 0; padding-left: 18px; }
.welcome-section li { margin-bottom: 3px; }
.welcome-warn { background: #1e293b; border-left: 3px solid #f59e0b; border-radius: 0 6px 6px 0; padding: 10px 14px; margin-top: 4px; }
.welcome-warn li { color: #fbbf24; }
.welcome-footer { display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 10px; margin-top: 20px; padding-top: 16px; border-top: 1px solid #334155; }
.welcome-check { display: flex; align-items: center; gap: 8px; font-size: 13px; color: #94a3b8; cursor: pointer; }
.welcome-check input[type=checkbox] { width: auto; margin: 0; accent-color: #60a5fa; cursor: pointer; }
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
/* ── Modals ── */
.modal-bg { position: fixed; inset: 0; background: rgba(0,0,0,.65); display: flex; align-items: center; justify-content: center; z-index: 60; }
.modal { background: #0f172a; border: 1px solid #334155; border-radius: 12px; padding: 22px; width: 760px; max-width: 94vw; max-height: 90vh; overflow-y: auto; }
.modal h3 { margin: 0 0 14px; }
.modal-actions { display: flex; gap: 8px; margin-top: 14px; }
.fmt { color: #facc15; }
table.export { width: 100%; border-collapse: collapse; font-size: 13px; }
table.export th, table.export td { padding: 7px 9px; border-bottom: 1px solid #334155; text-align: left; }
table.export th { color: #94a3b8; font-size: 11px; text-transform: uppercase; }
table.export tr.total td { font-weight: 700; background: #1e293b; }
/* ── Sheet handle (mobile only, invisible on desktop) ── */
.sheet-handle { display: none; }
/* ── Bottom nav & FAB (mobile only) ── */
.bottom-nav { display: none; }
.fab, .fab-menu { display: none; }
.sheet-backdrop { display: none; }
.desktop-btns { display: flex; gap: 6px; align-items: center; flex-wrap: wrap; }
/* ════════════════════════════════════════
MOBILE (≤ 767 px)
════════════════════════════════════════ */
@media (max-width: 767px) {
#root { flex-direction: column; height: 100dvh; overflow: hidden; }
/* main takes full width */
.main { width: 100%; }
.grid-wrap { padding-bottom: calc(72px + env(safe-area-inset-bottom, 0px) + 16px); }
/* hide desktop side panel & buttons */
.desktop-btns { display: none; }
.panel {
position: fixed; bottom: 0; left: 0; right: 0;
width: 100%; max-height: 82dvh;
border-left: none; border-top: 1px solid #334155;
border-radius: 20px 20px 0 0;
transform: translateY(102%);
transition: transform .32s cubic-bezier(.32,.72,0,1);
z-index: 45; overflow-y: auto;
padding-bottom: env(safe-area-inset-bottom, 0px);
}
.panel.open { transform: translateY(0); }
/* drag handle */
.sheet-handle { display: block; width: 38px; height: 4px; border-radius: 2px; background: #475569; margin: 10px auto 4px; flex-shrink: 0; }
/* backdrop */
.sheet-backdrop { display: block; position: fixed; inset: 0; background: rgba(0,0,0,.52); z-index: 44; }
/* bottom navigation */
.bottom-nav {
display: flex; position: fixed; bottom: 0; left: 0; right: 0;
height: calc(60px + env(safe-area-inset-bottom, 0px));
padding-bottom: env(safe-area-inset-bottom, 0px);
background: #0b1220; border-top: 1px solid #334155; z-index: 50;
}
.nav-item {
flex: 1; display: flex; flex-direction: column;
align-items: center; justify-content: center; gap: 2px;
color: #64748b; cursor: pointer; font-size: 10px; min-height: 44px;
-webkit-tap-highlight-color: transparent;
transition: color .15s;
}
.nav-item:active, .nav-item.active { color: #60a5fa; }
.nav-icon { font-size: 20px; line-height: 1.1; }
.nav-spacer { flex: 1; pointer-events: none; } /* center gap for FAB */
/* FAB */
.fab {
display: flex; position: fixed;
bottom: calc(36px + env(safe-area-inset-bottom, 0px));
left: 50%; transform: translateX(-50%);
width: 54px; height: 54px; border-radius: 27px;
background: #16a34a; border: 3px solid #0f172a;
color: white; font-size: 26px; font-weight: 300; cursor: pointer;
box-shadow: 0 4px 18px rgba(0,0,0,.55);
z-index: 51; align-items: center; justify-content: center;
transition: background .2s, transform .2s;
-webkit-tap-highlight-color: transparent;
}
.fab.fab-open { background: #7c3aed; }
/* FAB menu */
.fab-menu {
display: flex; position: fixed;
bottom: calc(102px + env(safe-area-inset-bottom, 0px));
left: 50%; transform: translateX(-50%);
flex-direction: column; gap: 10px; z-index: 51; align-items: center;
}
.fab-opt {
background: #1e293b; border: 1px solid #475569; color: #e2e8f0;
border-radius: 12px; padding: 14px 22px; font-size: 15px; cursor: pointer;
white-space: nowrap; display: flex; gap: 12px; align-items: center;
min-width: 210px; box-shadow: 0 6px 20px rgba(0,0,0,.45);
-webkit-tap-highlight-color: transparent;
}
/* tool sheet (fiscal / plus menus) */
.tool-sheet {
position: fixed;
bottom: calc(60px + env(safe-area-inset-bottom, 0px));
left: 0; right: 0; z-index: 46;
background: #0b1220; border: 1px solid #334155; border-bottom: none;
border-radius: 20px 20px 0 0;
animation: sheetUp .22s ease;
}
.tool-sheet-handle { width: 38px; height: 4px; border-radius: 2px; background: #475569; margin: 10px auto 2px; }
.tool-opt {
display: flex; align-items: center; gap: 16px;
padding: 17px 22px; font-size: 16px; cursor: pointer;
border: none; background: none; color: #e2e8f0; width: 100%; text-align: left;
-webkit-tap-highlight-color: transparent;
}
.tool-opt:active { background: #1e293b; }
.tool-icon { font-size: 22px; width: 30px; text-align: center; }
.tool-sep { height: 1px; background: #334155; margin: 0 22px; }
@keyframes sheetUp { from { transform: translateY(100%); } to { transform: translateY(0); } }
/* larger tap targets in panels */
button.act { padding: 13px 8px; font-size: 14px; min-height: 46px; }
.icon-btn { padding: 10px 14px; font-size: 13px; min-height: 40px; }
.tab { padding: 11px; font-size: 13px; min-height: 44px; }
/* cards: 1 column, bigger */
.grid { grid-template-columns: 1fr; gap: 10px; }
.card { padding: 14px 16px; min-height: 78px; }
.card-name { font-size: 16px; }
/* modals full-screen on mobile */
.modal-bg { align-items: flex-end; }
.modal { width: 100% !important; max-width: 100vw !important; border-radius: 20px 20px 0 0; border-left: none; border-right: none; border-bottom: none; max-height: 94dvh; padding-bottom: calc(16px + env(safe-area-inset-bottom, 0px)); }
/* topbar minimal */
.topbar { padding: 10px 14px; }
}
< / style >
< / head >
< body >
< div id = "root" > < / div >
< script type = "text/babel" >
const { useState, useEffect, useCallback, useRef } = React;
const fmt = n => Number(n).toLocaleString("fr-CA", {minimumFractionDigits:2, maximumFractionDigits:2});
const today = () => new Date().toISOString().slice(0,10);
const api = (url, method, body) => fetch(url, {method, headers:{"Content-Type":"application/json"}, body: body?JSON.stringify(body):undefined})
.then(r => r.ok ? r.json() : r.json().then(e=>Promise.reject(e)));
const PIECE_FIELDS = [
{label:"Nom / désignation", path:"name"},
{label:"Numéro de série", path:"view_admin.serial_number"},
{label:"Étiquette d'inventaire", path:"view_admin.inventory_tag"},
{label:"Fabricant", path:"view_admin.manufacturer"},
{label:"Modèle", path:"view_admin.model"},
{label:"Format", path:"view_admin.form_factor"},
{label:"Statut", path:"view_admin.operational_status"},
{label:"Coût (CAD)", path:"view_cpa.capitalizable_base_cad", num:true},
{label:"Coût de remplacement (CAD)", path:"view_admin.replacement_cost_cad", num:true},
{label:"Date d'acquisition", path:"view_cpa.acquisition_date", type:"date"},
{label:"Date de mise en service", path:"view_cpa.in_service_date", type:"date"},
{label:"Durée de vie utile (ans)", path:"view_admin.useful_life_years", num:true},
{label:"Fournisseur", path:"view_auditor.supplier"},
{label:"N° de facture", path:"view_auditor.invoice_number"},
];
const PARENT_FIELDS = [
{label:"Nom / désignation", path:"name"},
{label:"Assembleur", path:"view_admin.assembler"},
{label:"Statut", path:"view_admin.operational_status"},
{label:"Catégorie DPA", path:"view_cpa.cca_class.id", num:true, select:"cca"},
{label:"Date de mise en service", path:"view_cpa.in_service_date", type:"date"},
];
function getPath(obj, dotted) { return dotted.split(".").reduce((o,k)=> (o==null?o:o[k]), obj); }
const NOTE_CPA = "Ces calculs sont structurellement corrects mais indicatifs. DPA, FNACC, perte finale et récupération doivent être validés par un CPA avant toute déclaration fiscale. Aucun taux d'accélération n'est câblé en dur.";
function App() {
const [assets, setAssets] = useState([]);
const [currentParent, setCurrentParent] = useState(null);
const [selectedId, setSelectedId] = useState(null);
const [view, setView] = useState("owner");
const [accounting, setAccounting] = useState(null);
const [scrapCatId, setScrapCatId] = useState(null);
const [modal, setModal] = useState(null);
const [fabOpen, setFabOpen] = useState(false);
const [toolSheet, setToolSheet] = useState(null); // "fiscal" | "plus" | null
2026-06-10 14:04:34 -04:00
const [showWelcome, setShowWelcome] = useState(() => localStorage.getItem("immos_welcome_v1") !== "1");
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
const load = useCallback(()=>api("/api/assets","GET").then(setAssets), []);
useEffect(()=>{ load(); }, [load]);
const byId = id => assets.find(a=>a.id===id) || null;
const selected = byId(selectedId);
const parents = assets.filter(a=>a.type==="parent");
const childrenCount = id => assets.filter(a=>a.parent_id===id).length;
let levelItems, freePieces = [];
if (currentParent === null) {
levelItems = parents;
freePieces = assets.filter(a=>a.type==="piece" & & a.parent_id===null);
} else {
levelItems = assets.filter(a=>a.parent_id===currentParent);
}
const crumbs = [];
let p = currentParent;
while (p!=null){ const n=byId(p); if(!n)break; crumbs.unshift(n); p=n.parent_id; }
const goRoot = ()=>{ setCurrentParent(null); setSelectedId(null); setAccounting(null); setScrapCatId(null); setToolSheet(null); setFabOpen(false); };
const pick = n => { setSelectedId(n.id); setView("owner"); setAccounting(null); setScrapCatId(null); setFabOpen(false); setToolSheet(null); };
const drill = n => { if(n.type==="parent"){ setCurrentParent(n.id); setSelectedId(null); setAccounting(null); setFabOpen(false); } };
const closeSheet = () => { setSelectedId(null); setAccounting(null); setScrapCatId(null); };
const refresh = data => setAssets(data.assets);
const detach = disp => {
if(!selected) return;
const catIdNow = selected.view_cpa?.cca_class?.id || 50;
api(`/api/assets/${selected.id}/detach`,"POST",{disposition:disp, detachment_date:today()})
.then(d=>{ setAccounting(d.accounting); setAssets(d.assets); if(d.accounting===null) setSelectedId(null); if(disp==="scrap") setScrapCatId(catIdNow); })
.catch(e=>alert("Erreur : "+(e.detail||"inconnue")));
};
const attach = pid => {
if(!selected||!pid) return;
api(`/api/assets/${selected.id}/attach`,"POST",{parent_id:Number(pid)})
.then(d=>{ refresh(d); setSelectedId(null); }).catch(e=>alert("Erreur : "+(e.detail||"inconnue")));
};
const remove = () => {
if(!selected) return;
if(!confirm(`Supprimer « ${selected.name} » ? ${selected.type==="parent"?"Ses pièces deviendront libres.":""}`)) return;
api(`/api/assets/${selected.id}`,"DELETE").then(d=>{ refresh(d); setSelectedId(null); })
.catch(e=>alert("Erreur : "+(e.detail||"inconnue")));
};
const setTagBearer = pieceId => {
api(`/api/assets/${pieceId}/set-tag-bearer`,"POST")
.then(d=>refresh(d)).catch(e=>alert("Erreur : "+(e.detail||"inconnue")));
};
/* swipe-down-to-close the mobile sheet */
const swipeStartY = useRef(0);
const onTouchStart = e => { swipeStartY.current = e.touches[0].clientY; };
const onTouchEnd = e => { if(e.changedTouches[0].clientY - swipeStartY.current > 72) closeSheet(); };
const openModal = m => { setModal(m); setToolSheet(null); setFabOpen(false); };
return (
< React.Fragment >
{/* ── Backdrops ── */}
{selected & & < div className = "sheet-backdrop" onClick = {closeSheet}/ > }
{toolSheet & & < div className = "sheet-backdrop" style = {{zIndex:45}} onClick = {()= > setToolSheet(null)}/>}
{fabOpen & & < div className = "sheet-backdrop" style = {{zIndex:50, background: " rgba ( 0 , 0 , 0 , . 35 ) " } } onClick = {()= > setFabOpen(false)}/>}
{/* ══════════════════════════════
MAIN AREA
══════════════════════════════ */}
< div className = "main" >
< div className = "topbar" >
< span className = {"crumb"+(currentParent===null?" current " : " " ) } onClick = {goRoot} > Immobilisations< / span >
{crumbs.map(c=>(< React.Fragment key = {c.id} > < span className = "crumb-sep" > › < / span > < span className = "crumb current" > {c.name}< / span > < / React.Fragment > ))}
< div className = "spacer" / >
{/* Desktop buttons — hidden on mobile */}
< div className = "desktop-btns" >
< button className = "bar btn-export" onClick = {()= > openModal("rapport")}>Rapport fiscal< / button >
< button className = "bar btn-export" onClick = {()= > openModal("annexe8")}>Annexe 8< / button >
< button className = "bar btn-export" onClick = {()= > openModal("registry")}>Registre pool< / button >
< button className = "bar btn-export" onClick = {()= > openModal("export")}>Export immos< / button >
< button className = "bar btn-new" onClick = {()= > openModal("create_parent")}>+ Immobilisation< / button >
< button className = "bar btn-new" onClick = {()= > openModal("create_piece")}>+ Pièce< / button >
< a className = "bar btn-ghost" href = "/static/aide.html" target = "_blank" style = {{textDecoration:"none"}} > ? Aide< / a >
< button className = "bar btn-ghost" onClick = {()= > openModal("backup")}>Sauvegardes< / button >
< button className = "bar btn-ghost" onClick = {()= > api("/api/seed-demo","POST").then(refresh)}>Données démo< / button >
< button className = "bar btn-ghost" onClick = {()= > { if(confirm("Vider toute la base ?")) api("/api/reset","POST").then(d=>{refresh(d); setSelectedId(null);}); }}>Réinitialiser< / button >
< / div >
< / div >
< div className = "grid-wrap" >
{currentParent===null ? (
< React.Fragment >
< p className = "section-title" > Immobilisations fiscales ({parents.length})< / p >
{parents.length===0 & & < div className = "empty" > Aucune immobilisation. Crée-en une, ou charge les données démo.< / div > }
< Grid items = {levelItems} selectedId = {selectedId} childrenCount = {childrenCount} onPick = {pick} onDrill = {drill}/ >
< p className = "section-title" > Pièces libres ({freePieces.length})< / p >
{freePieces.length===0 & & < div className = "empty" > Aucune pièce libre.< / div > }
< Grid items = {freePieces} selectedId = {selectedId} childrenCount = {childrenCount} onPick = {pick} onDrill = {()= > {}} freeZone/>
< / React.Fragment >
) : (
< React.Fragment >
< p className = "section-title" > Pièces de ce parent ({levelItems.length})< / p >
{levelItems.length===0 & & < div className = "empty" > Aucune pièce rattachée.< / div > }
< Grid items = {levelItems} selectedId = {selectedId} childrenCount = {childrenCount} onPick = {pick} onDrill = {()= > {}}/>
< / React.Fragment >
)}
< / div >
< / div >
{/* ══════════════════════════════
DETAIL PANEL / MOBILE SHEET
══════════════════════════════ */}
< div className = {"panel"+(selected?" open " : " " ) } onTouchStart = {onTouchStart} onTouchEnd = {onTouchEnd} >
< div className = "sheet-handle" / >
{!selected & & < div className = "panel-body" style = {{color:"#64748b"}} >
Sélectionne une immobilisation ou une pièce pour voir et éditer ses informations.
< / div > }
{selected & & (
< React.Fragment >
< div className = "panel-head" >
< div >
< h3 > {selected.name}< / h3 >
< div className = "sub" > {selected.type==="parent"?"Immobilisation fiscale":"Pièce d'inventaire"} · {selected.view_admin.inventory_tag||"sans étiquette"}< / div >
< / div >
< div style = {{display:"flex", gap:6 , flexDirection: " column " } } >
< button className = "icon-btn" onClick = {()= > openModal("edit")}>Modifier< / button >
< button className = "icon-btn" onClick = {remove} style = {{borderColor:"#991b1b"}} > Supprimer< / button >
< / div >
< / div >
< div className = "tabs" >
{[["owner","Propriétaire"],["cpa","CPA"],["auditor","Auditeur"]].map(([k,l])=>(
< div key = {k} className = {"tab"+(view===k?" active " : " " ) } onClick = {()= > setView(k)}>{l}< / div >
))}
< / div >
< div className = "panel-body" >
{/* ===== PIÈCE : Propriétaire ===== */}
{selected.type==="piece" & & view==="owner" & & (
< div >
< div style = {{display:"flex", gap:8 , marginBottom:12 , flexWrap: " wrap " , alignItems: " center " } } >
< span style = {{fontSize:11, padding: " 2px 8px " , borderRadius:999 , background:
selected.view_admin.operational_status==="Actif"?"#15803d":
selected.view_admin.operational_status==="Rebut"?"#7f1d1d":"#44403c",
color:"#e2e8f0"}}>{selected.view_admin.operational_status}< / span >
< span style = {{fontSize:12, color: " # 94a3b8 " } } >
{selected.parent_id ? (byId(selected.parent_id)?.name||"—") : "Pièce libre"}
< / span >
< / div >
< Field label = "Fabricant · Modèle" value = {`${selected.view_admin.manufacturer||"—"} $ { selected . view_admin . model | | " " } ` . trim ( ) } / >
< Field label = "Format" value = {selected.view_admin.form_factor||"—"}/ >
{selected.parent_id !== null ? (
< React.Fragment >
< div className = "field" >
< b > {selected.view_admin.is_tag_bearer?"Étiquette d'assemblage (porteur ⭐)":"Étiquette d'assemblage (héritée)"}< / b >
{selected.view_admin.inventory_tag||< span style = {{color:"#64748b"}} > aucune< / span > }
< / div >
{selected.view_admin.is_tag_bearer
? < div className = "note" style = {{color:"#4ade80",borderLeft:"2px solid # 4ade80 " , paddingLeft:6 , marginBottom:8 } } > Cette pièce porte l'étiquette de l'assemblage.< / div >
: < button className = "act btn-attach" style = {{marginBottom:10,width:"100%"}} onClick = {()= > setTagBearer(selected.id)}>⭐ Désigner comme porteur d'étiquette< / button > }
< / React.Fragment >
) : (
< React.Fragment >
< Field label = "Étiquette d'inventaire" value = {selected.view_admin.inventory_tag||"—"}/ >
< / React.Fragment >
)}
< div className = "kpi" >
< div > < div className = "v" > {fmt(selected.view_cpa.capitalizable_base_cad)} $< / div > < div className = "l" > Coût d'acquisition< / div > < / div >
< div > < div className = "v" > {fmt(selected.view_cpa.nbv_linear_cad ?? selected.view_cpa.capitalizable_base_cad)} $< / div > < div className = "l" > VNC linéaire< / div > < / div >
< / div >
{(() => {
const cost = selected.view_cpa.capitalizable_base_cad;
const nbvL = selected.view_cpa.nbv_linear_cad ?? cost;
const pct = cost > 0 ? Math.min(100, Math.round((1 - nbvL / cost) * 100)) : 100;
const col = pct > 80 ? "#ef4444" : pct > 50 ? "#f59e0b" : "#4ade80";
return (
< div style = {{marginBottom:12}} >
< div style = {{display:"flex",justifyContent:"space-between",fontSize:11,color:"#94a3b8",marginBottom:4}} >
< span > Amortissement linéaire indicatif< / span > < span style = {{color:col}} > {pct}% amorti< / span >
< / div >
< div style = {{background:"#334155",height:6,borderRadius:3}} >
< div style = {{background:col,height:"100%",borderRadius:3,width:`${pct}%`,transition:"width . 4s " } } / >
< / div >
< div style = {{fontSize:11,color:"#64748b",marginTop:4}} > Durée utile : {selected.view_admin.useful_life_years} ans · Résiduelle : {selected.view_cpa.remaining_useful_life_years?.toFixed(1)} ans< / div >
< / div >
);
})()}
< Field label = "Coût de remplacement" value = {fmt(selected.view_admin.replacement_cost_cad)+" $ " } / >
{selected.parent_id === null & & (
< React.Fragment >
< div className = "section-title" style = {{marginTop:10}} > Rattacher à une immobilisation< / div >
< AttachControl parents = {parents} onAttach = {attach}/ >
< / React.Fragment >
)}
{selected.parent_id !== null & & selected.view_admin.operational_status !== "Rebut" & & (
< React.Fragment >
< div className = "section-title" style = {{marginTop:10}} > Détacher< / div >
< div className = "actions" >
< button className = "act btn-free" onClick = {()= > detach("free")}>Libre< / button >
< button className = "act btn-stock" onClick = {()= > detach("stock")}>Stock< / button >
< button className = "act btn-scrap" onClick = {()= > detach("scrap")}>Rebut< / button >
< / div >
< / React.Fragment >
)}
{accounting & & (
< div className = "journal" >
< div style = {{fontWeight:600, marginBottom:8 , fontSize:13 } } > Écriture comptable — {accounting.disposition_type}< / div >
< table > < tbody >
{accounting.entries?.map((e,i)=>(
< tr key = {i} > < td > {e.account}< / td > < td className = "num" > {e.debit?fmt(e.debit):""}< / td > < td className = "num" > {e.credit?fmt(e.credit):""}< / td > < / tr >
))}
< / tbody > < / table >
< / div >
)}
{accounting & & scrapCatId & & < DispForm accounting = {accounting} catId = {scrapCatId}/ > }
< / div >
)}
{/* ===== PIÈCE : CPA ===== */}
{selected.type==="piece" & & view==="cpa" & & (
< div >
< div className = "kpi" >
< div > < div className = "v" > {selected.view_cpa.cca_class.dpa_rate*100}%< / div > < div className = "l" > DPA · Cat. {selected.view_cpa.cca_class.id}< / div > < / div >
< div > < div className = "v" > {fmt(selected.view_cpa.capitalizable_base_cad)} $< / div > < div className = "l" > Base capitalisable< / div > < / div >
< / div >
< div className = "kpi" >
< div > < div className = "v" > {fmt(selected.view_cpa.net_book_value_cad||0)} $< / div > < div className = "l" > FNACC indicative< / div > < / div >
< div > < div className = "v" > {selected.view_cpa.reference_year}< / div > < div className = "l" > Exercice de référence< / div > < / div >
< / div >
< Field label = "Catégorie DPA" value = {`Cat. $ { selected . view_cpa . cca_class . id } — $ { selected . view_cpa . cca_class . description } ` } / >
< Field label = "Date d'acquisition" value = {selected.view_cpa.acquisition_date||"—"}/ >
< Field label = "Date de mise en service" value = {selected.view_cpa.in_service_date||"—"}/ >
< Field label = "Règle de la demi-année" value = {selected.view_cpa.half_year_rule_applies?"Applicable":"Non"}/ >
< div style = {{background:"#1e293b",border:"1px solid # 334155 " , borderRadius:8 , padding:12 , marginTop:8 } } >
< div style = {{fontSize:11,color:"#94a3b8",textTransform:"uppercase",marginBottom:8}} > Amortissement linéaire indicatif< / div >
< div className = "kpi" style = {{marginBottom:0}} >
< div > < div className = "v" > {fmt(selected.view_cpa.accum_deprec_linear_cad||0)} $< / div > < div className = "l" > Cumulé< / div > < / div >
< div > < div className = "v" > {fmt(selected.view_cpa.nbv_linear_cad ?? selected.view_cpa.capitalizable_base_cad)} $< / div > < div className = "l" > VNC< / div > < / div >
< div > < div className = "v" > {(selected.view_cpa.remaining_useful_life_years||0).toFixed(1)}< / div > < div className = "l" > Ans restants< / div > < / div >
< / div >
< / div >
< div className = "note cpa" > {NOTE_CPA}< / div >
< / div >
)}
{/* ===== PIÈCE : Auditeur ===== */}
{selected.type==="piece" & & view==="auditor" & & (
< div >
< Field label = "Numéro de série" value = {selected.view_admin.serial_number||"—"}/ >
< Field label = "Étiquette d'inventaire" value = {selected.view_admin.inventory_tag||"—"}/ >
< Field label = "Fournisseur" value = {selected.view_auditor.supplier||"—"}/ >
< Field label = "N° de facture" value = {selected.view_auditor.invoice_number||"—"}/ >
< InvoiceAttachment asset = {selected} onRefresh = {refresh}/ >
< div className = "section-title" style = {{marginTop:12}} > Piste d'audit< / div >
{(selected.view_auditor.audit_trail||[]).slice().reverse().map((e,i)=>(
< div key = {i} className = "audit-item" >
< div style = {{color:"#facc15"}} > {e.action}< / div >
< div > {e.description}< / div >
< div style = {{color:"#64748b"}} > {e.timestamp}< / div >
< / div >
))}
< / div >
)}
{/* ===== PARENT : Propriétaire ===== */}
{selected.type==="parent" & & view==="owner" & & (
< div >
< div className = "kpi" >
< div > < div className = "v" > {fmt(selected.view_cpa.nbv_linear_total_cad||0)} $< / div > < div className = "l" > VNC linéaire totale< / div > < / div >
< div > < div className = "v" > {fmt(selected.view_admin.total_replacement_cost_cad||0)} $< / div > < div className = "l" > Coût remplacement< / div > < / div >
< / div >
< div className = "kpi" >
< div > < div className = "v" > {fmt(selected.view_cpa.capitalizable_base_cad)} $< / div > < div className = "l" > Coût d'acquisition< / div > < / div >
< div > < div className = "v" > {selected.view_admin.pieces_count||0}< / div > < div className = "l" > Pièces rattachées< / div > < / div >
< / div >
< Field label = "Statut" value = {selected.view_admin.operational_status||"—"}/ >
< Field label = "Assembleur" value = {selected.view_admin.assembler||"—"}/ >
< Field label = "Date de mise en service" value = {selected.view_cpa.in_service_date||"—"}/ >
{(() => {
const pieces = assets.filter(a=>a.parent_id===selected.id);
const bearerTag = pieces.find(pc=>pc.view_admin.is_tag_bearer)?.view_admin?.inventory_tag;
return (< React.Fragment >
{bearerTag
? < div style = {{marginBottom:10,fontSize:12,color:"#94a3b8"}} > Étiquette : < b style = {{color:"#e2e8f0"}} > {bearerTag}< / b > < / div >
: < div style = {{marginBottom:10,fontSize:12,color:"#64748b"}} > Aucun porteur d'étiquette désigné.< / div > }
< div className = "section-title" style = {{marginBottom:8}} > Composition< / div >
{pieces.length===0 & & < div className = "empty" > Aucune pièce rattachée.< / div > }
{pieces.map(pc=>{
const cost=pc.view_cpa.capitalizable_base_cad;
const nbvL=pc.view_cpa.nbv_linear_cad??cost;
const pct=cost>0?Math.min(100,Math.round((1-nbvL/cost)*100)):100;
const col=pct>80?"#ef4444":pct>50?"#f59e0b":"#4ade80";
return (
< div key = {pc.id} className = "audit-item" style = {{borderLeftColor:pc.view_admin.is_tag_bearer?"#facc15":"#15803d"}} >
< div style = {{display:"flex",justifyContent:"space-between",cursor:"pointer"}} onClick = {()= > pick(pc)}>
< div >
< div style = {{display:"flex",alignItems:"center",gap:6}} >
{pc.view_admin.is_tag_bearer & & < span style = {{fontSize:10,background:"#713f12",color:"#facc15",padding:"1px 6px " , borderRadius:999 } } > PORTEUR ⭐< / span > }
< span > {pc.name}< / span >
< / div >
< div style = {{color:"#64748b",fontSize:11}} > {pc.view_admin.manufacturer} {pc.view_admin.model}< / div >
< / div >
< div style = {{textAlign:"right",whiteSpace:"nowrap",fontSize:12}} >
< div style = {{color:col}} > {fmt(nbvL)} $< / div >
< div style = {{color:"#64748b"}} > {pct}% amorti< / div >
< / div >
< / div >
< div style = {{background:"#334155",height:4,borderRadius:2,marginTop:4}} >
< div style = {{background:col,height:"100%",borderRadius:2,width:`${pct}%`}}/ >
< / div >
{!pc.view_admin.is_tag_bearer & & (
< button style = {{marginTop:4,fontSize:11,padding:"2px 8px " , background: " # 1e3a5f " , border: " 1px solid # 2563eb " , color: " # 93c5fd " , borderRadius:4 , cursor: " pointer " } }
onClick={e=>{e.stopPropagation();setTagBearer(pc.id);}}>⭐ Désigner porteur< / button >
)}
< / div >
);
})}
< div className = "kpi" style = {{marginTop:12}} >
< div > < div className = "v" > {fmt(selected.view_cpa.capitalizable_base_cad)}< / div > < div className = "l" > Coût total (Σ)< / div > < / div >
< div > < div className = "v" > {fmt(selected.view_cpa.nbv_linear_total_cad||0)}< / div > < div className = "l" > VNC linéaire (Σ)< / div > < / div >
< / div >
< / React.Fragment > );
})()}
< button className = "act btn-attach" style = {{width:"100%",marginTop:8}} onClick = {()= > drill(selected)}>
Voir les pièces de cette immobilisation →
< / button >
< / div >
)}
{/* ===== PARENT : CPA ===== */}
{selected.type==="parent" & & view==="cpa" & & (
< div >
< div className = "kpi" >
< div > < div className = "v" > {selected.view_cpa.cca_class.dpa_rate*100}%< / div > < div className = "l" > DPA · Cat. {selected.view_cpa.cca_class.id}< / div > < / div >
< div > < div className = "v" > {fmt(selected.view_cpa.capitalizable_base_cad)}< / div > < div className = "l" > Base fiscale (Σ coûts)< / div > < / div >
< / div >
< div className = "kpi" >
< div > < div className = "v" > {fmt(selected.view_cpa.net_book_value_cad||0)}< / div > < div className = "l" > FNACC indicative (Σ pièces)< / div > < / div >
< div > < div className = "v" > {fmt(selected.view_admin.total_replacement_cost_cad||0)}< / div > < div className = "l" > Coût remplacement total< / div > < / div >
< / div >
< Field label = "Catégorie DPA" value = {`Cat. $ { selected . view_cpa . cca_class . id } — $ { selected . view_cpa . cca_class . description } ` } / >
< Field label = "Date de mise en service" value = {selected.view_cpa.in_service_date||"—"}/ >
< Field label = "Règle de la demi-année" value = {selected.view_cpa.half_year_rule_applies?"Applicable":"Non"}/ >
< div className = "note cpa" > {NOTE_CPA}< / div >
< / div >
)}
{/* ===== PARENT : Auditeur ===== */}
{selected.type==="parent" & & view==="auditor" & & (
< div >
< Field label = "Assembleur" value = {selected.view_admin.assembler||"—"}/ >
< div className = "section-title" style = {{marginTop:8}} > Pièces composantes< / div >
{assets.filter(a=>a.parent_id===selected.id).map(pc=>(
< div key = {pc.id} className = "audit-item" style = {{marginBottom:8}} >
< div style = {{fontWeight:600}} > {pc.name}< / div >
< div style = {{color:"#64748b",fontSize:11}} > S/N : {pc.view_admin.serial_number||"—"} · {pc.view_auditor?.supplier||"—"} · {pc.view_auditor?.invoice_number||"—"}< / div >
< / div >
))}
< div className = "section-title" style = {{marginTop:12}} > Piste d'audit< / div >
{(selected.view_auditor.audit_trail||[]).slice().reverse().map((e,i)=>(
< div key = {i} className = "audit-item" >
< div style = {{color:"#facc15"}} > {e.action}< / div >
< div > {e.description}< / div >
< div style = {{color:"#64748b"}} > {e.timestamp}< / div >
< / div >
))}
< / div >
)}
< / div >
< / React.Fragment >
)}
< / div >
{/* ══════════════════════════════
MOBILE FAB
══════════════════════════════ */}
{fabOpen & & (
< div className = "fab-menu" >
< button className = "fab-opt" onClick = {()= > { setFabOpen(false); openModal("create_piece"); }}>
< span > 🔩< / span > Pièce d'inventaire
< / button >
< button className = "fab-opt" onClick = {()= > { setFabOpen(false); openModal("create_parent"); }}>
< span > 🖥️< / span > Immobilisation fiscale
< / button >
< / div >
)}
< button className = {"fab"+(fabOpen?" fab-open " : " " ) } onClick = {()= > setFabOpen(!fabOpen)}
style={{fontSize: fabOpen?"28px":"30px", letterSpacing:0}}>
{fabOpen ? "× " : "+"}
< / button >
{/* ══════════════════════════════
TOOL SHEETS (mobile)
══════════════════════════════ */}
{toolSheet==="fiscal" & & (
< div className = "tool-sheet" >
< div className = "tool-sheet-handle" / >
< button className = "tool-opt" onClick = {()= > openModal("rapport")}>< span className = "tool-icon" > 📊< / span > Rapport fiscal< / button >
< div className = "tool-sep" / >
< button className = "tool-opt" onClick = {()= > openModal("annexe8")}>< span className = "tool-icon" > 📋< / span > Annexe 8 / Schedule 8< / button >
< div className = "tool-sep" / >
< button className = "tool-opt" onClick = {()= > openModal("registry")}>< span className = "tool-icon" > 🗂️< / span > Registre pool DPA< / button >
< div className = "tool-sep" / >
< button className = "tool-opt" onClick = {()= > openModal("export")}>< span className = "tool-icon" > 📤< / span > Export immobilisations< / button >
< / div >
)}
{toolSheet==="plus" & & (
< div className = "tool-sheet" >
< div className = "tool-sheet-handle" / >
< button className = "tool-opt" onClick = {()= > openModal("backup")}>< span className = "tool-icon" > 💾< / span > Sauvegardes< / button >
< div className = "tool-sep" / >
< button className = "tool-opt" onClick = {()= > { setToolSheet(null); window.open('/static/aide.html','_blank'); }}>< span className = "tool-icon" > ❓< / span > Guide d'utilisation< / button >
< div className = "tool-sep" / >
< button className = "tool-opt" onClick = {()= > { setToolSheet(null); api("/api/seed-demo","POST").then(refresh); }}>< span className = "tool-icon" > 🎲< / span > Données démo< / button >
< div className = "tool-sep" / >
< button className = "tool-opt" style = {{color:"#f87171"}} onClick = {()= > { setToolSheet(null); if(confirm("Vider toute la base ?")) api("/api/reset","POST").then(d=>{refresh(d);setSelectedId(null);}); }}>< span className = "tool-icon" > 🗑️< / span > Réinitialiser la base< / button >
< / div >
)}
{/* ══════════════════════════════
BOTTOM NAVIGATION (mobile)
══════════════════════════════ */}
< nav className = "bottom-nav" >
< div className = {"nav-item"+(currentParent===null&&!toolSheet?" active " : " " ) } onClick = {goRoot} >
< span className = "nav-icon" > 🏠< / span > < span > Accueil< / span >
< / div >
< div className = {"nav-item"+(toolSheet==="fiscal"?" active " : " " ) } onClick = {()= > { setToolSheet(t=>t==="fiscal"?null:"fiscal"); setFabOpen(false); closeSheet(); }}>
< span className = "nav-icon" > 📊< / span > < span > Fiscal< / span >
< / div >
< div className = "nav-spacer" / >
< div className = {"nav-item"+(toolSheet==="plus"?" active " : " " ) } onClick = {()= > { setToolSheet(t=>t==="plus"?null:"plus"); setFabOpen(false); closeSheet(); }}>
< span className = "nav-icon" > ⚙️< / span > < span > Plus< / span >
< / div >
< div className = "nav-item" onClick = {()= > window.open('/static/aide.html','_blank')}>
< span className = "nav-icon" > ❓< / span > < span > Aide< / span >
< / div >
< / nav >
{/* ══════════════════════════════
MODALS
══════════════════════════════ */}
{modal==="create_parent" & & < CreateModal fixedType = "parent" onClose = {()= > setModal(null)} onCreated={d=>{refresh(d); setModal(null);}}/>}
{modal==="create_piece" & & < CreateModal fixedType = "piece" onClose = {()= > setModal(null)} onCreated={d=>{refresh(d); setModal(null);}}/>}
{modal==="edit" & & selected & & < EditModal asset = {selected} onClose = {()= > setModal(null)} onSaved={d=>{refresh(d); setModal(null);}}/>}
{modal==="export" & & < ExportModal onClose = {()= > setModal(null)}/>}
{modal==="rapport" & & < RapportModal onClose = {()= > setModal(null)}/>}
{modal==="registry" & & < RegistryModal assets = {assets} onClose = {()= > setModal(null)}/>}
{modal==="annexe8" & & < Annexe8Modal onClose = {()= > setModal(null)}/>}
{modal==="backup" & & < BackupModal onClose = {()= > setModal(null)} onRestore={()=>{load(); setSelectedId(null); setModal(null);}}/>}
2026-06-10 14:04:34 -04:00
{showWelcome & & < WelcomeModal onClose = {()= > setShowWelcome(false)}/>}
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
< / React.Fragment >
);
}
function Grid({items, selectedId, childrenCount, onPick, onDrill, freeZone}) {
return (
< div className = "grid" >
{items.map(node=>{
const isParent = node.type==="parent";
const kids = childrenCount(node.id);
const cls = isParent?"t-parent":(freeZone?"t-piece-free":"t-piece");
const lbl = isParent?"Immo fiscale":(freeZone?"Pièce libre":"Pièce");
return (
< div key = {node.id} className = {"card"+(selectedId===node.id?" selected " : " " ) } onClick = {()= > onPick(node)}>
< span className = {"card-type " + cls } > {lbl}< / span >
< div className = "card-name" > {node.name}< / div >
< div className = "card-sub" > {node.view_admin.manufacturer} {node.view_admin.model}< / div >
< div className = "card-foot" >
< span className = "badge-status" > {node.view_admin.operational_status}< / span >
< span > {fmt(node.view_cpa.capitalizable_base_cad)} $< / span >
< / div >
{isParent & & kids>0 & & (
< div className = "drill" onClick = {e= > {e.stopPropagation(); onDrill(node);}}>
{kids} pièce{kids>1?"s":""} ›
< / div >
)}
< / div >
);
})}
< / div >
);
}
function AttachControl({parents, onAttach}) {
const [sel,setSel]=useState("");
return (< div >
< select value = {sel} onChange = {e= > setSel(e.target.value)}>
< option value = "" > — Choisir un parent —< / option >
{parents.map(p=>< option key = {p.id} value = {p.id} > {p.name}< / option > )}
< / select >
< button className = "act btn-attach" onClick = {()= > onAttach(sel)} disabled={!sel}>Rattacher< / button >
< / div > );
}
function InvoiceAttachment({asset, onRefresh}) {
const fileId = asset.view_auditor?.invoice_pdf_file_id || "";
const [uploading, setUploading] = useState(false);
const saveFileId = newId =>
api(`/api/assets/${asset.id}`,"PATCH",{changes:{"view_auditor.invoice_pdf_file_id": newId}})
.then(d => onRefresh(d));
const upload = async e => {
const file = e.target.files[0];
if (!file) return;
e.target.value = "";
setUploading(true);
const fd = new FormData();
fd.append("file", file);
try {
const r = await fetch("/api/invoices", {method:"POST", body:fd});
const d = await r.json();
if (!r.ok) { alert("Erreur : " + (d.detail||"upload échoué")); return; }
if (fileId) await fetch(`/api/invoices/${fileId}`, {method:"DELETE"});
await saveFileId(d.file_id);
} catch { alert("Erreur lors de l'envoi."); }
finally { setUploading(false); }
};
const remove = async () => {
if (!confirm("Retirer la facture jointe ?")) return;
await fetch(`/api/invoices/${fileId}`, {method:"DELETE"});
await saveFileId("");
};
const ext = fileId.split(".").pop().toLowerCase();
const isImage = ["jpg","jpeg","png"].includes(ext);
return (
< div className = "field" >
< b > Facture jointe< / b >
{fileId ? (
< div style = {{display:"flex", gap:6 , marginTop:6 , flexWrap: " wrap " } } >
< a href = {`/api/invoices/${fileId}`} target = "_blank" rel = "noreferrer" className = "icon-btn" >
{isImage ? "🖼 Voir image" : "📄 Ouvrir PDF"}
< / a >
< label className = "icon-btn" style = {{cursor:"pointer"}} >
{uploading ? "Envoi…" : "↩ Remplacer"}
< input type = "file" accept = ".pdf,.jpg,.jpeg,.png" style = {{display:"none"}} onChange = {upload} disabled = {uploading}/ >
< / label >
< button className = "icon-btn" style = {{borderColor:"#dc2626",color:"#f87171"}} onClick = {remove} > ✕ Retirer< / button >
< / div >
) : (
< div style = {{marginTop:6}} >
< label className = "icon-btn" style = {{cursor:"pointer", display: " inline-block " } } >
{uploading ? "Envoi en cours…" : "📎 Joindre (PDF · JPG · PNG)"}
< input type = "file" accept = ".pdf,.jpg,.jpeg,.png" style = {{display:"none"}} onChange = {upload} disabled = {uploading}/ >
< / label >
< / div >
)}
< / div >
);
}
function CreateModal({fixedType, onClose, onCreated}) {
const isParent = fixedType==="parent";
const [cats,setCats]=useState([]);
const t = today();
const [f,setF]=useState({
type:fixedType, name:"", cost_cad:"", replacement_cost_cad:"",
manufacturer:"", model:"", form_factor:"", serial_number:"", inventory_tag:"",
acquisition_date: t,
in_service_date: isParent ? t : "",
cca_class_id:50, assembler:"Chezlepro Inc.", supplier:"", invoice_number:""
});
const up=(k,v)=>setF(s=>({...s,[k]:v}));
useEffect(()=>{ api("/api/cca-categories","GET").then(setCats); },[]);
const submit=()=>{
if(!f.name.trim()){alert("Nom requis."); return;}
api("/api/assets","POST",{...f, cost_cad:Number(f.cost_cad)||0, replacement_cost_cad:Number(f.replacement_cost_cad)||0})
.then(onCreated).catch(e=>alert("Erreur : "+(e.detail||"inconnue")));
};
return (< div className = "modal-bg" onClick = {onClose} > < div className = "modal" onClick = {e= > e.stopPropagation()}>
< h3 > Nouvelle {isParent?"immobilisation fiscale":"pièce d'inventaire"}< / h3 >
< label className = "fl" > Nom / désignation *< / label > < input value = {f.name} onChange = {e= > up("name",e.target.value)} autoFocus/>
{isParent ? (< React.Fragment >
< label className = "fl" > Assembleur< / label > < input value = {f.assembler} onChange = {e= > up("assembler",e.target.value)}/>
< label className = "fl" > Catégorie DPA< / label >
< select value = {f.cca_class_id} onChange = {e= > up("cca_class_id",Number(e.target.value))}>
{cats.map(c=>< option key = {c.id} value = {c.id} > Cat. {c.id} — {c.description} ({Math.round(c.dpa_rate*100)}%)< / option > )}
< / select >
< label className = "fl" > Date de mise en service< / label > < input type = "date" value = {f.in_service_date} onChange = {e= > up("in_service_date",e.target.value)}/>
< div className = "note fmt" > L'immobilisation est créée vide. Sa valeur = somme des pièces que tu y rattacheras.< / div >
< / React.Fragment > ) : (< React.Fragment >
< div style = {{display:"grid", gridTemplateColumns: " 1fr 1fr " , gap: " 0 14px " } } >
< div > < label className = "fl" > Coût (CAD)< / label > < input type = "number" value = {f.cost_cad} onChange = {e= > up("cost_cad",e.target.value)}/>< / div >
< div > < label className = "fl" > Coût remplacement (CAD)< / label > < input type = "number" value = {f.replacement_cost_cad} onChange = {e= > up("replacement_cost_cad",e.target.value)}/>< / div >
< div > < label className = "fl" > Numéro de série< / label > < input value = {f.serial_number} onChange = {e= > up("serial_number",e.target.value)}/>< / div >
< div > < label className = "fl" > Étiquette d'inventaire< / label > < input value = {f.inventory_tag} onChange = {e= > up("inventory_tag",e.target.value)}/>< / div >
< div > < label className = "fl" > Fabricant< / label > < input value = {f.manufacturer} onChange = {e= > up("manufacturer",e.target.value)}/>< / div >
< div > < label className = "fl" > Modèle< / label > < input value = {f.model} onChange = {e= > up("model",e.target.value)}/>< / div >
< div > < label className = "fl" > Format< / label > < input value = {f.form_factor} onChange = {e= > up("form_factor",e.target.value)}/>< / div >
< div > < label className = "fl" > Date d'acquisition< / label > < input type = "date" value = {f.acquisition_date} onChange = {e= > up("acquisition_date",e.target.value)}/>< / div >
< div > < label className = "fl" > Fournisseur< / label > < input value = {f.supplier} onChange = {e= > up("supplier",e.target.value)}/>< / div >
< div > < label className = "fl" > N° de facture< / label > < input value = {f.invoice_number} onChange = {e= > up("invoice_number",e.target.value)}/>< / div >
< / div >
< div className = "note fmt" > Créée libre. La date de mise en service sera posée au rattachement.< / div >
< / React.Fragment > )}
< div className = "modal-actions" >
< button className = "act btn-cancel" onClick = {onClose} > Annuler< / button >
< button className = "act btn-save" onClick = {submit} > Créer< / button >
< / div >
< / div > < / div > );
}
function EditModal({asset, onClose, onSaved}) {
const fields = asset.type==="parent" ? PARENT_FIELDS : PIECE_FIELDS;
const [cats,setCats]=useState([]);
const initial = {};
fields.forEach(fd => { initial[fd.path] = fd.path==="name" ? asset.name : getPath(asset, fd.path); });
const [vals,setVals]=useState(initial);
const up=(path,v)=>setVals(s=>({...s,[path]:v}));
useEffect(()=>{ api("/api/cca-categories","GET").then(setCats); },[]);
const submit=()=>{
const changes={};
fields.forEach(fd=>{
let v = vals[fd.path];
if(fd.num) v = Number(v);
const orig = fd.path==="name"?asset.name:getPath(asset,fd.path);
if(v!==orig & & v!==undefined & & v!=="") changes[fd.path]=v;
});
if(Object.keys(changes).length===0){ onClose(); return; }
api(`/api/assets/${asset.id}`,"PATCH",{changes}).then(onSaved).catch(e=>alert("Erreur : "+(e.detail||"inconnue")));
};
return (< div className = "modal-bg" onClick = {onClose} > < div className = "modal" onClick = {e= > e.stopPropagation()}>
< h3 > Modifier — {asset.name}< / h3 >
{asset.type==="parent" & & < div className = "locked" style = {{marginBottom:10}} > La base amortissable ({fmt(asset.view_cpa.capitalizable_base_cad)} $) est calculée à partir des pièces.< / div > }
< div style = {{display:"grid", gridTemplateColumns: " 1fr 1fr " , gap: " 0 14px " } } >
{fields.map(fd=>(
< div key = {fd.path} >
< label className = "fl" > {fd.label}< / label >
{fd.select==="cca" ? (
< select value = {vals[fd.path] ? ? " " } onChange = {e= > up(fd.path, Number(e.target.value))}>
{cats.map(c=>< option key = {c.id} value = {c.id} > Cat. {c.id} — {c.description} ({Math.round(c.dpa_rate*100)}%)< / option > )}
< / select >
) : (
< input type = {fd.type||(fd.num?"number":"text")} step = {fd.num?"0.01":undefined} value = {vals[fd.path] ? ? " " } onChange = {e= > up(fd.path, e.target.value)}/>
)}
< / div >
))}
< / div >
{asset.type==="piece" & & < div className = "note fmt" > Modifier le coût recalcule automatiquement la base de l'immobilisation parente.< / div > }
{asset.type==="parent" & & < div className = "note" > Chaque modification est journalisée dans la piste d'audit.< / div > }
< div className = "modal-actions" >
< button className = "act btn-cancel" onClick = {onClose} > Annuler< / button >
< button className = "act btn-save" onClick = {submit} > Enregistrer< / button >
< / div >
< / div > < / div > );
}
function ExportModal({onClose}) {
const [data,setData]=useState(null);
useEffect(()=>{ api("/api/fiscal-export","GET").then(setData); },[]);
return (< div className = "modal-bg" onClick = {onClose} > < div className = "modal" onClick = {e= > e.stopPropagation()}>
< h3 > Export fiscal — immobilisations par catégorie< / h3 >
< div className = "note cpa" style = {{marginBottom:12}} > {NOTE_CPA}< / div >
{!data & & < div className = "empty" > Chargement…< / div > }
{data & & data.classes.length===0 & & < div className = "empty" > Aucune immobilisation à exporter.< / div > }
{data & & data.classes.map(cls=>(
< div key = {cls.cca_class_id} style = {{marginBottom:20}} >
< div className = "section-title" > Catégorie {cls.cca_class_id} — {cls.description} · DPA {Math.round(cls.dpa_rate*100)}%< / div >
< table className = "export" >
< thead > < tr > < th > Immobilisation< / th > < th > Étiquette< / th > < th className = "num" > Base CAD< / th > < th > Mise en service< / th > < th > ½ an< / th > < th className = "num" > Pièces< / th > < / tr > < / thead >
< tbody >
{cls.items.map(it=>(< tr key = {it.id} > < td > {it.name}< / td > < td > {it.inventory_tag||"—"}< / td > < td className = "num" > {fmt(it.base_cad)}< / td > < td > {it.in_service_date}< / td > < td > {it.half_year_rule?"Oui":"Non"}< / td > < td className = "num" > {it.pieces_count}< / td > < / tr > ))}
< tr className = "total" > < td colSpan = "2" > TOTAL Cat. {cls.cca_class_id}< / td > < td className = "num" > {fmt(cls.total_base_cad)}< / td > < td colSpan = "3" > < / td > < / tr >
< / tbody >
< / table >
< / div >
))}
< div className = "modal-actions" >
< button className = "act btn-cancel" onClick = {onClose} > Fermer< / button >
< a className = "act btn-save" style = {{textAlign:"center",textDecoration:"none",lineHeight:"20px"}} href = "/api/fiscal-export.csv" > Télécharger CSV< / a >
< / div >
< / div > < / div > );
}
function RegistryModal({assets, onClose}) {
const [allCats, setAllCats] = useState([]);
const [catId, setCatId] = useState(null);
const [data, setData] = useState(null);
const activeCatIds = [...new Set(assets.filter(a=>a.type==="parent").map(a=>a.view_cpa.cca_class.id))].sort((a,b)=>a-b);
useEffect(()=>{
api("/api/cca-categories","GET").then(cats=>{
setAllCats(cats);
const first = activeCatIds[0] ?? 50;
setCatId(first);
});
},[]);
useEffect(()=>{
if(catId!==null) api(`/api/cca-registry?cca_class_id=${catId}`,"GET").then(setData);
},[catId]);
const saveYear = (y, patch) => {
const base = {year:y.year, cca_class_id:catId, dispositions:Number(y.dispositions),
adjustment_coeff:Number(y.adjustment_coeff), pool_physically_empty: y.pool_physically_empty==="1"||y.pool_physically_empty===true,
additions_override: y.additions_override===""||y.additions_override==null?null:Number(y.additions_override)};
api("/api/cca-registry/year","POST",{...base, ...patch}).then(setData).catch(e=>alert("Erreur : "+(e.detail||"")));
};
const activeCats = allCats.filter(c=>activeCatIds.includes(c.id));
const catInfo = allCats.find(c=>c.id===catId);
return (< div className = "modal-bg" onClick = {onClose} > < div className = "modal" onClick = {e= > e.stopPropagation()}>
< h3 > Registre fiscal — pool DPA par catégorie< / h3 >
< div className = "note cpa" style = {{marginBottom:10}} > {NOTE_CPA}< / div >
{activeCats.length > 1 & & (
< div style = {{marginBottom:12}} >
< label className = "fl" > Catégorie active< / label >
< select value = {catId??""} onChange = {e= > {setCatId(Number(e.target.value)); setData(null);}}>
{activeCats.map(c=>< option key = {c.id} value = {c.id} > Cat. {c.id} — {c.description} ({Math.round(c.dpa_rate*100)}%)< / option > )}
< / select >
< / div >
)}
{!data & & < div className = "empty" > Chargement…< / div > }
{data & & (< React.Fragment >
< div className = "note" style = {{marginBottom:10}} >
< b > Cat. {catId} — {catInfo?.description} ({Math.round((catInfo?.dpa_rate||0)*100)}%)< / b > · Actifs physiques : < b > {data.physical_count}< / b > .{" "}
Coeff 0,5 = demi-année standard.
< / div >
{data.years.length===0 & & < div className = "empty" > Aucune année. Ajoute des acquisitions datées, ou crée une année ci-dessous.< / div > }
< table className = "export" >
< thead > < tr >
< th > Année< / th > < th className = "num" > FNACC ouv.< / th > < th className = "num" > Ajouts< / th >
< th className = "num" > Dispo.< / th > < th className = "num" > Coeff< / th > < th className = "num" > DPA< / th >
< th className = "num" > FNACC clôt.< / th > < th > Vidé?< / th >
< / tr > < / thead >
< tbody >
{data.years.map(y=>(
< tr key = {y.year} >
< td > {y.year}< / td >
< td className = "num" > {fmt(y.opening_ucc)}< / td >
< td className = "num" > {fmt(y.additions)}< / td >
< td className = "num" >
< input style = {{width:80,margin:0,padding:4}} type = "number" defaultValue = {y.dispositions}
onBlur={e=>saveYear({...y,dispositions:e.target.value})}/>
< / td >
< td className = "num" >
< input style = {{width:60,margin:0,padding:4}} type = "number" step = "0.1" min = "0" max = "1" defaultValue = {y.adjustment_coeff}
onBlur={e=>saveYear({...y,adjustment_coeff:e.target.value})}/>
< / td >
< td className = "num" style = {{color:Number(y.terminal_loss) > 0?"#f87171":Number(y.recapture)>0?"#facc15":"#e2e8f0"}}>
{Number(y.terminal_loss)>0?"PF "+fmt(y.terminal_loss):Number(y.recapture)>0?"Réc "+fmt(y.recapture):fmt(y.cca_claimed)}
< / td >
< td className = "num" > {fmt(y.closing_ucc)}< / td >
< td > < input type = "checkbox" defaultChecked = {false} onChange = {e= > saveYear({...y,pool_physically_empty:e.target.checked})} title="Catégorie vidée → perte finale"/>< / td >
< / tr >
))}
< / tbody >
< / table >
< div className = "note" > PF = perte finale (déductible 100 %) · Réc = récupération (revenu imposable).< / div >
< AddYearRow onAdd = {(yr)= > saveYear({year:yr,dispositions:0,adjustment_coeff:0.5,pool_physically_empty:false,additions_override:null})}/>
< / React.Fragment > )}
< div className = "modal-actions" >
< button className = "act btn-cancel" onClick = {onClose} > Fermer< / button >
{catId & & < a className = "act btn-save" style = {{textAlign:"center",textDecoration:"none",lineHeight:"20px"}} href = {`/api/cca-registry.csv?cca_class_id=${catId}`} > Télécharger CSV< / a > }
< / div >
< / div > < / div > );
}
function AddYearRow({onAdd}) {
const [yr,setYr]=useState(new Date().getFullYear());
return (< div style = {{display:"flex",gap:8,alignItems:"flex-end",marginTop:12}} >
< div style = {{flex:"0 0 140px " } } > < label className = "fl" > Ajouter une année< / label >
< input type = "number" value = {yr} onChange = {e= > setYr(e.target.value)} style={{margin:0}}/>< / div >
< button className = "act btn-attach" style = {{flex:"0 0 120px " } } onClick = {()= > onAdd(Number(yr))}>Ajouter< / button >
< / div > );
}
function Field({label, value}) { return < div className = "field" > < b > {label}< / b > {String(value)}< / div > ; }
function DispForm({accounting, catId}) {
const detachYear = accounting.detachment_date ? Number(accounting.detachment_date.slice(0,4)) : new Date().getFullYear();
const [year, setYear] = useState(detachYear);
const [amount, setAmount] = useState(0);
const [done, setDone] = useState(false);
const [err, setErr] = useState(null);
if(done) return (
< div className = "note" style = {{color:"#4ade80",marginTop:10,borderLeft:"2px solid # 4ade80 " , paddingLeft:6 } } >
Disposition inscrite au pool Cat. {catId} — exercice {year} ({fmt(amount)} $).
< / div >
);
return (
< div style = {{marginTop:12,background:"#1e293b",border:"1px solid # 475569 " , borderRadius:8 , padding:12 } } >
< div style = {{fontWeight:600,marginBottom:8,fontSize:13}} > Inscrire la disposition au pool fiscal · Cat. {catId}< / div >
< div style = {{display:"grid",gridTemplateColumns:"1fr 1fr " , gap: " 0 12px " } } >
< div > < label className = "fl" > Exercice fiscal< / label >
< input type = "number" value = {year} onChange = {e= > setYear(Number(e.target.value))} style={{margin:0}}/>< / div >
< div > < label className = "fl" > Produit de disposition (CAD)< / label >
< input type = "number" step = "0.01" value = {amount} onChange = {e= > setAmount(e.target.value)} style={{margin:0}}/>< / div >
< / div >
< div className = "note" style = {{marginTop:6,marginBottom:8}} >
Chezlepro ne revend pas de pièces usagées → produit = < b > 0 $< / b > . N'inscrire un montant que si un produit réel a été encaissé.
< / div >
{err & & < div className = "note" style = {{color:"#f87171"}} > {err}< / div > }
< button className = "act btn-save" style = {{width:"100%",marginTop:4}}
onClick={()=>{
api("/api/cca-registry/year/add-disposition","POST",{year:Number(year),cca_class_id:catId,amount:Number(amount)})
.then(()=>setDone(true)).catch(e=>setErr("Erreur : "+(e.detail||"inconnue")));
}}>
Confirmer ({Number(amount)===0?"aucun produit, pool inchangé":`${fmt(amount)} $ inscrits`})
< / button >
< / div >
);
}
function Annexe8Modal({onClose}) {
const [year, setYear] = useState(new Date().getFullYear()-1);
const [data, setData] = useState(null);
const loadData = y => api(`/api/annexe8?year=${y}`,"GET").then(setData).catch(()=>setData(null));
useEffect(()=>{ loadData(year); },[year]);
const totalDpa = data ? data.classes.reduce((s,c)=>s+(c.year_data?Number(c.year_data.cca_claimed):0),0) : 0;
return (< div className = "modal-bg" onClick = {onClose} > < div className = "modal" onClick = {e= > e.stopPropagation()}>
< h3 > Annexe 8 — Déduction pour amortissement (DPA)< / h3 >
< div className = "note cpa" style = {{marginBottom:12}} > {NOTE_CPA}< / div >
< div style = {{display:"flex",gap:10,alignItems:"flex-end",marginBottom:16}} >
< div style = {{flex:"0 0 180px " } } >
< label className = "fl" > Exercice fiscal< / label >
< input type = "number" value = {year} onChange = {e= > setYear(Number(e.target.value))} style={{margin:0}}/>
< / div >
< div className = "note" style = {{alignSelf:"center"}} > Chezlepro Inc. — toutes catégories actives< / div >
< / div >
{!data & & < div className = "empty" > Chargement…< / div > }
{data & & data.classes.length===0 & & < div className = "empty" > Aucune catégorie active.< / div > }
{data & & data.classes.length>0 & & (
< React.Fragment >
< table className = "export" >
< thead > < tr >
< th > Catégorie< / th >
< th className = "num" > FNACC ouv.< / th > < th className = "num" > Ajouts< / th >
< th className = "num" > Dispos.< / th > < th className = "num" > Ajust.< / th >
< th className = "num" > Base DPA< / th > < th className = "num" > DPA< / th >
< th className = "num" > FNACC clôt.< / th >
< / tr > < / thead >
< tbody >
{data.classes.map(cls=>{
const y = cls.year_data;
return (
< tr key = {cls.cca_class_id} >
< td > Cat. {cls.cca_class_id}< br / > < span style = {{color:"#64748b",fontSize:10}} > {cls.description} · {Math.round(cls.dpa_rate*100)}%< / span > < / td >
{y ? (< React.Fragment >
< td className = "num" > {fmt(y.opening_ucc)}< / td >
< td className = "num" > {fmt(y.additions)}< / td >
< td className = "num" > {fmt(y.dispositions)}< / td >
< td className = "num" > {fmt(y.adjustment)}< / td >
< td className = "num" > {fmt(y.base_for_cca)}< / td >
< td className = "num" style = {{color:Number(y.terminal_loss) > 0?"#f87171":Number(y.recapture)>0?"#facc15":"#e2e8f0"}}>
{Number(y.terminal_loss)>0?"PF "+fmt(y.terminal_loss):Number(y.recapture)>0?"Réc "+fmt(y.recapture):fmt(y.cca_claimed)}
< / td >
< td className = "num" > {fmt(y.closing_ucc)}< / td >
< / React.Fragment > ) : (
< td colSpan = "7" style = {{color:"#64748b"}} > — Aucune donnée pour {year} —< / td >
)}
< / tr >
);
})}
< tr className = "total" >
< td > TOTAL DPA réclamée< / td > < td colSpan = "5" > < / td >
< td className = "num" > {fmt(totalDpa)}< / td > < td > < / td >
< / tr >
< / tbody >
< / table >
< div className = "note" style = {{marginTop:8}} > PF = perte finale · Réc = récupération · Généré le {data.generated_at}< / div >
< / React.Fragment >
)}
< div className = "modal-actions" >
< button className = "act btn-cancel" onClick = {onClose} > Fermer< / button >
< a className = "act btn-save" style = {{textAlign:"center",textDecoration:"none",lineHeight:"20px"}} href = {`/api/annexe8.csv?year=${year}`} > Télécharger CSV< / a >
< / div >
< / div > < / div > );
}
function RapportModal({onClose}) {
const thisYear = new Date().getFullYear();
const [year, setYear] = useState(thisYear - 1);
const [data, setData] = useState(null);
const [busy, setBusy] = useState(false);
const loadData = y => {
setBusy(true);
api(`/api/rapport-fiscal?year=${y}`,"GET")
.then(d=>{ setData(d); setBusy(false); })
.catch(()=>setBusy(false));
};
useEffect(()=>{ loadData(year); }, [year]);
const s1 = data?.schedule1;
const net = s1 ? s1.book_deprec_cad + s1.recapture_cad - s1.cca_claimed_cad - s1.terminal_loss_cad : 0;
return (< div className = "modal-bg" onClick = {onClose} > < div className = "modal" style = {{width:820}} onClick = {e= > e.stopPropagation()}>
< div style = {{display:"flex",justifyContent:"space-between",alignItems:"center",marginBottom:16}} >
< h3 style = {{margin:0}} > Rapport fiscal< / h3 >
< div style = {{display:"flex",gap:8,alignItems:"center"}} >
< label style = {{fontSize:12,color:"#94a3b8"}} > Exercice< / label >
< select style = {{width:90}} value = {year} onChange = {e= > setYear(Number(e.target.value))}>
{[...Array(6)].map((_,i)=>{const y=thisYear-1-i; return < option key = {y} value = {y} > {y}< / option > ;})}
< / select >
< a href = {`/api/rapport-fiscal.html?year=${year}`} target = "_blank"
className="bar btn-export" style={{textDecoration:"none",padding:"9px 13px",fontSize:13}}>
Ouvrir rapport imprimable
< / a >
< / div >
< / div >
{busy & & < div className = "empty" > Calcul en cours…< / div > }
{!busy & & s1 & & (< React.Fragment >
< p style = {{fontSize:12,color:"#94a3b8",margin:"0 0 14px " } } >
Ajustements à inscrire dans l'< b > Annexe 1 / Schedule 1< / b > de ton T2 et CO-17.
< / p >
< table className = "export" >
< thead >
< tr > < th > Ajustement< / th > < th > Ligne T2 Sch. 1< / th > < th > Ligne CO-17 Ann. 1< / th > < th style = {{textAlign:"right"}} > Montant< / th > < th > Effet< / th > < / tr >
< / thead >
< tbody >
< tr >
< td > Amortissement comptable (add-back)< div style = {{fontSize:10,color:"#64748b"}} > estimé selon durées de vie utiles< / div > < / td >
< td > 104< / td > < td > 130< / td >
< td className = "num fmt" > {fmt(s1.book_deprec_cad)} $< / td >
< td style = {{color:"#f87171"}} > + revenu< / td >
< / tr >
< tr >
< td > Récupération d'amortissement< / td >
< td > 107< / td > < td > 135< / td >
< td className = "num fmt" > {s1.recapture_cad ? fmt(s1.recapture_cad)+" $" : "—"}< / td >
< td style = {{color:"#f87171"}} > + revenu< / td >
< / tr >
< tr style = {{background:"#1e3a5f"}} >
< td > < b > DPA réclamée — toutes catégories< / b > < / td >
< td > < b > 410< / b > < / td > < td > < b > 150< / b > < / td >
< td className = "num fmt" style = {{color:"#4ade80"}} > < b > {fmt(s1.cca_claimed_cad)} $< / b > < / td >
< td style = {{color:"#4ade80"}} > − revenu< / td >
< / tr >
< tr >
< td > Perte finale< / td >
< td > 414< / td > < td > 155< / td >
< td className = "num fmt" > {s1.terminal_loss_cad ? fmt(s1.terminal_loss_cad)+" $" : "—"}< / td >
< td style = {{color:"#4ade80"}} > − revenu< / td >
< / tr >
< tr style = {{background:"#1e293b",borderTop:"2px solid # 334155 " } } >
< td colSpan = {3} > < b > {net > 0 ? "Augmentation nette du revenu imposable" : "Réduction nette du revenu imposable"}< / b > < / td >
< td className = "num fmt" > < b style = {{color:net > 0?"#f87171":"#4ade80"}}>{fmt(Math.abs(net))} $< / b > < / td >
< td style = {{color:net > 0?"#f87171":"#4ade80"}}>< b > {net>0?"+":"− "}< / b > < / td >
< / tr >
< / tbody >
< / table >
< div className = "note" style = {{marginTop:12}} >
Numéros de lignes pour l'exercice {year}. Vérifier avec les formulaires officiels de l'année de production.
< / div >
< / React.Fragment > )}
< div className = "modal-actions" >
< button className = "act btn-cancel" onClick = {onClose} > Fermer< / button >
< / div >
< / div > < / div > );
}
function BackupModal({onClose, onRestore}) {
const [backups, setBackups] = useState([]);
const [busy, setBusy] = useState(false);
const loadBackups = () => api("/api/backups","GET").then(setBackups);
useEffect(()=>{ loadBackups(); }, []);
const createBackup = () => {
setBusy(true);
api("/api/backup","POST")
.then(d=>setBackups(d.backups))
.catch(e=>alert("Erreur : "+(e.detail||"inconnue")))
.finally(()=>setBusy(false));
};
const restore = filename => {
if(!confirm(`Restaurer « ${filename} » ?\n\nToutes les données actuelles seront remplacées.`)) return;
setBusy(true);
api("/api/restore","POST",{filename})
.then(()=>onRestore())
.catch(e=>{ alert("Erreur : "+(e.detail||"inconnue")); setBusy(false); });
};
return (< div className = "modal-bg" onClick = {onClose} > < div className = "modal" onClick = {e= > e.stopPropagation()}>
< h3 > Sauvegardes< / h3 >
< button onClick = {createBackup} disabled = {busy} className = "bar btn-new" style = {{marginBottom:16}} >
{busy?"En cours…":"Créer une sauvegarde maintenant"}
< / button >
{backups.length===0
? < div className = "empty" > Aucune sauvegarde. Crée-en une ci-dessus.< / div >
: < table className = "export" >
< thead > < tr > < th > Fichier< / th > < th > Date< / th > < th className = "num" > Taille< / th > < th > < / th > < / tr > < / thead >
< tbody >
{backups.map(b=>(
< tr key = {b.filename} >
< td style = {{fontFamily:"monospace",fontSize:12}} > {b.filename}< / td >
< td > {b.created_at}< / td >
< td className = "num" > {b.size_kb} ko< / td >
< td > < button onClick = {()= > restore(b.filename)} disabled={busy} className="icon-btn" style={{borderColor:"#dc2626",color:"#f87171"}}>Restaurer< / button > < / td >
< / tr >
))}
< / tbody >
< / table > }
< div className = "note" style = {{marginTop:12}} > Sauvegardes stockées dans < code > data/backups/< / code > .< / div >
< div className = "modal-actions" >
< button className = "act btn-cancel" onClick = {onClose} > Fermer< / button >
< / div >
< / div > < / div > );
}
2026-06-10 14:04:34 -04:00
function WelcomeModal({ onClose }) {
const [nePlus, setNePlus] = useState(false);
const fermer = () => {
if (nePlus) localStorage.setItem("immos_welcome_v1", "1");
onClose();
};
return (
< div className = "modal-bg" style = {{zIndex:70}} >
< div className = "modal welcome-modal" onClick = {e= > e.stopPropagation()}>
Produit v1.1 — Mon Registraire : page de bienvenue, À propos, citations législatives
UX :
- Page de bienvenue au lancement (WelcomeModal) : trois personas (dirigeant,
CPA, auditeur) avec codes couleur, liste des capacités et avertissements
- Case "Ne plus afficher" persistée dans localStorage (clé immos_welcome_v1)
- Bouton "En savoir plus →" ouvre static/apropos.html dans un nouvel onglet
- Nom de produit : Mon Registraire (titre, onglets, toutes les pages statiques)
Documentation :
- Nouvelle page static/apropos.html : intention, pour qui, pas pour qui,
capacités détaillées (3 tableaux), limitations (6 cartes) — même style
visuel que aide.html, navigation latérale avec scroll actif
Conformité fiscale :
- cca_pool.py : bloc de références législatives dans le docstring
(LIR 20(1)(a), 13(21), 13(1), 20(16) ; Règl. 1100(1)/(2) ; Règl. Annexe II)
+ 3 commentaires ciblés sur la récupération, la perte finale et la règle
de la demi-année
- cca_registry.py : bloc de références (LIR 13(21), Règl. Partie XI, Guide T2 T4012)
- store.py : références Règl. Annexe II sur le tableau CCA_CATEGORIES et
Règl. 1100(1)/(2) sur le calcul FNACC indicatif par pièce
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-10 15:26:38 -04:00
< div className = "welcome-logo" > 📊< / div >
< div className = "welcome-title" > Mon Registraire< / div >
< div className = "welcome-sub" > Registre DPA · Annexe 8 · Piste d'audit — pour PME canadiennes< / div >
2026-06-10 14:04:34 -04:00
< div className = "welcome-section" >
Produit v1.1 — Mon Registraire : page de bienvenue, À propos, citations législatives
UX :
- Page de bienvenue au lancement (WelcomeModal) : trois personas (dirigeant,
CPA, auditeur) avec codes couleur, liste des capacités et avertissements
- Case "Ne plus afficher" persistée dans localStorage (clé immos_welcome_v1)
- Bouton "En savoir plus →" ouvre static/apropos.html dans un nouvel onglet
- Nom de produit : Mon Registraire (titre, onglets, toutes les pages statiques)
Documentation :
- Nouvelle page static/apropos.html : intention, pour qui, pas pour qui,
capacités détaillées (3 tableaux), limitations (6 cartes) — même style
visuel que aide.html, navigation latérale avec scroll actif
Conformité fiscale :
- cca_pool.py : bloc de références législatives dans le docstring
(LIR 20(1)(a), 13(21), 13(1), 20(16) ; Règl. 1100(1)/(2) ; Règl. Annexe II)
+ 3 commentaires ciblés sur la récupération, la perte finale et la règle
de la demi-année
- cca_registry.py : bloc de références (LIR 13(21), Règl. Partie XI, Guide T2 T4012)
- store.py : références Règl. Annexe II sur le tableau CCA_CATEGORIES et
Règl. 1100(1)/(2) sur le calcul FNACC indicatif par pièce
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-10 15:26:38 -04:00
< h4 > Trois personnes. Un seul registre.< / h4 >
< div style = {{display:"grid", gridTemplateColumns: " repeat ( auto-fit , minmax ( 150px , 1fr ) ) " , gap:10 , marginTop:8 } } >
< div style = {{background:"#1e293b", borderRadius:8 , padding: " 12px 14px " , borderTop: " 2px solid # 2563eb " } } >
< div style = {{fontWeight:700, fontSize:13 , marginBottom:4 } } > Le dirigeant< / div >
< div style = {{fontSize:12, color: " # 94a3b8 " , lineHeight:1 . 5 } } > Voit la valeur nette, l'état du parc et le coût de remplacement — sans jargon comptable.< / div >
< / div >
< div style = {{background:"#1e293b", borderRadius:8 , padding: " 12px 14px " , borderTop: " 2px solid # 16a34a " } } >
< div style = {{fontWeight:700, fontSize:13 , marginBottom:4 } } > Le CPA conseil< / div >
< div style = {{fontSize:12, color: " # 94a3b8 " , lineHeight:1 . 5 } } > Obtient la FNACC, les pools DPA par catégorie et l'Annexe 8 CSV prête à signer.< / div >
< / div >
< div style = {{background:"#1e293b", borderRadius:8 , padding: " 12px 14px " , borderTop: " 2px solid # 7c3aed " } } >
< div style = {{fontWeight:700, fontSize:13 , marginBottom:4 } } > L'auditeur< / div >
< div style = {{fontSize:12, color: " # 94a3b8 " , lineHeight:1 . 5 } } > Trouve les numéros de série, les factures jointes et la piste horodatée de chaque mouvement.< / div >
< / div >
< / div >
2026-06-10 14:04:34 -04:00
< / div >
< div className = "welcome-section" >
Produit v1.1 — Mon Registraire : page de bienvenue, À propos, citations législatives
UX :
- Page de bienvenue au lancement (WelcomeModal) : trois personas (dirigeant,
CPA, auditeur) avec codes couleur, liste des capacités et avertissements
- Case "Ne plus afficher" persistée dans localStorage (clé immos_welcome_v1)
- Bouton "En savoir plus →" ouvre static/apropos.html dans un nouvel onglet
- Nom de produit : Mon Registraire (titre, onglets, toutes les pages statiques)
Documentation :
- Nouvelle page static/apropos.html : intention, pour qui, pas pour qui,
capacités détaillées (3 tableaux), limitations (6 cartes) — même style
visuel que aide.html, navigation latérale avec scroll actif
Conformité fiscale :
- cca_pool.py : bloc de références législatives dans le docstring
(LIR 20(1)(a), 13(21), 13(1), 20(16) ; Règl. 1100(1)/(2) ; Règl. Annexe II)
+ 3 commentaires ciblés sur la récupération, la perte finale et la règle
de la demi-année
- cca_registry.py : bloc de références (LIR 13(21), Règl. Partie XI, Guide T2 T4012)
- store.py : références Règl. Annexe II sur le tableau CCA_CATEGORIES et
Règl. 1100(1)/(2) sur le calcul FNACC indicatif par pièce
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-10 15:26:38 -04:00
< h4 > Ce que ça produit< / h4 >
2026-06-10 14:04:34 -04:00
< ul >
Produit v1.1 — Mon Registraire : page de bienvenue, À propos, citations législatives
UX :
- Page de bienvenue au lancement (WelcomeModal) : trois personas (dirigeant,
CPA, auditeur) avec codes couleur, liste des capacités et avertissements
- Case "Ne plus afficher" persistée dans localStorage (clé immos_welcome_v1)
- Bouton "En savoir plus →" ouvre static/apropos.html dans un nouvel onglet
- Nom de produit : Mon Registraire (titre, onglets, toutes les pages statiques)
Documentation :
- Nouvelle page static/apropos.html : intention, pour qui, pas pour qui,
capacités détaillées (3 tableaux), limitations (6 cartes) — même style
visuel que aide.html, navigation latérale avec scroll actif
Conformité fiscale :
- cca_pool.py : bloc de références législatives dans le docstring
(LIR 20(1)(a), 13(21), 13(1), 20(16) ; Règl. 1100(1)/(2) ; Règl. Annexe II)
+ 3 commentaires ciblés sur la récupération, la perte finale et la règle
de la demi-année
- cca_registry.py : bloc de références (LIR 13(21), Règl. Partie XI, Guide T2 T4012)
- store.py : références Règl. Annexe II sur le tableau CCA_CATEGORIES et
Règl. 1100(1)/(2) sur le calcul FNACC indicatif par pièce
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-10 15:26:38 -04:00
< li > Registre pool DPA multi-catégories, multi-exercices (Cat. 8, 10, 12, 46, 50…)< / li >
< li > DPA, FNACC, perte finale et récupération — règle de la demi-année incluse< / li >
< li > Annexe 8 / Schedule 8 avec export CSV, rapport fiscal T2 / CO-17< / li >
< li > Inventaire à deux niveaux : immobilisation fiscale → pièces composantes< / li >
< li > Factures jointes, sauvegardes horodatées, piste d'audit complète< / li >
2026-06-10 14:04:34 -04:00
< / ul >
< / div >
< div className = "welcome-section" >
Produit v1.1 — Mon Registraire : page de bienvenue, À propos, citations législatives
UX :
- Page de bienvenue au lancement (WelcomeModal) : trois personas (dirigeant,
CPA, auditeur) avec codes couleur, liste des capacités et avertissements
- Case "Ne plus afficher" persistée dans localStorage (clé immos_welcome_v1)
- Bouton "En savoir plus →" ouvre static/apropos.html dans un nouvel onglet
- Nom de produit : Mon Registraire (titre, onglets, toutes les pages statiques)
Documentation :
- Nouvelle page static/apropos.html : intention, pour qui, pas pour qui,
capacités détaillées (3 tableaux), limitations (6 cartes) — même style
visuel que aide.html, navigation latérale avec scroll actif
Conformité fiscale :
- cca_pool.py : bloc de références législatives dans le docstring
(LIR 20(1)(a), 13(21), 13(1), 20(16) ; Règl. 1100(1)/(2) ; Règl. Annexe II)
+ 3 commentaires ciblés sur la récupération, la perte finale et la règle
de la demi-année
- cca_registry.py : bloc de références (LIR 13(21), Règl. Partie XI, Guide T2 T4012)
- store.py : références Règl. Annexe II sur le tableau CCA_CATEGORIES et
Règl. 1100(1)/(2) sur le calcul FNACC indicatif par pièce
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-10 15:26:38 -04:00
< h4 > À savoir avant de commencer< / h4 >
2026-06-10 14:04:34 -04:00
< div className = "welcome-warn" >
< ul >
Produit v1.1 — Mon Registraire : page de bienvenue, À propos, citations législatives
UX :
- Page de bienvenue au lancement (WelcomeModal) : trois personas (dirigeant,
CPA, auditeur) avec codes couleur, liste des capacités et avertissements
- Case "Ne plus afficher" persistée dans localStorage (clé immos_welcome_v1)
- Bouton "En savoir plus →" ouvre static/apropos.html dans un nouvel onglet
- Nom de produit : Mon Registraire (titre, onglets, toutes les pages statiques)
Documentation :
- Nouvelle page static/apropos.html : intention, pour qui, pas pour qui,
capacités détaillées (3 tableaux), limitations (6 cartes) — même style
visuel que aide.html, navigation latérale avec scroll actif
Conformité fiscale :
- cca_pool.py : bloc de références législatives dans le docstring
(LIR 20(1)(a), 13(21), 13(1), 20(16) ; Règl. 1100(1)/(2) ; Règl. Annexe II)
+ 3 commentaires ciblés sur la récupération, la perte finale et la règle
de la demi-année
- cca_registry.py : bloc de références (LIR 13(21), Règl. Partie XI, Guide T2 T4012)
- store.py : références Règl. Annexe II sur le tableau CCA_CATEGORIES et
Règl. 1100(1)/(2) sur le calcul FNACC indicatif par pièce
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-10 15:26:38 -04:00
< li > Les calculs sont structurellement corrects mais < strong > indicatifs< / strong > — validation CPA obligatoire avant toute déclaration.< / li >
< li > Non certifié par l'ARC ni Revenu Québec. Aucun taux d'accélération temporaire automatique.< / li >
< li > Conçu pour une seule entité juridique — pas de consolidation multi-entités.< / li >
2026-06-10 14:04:34 -04:00
< / ul >
< / div >
< / div >
< div className = "welcome-footer" >
< label className = "welcome-check" >
< input type = "checkbox" checked = {nePlus} onChange = {e= > setNePlus(e.target.checked)}/>
Ne plus afficher cette page
< / label >
Produit v1.1 — Mon Registraire : page de bienvenue, À propos, citations législatives
UX :
- Page de bienvenue au lancement (WelcomeModal) : trois personas (dirigeant,
CPA, auditeur) avec codes couleur, liste des capacités et avertissements
- Case "Ne plus afficher" persistée dans localStorage (clé immos_welcome_v1)
- Bouton "En savoir plus →" ouvre static/apropos.html dans un nouvel onglet
- Nom de produit : Mon Registraire (titre, onglets, toutes les pages statiques)
Documentation :
- Nouvelle page static/apropos.html : intention, pour qui, pas pour qui,
capacités détaillées (3 tableaux), limitations (6 cartes) — même style
visuel que aide.html, navigation latérale avec scroll actif
Conformité fiscale :
- cca_pool.py : bloc de références législatives dans le docstring
(LIR 20(1)(a), 13(21), 13(1), 20(16) ; Règl. 1100(1)/(2) ; Règl. Annexe II)
+ 3 commentaires ciblés sur la récupération, la perte finale et la règle
de la demi-année
- cca_registry.py : bloc de références (LIR 13(21), Règl. Partie XI, Guide T2 T4012)
- store.py : références Règl. Annexe II sur le tableau CCA_CATEGORIES et
Règl. 1100(1)/(2) sur le calcul FNACC indicatif par pièce
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-10 15:26:38 -04:00
< div style = {{display:"flex", gap:8 } } >
< a href = "/static/apropos.html" target = "_blank" rel = "noopener" style = {{textDecoration:"none"}} >
< button className = "act btn-cancel" style = {{minWidth:130}} type = "button" > En savoir plus →< / button >
< / a >
< button className = "act btn-save" style = {{minWidth:120}} onClick = {fermer} > Commencer< / button >
< / div >
2026-06-10 14:04:34 -04:00
< / div >
< / div >
< / div >
);
}
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
ReactDOM.createRoot(document.getElementById("root")).render(< App / > );
< / script >
< / body >
< / html >