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:
176
frontend/tests/totauxLocataire.test.js
Normal file
176
frontend/tests/totauxLocataire.test.js
Normal file
@@ -0,0 +1,176 @@
|
||||
// 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: 0, regles: 0 })
|
||||
})
|
||||
|
||||
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: -431.98 })
|
||||
})
|
||||
|
||||
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: 0 })
|
||||
})
|
||||
|
||||
it('renvoie la valeur déduite des lignes, jamais celle du compte rendu', () => {
|
||||
const calcules = totauxCalcules([{ type: 'loyer', loyers: 500 }])
|
||||
expect(ecartsAvecLignes({ loyers: 800 }, calcules)).toEqual({ loyers: 500 })
|
||||
})
|
||||
|
||||
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({})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user