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
@@ -239,6 +239,11 @@ const empty = {
|
||||
statut: 'en_cours', reference: '', notes: '',
|
||||
echeance_fin_de_mois: false, methode_remboursement: '', nom_compte_courant: '', compte_id: '', pays_exposition: 'FR',
|
||||
categories_inv_ids: [], secteurs_inv_ids: [],
|
||||
// Frais liés à la plateforme (étape conditionnelle, visible création ET modification —
|
||||
// cf. project_frais_operation.md). frais_id : id de l'entrée frais_operations de catégorie
|
||||
// 'souscription' déjà existante pour cet investissement (pré-remplie par openEdit), null si
|
||||
// aucune — pilote l'upsert (POST/PUT/DELETE) au submit, jamais affiché dans le formulaire.
|
||||
frais_mode_saisie: 'montant', frais_valeur: '', frais_mode_reglement: 'source', frais_compte_id: '', frais_id: null,
|
||||
};
|
||||
|
||||
/* ── Composant principal ─────────────────────────────────────── */
|
||||
@@ -702,9 +707,31 @@ export default function Investissements() {
|
||||
pays_exposition: row.pays_exposition || 'FR',
|
||||
categories_inv_ids: (row.categories_inv || []).map(c => c.id),
|
||||
secteurs_inv_ids: (row.secteurs_inv || []).map(s => s.id),
|
||||
frais_mode_saisie: 'montant', frais_valeur: '', frais_mode_reglement: 'source', frais_compte_id: '', frais_id: null,
|
||||
});
|
||||
setErr(null);
|
||||
setModalOpen(true);
|
||||
|
||||
// Pré-remplissage de l'étape "Frais" avec le frais de souscription déjà documenté pour cet
|
||||
// investissement, s'il existe (catégorie 'souscription', unique par investissement — cf.
|
||||
// project_frais_operation.md). Chargé après l'ouverture de la modale pour ne pas bloquer
|
||||
// l'affichage ; mise à jour du form dès la réponse.
|
||||
try {
|
||||
const fraisOps = await api.get('/frais-operations', { investissement_id: row.id });
|
||||
const souscriptionFrais = (fraisOps || []).find(f => f.categorie === 'souscription');
|
||||
if (souscriptionFrais) {
|
||||
setForm(f => ({
|
||||
...f,
|
||||
frais_mode_saisie: 'montant',
|
||||
frais_valeur: souscriptionFrais.montant,
|
||||
frais_mode_reglement: souscriptionFrais.mode_reglement,
|
||||
frais_compte_id: souscriptionFrais.compte_id || '',
|
||||
frais_id: souscriptionFrais.id,
|
||||
}));
|
||||
}
|
||||
} catch {
|
||||
// Pré-remplissage best-effort — une erreur réseau ne doit pas empêcher l'édition.
|
||||
}
|
||||
};
|
||||
|
||||
/** Changement de plateforme dans le formulaire : résout la méthode de remboursement,
|
||||
@@ -724,10 +751,12 @@ export default function Investissements() {
|
||||
methode_remboursement: newMethode, compte_id: '',
|
||||
categories_inv_ids: platCats.map(c => c.id),
|
||||
secteurs_inv_ids: platSects.map(s => s.id),
|
||||
frais_mode_saisie: 'montant', frais_valeur: '', frais_mode_reglement: 'source', frais_compte_id: '',
|
||||
}));
|
||||
});
|
||||
} else {
|
||||
setForm(f => ({ ...f, plateforme_id: platId, investisseur_id: investisseurForPlat(platId), methode_remboursement: newMethode, compte_id: '', categories_inv_ids: [], secteurs_inv_ids: [] }));
|
||||
setForm(f => ({ ...f, plateforme_id: platId, investisseur_id: investisseurForPlat(platId), methode_remboursement: newMethode, compte_id: '', categories_inv_ids: [], secteurs_inv_ids: [],
|
||||
frais_mode_saisie: 'montant', frais_valeur: '', frais_mode_reglement: 'source', frais_compte_id: '' }));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -751,6 +780,26 @@ export default function Investissements() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Frais liés à la plateforme (étape conditionnelle, création ET modification — le bloc
|
||||
// "Frais d'opération" de la fiche investissement reste le lieu pour les frais de
|
||||
// catégorie gestion/distribution/autre, cf. project_frais_operation.md) : à la création,
|
||||
// si prélevés "à la source", le capital net (montant investi diminué des frais) est ce
|
||||
// qui est enregistré comme montant_investi ; en modification, montant_investi n'est
|
||||
// JAMAIS réajusté automatiquement (demande explicite d'Olivier, 31/08/26) — c'est à
|
||||
// l'utilisateur de l'ajuster lui-même à l'étape 1 si besoin. Dans les deux cas, le frais
|
||||
// est journalisé (POST/PUT/DELETE selon form.frais_id) après l'enregistrement principal.
|
||||
const platFraisType = plats.find(p => String(p.id) === String(form.plateforme_id))?.frais_plateforme;
|
||||
const hasFrais = platFraisType && platFraisType !== 'aucun'
|
||||
&& form.frais_valeur !== '' && Number(form.frais_valeur) > 0;
|
||||
const fraisTTC = hasFrais
|
||||
? Math.round((form.frais_mode_saisie === 'pourcentage'
|
||||
? Number(form.montant_investi) * Number(form.frais_valeur) / 100
|
||||
: Number(form.frais_valeur)) * 100) / 100
|
||||
: 0;
|
||||
const montantInvestiFinal = !editingId && hasFrais && form.frais_mode_reglement === 'source'
|
||||
? Math.max(0, Math.round((Number(form.montant_investi) - fraisTTC) * 100) / 100)
|
||||
: Number(form.montant_investi);
|
||||
|
||||
const payload = {
|
||||
investisseur_id: Number(form.investisseur_id) || undefined,
|
||||
plateforme_id: Number(form.plateforme_id),
|
||||
@@ -759,7 +808,7 @@ export default function Investissements() {
|
||||
date_souscription: form.date_souscription,
|
||||
date_premiere_echeance: form.date_premiere_echeance || '',
|
||||
date_cible: form.date_cible || '',
|
||||
montant_investi: Number(form.montant_investi),
|
||||
montant_investi: montantInvestiFinal,
|
||||
taux_interet: form.taux_interet === '' ? undefined : Number(form.taux_interet),
|
||||
duree_mois: form.duree_mois === '' ? undefined : Number(form.duree_mois),
|
||||
type_remb: form.type_remb || '',
|
||||
@@ -779,6 +828,31 @@ export default function Investissements() {
|
||||
api.put(`/investissements/${editingId}/categories-inv`, { ids: form.categories_inv_ids || [] }),
|
||||
api.put(`/investissements/${editingId}/secteurs-inv`, { ids: form.secteurs_inv_ids || [] }),
|
||||
]);
|
||||
|
||||
// Upsert du frais de souscription (catégorie unique par investissement) : PUT si déjà
|
||||
// chargé via form.frais_id (pré-rempli par openEdit), POST sinon ; DELETE si le montant
|
||||
// a été vidé alors qu'une entrée existait — best-effort, n'empêche jamais l'édition
|
||||
// principale de l'investissement.
|
||||
try {
|
||||
if (hasFrais) {
|
||||
const fraisPayload = {
|
||||
investissement_id: editingId,
|
||||
montant: fraisTTC,
|
||||
mode_reglement: form.frais_mode_reglement,
|
||||
compte_id: form.frais_mode_reglement === 'compte_courant' && form.frais_compte_id ? Number(form.frais_compte_id) : null,
|
||||
date_operation: form.date_souscription,
|
||||
origine: 'souscription',
|
||||
categorie: 'souscription',
|
||||
};
|
||||
if (form.frais_id) await api.put(`/frais-operations/${form.frais_id}`, fraisPayload);
|
||||
else await api.post('/frais-operations', fraisPayload);
|
||||
} else if (form.frais_id) {
|
||||
await api.del(`/frais-operations/${form.frais_id}`);
|
||||
}
|
||||
} catch {
|
||||
// Non bloquant — rattrapable a posteriori depuis le bloc "Frais d'opération".
|
||||
}
|
||||
|
||||
close(); await load();
|
||||
} else {
|
||||
const created = await api.post('/investissements', payload);
|
||||
@@ -787,6 +861,23 @@ export default function Investissements() {
|
||||
api.put(`/investissements/${created.id}/secteurs-inv`, { ids: form.secteurs_inv_ids || [] }),
|
||||
]);
|
||||
|
||||
if (hasFrais) {
|
||||
try {
|
||||
await api.post('/frais-operations', {
|
||||
investissement_id: created.id,
|
||||
montant: fraisTTC,
|
||||
mode_reglement: form.frais_mode_reglement,
|
||||
compte_id: form.frais_mode_reglement === 'compte_courant' && form.frais_compte_id ? Number(form.frais_compte_id) : null,
|
||||
date_operation: form.date_souscription,
|
||||
origine: 'souscription',
|
||||
categorie: 'souscription',
|
||||
});
|
||||
} catch {
|
||||
// Frais non journalisés (erreur réseau…) — n'empêche pas la création de l'investissement,
|
||||
// qui a déjà pris en compte le capital net le cas échéant. Rattrapable a posteriori.
|
||||
}
|
||||
}
|
||||
|
||||
// Contrôle du porte-monnaie — création uniquement (jamais en édition). Le solde
|
||||
// recalculé APRÈS création tient déjà compte du capital de ce nouvel investissement
|
||||
// (formule officielle, cf. mémoire project_solde_portefeuille) : s'il ressort négatif,
|
||||
|
||||
Reference in new issue
Block a user