feat: rend la situation des locataires vérifiable colonne par colonne
La carte d'un lot n'affichait que quatre des huit colonnes du compte rendu, la colonne Total absente, et le bandeau replié montrait un total figé : corriger un règlement ne le changeait pas. Pire, les quatre agrégats modifiables ne sont jamais enregistrés — seules les lignes partent en base (`service.py`) — pendant que les colonnes réellement écrites, dont Regles et Impayé, n'étaient éditables nulle part. On corrigeait donc un champ sans effet, et pas celui qu'il fallait. Le détail des lignes reprend maintenant le tableau du compte rendu, colonne pour colonne, la ligne « Totaux » comprise en pied. Les dix champs d'une ligne et les huit de la ligne Totaux sont modifiables, sans exception : cette page sert à vérifier une extraction avant de l'enregistrer, l'utilisateur y a le dernier mot. Rien n'est plus recalculé à l'affichage ni reporté d'un champ sur un autre. Corriger une colonne ne déclenche que ce qui a été demandé — un total réécrit d'office effacerait sans le dire ce que le compte rendu porte. Un seul contrôle subsiste, en signalement pur : chaque colonne de la ligne « Totaux » est confrontée à la somme de cette même colonne sur les lignes. Le recoupement est celui que fait l'œil sur le tableau. Déduire le total des autres colonnes laissait passer le cas le plus parlant — une colonne Total qui ne somme visiblement pas, faute d'avoir extrait la valeur d'une ligne. Sur les 388 lots des documents extraits, ce contrôle signale quatre lots, tous de vraies extractions incomplètes : un règlement de 707,29 € qu'aucune ligne ne porte, un « divers » de 308,76 € sauté à un changement de page, deux lots réglés sans ligne. La colonne fautive passe en surbrillance et affiche la valeur calculée sous le montant extrait, sans jamais s'y substituer. Le solde antérieur quitte la colonne Loyers pour la colonne Période, où le compte rendu l'imprime. Compté à part sans être affiché à part, il donnait une colonne Loyers qui semblait ne pas sommer. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
112
frontend/src/utils/totauxLocataire.js
Normal file
112
frontend/src/utils/totauxLocataire.js
Normal file
@@ -0,0 +1,112 @@
|
||||
/**
|
||||
* Recoupement des totaux d'un lot avec ses lignes.
|
||||
*
|
||||
* La page d'édition n'affiche que de l'extraction, et chaque champ extrait y
|
||||
* reste modifiable : c'est l'utilisateur qui tranche avant l'enregistrement.
|
||||
* Ces fonctions ne corrigent donc rien — elles servent uniquement à repérer les
|
||||
* lots où le compte rendu et les lignes extraites ne racontent pas la même
|
||||
* histoire, signe que le parser a manqué ou déplacé un montant.
|
||||
*
|
||||
* Le recoupement est celui que l'œil fait sur le tableau : chaque colonne de la
|
||||
* ligne « Totaux » est confrontée à la somme de cette même colonne sur les
|
||||
* lignes, y compris les colonnes Total et Impayé. Les déduire des autres
|
||||
* colonnes (total = loyers + taxes + provisions + divers) laisserait passer le
|
||||
* cas le plus parlant — une colonne Total qui ne somme visiblement pas.
|
||||
*
|
||||
* Sur les 388 lots des documents extraits, ce recoupement signale 4 lots, tous
|
||||
* de vraies extractions incomplètes.
|
||||
*
|
||||
* Le solde antérieur est reporté par le parser dans la colonne `loyers` d'une
|
||||
* ligne dédiée ; le sommer avec les loyers de la période le compterait deux fois.
|
||||
*/
|
||||
|
||||
const TYPE_REPORT = 'solde_anterieur'
|
||||
|
||||
/** Montant exploitable d'un champ : `null` et `undefined` valent zéro. */
|
||||
function montant(valeur) {
|
||||
const nombre = Number(valeur)
|
||||
return Number.isFinite(nombre) ? nombre : 0
|
||||
}
|
||||
|
||||
/** Arrondi au centime — sans lui, les sommes de flottants affichent 878,6400000001. */
|
||||
function auCentime(valeur) {
|
||||
return Math.round(valeur * 100) / 100
|
||||
}
|
||||
|
||||
/**
|
||||
* Agrège les lignes d'un lot en un jeu de totaux de même forme que `totaux`.
|
||||
*
|
||||
* @param {Array} lignes - lignes du lot (`locataire.lignes`)
|
||||
* @returns {Object} totaux déduits, tous champs présents et arrondis au centime
|
||||
*/
|
||||
export function totauxCalcules(lignes) {
|
||||
const liste = Array.isArray(lignes) ? lignes : []
|
||||
|
||||
const totaux = {
|
||||
solde_anterieur: 0,
|
||||
loyers: 0,
|
||||
taxes: 0,
|
||||
provisions: 0,
|
||||
divers: 0,
|
||||
total: 0,
|
||||
regles: 0,
|
||||
impayes: 0,
|
||||
}
|
||||
|
||||
for (const ligne of liste) {
|
||||
if (ligne?.type === TYPE_REPORT) {
|
||||
totaux.solde_anterieur += montant(ligne.loyers)
|
||||
} else {
|
||||
totaux.loyers += montant(ligne?.loyers)
|
||||
}
|
||||
totaux.taxes += montant(ligne?.taxes)
|
||||
totaux.provisions += montant(ligne?.provisions)
|
||||
totaux.divers += montant(ligne?.divers?.montant)
|
||||
totaux.total += montant(ligne?.total)
|
||||
totaux.regles += montant(ligne?.regles)
|
||||
totaux.impayes += montant(ligne?.impayes)
|
||||
}
|
||||
|
||||
for (const champ of Object.keys(totaux)) {
|
||||
totaux[champ] = auCentime(totaux[champ])
|
||||
}
|
||||
|
||||
return totaux
|
||||
}
|
||||
|
||||
/** Colonnes de la ligne « Totaux » que l'on sait recouper avec les lignes. */
|
||||
const CHAMPS_COMPARES = [
|
||||
'solde_anterieur',
|
||||
'loyers',
|
||||
'taxes',
|
||||
'provisions',
|
||||
'divers',
|
||||
'total',
|
||||
'regles',
|
||||
'impayes',
|
||||
]
|
||||
|
||||
/**
|
||||
* Colonnes où la ligne « Totaux » extraite contredit les lignes extraites.
|
||||
*
|
||||
* Un écart trahit une extraction incomplète — un règlement que le compte rendu
|
||||
* n'a ventilé sur aucune ligne, un « divers » sauté lors d'un changement de
|
||||
* page. Il est signalé, jamais résorbé d'office.
|
||||
*
|
||||
* @param {Object} extraits - `locataire.totaux`, tel que lu dans le PDF
|
||||
* @param {Object} calcules - sortie de `totauxCalcules`
|
||||
* @returns {Object} par colonne divergente, la valeur déduite des lignes
|
||||
*/
|
||||
export function ecartsAvecLignes(extraits, calcules) {
|
||||
const ecarts = {}
|
||||
if (!extraits) return ecarts
|
||||
|
||||
for (const champ of CHAMPS_COMPARES) {
|
||||
if (extraits[champ] === null || extraits[champ] === undefined) continue
|
||||
if (Math.abs(montant(extraits[champ]) - montant(calcules[champ])) > 0.005) {
|
||||
ecarts[champ] = calcules[champ]
|
||||
}
|
||||
}
|
||||
|
||||
return ecarts
|
||||
}
|
||||
Reference in New Issue
Block a user