refactor: donne une source unique aux colonnes du compte rendu
L'ordre et les libellés des colonnes étaient retapés à six endroits : l'en-tête du tableau, deux boucles de cellules, la ligne « Totaux », la table des libellés et la liste des colonnes recoupées. Ces copies avaient déjà divergé — le solde antérieur, rendu hors de la boucle, était comparé aux lignes sans que son écart puisse s'afficher dans sa cellule. `COLONNES_CRG` devient la seule déclaration, et tout le reste en dérive ; l'écart du solde antérieur s'affiche du coup là où on le cherche. `ecartsAvecLignes` rapporte maintenant l'extrait, le calculé et ce qui manque entre les deux. Le composant refaisait cette soustraction pour son infobulle, avec un `|| 0` là où l'utilitaire emploie `Number` et `Number.isFinite` : deux règles de coercition pour un même calcul, libres de diverger. Le contenu déplié d'un lot passe de `v-show` à `v-if`. Le tableau des lignes compte une centaine de champs éditables ; les garder montés pour la vingtaine de lots d'un document faisait re-rendre à chaque frappe deux mille champs que personne ne regardait. `setNestedValue` était recopié à l'identique dans trois composants et `formatCurrency` redéfini dans le composant alors que `utils/format.js` existe et dit lui-même que les nouveaux affichages passent par lui. Le premier part dans `utils/chemin.js`, le second cède la place à `formatMontantPrecis`, dont le formateur `Intl` est construit une fois pour toutes. `EditableField` affirmait en dur que sa valeur venait du compte rendu. C'est vrai des trois écrans qui l'emploient aujourd'hui, mais un formulaire de saisie manuelle mentirait sans le savoir : l'origine devient une prop, avec cette valeur par défaut. Le type des lignes de report est défini des deux côtés de l'application sans lien entre eux ; chacun renvoie désormais à l'autre. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
20
frontend/src/utils/chemin.js
Normal file
20
frontend/src/utils/chemin.js
Normal file
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* Écriture d'une valeur désignée par un chemin pointé (`"divers.montant"`).
|
||||
*
|
||||
* Les formulaires d'édition adressent leurs champs par ce chemin plutôt que par
|
||||
* une référence : ils travaillent sur une copie fraîche de l'objet à chaque
|
||||
* modification, et une référence prise avant la copie viserait l'ancien.
|
||||
*/
|
||||
|
||||
/** Écrit `valeur` à `chemin`, en créant les objets intermédiaires manquants. */
|
||||
export function setNestedValue(objet, chemin, valeur) {
|
||||
const parties = chemin.split('.')
|
||||
let courant = objet
|
||||
for (let i = 0; i < parties.length - 1; i++) {
|
||||
if (!courant[parties[i]]) {
|
||||
courant[parties[i]] = {}
|
||||
}
|
||||
courant = courant[parties[i]]
|
||||
}
|
||||
courant[parties[parties.length - 1]] = valeur
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Recoupement des totaux d'un lot avec ses lignes.
|
||||
* Colonnes du compte rendu de gérance, et recoupement des totaux d'un lot.
|
||||
*
|
||||
* 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.
|
||||
@@ -15,13 +15,43 @@
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Type des lignes qui reportent le solde du compte rendu précédent.
|
||||
*
|
||||
* Même valeur que `TYPE_LIGNE_REPORT` côté Python
|
||||
* (`services/revenus_query.py`), où elle sépare les stocks des flux. Les deux
|
||||
* définitions décrivent la sortie du même parser et doivent bouger ensemble.
|
||||
*/
|
||||
const TYPE_REPORT = 'solde_anterieur'
|
||||
|
||||
/**
|
||||
* Les colonnes du compte rendu, dans son ordre d'impression.
|
||||
*
|
||||
* Source unique de l'ordre et des libellés : le tableau d'édition, ses en-têtes
|
||||
* et le recoupement des totaux s'en déduisent tous, plutôt que d'en tenir
|
||||
* chacun sa copie.
|
||||
*
|
||||
* - `champ` : clé dans les totaux d'un lot
|
||||
* - `champLigne` : clé correspondante sur une ligne, quand elle diffère
|
||||
* - `entete` : le compte rendu ne donne pas de colonne au solde antérieur, il
|
||||
* l'imprime dans la colonne « Période »
|
||||
*/
|
||||
export const COLONNES_CRG = [
|
||||
{ champ: 'solde_anterieur', libelle: 'Solde anterieur', entete: false },
|
||||
{ champ: 'loyers', libelle: 'Loyers' },
|
||||
{ champ: 'taxes', libelle: 'Taxes' },
|
||||
{ champ: 'provisions', libelle: 'Provisions' },
|
||||
{ champ: 'divers', libelle: 'Divers', champLigne: 'divers.montant' },
|
||||
{ champ: 'total', libelle: 'Total' },
|
||||
{ champ: 'regles', libelle: 'Regles' },
|
||||
{ champ: 'impayes', libelle: 'Impaye' },
|
||||
]
|
||||
|
||||
/** Les colonnes qui ont un en-tête dans le tableau. */
|
||||
export const COLONNES_TABLEAU = COLONNES_CRG.filter((c) => c.entete !== false)
|
||||
|
||||
/** Montant exploitable d'un champ : `null` et `undefined` valent zéro. */
|
||||
function montant(valeur) {
|
||||
const nombre = Number(valeur)
|
||||
@@ -36,22 +66,15 @@ function auCentime(valeur) {
|
||||
/**
|
||||
* Agrège les lignes d'un lot en un jeu de totaux de même forme que `totaux`.
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* @param {Array} lignes - lignes du lot (`locataire.lignes`)
|
||||
* @returns {Object} totaux déduits, tous champs présents et arrondis au centime
|
||||
* @returns {Object} totaux déduits, une clé par colonne, 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,
|
||||
}
|
||||
const totaux = Object.fromEntries(COLONNES_CRG.map(({ champ }) => [champ, 0]))
|
||||
|
||||
for (const ligne of liste) {
|
||||
if (ligne?.type === TYPE_REPORT) {
|
||||
@@ -74,18 +97,6 @@ export function totauxCalcules(lignes) {
|
||||
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.
|
||||
*
|
||||
@@ -95,16 +106,19 @@ const CHAMPS_COMPARES = [
|
||||
*
|
||||
* @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
|
||||
* @returns {Object} par colonne divergente, `{ extrait, calcule, manquant }`
|
||||
*/
|
||||
export function ecartsAvecLignes(extraits, calcules) {
|
||||
const ecarts = {}
|
||||
if (!extraits) return ecarts
|
||||
|
||||
for (const champ of CHAMPS_COMPARES) {
|
||||
for (const champ of Object.keys(calcules)) {
|
||||
if (extraits[champ] === null || extraits[champ] === undefined) continue
|
||||
if (Math.abs(montant(extraits[champ]) - montant(calcules[champ])) > 0.005) {
|
||||
ecarts[champ] = calcules[champ]
|
||||
|
||||
const extrait = montant(extraits[champ])
|
||||
const calcule = montant(calcules[champ])
|
||||
if (Math.abs(extrait - calcule) > 0.005) {
|
||||
ecarts[champ] = { extrait, calcule, manquant: auCentime(extrait - calcule) }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user