feat: reimport pdf

This commit is contained in:
2026-01-22 21:23:46 +01:00
parent 981fabcc27
commit 4f0ca67dc4
13 changed files with 1068 additions and 14 deletions

View File

@@ -3,13 +3,15 @@
import json
from datetime import datetime
from fastapi import APIRouter, Depends, HTTPException
from fastapi import APIRouter, Depends, HTTPException, File, UploadFile, Form
from fastapi.responses import Response, JSONResponse
from sqlalchemy.orm import Session
from sqlalchemy import func, select
from ...database import get_session, DatabaseService
from ...database import get_session, DatabaseService, storage
from ...database.models import Document, Immeuble, Lot, Locataire, Revenu, Depense
from ...database.service import DuplicateDocumentError
from ...extractor import extract_compte_rendu
from ..schemas import SaveRequest, SaveResponse, DocumentSummary
router = APIRouter(prefix="/api", tags=["documents"])
@@ -64,6 +66,80 @@ async def save_document(
)
@router.post("/save-with-pdf", response_model=SaveResponse)
async def save_document_with_pdf(
pdf_file: UploadFile = File(..., description="Fichier PDF original"),
data: str = Form(..., description="Donnees JSON extraites"),
depenses_tags: str | None = Form(None, description="Tags JSON pour les depenses"),
overwrite: bool = Form(False, description="Ecraser si existant"),
session: Session = Depends(get_session),
) -> SaveResponse:
"""Sauvegarde les donnees extraites avec le fichier PDF original.
Cette version stocke le PDF et le JSON sur le disque pour tracabilite.
- **pdf_file**: Fichier PDF original (multipart)
- **data**: Donnees JSON extraites (string JSON)
- **depenses_tags**: Tags JSON pour les depenses (optionnel)
- **overwrite**: Si True, ecrase le document existant
"""
# Parse JSON data
try:
parsed_data = json.loads(data)
except json.JSONDecodeError as e:
raise HTTPException(status_code=400, detail=f"JSON invalide pour data: {e}")
# Parse depenses_tags if provided
parsed_tags = None
if depenses_tags:
try:
parsed_tags = json.loads(depenses_tags)
except json.JSONDecodeError as e:
raise HTTPException(
status_code=400, detail=f"JSON invalide pour depenses_tags: {e}"
)
# Read PDF content
try:
pdf_content = await pdf_file.read()
except Exception as e:
raise HTTPException(status_code=400, detail=f"Erreur lecture du PDF: {str(e)}")
try:
db_service = DatabaseService(session)
document = db_service.save_document(
data=parsed_data,
source_file=pdf_file.filename,
pdf_content=pdf_content,
depenses_tags=parsed_tags,
overwrite=overwrite,
)
return SaveResponse(
success=True,
message="Document sauvegarde avec succes (PDF et JSON stockes)",
document_id=document.id,
reference=document.reference,
date=str(document.date),
)
except DuplicateDocumentError as e:
return SaveResponse(
success=False,
message=f"Document deja existant: 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 as e:
raise HTTPException(
status_code=500, detail=f"Erreur lors de la sauvegarde: {str(e)}"
)
@router.get("/stats")
async def get_stats(
session: Session = Depends(get_session),
@@ -110,6 +186,8 @@ async def list_documents(
solde_montant=doc.solde_montant,
solde_type=doc.solde_type,
created_at=doc.created_at.isoformat() if doc.created_at else None,
has_pdf=doc.pdf_path is not None,
has_json=doc.json_path is not None,
)
for doc in documents
]
@@ -157,6 +235,8 @@ async def get_document(
},
"json_data": json.loads(document.json_data) if document.json_data else None,
"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,
}
@@ -189,3 +269,135 @@ async def check_duplicate(
"reference": reference,
"date": date,
}
@router.get("/documents/{document_id}/pdf")
async def download_document_pdf(
document_id: int,
session: Session = Depends(get_session),
) -> Response:
"""Telecharge le fichier PDF original d'un document.
Retourne le fichier PDF si disponible, sinon erreur 404.
"""
db_service = DatabaseService(session)
document = db_service.get_document_by_id(document_id)
if not document:
raise HTTPException(status_code=404, detail="Document non trouve")
if not document.pdf_path:
raise HTTPException(
status_code=404,
detail="Fichier PDF non disponible pour ce document",
)
try:
pdf_content = storage.read_pdf(document.pdf_path)
filename = document.source_file or f"{document.reference}.pdf"
return Response(
content=pdf_content,
media_type="application/pdf",
headers={"Content-Disposition": f'inline; filename="{filename}"'},
)
except FileNotFoundError:
raise HTTPException(
status_code=404,
detail="Fichier PDF introuvable sur le disque",
)
@router.get("/documents/{document_id}/json")
async def download_document_json(
document_id: int,
session: Session = Depends(get_session),
) -> Response:
"""Telecharge le fichier JSON extrait d'un document.
Retourne le fichier JSON si disponible sur disque,
sinon retourne le json_data de la base de donnees.
"""
db_service = DatabaseService(session)
document = db_service.get_document_by_id(document_id)
if not document:
raise HTTPException(status_code=404, detail="Document non trouve")
filename = f"{document.reference}_{document.date}.json"
# Try to read from storage first
if document.json_path:
try:
json_data = storage.read_json(document.json_path)
return JSONResponse(
content=json_data,
headers={"Content-Disposition": f'attachment; filename="{filename}"'},
)
except FileNotFoundError:
pass # Fall back to database
# Fall back to json_data in database
if document.json_data:
json_data = json.loads(document.json_data)
return JSONResponse(
content=json_data,
headers={"Content-Disposition": f'attachment; filename="{filename}"'},
)
raise HTTPException(
status_code=404,
detail="Donnees JSON non disponibles pour ce document",
)
@router.post("/documents/{document_id}/re-extract")
async 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.
Retourne les donnees re-extraites du PDF.
"""
db_service = DatabaseService(session)
document = db_service.get_document_by_id(document_id)
if not document:
raise HTTPException(status_code=404, detail="Document non trouve")
if not document.pdf_path:
raise HTTPException(
status_code=404,
detail="Fichier PDF non disponible pour ce document - re-extraction impossible",
)
# Get absolute path
pdf_full_path = storage.get_absolute_path(document.pdf_path)
if not pdf_full_path.exists():
raise HTTPException(
status_code=404,
detail="Fichier PDF introuvable sur le disque",
)
# Re-extract
try:
new_data = extract_compte_rendu(str(pdf_full_path))
except Exception as e:
raise HTTPException(
status_code=422,
detail=f"Erreur lors de la re-extraction: {str(e)}",
)
return {
"document_id": document_id,
"reference": document.reference,
"date": str(document.date),
"original_json_path": document.json_path,
"re_extracted_data": new_data,
"message": "Donnees re-extraites. Utilisez PUT /api/documents/{id} pour mettre a jour.",
}

View File

@@ -54,6 +54,8 @@ class DocumentSummary(BaseModel):
solde_montant: float | None
solde_type: str | None
created_at: str
has_pdf: bool = False
has_json: bool = False
# ============================================================