Scope versement reste-à-verser to its own compte(s), not the entrée total

versementStatus() was reusing reconciliationByEntree()'s aggregate
resteAVerser/surVerse, which sums residuals across every compte the
entrée touches -- including comptes tied to *other* sorties linked to
the same entrée. A versement paid entirely through Maud could show
"reste à verser" driven by an unrelated Sandrine/Chloé shortfall on
the same entrée, or vice versa mask its own compte's sur-versement
behind an unrelated compte's surplus.

reconciliationByEntree() now also keeps a per-compte residual map
(parCompteResidual), and versementStatus() sums only the residuals for
the compte(s) this specific versement's own répartition touches.

Verified against a real case: entrée EPAU F2549-50-51 (répartition
across 8 comptes) with one linked "Versement Maud" of -10 000€ against
an entrée-side Maud share of 5 632,86€. The entrée's own badge still
correctly shows the aggregate ("reste 24 325,54 € · sur-versé
4 367,14 €"), but the versement row itself now shows "Sur-versé :
4 367,14 €" -- its actual Maud-only residual -- instead of the
previous "Reste à verser", which was purely an artifact of the other
7 comptes' unrelated shortfalls.
This commit is contained in:
2026-09-05 11:09:37 +02:00
parent 6bdf12b8fe
commit 9a70205c83
@@ -304,8 +304,15 @@
let resteAVerser = 0; let resteAVerser = 0;
let surVerse = 0; let surVerse = 0;
const detail = []; const detail = [];
// Kept per-compte (not just folded into the two totals above) --
// versementStatus() below needs to check a single sortie's own
// compte(s) against the entrée, not the entrée's overall
// reconciliation, which can span *other* comptes tied to other
// sorties linked to the same entrée.
const parCompteResidual = {};
for (const c of comptes) { for (const c of comptes) {
const residual = Math.round(((entreeRow.parCompte[c] || 0) + (versementsParCompte[c] || 0)) * 100) / 100; const residual = Math.round(((entreeRow.parCompte[c] || 0) + (versementsParCompte[c] || 0)) * 100) / 100;
parCompteResidual[c] = residual;
if (residual > 0.01) resteAVerser += residual; if (residual > 0.01) resteAVerser += residual;
else if (residual < -0.01) surVerse += -residual; else if (residual < -0.01) surVerse += -residual;
if (Math.abs(residual) > 0.01) detail.push(c + ' : ' + this.formatEur(residual)); if (Math.abs(residual) > 0.01) detail.push(c + ' : ' + this.formatEur(residual));
@@ -315,6 +322,7 @@
resteAVerser: Math.round(resteAVerser * 100) / 100, resteAVerser: Math.round(resteAVerser * 100) / 100,
surVerse: Math.round(surVerse * 100) / 100, surVerse: Math.round(surVerse * 100) / 100,
detail: detail.join(', ') || 'Entièrement soldé', detail: detail.join(', ') || 'Entièrement soldé',
parCompteResidual,
}); });
} }
return map; return map;
@@ -423,13 +431,16 @@
}, },
// Flags a "versement freelance" row that isn't (fully) backed by // Flags a "versement freelance" row that isn't (fully) backed by
// the entrée client it pays out against: either not linked at all, // the entrée client it pays out against: either not linked at all,
// or linked but reconciliationByEntree still shows a residual on // or linked but its own compte(s) still show a residual against
// that entrée. The residual belongs to the entrée as a whole, not // that entrée. Deliberately scoped to just the compte(s) this
// to any one sortie -- when several versements share an entrée, // versement's own répartition touches (reconciliationByEntree's
// each shows the same aggregate residual, since there's no way to // parCompteResidual), not the entrée's overall resteAVerser/
// say which specific one is "the" shortfall. Null (no highlight) // surVerse -- those can be driven entirely by a *different* compte
// when the linked entrée isn't in the currently loaded window -- // tied to some other sortie linked to the same entrée, which says
// same accepted trade-off as reconciliationByEntree itself. // nothing about whether this versement's own répartition is
// settled. Null (no highlight) when the linked entrée isn't in the
// currently loaded window -- same accepted trade-off as
// reconciliationByEntree itself.
versementStatus(item) { versementStatus(item) {
if (item.type !== 'versement') return null; if (item.type !== 'versement') return null;
if (!item.entreeLieeId) { if (!item.entreeLieeId) {
@@ -437,11 +448,18 @@
} }
const recon = this.reconciliationByEntree.get(item.entreeLieeId); const recon = this.reconciliationByEntree.get(item.entreeLieeId);
if (!recon) return null; if (!recon) return null;
if (recon.resteAVerser > 0.01) { let resteAVerser = 0;
return { kind: 'reste', detail: 'Reste à verser sur lentrée liée : ' + this.formatEur(recon.resteAVerser) }; let surVerse = 0;
for (const c of Object.keys(item.parCompte)) {
const residual = recon.parCompteResidual[c] || 0;
if (residual > 0.01) resteAVerser += residual;
else if (residual < -0.01) surVerse += -residual;
} }
if (recon.surVerse > 0.01) { if (resteAVerser > 0.01) {
return { kind: 'sur-verse', detail: 'Sur-versé sur lentrée liée : ' + this.formatEur(recon.surVerse) }; return { kind: 'reste', detail: 'Reste à verser (comptes de cette ligne) : ' + this.formatEur(Math.round(resteAVerser * 100) / 100) };
}
if (surVerse > 0.01) {
return { kind: 'sur-verse', detail: 'Sur-versé (comptes de cette ligne) : ' + this.formatEur(Math.round(surVerse * 100) / 100) };
} }
return null; return null;
}, },