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>
158 lines
5.4 KiB
JavaScript
158 lines
5.4 KiB
JavaScript
// La carte locataire est la vue où l'on vérifie une extraction avant de
|
||
// l'enregistrer : elle ne doit afficher que des champs extraits, tous modifiables,
|
||
// et ne jamais substituer un calcul à ce que le compte rendu porte.
|
||
import { describe, expect, it } from 'vitest'
|
||
import { createSSRApp, h } from 'vue'
|
||
import { renderToString } from 'vue/server-renderer'
|
||
|
||
import LocataireCard from '../src/components/LocataireCard.vue'
|
||
|
||
// Lot 11 de M_33670000_2025-09-22, avec le règlement mal extrait observé sur un
|
||
// compte rendu ultérieur : le PDF annonce 7 878,00 € réglés pour 867,45 € dus.
|
||
function lotCharlot(surcharges = {}) {
|
||
return {
|
||
lot: { numero: '11', type: 'Appartement T3' },
|
||
locataire: { nom: 'CHARLOT ANDREE' },
|
||
lignes: [
|
||
{
|
||
type: 'loyer',
|
||
periode: { debut: '2025-09-01', fin: '2025-09-30' },
|
||
loyers: 798.45,
|
||
taxes: 0,
|
||
provisions: 69,
|
||
divers: { montant: 0, libelle: null },
|
||
total: 867.45,
|
||
regles: 867.45,
|
||
impayes: 0,
|
||
},
|
||
],
|
||
totaux: {
|
||
solde_anterieur: 0,
|
||
loyers: 798.45,
|
||
taxes: 0,
|
||
provisions: 69,
|
||
divers: 0,
|
||
total: 867.45,
|
||
regles: 867.45,
|
||
impayes: 0,
|
||
},
|
||
...surcharges,
|
||
}
|
||
}
|
||
|
||
// Les montants sont formatés avec des espaces insécables (séparateur de milliers,
|
||
// espace avant €) et le template en insère aux sauts de ligne : tous les blancs
|
||
// sont ramenés à un espace simple pour pouvoir chercher un montant.
|
||
// `highlighted` déplie la carte : le tableau n'est monté qu'à l'ouverture.
|
||
async function rendre(locataire) {
|
||
const app = createSSRApp({
|
||
render: () => h(LocataireCard, { locataire, highlighted: true }),
|
||
})
|
||
const html = await renderToString(app)
|
||
return html.replace(/\s+/g, ' ')
|
||
}
|
||
|
||
/** Les lignes seules, sans la ligne « Totaux » qui porte les valeurs déduites. */
|
||
function corpsDuTableau(html) {
|
||
return html.match(/<tbody>(.*)<\/tbody>/)[1]
|
||
}
|
||
|
||
describe('LocataireCard', () => {
|
||
it('reprend les colonnes du compte rendu, ligne « Totaux » comprise', async () => {
|
||
const html = await rendre(lotCharlot())
|
||
for (const colonne of ['Loyers', 'Taxes', 'Provisions', 'Divers', 'Total', 'Regles', 'Impaye']) {
|
||
expect(html).toContain(colonne)
|
||
}
|
||
expect(html).toContain('Totaux')
|
||
expect(html).toContain('Solde ant.')
|
||
})
|
||
|
||
it('laisse la ligne porter le total du compte rendu, sans le recalculer', async () => {
|
||
// Lot 13 de M_33670000_2025-08-26 : colonne « total » vide sur cette ligne,
|
||
// le compte rendu la porte sur une autre ligne du même bloc.
|
||
const lot = lotCharlot()
|
||
lot.lignes[0].loyers = 997.02
|
||
lot.lignes[0].provisions = 64
|
||
lot.lignes[0].total = 0
|
||
|
||
const lignes = corpsDuTableau(await rendre(lot))
|
||
expect(lignes).toContain('997,02 €')
|
||
expect(lignes).toContain('64,00 €')
|
||
// La somme des colonnes n'a pas à apparaître : la ligne n'est pas déduite.
|
||
expect(lignes).not.toContain('1 061,02 €')
|
||
})
|
||
|
||
it('n’affiche aucune valeur déduite quand l’extraction se recoupe', async () => {
|
||
const html = await rendre(lotCharlot())
|
||
expect(html).not.toContain('calc.')
|
||
expect(html).not.toContain('A verifier')
|
||
})
|
||
|
||
it('signale la colonne qui ne somme pas, sans toucher au montant extrait', async () => {
|
||
// Cas du lot 07 de S_33680000_2025-11-25 : la colonne Total des lignes reste
|
||
// à 0 alors que le compte rendu annonce 707,29 € pour le lot.
|
||
const lot = lotCharlot()
|
||
lot.lignes[0].total = 0
|
||
lot.totaux.total = 707.29
|
||
|
||
const html = await rendre(lot)
|
||
expect(html).toContain('A verifier')
|
||
expect(html).toContain('calculé : 0,00 €')
|
||
// Le montant du compte rendu reste affiché tel quel.
|
||
expect(html).toContain('707,29 €')
|
||
})
|
||
|
||
it('met la colonne en défaut en évidence', async () => {
|
||
const lot = lotCharlot()
|
||
lot.totaux.regles = 707.29
|
||
|
||
const html = await rendre(lot)
|
||
expect(html).toContain('bg-amber-500/10')
|
||
})
|
||
|
||
it('signale aussi le solde antérieur, qui n’a pas de colonne à lui', async () => {
|
||
// Il occupe la colonne « Période » de la ligne Totaux : son écart doit y
|
||
// apparaître comme celui des colonnes rendues par la boucle.
|
||
const lot = lotCharlot()
|
||
lot.totaux.solde_anterieur = 49170.47
|
||
|
||
const html = await rendre(lot)
|
||
expect(html).toContain('calculé : 0,00 €')
|
||
expect(html).toContain('A verifier')
|
||
})
|
||
|
||
it('dit au survol ce qui est extrait et ce qui est calculé', async () => {
|
||
const lot = lotCharlot()
|
||
lot.totaux.regles = 707.29
|
||
|
||
const html = await rendre(lot)
|
||
expect(html).toContain('Valeur extraite du compte rendu — cliquer pour la modifier')
|
||
expect(html).toContain('Extrait du compte rendu : 707,29 €')
|
||
expect(html).toContain('Calculé sur les 1 ligne(s) : 867,45 €')
|
||
})
|
||
|
||
it('signale un lot dont aucune ligne n’a été extraite', async () => {
|
||
const lot = lotCharlot({ lignes: [] })
|
||
|
||
const html = await rendre(lot)
|
||
expect(html).toContain('Aucune ligne extraite')
|
||
expect(html).toContain('A verifier')
|
||
})
|
||
|
||
it('montre l’impayé du compte rendu dans le bandeau replié', async () => {
|
||
const lot = lotCharlot()
|
||
lot.totaux.impayes = 67.45
|
||
|
||
const html = await rendre(lot)
|
||
expect(html).toContain('Impayes: 67,45 €')
|
||
})
|
||
|
||
it('distingue un trop-perçu d’un impayé', async () => {
|
||
const lot = lotCharlot()
|
||
lot.totaux.impayes = -32.55
|
||
|
||
const html = await rendre(lot)
|
||
expect(html).toContain('Trop-percu: 32,55 €')
|
||
})
|
||
})
|