Flag ouverture/clôture discrepancies between consecutive years

Each year's ouverture (opening balance) should match the previous
year's calculated closing balance (that year's own ouverture + every
movement dated within it). The historical spreadsheets carry real
gaps here that were preserved as-is during migration -- this surfaces
them per compte, on the ouverture row(s) they affect, the same way
the per-ligne écart column already does, rather than correcting them.

New LedgerStatsController::reconciliationOuverture() endpoint (a
single grouped SQL aggregate over ouverture vs. non-ouverture lines
per year/compte, not per-node loading) backs a small badge shown only
on the affected ouverture row(s), scoped to whichever compte that
specific row's répartition touches.
This commit is contained in:
2026-09-04 21:21:57 +02:00
parent 831c0a0e62
commit 7eabcc6bd8
4 changed files with 116 additions and 0 deletions
@@ -119,6 +119,16 @@
return json.annees || [];
}
// { "2023": { "Bachir": -0.03, ... }, ... } -- years where the actual
// ouverture doesn't match the previous year's calculated closing
// balance (that year's own ouverture + every movement dated within
// it). Small dataset (a handful of year boundaries), fetched once.
async function fetchOuvertureEcarts() {
const res = await fetch('/lignes/api/reconciliation-ouverture', { headers: { Accept: 'application/json' } });
if (!res.ok) throw new Error('/lignes/api/reconciliation-ouverture a répondu ' + res.status);
return res.json();
}
async function fetchYearTotals(annee) {
const res = await fetch('/lignes/api/totaux?annee=' + encodeURIComponent(annee), { headers: { Accept: 'application/json' } });
if (!res.ok) throw new Error('/lignes/api/totaux a répondu ' + res.status);
@@ -182,6 +192,7 @@
allComptes: ['Sandrine', 'Maud', 'Ouidade', 'Chloé', 'Bachir', 'Valentin', 'EXT.', 'EXT.WEB'],
allClientsList: [],
allYearsList: [],
ouvertureEcarts: {},
filterCompte: '',
filterClient: '',
filterType: '',
@@ -316,6 +327,22 @@
formatEur(v) {
return v === null || v === undefined ? '' : EUR.format(v);
},
// Écart between this ouverture row's year and the previous year's
// calculated closing balance -- restricted to whichever compte(s)
// this specific row's répartition actually touches (ouverture lines
// are entered one per compte, so showing every compte's écart on
// every row would just repeat the same list). Null if this row's
// compte(s) reconcile cleanly.
ouvertureEcart(item) {
if (item.type !== 'ouverture' || !item.date) return null;
const annee = item.date.slice(0, 4);
const ecarts = this.ouvertureEcarts[annee];
if (!ecarts) return null;
const relevant = Object.keys(item.parCompte).filter((c) => ecarts[c] !== undefined);
if (!relevant.length) return null;
const detail = relevant.map((compte) => compte + ' : ' + this.formatEur(ecarts[compte])).join(', ');
return { detail, comptes: relevant.length };
},
// jj/mm/aa -- shorter than the API's ISO yyyy-mm-dd, saves column
// width in a table already packed with 8 compte columns.
formatDate(iso) {
@@ -638,6 +665,7 @@
// "whatever happens to be loaded right now".
fetchClientNames().then((names) => { this.allClientsList = names; }).catch(() => {});
fetchYearsList().then((years) => { this.allYearsList = years; }).catch(() => {});
fetchOuvertureEcarts().then((ecarts) => { this.ouvertureEcarts = ecarts; }).catch(() => {});
jQuery(document).on('dialog:afterclose', () => this.reloadWindow());
await this.$nextTick();
const wrap = this.$refs.tableWrap;