From 9a70205c831acc0a00da3ac143558868cfb5efb7 Mon Sep 17 00:00:00 2001 From: bach Date: Sat, 5 Sep 2026 11:09:37 +0200 Subject: [PATCH] =?UTF-8?q?Scope=20versement=20reste-=C3=A0-verser=20to=20?= =?UTF-8?q?its=20own=20compte(s),=20not=20the=20entr=C3=A9e=20total?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .../custom/figli_compta_ledger/js/home.js | 40 ++++++++++++++----- 1 file changed, 29 insertions(+), 11 deletions(-) diff --git a/web/modules/custom/figli_compta_ledger/js/home.js b/web/modules/custom/figli_compta_ledger/js/home.js index 0be93c9..bac379a 100644 --- a/web/modules/custom/figli_compta_ledger/js/home.js +++ b/web/modules/custom/figli_compta_ledger/js/home.js @@ -304,8 +304,15 @@ let resteAVerser = 0; let surVerse = 0; 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) { const residual = Math.round(((entreeRow.parCompte[c] || 0) + (versementsParCompte[c] || 0)) * 100) / 100; + parCompteResidual[c] = residual; if (residual > 0.01) resteAVerser += residual; else if (residual < -0.01) surVerse += -residual; if (Math.abs(residual) > 0.01) detail.push(c + ' : ' + this.formatEur(residual)); @@ -315,6 +322,7 @@ resteAVerser: Math.round(resteAVerser * 100) / 100, surVerse: Math.round(surVerse * 100) / 100, detail: detail.join(', ') || 'Entièrement soldé', + parCompteResidual, }); } return map; @@ -423,13 +431,16 @@ }, // Flags a "versement freelance" row that isn't (fully) backed by // the entrée client it pays out against: either not linked at all, - // or linked but reconciliationByEntree still shows a residual on - // that entrée. The residual belongs to the entrée as a whole, not - // to any one sortie -- when several versements share an entrée, - // each shows the same aggregate residual, since there's no way to - // say which specific one is "the" shortfall. Null (no highlight) - // when the linked entrée isn't in the currently loaded window -- - // same accepted trade-off as reconciliationByEntree itself. + // or linked but its own compte(s) still show a residual against + // that entrée. Deliberately scoped to just the compte(s) this + // versement's own répartition touches (reconciliationByEntree's + // parCompteResidual), not the entrée's overall resteAVerser/ + // surVerse -- those can be driven entirely by a *different* compte + // tied to some other sortie linked to the same entrée, which says + // 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) { if (item.type !== 'versement') return null; if (!item.entreeLieeId) { @@ -437,11 +448,18 @@ } const recon = this.reconciliationByEntree.get(item.entreeLieeId); if (!recon) return null; - if (recon.resteAVerser > 0.01) { - return { kind: 'reste', detail: 'Reste à verser sur l’entrée liée : ' + this.formatEur(recon.resteAVerser) }; + let resteAVerser = 0; + 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) { - return { kind: 'sur-verse', detail: 'Sur-versé sur l’entrée liée : ' + this.formatEur(recon.surVerse) }; + if (resteAVerser > 0.01) { + 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; },