Mise en place de la feature de gestion des Frais

This commit is contained in:
ocroguennec committed 2026-09-01 22:16:18 +02:00
1 parent 9ea078302d
commit 0334a63139
27 files changed
+2401 -117

No files matched your search

+74 -2
View File
@@ -6,8 +6,13 @@ import { fmtEUR } from '../utils/format.js';
const round2 = (n) => Math.round(n * 100) / 100;
const fmtRate = (n) => n != null ? n.toFixed(1).replace('.', ',') + ' %' : '…';
// Le frais ne réduit interets_nets que lorsqu'il est retenu SUR ce remboursement (mode
// 'remboursement', par défaut) — un frais réglé à part ('portefeuille'/'compte_courant',
// Étape 5 bis, 01/09/26 — cas Baltis) n'a aucun impact ici, cf. computeChamps() côté backend.
const fraisReduitBase = (f) => !f.frais_mode_reglement || f.frais_mode_reglement === 'remboursement';
const computeInteretsNets = (f) =>
round2(Number(f.interets_bruts || 0) - Number(f.prelev_sociaux || 0) - Number(f.prelev_forfaitaire || 0));
round2(Number(f.interets_bruts || 0) - (fraisReduitBase(f) ? Number(f.frais_ttc || 0) : 0) - Number(f.prelev_sociaux || 0) - Number(f.prelev_forfaitaire || 0));
const computeNet = (f) =>
round2(Number(f.capital || 0) + Number(f.cashback || 0) + computeInteretsNets(f));
@@ -86,10 +91,20 @@ export default function RembFormModal({
const hasLocalTax = !isBonus && currentPlat?.fiscalite === 'avec_fiscalite_locale' && currentPlat?.taux_fiscalite_locale;
const isExonere = !isBonus && currentInv?.fiscalite_override === 'exonere';
const isIndicatif = Boolean(isExonere || (!isBonus && currentPlat && currentPlat.fiscalite !== 'flat_tax'));
// Cas indicatif (plateforme étrangère ou exonérée) : le montant versé est net de frais mais
// pas des prélèvements (indicatifs) — même formule que net_recu côté backend (computeChamps,
// branche isTaxIndicatif) — cf. Étape 4, 31/08/26.
const montantVerse = isIndicatif
? round2(Number(form.capital || 0) + Number(form.cashback || 0) + Number(form.interets_bruts || 0))
? round2(Number(form.capital || 0) + Number(form.cashback || 0) + Number(form.interets_bruts || 0) - (fraisReduitBase(form) ? Number(form.frais_ttc || 0) : 0))
: computeNet(form);
// Section "Frais" (frais de distribution, Étape 4) : visible si la plateforme facture des
// frais (référentiel frais_plateforme ≠ 'aucun'), ou si un montant est déjà saisi (garde
// l'accès même si la config de la plateforme change ensuite) — même logique que le bloc
// "Frais d'opération" de la fiche investissement, cf. project_frais_operation.md.
const showFraisSection = !isBonus
&& ((currentPlat?.frais_plateforme && currentPlat.frais_plateforme !== 'aucun') || Number(form.frais_ttc) > 0);
const modalTitle = title ?? (editingId ? 'Modifier le remboursement' : (locked ? 'Saisir le remboursement' : 'Nouveau remboursement'));
const handleMethodeChange = (m) => {
@@ -97,6 +112,16 @@ export default function RembFormModal({
onFormPatch({ methode_remboursement: m, compte_id: def ? String(def.id) : '' });
};
// Prélèvement du frais (Étape 5 bis, 01/09/26) : passe par onFieldChange pour déclencher le
// recalcul des prélèvements (comme un changement de frais_ttc, cf. setField/setRembField dans
// les 3 pages appelantes), puis pré-sélectionne un compte par défaut si on bascule sur
// 'compte_courant' — même pattern que handleFraisReglementChange côté investissement.
const handleFraisModeChange = (mode) => {
onFieldChange('frais_mode_reglement', mode);
const def = mode === 'compte_courant' ? (comptesInvestisseur?.find(c => c.type === 'compte_courant') ?? comptesInvestisseur?.[0]) : null;
onFormPatch({ frais_compte_id: def ? String(def.id) : '' });
};
return (
<Modal
open={open}
@@ -202,6 +227,53 @@ export default function RembFormModal({
onChange={e => onFieldChange('cashback', e.target.value)} />
</div>
)}
{showFraisSection && (<>
<div style={{ gridColumn: 'span 3', borderTop: '1px solid var(--border)', paddingTop: 4, marginTop: 4 }}>
<span style={{ fontSize: 11, fontWeight: 600, textTransform: 'uppercase', letterSpacing: '0.06em', color: 'var(--text-muted)' }}>Frais</span>
</div>
<div>
<label>Montant des frais (€ TTC)</label>
<input type="number" step="0.01" min="0" value={form.frais_ttc ?? 0}
onChange={e => onFieldChange('frais_ttc', e.target.value)} />
</div>
{!isInteretsPlateforme ? (<>
<div style={{ gridColumn: 'span 2' }}>
<label>Prélèvement</label>
<select value={form.frais_mode_reglement || 'remboursement'} onChange={e => handleFraisModeChange(e.target.value)}>
<option value="remboursement">Retenu sur ce remboursement (réduit la base imposable et le montant versé)</option>
<option value="portefeuille">Versement complémentaire — Porte-monnaie de la plateforme</option>
<option value="compte_courant">Versement complémentaire — Compte courant du détenteur</option>
</select>
</div>
{form.frais_mode_reglement === 'compte_courant' && (
<div style={{ gridColumn: 'span 3' }}>
<label>Compte de règlement des frais *</label>
{comptesInvestisseur?.length > 0 ? (
<select required value={form.frais_compte_id || ''} onChange={e => onFieldChange('frais_compte_id', e.target.value)}>
<option value="">— Choisir un compte —</option>
{comptesInvestisseur.map(c => (
<option key={c.id} value={c.id}>{c.nom}{c.banque ? ` — ${c.banque}` : ''}</option>
))}
</select>
) : (
<p className="text-muted" style={{ fontSize: 'var(--fs-sm)', margin: '4px 0' }}>
Aucun compte défini. <a href="/settings?section=comptes" target="_blank" rel="noreferrer">Créer un compte →</a>
</p>
)}
</div>
)}
<div style={{ gridColumn: 'span 3', display: 'flex', alignItems: 'flex-end', fontSize: 12, color: 'var(--text-muted)', paddingBottom: 6 }}>
{form.frais_mode_reglement === 'compte_courant' || form.frais_mode_reglement === 'portefeuille'
? "Frais réglé à part (comme pour la souscription) — aucun impact sur l'imposition ni le montant versé de ce remboursement ; compté séparément dans le Frais payés et le XIRR net."
: "Frais d'entrée/de gestion prélevés par la plateforme sur cette distribution (ex. avis d'opéré) — vient réduire la base imposable des intérêts bruts avant calcul des prélèvements."}
</div>
</>) : (
<div style={{ gridColumn: 'span 2', display: 'flex', alignItems: 'flex-end', fontSize: 12, color: 'var(--text-muted)', paddingBottom: 6 }}>
Frais d'entrée/de gestion prélevés par la plateforme sur cette distribution (ex. avis d'opéré) —
vient réduire la base imposable des intérêts bruts avant calcul des prélèvements.
</div>
)}
</>)}
<div style={{ gridColumn: 'span 3', borderTop: '1px solid var(--border)', paddingTop: 4, marginTop: 4 }}>
<span style={{ fontSize: 11, fontWeight: 600, textTransform: 'uppercase', letterSpacing: '0.06em', color: 'var(--text-muted)' }}>Imposition{isIndicatif ? ' — indicatif' : ''}</span>
</div>