feat: annonce les reecritures que l'enregistrement imposera

Refuser une modification, c'est reenvoyer la valeur actuelle du document. Mais
l'enregistrement ramene les donnees a une forme canonique (aujourd'hui les
numeros de lot) : sur ces champs, le refus n'est pas conserve. L'interface le
laissait croire.

Plutot que de traiter le cas des lots en dur, le serveur devient seul juge de ce
qu'il stocke et l'annonce : `canonicalize_extraction` regroupe les reecritures
faites a la persistance, `save_document` et l'apercu passent par elle, et
/api/documents/{id}/re-extract renvoie `previous_canonical` a cote de
`previous_data`. Le front compare ces deux jeux et signale, sous chaque case
decochee, la valeur que l'enregistrement reecrira — sans connaitre aucune regle.
Une transformation ajoutee plus tard remontera donc toute seule dans l'interface.

Toutes les differences restent visibles et toutes les cases actives : voir ce
qui change et pouvoir se prononcer prime, quitte a etre prevenu que ce champ-la
reviendra reecrit.

Supprime au passage frontend/src/utils/lots.js, qui dupliquait la regle de
normalisation cote client.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-26 06:43:02 +02:00
parent 43efea4945
commit a1da4fc97f
10 changed files with 268 additions and 86 deletions

View File

@@ -13,6 +13,7 @@ from ...database import DatabaseService, get_session, storage
from ...database.models import Depense, Document, Immeuble, Locataire, Lot, Revenu
from ...database.service import DuplicateDocumentError
from ...extractor import extract_compte_rendu
from ...utils.canonical import canonical_copy
from ...utils.uploads import UploadTooLargeError, read_upload_limited
from ..schemas import DocumentSummary, SaveRequest, SaveResponse
@@ -429,6 +430,12 @@ def re_extract_document(
`depenses_tags` porte les tags actuels du document, pour pouvoir etre
reportes sur la nouvelle extraction plutot que perdus au reenregistrement.
`previous_canonical` donne les donnees actuelles telles qu'elles seraient
reecrites a l'enregistrement (cf. utils.canonical). L'appelant compare avec
`previous_data` pour savoir quels champs le serveur imposera de toute facon,
et le signaler avant que l'utilisateur ne se prononce — sans avoir a
connaitre les regles de reecriture.
Endpoint synchrone (`def`) : l'extraction est bloquante et prend plusieurs
secondes, FastAPI l'execute donc dans un thread pour ne pas figer le serveur.
"""
@@ -462,13 +469,16 @@ def re_extract_document(
detail=f"Erreur lors de la re-extraction: {str(e)}",
)
previous_data = json.loads(document.json_data) if document.json_data else None
return {
"document_id": document_id,
"reference": document.reference,
"date": str(document.date),
"source_file": document.source_file,
"original_json_path": document.json_path,
"previous_data": json.loads(document.json_data) if document.json_data else None,
"previous_data": previous_data,
"previous_canonical": canonical_copy(previous_data),
"re_extracted_data": new_data,
"depenses_tags": db_service.get_depenses_tags(document_id),
"message": "Donnees re-extraites. Utilisez PUT /api/documents/{id} pour mettre a jour.",

View File

@@ -8,9 +8,9 @@ from sqlalchemy import select
from sqlalchemy.orm import Session
from ..utils.amounts import parse_amount
from ..utils.canonical import canonicalize_extraction
from ..utils.lots import (
LOT_NUMERO_INCONNU,
normalize_extraction_lots,
normalize_lot_numero,
)
from . import storage
@@ -152,8 +152,10 @@ class DatabaseService:
`data` lors d'un remplacement par ID
ValueError: Si `replace_document_id` ne designe aucun document
"""
# Uniformiser les numéros de lot avant toute persistance (JSON + tables)
normalize_extraction_lots(data)
# Ramener l'extraction à sa forme canonique avant toute persistance
# (JSON + tables). Point de passage unique : l'aperçu de re-extraction
# interroge la même fonction pour annoncer ces réécritures.
canonicalize_extraction(data)
metadata = data.get("metadata", {})
doc_info = metadata.get("document", {})

View File

@@ -0,0 +1,39 @@
"""Forme canonique d'une extraction, telle qu'elle sera persistee.
L'enregistrement ne stocke pas toujours les donnees telles qu'on les lui donne :
certaines valeurs sont ramenees a une ecriture canonique (aujourd'hui les
numeros de lot). L'interface de re-extraction doit pouvoir annoncer ces
reecritures *avant* d'enregistrer, sans reimplementer les regles : elle demande
au serveur la forme canonique et compare.
Toute transformation appliquee a la persistance doit donc etre ajoutee ici, et
nulle part ailleurs : `save_document` et l'apercu passent par cette fonction.
"""
import copy
from typing import Any
from .lots import normalize_extraction_lots
def canonicalize_extraction(data: dict[str, Any]) -> dict[str, Any]:
"""Applique, sur place, les reecritures faites a l'enregistrement.
Args:
data: Extraction (metadata, situation_locataires, recapitulatif_operations)
Returns:
Le meme dict, sous sa forme canonique
"""
normalize_extraction_lots(data)
return data
def canonical_copy(data: dict[str, Any] | None) -> dict[str, Any] | None:
"""Comme `canonicalize_extraction`, mais sans toucher a l'original.
Sert a l'apercu : on montre ce qui serait ecrit sans rien modifier.
"""
if data is None:
return None
return canonicalize_extraction(copy.deepcopy(data))