feat: affiche la date de derniere extraction et revient aux documents

La liste des documents montrait la date d'import, figee au premier
enregistrement : c'est la date de la derniere extraction ayant produit les
donnees affichees qui renseigne, surtout apres un balayage de re-extraction. La
colonne `extracted_at` la porte, mise a jour a chaque enregistrement, et la date
d'import reste consultable en infobulle.

Le projet n'ayant pas d'outil de migration (`create_all` laisse intactes les
tables existantes), `init_db` rattrape les colonnes ajoutees apres coup : sans
cela une base deja installee cesserait de fonctionner. Les documents deja
presents recoivent leur date d'import, qui est bien celle de leur extraction.

Une fois la selection appliquee sans echec, le balayage renvoie vers la liste des
documents ; en cas d'echec on reste sur place, les lignes concernees portant leur
message.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-26 06:52:49 +02:00
parent a1da4fc97f
commit 92cf21309f
8 changed files with 129 additions and 3 deletions

View File

@@ -195,6 +195,7 @@ 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,
extracted_at=doc.extracted_at.isoformat() if doc.extracted_at else None,
has_pdf=doc.pdf_path is not None,
has_json=doc.json_path is not None,
)
@@ -245,6 +246,9 @@ async def get_document(
"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,
"extracted_at": document.extracted_at.isoformat()
if document.extracted_at
else None,
"has_pdf": document.pdf_path is not None,
"has_json": document.json_path is not None,
}

View File

@@ -53,6 +53,9 @@ class DocumentSummary(BaseModel):
solde_montant: float | None
solde_type: str | None
created_at: str
#: Derniere extraction ayant produit les donnees (absente sur une base
#: anterieure a ce champ : l'appelant retombe alors sur `created_at`).
extracted_at: str | None = None
has_pdf: bool = False
has_json: bool = False

View File

@@ -87,6 +87,7 @@ def init_db(db_path: Path | None = None) -> Path:
# Build the engine (reuses the shared configuration) and create tables
engine = get_engine(db_path)
Base.metadata.create_all(bind=engine)
_apply_schema_updates(engine)
# Seed predefined tags if the table is empty
_seed_tags_if_empty(engine)
@@ -94,6 +95,34 @@ def init_db(db_path: Path | None = None) -> Path:
return db_path
#: Colonnes ajoutees apres coup, par table : nom -> (definition SQL, valeur de
#: rattrapage pour les lignes existantes). `create_all` ne modifie pas une table
#: deja presente, et le projet n'utilise pas d'outil de migration : sans ce
#: rattrapage, une base installee cesserait de fonctionner apres mise a jour.
_ADDED_COLUMNS = {
"documents": {
# Les documents deja en base ont ete extraits lors de leur import.
"extracted_at": ("DATETIME", "created_at"),
},
}
def _apply_schema_updates(engine):
"""Ajoute a une base existante les colonnes apparues depuis sa creation."""
with engine.begin() as conn:
for table, columns in _ADDED_COLUMNS.items():
existing = {
row[1] for row in conn.execute(text(f"PRAGMA table_info({table})"))
}
if not existing: # table absente : create_all vient de la creer
continue
for name, (definition, backfill) in columns.items():
if name in existing:
continue
conn.execute(text(f"ALTER TABLE {table} ADD COLUMN {name} {definition}"))
conn.execute(text(f"UPDATE {table} SET {name} = {backfill}"))
def _seed_tags_if_empty(engine):
"""Insert predefined tags if the tags table is empty."""
from ..scripts.seed_tags import PREDEFINED_TAGS

View File

@@ -161,6 +161,10 @@ class Document(Base):
solde_date_arrete = Column(Date, nullable=True)
created_at = Column(DateTime, default=_utcnow)
#: Date de la derniere extraction ayant produit les donnees stockees. Une
#: re-extraction validee la met a jour, contrairement a `created_at` qui
#: reste la date du premier import.
extracted_at = Column(DateTime, default=_utcnow)
# Chemins vers les fichiers stockés (relatifs à PLESNA_STORAGE_PATH)
pdf_path = Column(

View File

@@ -1,7 +1,7 @@
"""Database service for saving and querying extracted data."""
import json
from datetime import date, datetime
from datetime import date, datetime, timezone
from typing import Any
from sqlalchemy import select
@@ -249,6 +249,8 @@ class DatabaseService:
"solde_date_arrete": self._parse_date(solde_info.get("date_arrete")),
"pdf_path": pdf_path,
"json_path": json_path,
# Ces donnees viennent de l'extraction qu'on est en train d'enregistrer.
"extracted_at": datetime.now(timezone.utc),
}
if reused is not None:
document = reused