Mise en place de la feature de gestion des Frais
This commit is contained in:
1 parent
9ea078302d
commit
0334a63139
27 files changed
+2401
-117
No files matched your search
@@ -2613,4 +2613,118 @@ if (!smtpCols.includes('documents_quota_count')) {
|
||||
}
|
||||
}
|
||||
|
||||
// ── Migration : frais d'opération (Étape 1 — fondations) ────────────────────
|
||||
// Modélise les frais facturés par certaines plateformes (frais d'entrée,
|
||||
// frais de gestion, frais sur distribution) : un montant TTC unique par
|
||||
// événement (pas de détail HT/TVA, non pertinent pour un particulier).
|
||||
// - remboursements.frais_ttc : montant de frais prélevé sur CE coupon,
|
||||
// vient réduire la base imposable au même titre que taxe_locale
|
||||
// (imposition = f(interets_bruts - frais_ttc)), cf. feedback Olivier
|
||||
// 30/08/26 sur l'avis d'opéré/notice de distribution Fundora (Kyoseil AM).
|
||||
// - frais_operations : table de traçabilité pour tout frais, qu'il soit
|
||||
// prélevé "à la source" (souscription ou coupon, aucun mouvement de
|
||||
// trésorerie réel) ou payé en "versement complémentaire" (porte-monnaie
|
||||
// de la plateforme ou compte courant du détenteur — dans ce dernier cas
|
||||
// sans impact sur le solde du porte-monnaie). Pas encore d'UI ni de route
|
||||
// de création automatique à ce stade : fondations DB + formule fiscale
|
||||
// uniquement, le reste (étapes souscription/remboursement/modale dédiée)
|
||||
// viendra dans une prochaine passe.
|
||||
{
|
||||
const rembColsFraisTtc = db.prepare('PRAGMA table_info(remboursements)').all().map(c => c.name);
|
||||
if (!rembColsFraisTtc.includes('frais_ttc')) {
|
||||
db.exec('ALTER TABLE remboursements ADD COLUMN frais_ttc REAL NOT NULL DEFAULT 0');
|
||||
}
|
||||
}
|
||||
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS frais_operations (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
investissement_id INTEGER NOT NULL REFERENCES investissements(id) ON DELETE CASCADE,
|
||||
plateforme_id INTEGER NOT NULL REFERENCES plateformes(id) ON DELETE CASCADE,
|
||||
remboursement_id INTEGER REFERENCES remboursements(id) ON DELETE CASCADE,
|
||||
montant REAL NOT NULL,
|
||||
mode_reglement TEXT NOT NULL DEFAULT 'source'
|
||||
CHECK(mode_reglement IN ('source','portefeuille','compte_courant')),
|
||||
compte_id INTEGER REFERENCES comptes(id) ON DELETE SET NULL,
|
||||
date_operation TEXT NOT NULL,
|
||||
origine TEXT NOT NULL DEFAULT 'manuel'
|
||||
CHECK(origine IN ('souscription','remboursement','manuel')),
|
||||
notes TEXT,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
)
|
||||
`);
|
||||
db.exec('CREATE INDEX IF NOT EXISTS idx_fraisop_inv ON frais_operations(investissement_id)');
|
||||
db.exec('CREATE INDEX IF NOT EXISTS idx_fraisop_plat ON frais_operations(plateforme_id)');
|
||||
db.exec('CREATE INDEX IF NOT EXISTS idx_fraisop_remb ON frais_operations(remboursement_id)');
|
||||
db.exec('CREATE INDEX IF NOT EXISTS idx_fraisop_date ON frais_operations(date_operation)');
|
||||
|
||||
// ── Migration : catégories de frais (31/08/26, demande Olivier) ─────────────
|
||||
// `origine` (souscription/remboursement/manuel) reste un détail technique (comment la
|
||||
// ligne a été créée) ; `categorie` devient le classement visible côté utilisateur, utile
|
||||
// pour le reporting "Frais payés" de l'Étape 5 : souscription (frais d'entrée, unique par
|
||||
// investissement — lu/écrit uniquement depuis l'étape "Frais" du formulaire de
|
||||
// création/modification) / gestion / distribution (liée à un remboursement) / autre
|
||||
// (saisie libre depuis le bloc "Frais d'opération" de la fiche investissement).
|
||||
{
|
||||
const fraisOpCols = db.prepare('PRAGMA table_info(frais_operations)').all().map(c => c.name);
|
||||
if (!fraisOpCols.includes('categorie')) {
|
||||
db.exec("ALTER TABLE frais_operations ADD COLUMN categorie TEXT NOT NULL DEFAULT 'autre'");
|
||||
// Rattrapage pour les lignes déjà créées avant l'ajout de cette colonne (tests Olivier,
|
||||
// 30-31/08/26) : on déduit la catégorie la plus probable depuis `origine`.
|
||||
db.exec(`
|
||||
UPDATE frais_operations SET categorie = CASE origine
|
||||
WHEN 'souscription' THEN 'souscription'
|
||||
WHEN 'remboursement' THEN 'distribution'
|
||||
ELSE 'autre'
|
||||
END
|
||||
`);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Migration : mode_reglement 'remboursement' (frais de distribution liés
|
||||
// automatiquement à un remboursement — Étape 4, 31/08/26) ───────────────────
|
||||
// SQLite ne permet pas d'altérer un CHECK constraint en place (better-sqlite3
|
||||
// bloque writable_schema) → même stratégie de reconstruction de table que pour
|
||||
// investissements/plateformes/remboursements ailleurs dans ce fichier.
|
||||
{
|
||||
const schemaFraisOp = db.prepare(
|
||||
"SELECT sql FROM sqlite_master WHERE type='table' AND name='frais_operations'"
|
||||
).get()?.sql ?? '';
|
||||
|
||||
if (schemaFraisOp.includes("mode_reglement IN ('source','portefeuille','compte_courant')")) {
|
||||
const tempName = '__repair_frais_operations';
|
||||
|
||||
const fixedDdl = schemaFraisOp
|
||||
.replace(/CREATE TABLE frais_operations\b/, `CREATE TABLE "${tempName}"`)
|
||||
.replace(
|
||||
"mode_reglement IN ('source','portefeuille','compte_courant')",
|
||||
"mode_reglement IN ('source','portefeuille','compte_courant','remboursement')",
|
||||
);
|
||||
|
||||
const colDefs = db.prepare('PRAGMA table_info(frais_operations)').all();
|
||||
const colNames = colDefs.map(c => `"${c.name}"`).join(', ');
|
||||
const idxs = db.prepare(
|
||||
"SELECT name FROM sqlite_master WHERE type='index' AND tbl_name='frais_operations' AND sql IS NOT NULL"
|
||||
).all();
|
||||
|
||||
db.exec('PRAGMA foreign_keys = OFF');
|
||||
db.exec(`DROP TABLE IF EXISTS "${tempName}"`);
|
||||
db.exec(fixedDdl);
|
||||
db.exec(`INSERT INTO "${tempName}" (${colNames}) SELECT ${colNames} FROM frais_operations`);
|
||||
for (const idx of idxs) db.exec(`DROP INDEX IF EXISTS "${idx.name}"`);
|
||||
db.exec('DROP TABLE frais_operations');
|
||||
// legacy_alter_table = ON : empêche la réécriture automatique des FK enfants
|
||||
db.exec('PRAGMA legacy_alter_table = ON');
|
||||
db.exec(`ALTER TABLE "${tempName}" RENAME TO frais_operations`);
|
||||
db.exec('PRAGMA legacy_alter_table = OFF');
|
||||
db.exec('PRAGMA foreign_keys = ON');
|
||||
|
||||
db.exec('CREATE INDEX IF NOT EXISTS idx_fraisop_inv ON frais_operations(investissement_id)');
|
||||
db.exec('CREATE INDEX IF NOT EXISTS idx_fraisop_plat ON frais_operations(plateforme_id)');
|
||||
db.exec('CREATE INDEX IF NOT EXISTS idx_fraisop_remb ON frais_operations(remboursement_id)');
|
||||
db.exec('CREATE INDEX IF NOT EXISTS idx_fraisop_date ON frais_operations(date_operation)');
|
||||
}
|
||||
}
|
||||
|
||||
export default db;
|
||||
Reference in new issue
Block a user