Fix
This commit is contained in:
1 parent
d5792fb3e2
commit
4cd4cbce71
4 files changed
+79
-12
No files matched your search
@@ -3360,6 +3360,39 @@ if (!invPeValCols.includes('plus_value_latente')) {
|
||||
db.exec('ALTER TABLE investissements_pe_valorisations ADD COLUMN plus_value_latente REAL');
|
||||
}
|
||||
|
||||
// Migration : capital investissable (18/09/26, demande Olivier — bug remonté : le "capital non
|
||||
// déployé" d'une valorisation détaillée était calculé sur montant_investi (le montant souscrit
|
||||
// BRUT), alors qu'une partie a pu être immédiatement prélevée en frais "à la source" à la
|
||||
// souscription (cf. syncValorisationInitiale, InvestissementsPe.jsx) — le capital réellement
|
||||
// disponible à déployer est montant_investi − ces frais, jamais le montant brut. Colonne
|
||||
// nullable, calculée et fournie par le front à chaque sauvegarde du deal (POST/PUT
|
||||
// /investissements-pe) à partir des frais "à la source" saisis à l'étape "Frais" de la modale ;
|
||||
// NULL = deal sans frais à la source connus, auquel cas montant_investi fait référence partout
|
||||
// où capital_investissable est utilisé (cf. investissementsPe.js).
|
||||
const invPeCols = db.prepare('PRAGMA table_info(investissements_pe)').all().map(c => c.name);
|
||||
if (!invPeCols.includes('capital_investissable')) {
|
||||
db.exec('ALTER TABLE investissements_pe ADD COLUMN capital_investissable REAL');
|
||||
// Backfill : pour tout deal ayant déjà une "Valorisation initiale" auto-générée (créée par
|
||||
// syncValorisationInitiale AVANT cette migration, donc déjà nette des frais à la source), son
|
||||
// montant EST la définition de capital_investissable — évite d'obliger Olivier à rouvrir et
|
||||
// ré-enregistrer chaque deal existant pour que le calcul se corrige. Les libellés doivent
|
||||
// rester synchronisés avec AUTO_VAL_NOTE / AUTO_VAL_NOTE_LEGACY (InvestissementsPe.jsx).
|
||||
db.exec(`
|
||||
UPDATE investissements_pe
|
||||
SET capital_investissable = (
|
||||
SELECT v.valorisation_nette FROM investissements_pe_valorisations v
|
||||
WHERE v.investissement_pe_id = investissements_pe.id
|
||||
AND v.notes IN ('Valorisation initiale (souscription)', 'Valorisation initiale (frais de souscription)')
|
||||
ORDER BY v.date_valorisation ASC, v.id ASC LIMIT 1
|
||||
)
|
||||
WHERE EXISTS (
|
||||
SELECT 1 FROM investissements_pe_valorisations v
|
||||
WHERE v.investissement_pe_id = investissements_pe.id
|
||||
AND v.notes IN ('Valorisation initiale (souscription)', 'Valorisation initiale (frais de souscription)')
|
||||
)
|
||||
`);
|
||||
}
|
||||
|
||||
// Lien optionnel remboursement → deal PE (17/09/26) : une distribution (retour
|
||||
// de capital, plus-value) perçue sur un deal PE est saisie comme un
|
||||
// remboursement bonus_plateforme/interets_plateforme, comme demandé par
|
||||
|
||||
@@ -29,6 +29,13 @@ const Schema = z.object({
|
||||
date_souscription: z.string().regex(/^\d{4}-\d{2}-\d{2}$/),
|
||||
duree_mois: z.number().int().positive().nullable().optional(),
|
||||
montant_investi: z.number().positive(),
|
||||
// Capital réellement disponible à déployer, net des frais prélevés "à la source" à la
|
||||
// souscription (18/09/26, demande Olivier — cf. migration capital_investissable dans
|
||||
// db/index.js). Calculé et fourni par le front (InvestissementsPe.jsx / InvestissementPeDetail
|
||||
// .jsx) à partir de l'étape "Frais" de la modale ; NULL si inconnu (deal sans frais à la
|
||||
// source, ou pas encore resauvegardé depuis cette fonctionnalité) — auquel cas montant_investi
|
||||
// sert de repli partout où capital_investissable est utilisé.
|
||||
capital_investissable: z.number().nonnegative().nullable().optional(),
|
||||
objectif: z.string().optional(),
|
||||
statut: z.enum(['en_attente', 'valide', 'cloture', 'perte_definitive']).default('valide'),
|
||||
reference: z.string().optional(),
|
||||
@@ -170,11 +177,12 @@ router.post('/', requireInvestisseur, (req, res, next) => {
|
||||
const r = db.prepare(`
|
||||
INSERT INTO investissements_pe
|
||||
(investisseur_id, plateforme_id, workspace_id, nom_deal, strategie, date_souscription,
|
||||
duree_mois, montant_investi, objectif, statut, reference, notes)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?)
|
||||
duree_mois, montant_investi, capital_investissable, objectif, statut, reference, notes)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)
|
||||
`).run(
|
||||
investisseurId, body.plateforme_id, workspaceId, body.nom_deal, body.strategie || null,
|
||||
body.date_souscription, body.duree_mois ?? null, body.montant_investi, body.objectif || null,
|
||||
body.date_souscription, body.duree_mois ?? null, body.montant_investi,
|
||||
body.capital_investissable ?? null, body.objectif || null,
|
||||
body.statut, body.reference || null, body.notes || null,
|
||||
);
|
||||
res.status(201).json({ id: r.lastInsertRowid, ...body });
|
||||
@@ -227,13 +235,13 @@ router.put('/:id', requireInvestisseur, (req, res, next) => {
|
||||
db.prepare(`
|
||||
UPDATE investissements_pe
|
||||
SET investisseur_id=?, plateforme_id=?, nom_deal=?, strategie=?, date_souscription=?,
|
||||
duree_mois=?, montant_investi=?, objectif=?, statut=?, reference=?, notes=?,
|
||||
updated_at=datetime('now')
|
||||
duree_mois=?, montant_investi=?, capital_investissable=?, objectif=?, statut=?,
|
||||
reference=?, notes=?, updated_at=datetime('now')
|
||||
WHERE id=?
|
||||
`).run(
|
||||
investisseurId, body.plateforme_id, body.nom_deal, body.strategie || null, body.date_souscription,
|
||||
body.duree_mois ?? null, body.montant_investi, body.objectif || null, body.statut,
|
||||
body.reference || null, body.notes || null, id,
|
||||
body.duree_mois ?? null, body.montant_investi, body.capital_investissable ?? null,
|
||||
body.objectif || null, body.statut, body.reference || null, body.notes || null, id,
|
||||
);
|
||||
res.json({ id, ...body });
|
||||
} catch (e) { next(e); }
|
||||
@@ -275,16 +283,21 @@ router.post('/:id/valorisations', requireInvestisseur, (req, res, next) => {
|
||||
|
||||
// Quand la répartition est renseignée (capital_deploye présent), la valorisation nette est
|
||||
// recalculée ici plutôt que reprise du body : capital_deploye + plus_value_latente +
|
||||
// capital_non_deploye (déduit = montant_investi du deal − capital_deploye, jamais négatif).
|
||||
// capital_non_deploye (déduit = capital investissable du deal − capital_deploye, jamais
|
||||
// négatif). Capital investissable = montant_investi net des frais "à la source" prélevés à
|
||||
// la souscription quand ils sont connus (deal.capital_investissable), sinon montant_investi
|
||||
// brut par repli (18/09/26, correctif Olivier — jusque-là on utilisait toujours le montant
|
||||
// brut, ce qui gonflait à tort le capital non déployé pour un deal avec frais à la source).
|
||||
// Sans répartition (mode simple, historique), comportement inchangé : le montant saisi est
|
||||
// utilisé tel quel et les deux colonnes restent NULL.
|
||||
let valorisationNette = body.valorisation_nette;
|
||||
let capitalDeploye = null;
|
||||
let plusValueLatente = null;
|
||||
if (body.capital_deploye != null) {
|
||||
const capitalInvestissable = deal.capital_investissable ?? deal.montant_investi;
|
||||
capitalDeploye = body.capital_deploye;
|
||||
plusValueLatente = body.plus_value_latente ?? 0;
|
||||
const capitalNonDeploye = Math.max(0, deal.montant_investi - capitalDeploye);
|
||||
const capitalNonDeploye = Math.max(0, capitalInvestissable - capitalDeploye);
|
||||
valorisationNette = Math.round((capitalDeploye + plusValueLatente + capitalNonDeploye) * 100) / 100;
|
||||
}
|
||||
|
||||
|
||||
@@ -204,6 +204,10 @@ export default function InvestissementPeDetail() {
|
||||
(hasFraisEntree && form.frais_entree_mode_reglement === 'source' ? fraisEntreeTTC : 0) +
|
||||
(hasFraisGestion && form.frais_gestion_mode_reglement === 'source' ? fraisGestionTTC : 0);
|
||||
|
||||
// Capital réellement disponible à déployer = montant investi net des frais prélevés "à la
|
||||
// source" — même correctif que InvestissementsPe.jsx::submit (18/09/26, Olivier).
|
||||
const capitalInvestissable = Math.max(0, Math.round((Number(form.montant_investi) - totalFraisSource) * 100) / 100);
|
||||
|
||||
const payload = {
|
||||
plateforme_id: Number(form.plateforme_id),
|
||||
investisseur_id: Number(form.investisseur_id) || undefined,
|
||||
@@ -212,6 +216,7 @@ export default function InvestissementPeDetail() {
|
||||
date_souscription: form.date_souscription,
|
||||
duree_mois: form.duree_annees ? Math.round(Number(form.duree_annees) * 12) : undefined,
|
||||
montant_investi: Number(form.montant_investi),
|
||||
capital_investissable: capitalInvestissable,
|
||||
objectif: form.objectif || undefined,
|
||||
statut: form.statut,
|
||||
reference: form.reference || undefined,
|
||||
@@ -250,8 +255,11 @@ export default function InvestissementPeDetail() {
|
||||
const hasBreakdown = valForm.capital_deploye !== '';
|
||||
const capitalDeploye = hasBreakdown ? (Number(valForm.capital_deploye) || 0) : undefined;
|
||||
const plusValueLatente = hasBreakdown ? (Number(valForm.plus_value_latente) || 0) : undefined;
|
||||
// Capital investissable (net des frais "à la source"), pas le montant investi brut —
|
||||
// correctif 18/09/26, cf. deal.capital_investissable.
|
||||
const capitalInvestissable = Number(deal.capital_investissable ?? deal.montant_investi);
|
||||
const valorisationNette = hasBreakdown
|
||||
? Math.round((capitalDeploye + plusValueLatente + Math.max(0, Number(deal.montant_investi) - capitalDeploye)) * 100) / 100
|
||||
? Math.round((capitalDeploye + plusValueLatente + Math.max(0, capitalInvestissable - capitalDeploye)) * 100) / 100
|
||||
: Number(valForm.valorisation_nette);
|
||||
await api.post(`/investissements-pe/${id}/valorisations`, {
|
||||
date_valorisation: valForm.date_valorisation,
|
||||
@@ -402,7 +410,7 @@ export default function InvestissementPeDetail() {
|
||||
const valFormCapitalDeploye = valFormHasBreakdown ? (Number(valForm.capital_deploye) || 0) : 0;
|
||||
const valFormPlusValue = valFormHasBreakdown ? (Number(valForm.plus_value_latente) || 0) : 0;
|
||||
const valFormCapitalNonDeploye = valFormHasBreakdown
|
||||
? Math.max(0, Number(deal.montant_investi) - valFormCapitalDeploye) : 0;
|
||||
? Math.max(0, Number(deal.capital_investissable ?? deal.montant_investi) - valFormCapitalDeploye) : 0;
|
||||
const valFormComputedNette = valFormHasBreakdown
|
||||
? Math.round((valFormCapitalDeploye + valFormPlusValue + valFormCapitalNonDeploye) * 100) / 100
|
||||
: null;
|
||||
@@ -441,7 +449,12 @@ export default function InvestissementPeDetail() {
|
||||
<div className="card" style={{ marginBottom: 16 }}>
|
||||
<div className="dr-kpi-row" style={{ marginBottom: 0 }}>
|
||||
<div className="kpi"><div className="label">Montant investi</div>
|
||||
<div style={{ fontSize: '1.2rem', fontWeight: 700, marginTop: 4 }}>{fmtEUR(deal.montant_investi)}</div></div>
|
||||
<div style={{ fontSize: '1.2rem', fontWeight: 700, marginTop: 4 }}>{fmtEUR(deal.montant_investi)}</div>
|
||||
{deal.capital_investissable != null && deal.capital_investissable !== deal.montant_investi && (
|
||||
<div className="text-muted" style={{ fontSize: 11, marginTop: 2 }}>
|
||||
dont {fmtEUR(deal.capital_investissable)} investissables (après frais à la source)
|
||||
</div>
|
||||
)}</div>
|
||||
<div className="kpi"><div className="label">TVPI</div>
|
||||
<div style={{ fontSize: '1.2rem', fontWeight: 700, marginTop: 4 }}>{fmtMultiple(deal.tvpi)}</div></div>
|
||||
<div className="kpi"><div className="label">Distributions cumulées</div>
|
||||
|
||||
@@ -404,6 +404,13 @@ export default function InvestissementsPe() {
|
||||
(hasFraisEntree && form.frais_entree_mode_reglement === 'source' ? fraisEntreeTTC : 0) +
|
||||
(hasFraisGestion && form.frais_gestion_mode_reglement === 'source' ? fraisGestionTTC : 0);
|
||||
|
||||
// Capital réellement disponible à déployer = montant investi net des frais prélevés "à la
|
||||
// source" (correctif Olivier 18/09/26 : le calcul de "capital non déployé" d'une valorisation
|
||||
// détaillée utilisait à tort le montant investi BRUT). Même montant que celui utilisé pour la
|
||||
// "Valorisation initiale" auto-générée ci-dessus (syncValorisationInitiale) — persisté ici
|
||||
// sur le deal lui-même pour que le backend n'ait pas à le redéduire à chaque valorisation.
|
||||
const capitalInvestissable = Math.max(0, Math.round((Number(form.montant_investi) - totalFraisSource) * 100) / 100);
|
||||
|
||||
const payload = {
|
||||
plateforme_id: Number(form.plateforme_id),
|
||||
investisseur_id: Number(form.investisseur_id) || undefined,
|
||||
@@ -412,6 +419,7 @@ export default function InvestissementsPe() {
|
||||
date_souscription: form.date_souscription,
|
||||
duree_mois: form.duree_annees ? Math.round(Number(form.duree_annees) * 12) : undefined,
|
||||
montant_investi: Number(form.montant_investi),
|
||||
capital_investissable: capitalInvestissable,
|
||||
objectif: form.objectif || undefined,
|
||||
statut: form.statut,
|
||||
reference: form.reference || undefined,
|
||||
|
||||
Reference in new issue
Block a user