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
@@ -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>
|
||||
|
||||
Reference in new issue
Block a user