feat: expose la re-extraction verifiable d'un document

POST /api/documents/{id}/re-extract retourne desormais, en plus des donnees
re-extraites, les donnees actuelles et les tags des depenses : un seul appel
suffit pour comparer avant/apres et reporter les tags. L'endpoint devient
synchrone pour que FastAPI l'execute dans un thread, l'extraction bloquant
plusieurs secondes par PDF.

PUT /api/documents/{id} enregistre la nouvelle extraction en visant le
document par son ID (save_document(replace_document_id=...)) : une extraction
qui corrige la reference ou la date met a jour le bon document au lieu d'en
creer un second, et refuse la collision avec un document voisin. Le PDF
stocke est conserve.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-25 19:35:58 +02:00
parent 50508c98a8
commit 9cf0a89aab
3 changed files with 233 additions and 7 deletions

View File

@@ -242,6 +242,7 @@ async def get_document(
"siret": document.editeur_siret,
},
"json_data": json.loads(document.json_data) if document.json_data else None,
"depenses_tags": db_service.get_depenses_tags(document_id),
"created_at": document.created_at.isoformat() if document.created_at else None,
"has_pdf": document.pdf_path is not None,
"has_json": document.json_path is not None,
@@ -358,18 +359,78 @@ async def download_document_json(
)
@router.put("/documents/{document_id}", response_model=SaveResponse)
async def update_document(
document_id: int,
request: SaveRequest,
session: Session = Depends(get_session),
) -> SaveResponse:
"""Remplace les donnees d'un document existant, identifie par son ID.
A la difference de `POST /api/save` avec `overwrite`, qui retrouve le
document par (reference, date), le document vise est ici designe par son ID :
une nouvelle extraction qui corrige la reference ou la date met a jour le bon
document au lieu d'en creer un second. Le PDF stocke est conserve.
- **data**: Nouvelles donnees extraites
- **depenses_tags**: Tags a appliquer aux depenses (par index d'operation)
- **source_file**: Nom du fichier source (optionnel, conserve si absent)
"""
db_service = DatabaseService(session)
existing = db_service.get_document_by_id(document_id)
if not existing:
raise HTTPException(status_code=404, detail="Document non trouve")
try:
document = db_service.save_document(
data=request.data,
source_file=request.source_file or existing.source_file,
depenses_tags=request.depenses_tags,
replace_document_id=document_id,
)
except DuplicateDocumentError as e:
return SaveResponse(
success=False,
message=(
f"Un autre document porte deja reference={e.reference}, date={e.date}"
),
reference=e.reference,
date=str(e.date),
)
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
except Exception:
logger.exception("Erreur lors de la mise a jour du document %s", document_id)
raise HTTPException(
status_code=500, detail="Erreur lors de la mise a jour du document."
)
return SaveResponse(
success=True,
message="Document mis a jour avec succes",
document_id=document.id,
reference=document.reference,
date=str(document.date),
)
@router.post("/documents/{document_id}/re-extract")
async def re_extract_document(
def re_extract_document(
document_id: int,
session: Session = Depends(get_session),
) -> dict:
"""Re-extrait les donnees depuis le PDF stocke.
Utile pour corriger l'extraction apres amelioration des parsers.
Ne modifie pas automatiquement la base - retourne les nouvelles donnees
pour validation par l'utilisateur.
Utile pour corriger l'extraction apres amelioration des parsers, document par
document ou lors d'un balayage de toute la base. Ne modifie pas la base :
retourne cote a cote les donnees actuelles et les donnees re-extraites, pour
que l'appelant compare et decide s'il enregistre (PUT /api/documents/{id}).
Retourne les donnees re-extraites du PDF.
`depenses_tags` porte les tags actuels du document, pour pouvoir etre
reportes sur la nouvelle extraction plutot que perdus au reenregistrement.
Endpoint synchrone (`def`) : l'extraction est bloquante et prend plusieurs
secondes, FastAPI l'execute donc dans un thread pour ne pas figer le serveur.
"""
db_service = DatabaseService(session)
document = db_service.get_document_by_id(document_id)
@@ -405,7 +466,10 @@ async def re_extract_document(
"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,
"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.",
}