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>
184 lines
6.6 KiB
JavaScript
184 lines
6.6 KiB
JavaScript
// Le recoupement des totaux avec les lignes sert d'alerte sur les extractions
|
||
// incomplètes : il est éprouvé sur les configurations des comptes rendus réels.
|
||
import { describe, expect, it } from 'vitest'
|
||
|
||
import { ecartsAvecLignes, totauxCalcules } from '../src/utils/totauxLocataire.js'
|
||
|
||
// Lot 11 CHARLOT ANDREE : un loyer avec provision, réglé.
|
||
const ligneLoyer = {
|
||
type: 'loyer',
|
||
periode: { debut: '2026-06-01', fin: '2026-06-30' },
|
||
loyers: 809.64,
|
||
taxes: 0,
|
||
provisions: 69,
|
||
divers: { montant: 0, libelle: null },
|
||
total: 878.64,
|
||
regles: 878.64,
|
||
impayes: 0,
|
||
}
|
||
|
||
describe('totauxCalcules', () => {
|
||
it('agrège colonne par colonne, comme l’œil sur le tableau', () => {
|
||
expect(totauxCalcules([ligneLoyer])).toEqual({
|
||
solde_anterieur: 0,
|
||
loyers: 809.64,
|
||
taxes: 0,
|
||
provisions: 69,
|
||
divers: 0,
|
||
total: 878.64,
|
||
regles: 878.64,
|
||
impayes: 0,
|
||
})
|
||
})
|
||
|
||
it('somme la colonne « total » telle quelle, sans la déduire des autres', () => {
|
||
// Le compte rendu ne remplit cette colonne que sur une ligne par bloc : les
|
||
// blocs se recomposent à l'échelle du lot, c'est ce total-là qui fait foi.
|
||
const totaux = totauxCalcules([
|
||
{ type: 'loyer', loyers: 997.02, provisions: 64, total: 0 },
|
||
{ type: 'loyer', loyers: 500, total: 1561.02 },
|
||
])
|
||
expect(totaux.total).toBe(1561.02)
|
||
expect(totaux.loyers).toBe(1497.02)
|
||
})
|
||
|
||
it('somme la colonne « impayé » plutôt que de la recalculer', () => {
|
||
const totaux = totauxCalcules([
|
||
{ type: 'loyer', loyers: 500, total: 500, regles: 400, impayes: 100 },
|
||
])
|
||
expect(totaux.impayes).toBe(100)
|
||
})
|
||
|
||
it('range le report de solde à part sans le confondre avec les loyers', () => {
|
||
// Lot 03 du compte rendu de février : un report de 0,63 et un loyer réglé.
|
||
const totaux = totauxCalcules([
|
||
{ type: 'solde_anterieur', loyers: 0.63, total: 0.63, regles: 0, impayes: 0.63 },
|
||
{ type: 'loyer', loyers: 640, provisions: 31, total: 671, regles: 671, impayes: 0 },
|
||
])
|
||
expect(totaux.solde_anterieur).toBe(0.63)
|
||
expect(totaux.loyers).toBe(640)
|
||
expect(totaux.total).toBe(671.63)
|
||
expect(totaux.impayes).toBe(0.63)
|
||
})
|
||
|
||
it('additionne les lignes de types différents', () => {
|
||
// Lot 09 TERRIER ADILE : régularisation de sortie, montants négatifs.
|
||
const totaux = totauxCalcules([
|
||
{ type: 'loyer', provisions: -6, total: 0 },
|
||
{ type: 'rappel_loyer', loyers: -138.98, total: 0 },
|
||
{ type: 'divers', divers: { montant: -455, libelle: 'Rembt dépot de garantie' }, total: 0 },
|
||
{ type: 'divers', divers: { montant: 23.02 }, total: -268.2, regles: -268.2 },
|
||
])
|
||
expect(totaux.loyers).toBe(-138.98)
|
||
expect(totaux.provisions).toBe(-6)
|
||
expect(totaux.divers).toBe(-431.98)
|
||
expect(totaux.total).toBe(-268.2)
|
||
expect(totaux.regles).toBe(-268.2)
|
||
})
|
||
|
||
it('traite les champs absents comme des zéros', () => {
|
||
const totaux = totauxCalcules([{ type: 'loyer', loyers: 500 }])
|
||
expect(totaux.loyers).toBe(500)
|
||
expect(totaux.total).toBe(0)
|
||
expect(totauxCalcules([]).total).toBe(0)
|
||
expect(totauxCalcules(undefined).total).toBe(0)
|
||
})
|
||
|
||
it('arrondit au centime plutôt que de traîner les flottants', () => {
|
||
const totaux = totauxCalcules([
|
||
{ type: 'loyer', loyers: 0.1 },
|
||
{ type: 'loyer', loyers: 0.2 },
|
||
])
|
||
expect(totaux.loyers).toBe(0.3)
|
||
})
|
||
})
|
||
|
||
describe('ecartsAvecLignes', () => {
|
||
const extraitsCharlot = {
|
||
solde_anterieur: 0,
|
||
loyers: 809.64,
|
||
taxes: 0,
|
||
provisions: 69,
|
||
divers: 0,
|
||
total: 878.64,
|
||
regles: 878.64,
|
||
impayes: 0,
|
||
}
|
||
|
||
it('ne signale rien quand les lignes recoupent la ligne « Totaux »', () => {
|
||
const calcules = totauxCalcules([ligneLoyer])
|
||
expect(ecartsAvecLignes(extraitsCharlot, calcules)).toEqual({})
|
||
})
|
||
|
||
it('signale une colonne Total qui ne somme pas, et le règlement qui manque avec', () => {
|
||
// Lot 07 de S_33680000_2025-11-25 : la dernière ligne a perdu ses colonnes
|
||
// Total et Regles à l'extraction, le compte rendu porte 707,29 € pour les deux.
|
||
const calcules = totauxCalcules([
|
||
{ type: 'loyer', loyers: -265.29, provisions: -50, total: 0, regles: 0 },
|
||
{ type: 'divers', divers: { montant: -87.85 }, total: -403.14, regles: -403.14 },
|
||
{ type: 'loyer', loyers: 265.29, provisions: 50, total: 0, regles: 0 },
|
||
{ type: 'divers', divers: { montant: 87.85 }, total: 403.14, regles: 403.14 },
|
||
{ type: 'loyer', loyers: 657.29, provisions: 50, total: 0, regles: 0 },
|
||
])
|
||
// Les colonnes de détail se recoupent, seules Total et Regles décrochent.
|
||
expect(calcules.loyers).toBe(657.29)
|
||
expect(calcules.provisions).toBe(50)
|
||
expect(calcules.divers).toBe(0)
|
||
|
||
const ecarts = ecartsAvecLignes(
|
||
{
|
||
solde_anterieur: 0,
|
||
loyers: 657.29,
|
||
taxes: 0,
|
||
provisions: 50,
|
||
divers: 0,
|
||
total: 707.29,
|
||
regles: 707.29,
|
||
impayes: 0,
|
||
},
|
||
calcules
|
||
)
|
||
expect(ecarts).toEqual({
|
||
total: { extrait: 707.29, calcule: 0, manquant: 707.29 },
|
||
regles: { extrait: 707.29, calcule: 0, manquant: 707.29 },
|
||
})
|
||
})
|
||
|
||
it('signale un divers absent des lignes extraites', () => {
|
||
const calcules = totauxCalcules([
|
||
{ type: 'divers', divers: { montant: -455 } },
|
||
{ type: 'divers', divers: { montant: 23.02 } },
|
||
])
|
||
expect(ecartsAvecLignes({ divers: -123.22 }, calcules)).toEqual({
|
||
divers: { extrait: -123.22, calcule: -431.98, manquant: 308.76 },
|
||
})
|
||
})
|
||
|
||
it('signale un lot dont aucune ligne n’a été extraite', () => {
|
||
// Lot 08 de S_33680000_2025-08-26 : 100 € réglés, aucune ligne.
|
||
const ecarts = ecartsAvecLignes({ loyers: 0, regles: 100 }, totauxCalcules([]))
|
||
expect(ecarts).toEqual({ regles: { extrait: 100, calcule: 0, manquant: 100 } })
|
||
})
|
||
|
||
it('rapporte l’extrait, le calculé et ce qui manque entre les deux', () => {
|
||
const calcules = totauxCalcules([{ type: 'loyer', loyers: 500 }])
|
||
expect(ecartsAvecLignes({ loyers: 800 }, calcules)).toEqual({
|
||
loyers: { extrait: 800, calcule: 500, manquant: 300 },
|
||
})
|
||
})
|
||
|
||
it('tolère un écart d’arrondi sous le centime', () => {
|
||
const calcules = totauxCalcules([{ type: 'loyer', loyers: 100 }])
|
||
expect(ecartsAvecLignes({ loyers: 100.004 }, calcules)).toEqual({})
|
||
})
|
||
|
||
it('ignore une colonne que le compte rendu ne renseigne pas', () => {
|
||
const calcules = totauxCalcules([{ type: 'loyer', loyers: 100 }])
|
||
expect(ecartsAvecLignes({ loyers: 100, taxes: null }, calcules)).toEqual({})
|
||
})
|
||
|
||
it('ne compare rien sans totaux extraits', () => {
|
||
expect(ecartsAvecLignes(null, totauxCalcules([ligneLoyer]))).toEqual({})
|
||
})
|
||
})
|