feat: extraction locataires et opérations par cellules de tableau
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 <noreply@anthropic.com>
This commit is contained in:
@@ -25,7 +25,24 @@
|
||||
>
|
||||
Annuler
|
||||
</button>
|
||||
<button
|
||||
<button
|
||||
v-if="!showTagging && extractedData"
|
||||
@click="reExtract"
|
||||
:disabled="isSaving || isReExtracting || !documentData?.has_pdf"
|
||||
:title="documentData?.has_pdf ? 'Relancer l\'extraction depuis le PDF stocké' : 'PDF non disponible pour ce document'"
|
||||
class="px-4 py-2 text-sm bg-amber-600 text-white rounded-lg hover:bg-amber-700 transition-colors disabled:opacity-50 flex items-center gap-2"
|
||||
>
|
||||
<svg
|
||||
class="w-4 h-4"
|
||||
:class="{ 'animate-spin': isReExtracting }"
|
||||
fill="none" stroke="currentColor" viewBox="0 0 24 24"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
||||
</svg>
|
||||
{{ isReExtracting ? 'Extraction…' : "Relancer l'extraction" }}
|
||||
</button>
|
||||
<button
|
||||
v-if="!showTagging && extractedData"
|
||||
@click="goToTagging"
|
||||
:disabled="isSaving"
|
||||
@@ -76,6 +93,19 @@
|
||||
|
||||
<!-- Droite : Édition ou Tagging -->
|
||||
<div class="w-1/2 flex flex-col bg-gray-50">
|
||||
<!-- Bandeau erreur de re-extraction -->
|
||||
<div v-if="reExtractError" class="flex-shrink-0 px-4 py-2 bg-red-500/10 border-b border-red-500/30 text-sm text-red-600">
|
||||
{{ reExtractError }}
|
||||
</div>
|
||||
|
||||
<!-- Panneau des différences après re-extraction -->
|
||||
<ReExtractionDiff
|
||||
v-if="diff && !showTagging"
|
||||
:diff="diff"
|
||||
@revert="revertReExtract"
|
||||
@close="diff = null"
|
||||
/>
|
||||
|
||||
<!-- Vue 1 : JsonViewer (mode édition) -->
|
||||
<JsonViewer
|
||||
v-if="!showTagging && extractedData"
|
||||
@@ -83,6 +113,7 @@
|
||||
@update:data="extractedData = $event"
|
||||
:is-loading="false"
|
||||
:highlight="highlightInfo"
|
||||
:diff="diff"
|
||||
class="flex-1"
|
||||
/>
|
||||
|
||||
@@ -116,6 +147,8 @@ import { useRouter, useRoute } from 'vue-router'
|
||||
import PdfPreview from '../components/PdfPreview.vue'
|
||||
import JsonViewer from '../components/JsonViewer.vue'
|
||||
import TaggingStep from '../components/TaggingStep.vue'
|
||||
import ReExtractionDiff from '../components/ReExtractionDiff.vue'
|
||||
import { computeExtractionDiff } from '../utils/diffExtraction.js'
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
@@ -127,6 +160,10 @@ const isLoading = ref(true)
|
||||
const showTagging = ref(false)
|
||||
const isSaving = ref(false)
|
||||
const saveError = ref(null)
|
||||
const isReExtracting = ref(false)
|
||||
const reExtractError = ref(null)
|
||||
const diff = ref(null)
|
||||
const previousData = ref(null)
|
||||
|
||||
const highlightInfo = computed(() => {
|
||||
const q = route.query
|
||||
@@ -183,6 +220,41 @@ function goToTagging() {
|
||||
saveError.value = null
|
||||
}
|
||||
|
||||
async function reExtract() {
|
||||
isReExtracting.value = true
|
||||
reExtractError.value = null
|
||||
try {
|
||||
const response = await fetch(`/api/documents/${documentId}/re-extract`, {
|
||||
method: 'POST',
|
||||
})
|
||||
const result = await response.json()
|
||||
if (!response.ok) {
|
||||
throw new Error(result.detail || 'Échec de la re-extraction')
|
||||
}
|
||||
|
||||
// Sauvegarder l'état actuel pour pouvoir revenir en arrière.
|
||||
previousData.value = JSON.parse(JSON.stringify(extractedData.value.data))
|
||||
|
||||
const newData = result.re_extracted_data
|
||||
diff.value = computeExtractionDiff(previousData.value, newData)
|
||||
|
||||
// Remplacer les données de travail par la nouvelle extraction.
|
||||
extractedData.value = { ...extractedData.value, data: newData }
|
||||
} catch (err) {
|
||||
console.error('Re-extraction error:', err)
|
||||
reExtractError.value = err.message || 'Une erreur est survenue lors de la re-extraction'
|
||||
} finally {
|
||||
isReExtracting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function revertReExtract() {
|
||||
if (!previousData.value) return
|
||||
extractedData.value = { ...extractedData.value, data: previousData.value }
|
||||
previousData.value = null
|
||||
diff.value = null
|
||||
}
|
||||
|
||||
async function handleSave(depensesTags, shouldOverwrite) {
|
||||
isSaving.value = true
|
||||
saveError.value = null
|
||||
|
||||
Reference in New Issue
Block a user