From c79ecc45ff66b8203341bcd344f480c3c0fb9e0c Mon Sep 17 00:00:00 2001 From: Bertrand Benjamin Date: Sun, 19 Jul 2026 21:29:04 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20extraction=20locataires=20et=20op=C3=A9?= =?UTF-8?q?rations=20par=20cellules=20de=20tableau?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remplace les parseurs texte+regex (fragiles sur l'alignement en colonnes des PDF Oralia mal construits) par une extraction géométrique : reconstruction des lignes visuelles par regroupement vertical tolérant des mots, puis affectation de chaque valeur à sa colonne via les filets du tableau (pdfplumber find_tables). Corrections apportées : - locataires : montant/libellé « divers » dans la bonne colonne (plus le total cumulé du lot), nom de locataire correct (le logo/en-tête hors filets est ignoré), lignes multi-période et pages recollées. - opérations : fournisseur séparé de la description (TOTALENERGIES ≠ DIDIER NETTOYAGE, PPR ≠ BOUVARD), colonne Déductible remplie, Débit/Crédit distingués, fournisseur des honoraires reporté sur le bloc, fragments de description recollés (LATAPY, AUDOUIN). - code lot : gère « S10 - », « S 17 - » (espace) et « S01 SOLDE » (sans tiret). Branchés dans extractor.py avec repli sur les anciens parseurs si un tableau n'a pas de filets détectables. Validés par réconciliation comptable : locataires 124/124 lots, opérations 7/7 PDF et 25/25 catégories, au centime. Ajoute le bouton « Relancer l'extraction » dans l'écran d'édition : re-extrait depuis le PDF stocké et met en évidence les différences avec la version précédente (panneau récapitulatif + anneaux « modifié » sur les cartes). Le diff des opérations s'aligne par contenu (robuste aux changements d'ordre/nombre). Co-Authored-By: Claude Opus 4.8 --- frontend/src/components/JsonViewer.vue | 41 +++ frontend/src/components/LocataireCard.vue | 38 ++- frontend/src/components/OperationCard.vue | 12 +- frontend/src/components/ReExtractionDiff.vue | 132 +++++++ frontend/src/pages/EditDocumentPage.vue | 74 +++- frontend/src/utils/diffExtraction.js | 215 ++++++++++++ src/plesna_gerance/extractor.py | 30 +- .../parsers/locataires_table.py | 322 ++++++++++++++++++ src/plesna_gerance/parsers/operations.py | 6 +- .../parsers/operations_table.py | 242 +++++++++++++ 10 files changed, 1101 insertions(+), 11 deletions(-) create mode 100644 frontend/src/components/ReExtractionDiff.vue create mode 100644 frontend/src/utils/diffExtraction.js create mode 100644 src/plesna_gerance/parsers/locataires_table.py create mode 100644 src/plesna_gerance/parsers/operations_table.py diff --git a/frontend/src/components/JsonViewer.vue b/frontend/src/components/JsonViewer.vue index 01f5aaf..615954c 100644 --- a/frontend/src/components/JsonViewer.vue +++ b/frontend/src/components/JsonViewer.vue @@ -116,6 +116,7 @@ :locataire="loc" :index="idx" :highlighted="highlightedLocataireIndex === idx" + :changed="changedLocataireSet.has(idx)" @update:locataire="updateLocataire(idx, $event)" @remove="removeLocataire(idx)" /> @@ -147,6 +148,7 @@ :categorie="group.categorie" :operations="group.operations" :highlightIndex="group.categorie === highlightOperationCategorie ? highlightOperationIndex : -1" + :changedIndices="changedOpByCategorie[group.categorie] || []" @update:operations="updateOperationsGroup(group.categorie, $event)" /> @@ -187,6 +189,10 @@ const props = defineProps({ highlight: { type: Object, default: null + }, + diff: { + type: Object, + default: null } }) @@ -213,6 +219,30 @@ const operationsGroupedByCategory = computed(() => { return groups }) +// Indices de locataires modifiés par la re-extraction (anneau « modifié »). +const changedLocataireSet = computed( + () => new Set(props.diff?.changedLocataireIndices || []) +) + +// Indices d'opérations modifiées, ramenés au repère (catégorie, index dans le groupe). +const changedOpByCategorie = computed(() => { + const result = {} + const operations = props.data?.data?.recapitulatif_operations + const changed = new Set(props.diff?.changedOperationIndices || []) + if (!operations || !changed.size) return result + const counters = {} + operations.forEach((op, absIdx) => { + const cat = op.categorie || 'AUTRES' + if (!(cat in counters)) counters[cat] = 0 + if (changed.has(absIdx)) { + if (!result[cat]) result[cat] = [] + result[cat].push(counters[cat]) + } + counters[cat]++ + }) + return result +}) + const copyLabel = ref('Copier') const expandedSections = reactive({ metadata: true, @@ -269,6 +299,17 @@ watch( { immediate: true } ) +// Déplier automatiquement les sections qui contiennent des différences. +watch( + () => props.diff, + (diff) => { + if (!diff) return + if (diff.summary?.metadata) expandedSections.metadata = true + if (diff.summary?.locataires) expandedSections.locataires = true + if (diff.summary?.operations) expandedSections.operations = true + } +) + function toggleSection(section) { expandedSections[section] = !expandedSections[section] } diff --git a/frontend/src/components/LocataireCard.vue b/frontend/src/components/LocataireCard.vue index 8759117..e6d923c 100644 --- a/frontend/src/components/LocataireCard.vue +++ b/frontend/src/components/LocataireCard.vue @@ -1,5 +1,5 @@