feat: add depense tagging

This commit is contained in:
2026-01-19 05:19:11 +01:00
parent c5e51a7513
commit 0fd6bdaeb3
8 changed files with 711 additions and 43 deletions

View File

@@ -24,6 +24,22 @@ class Base(DeclarativeBase):
pass
class Tag(Base):
"""Table des tags pour catégoriser les dépenses."""
__tablename__ = "tags"
id = Column(Integer, primary_key=True, autoincrement=True)
nom = Column(String(100), unique=True, nullable=False, index=True)
created_at = Column(DateTime, default=datetime.utcnow)
# Relations
depenses = relationship("Depense", back_populates="tag")
def __repr__(self) -> str:
return f"<Tag(nom={self.nom})>"
class Immeuble(Base):
"""Table des immeubles gérés."""
@@ -201,8 +217,11 @@ class Depense(Base):
lot_id = Column(
Integer, ForeignKey("lots.id"), nullable=True
) # NULL si dépense immeuble
tag_id = Column(
Integer, ForeignKey("tags.id"), nullable=True
) # Tag pour catégorisation manuelle
# Catégorisation
# Catégorisation (ancienne, conservée pour historique)
categorie = Column(String(100), nullable=True) # DEPENSES_LOCATIVES, etc.
sous_categorie = Column(String(255), nullable=True) # Nettoyage immeuble, etc.
fournisseur = Column(String(255), nullable=True)
@@ -221,12 +240,14 @@ class Depense(Base):
Index("ix_depense_document", "document_id"),
Index("ix_depense_immeuble", "immeuble_id"),
Index("ix_depense_categorie", "categorie"),
Index("ix_depense_tag", "tag_id"),
)
# Relations
document = relationship("Document", back_populates="depenses")
immeuble = relationship("Immeuble", back_populates="depenses")
lot = relationship("Lot", back_populates="depenses")
tag = relationship("Tag", back_populates="depenses")
def __repr__(self) -> str:
return f"<Depense(categorie={self.categorie}, debit={self.debit})>"

View File

@@ -8,7 +8,7 @@ from sqlalchemy import select
from sqlalchemy.orm import Session
from sqlalchemy.exc import IntegrityError
from .models import Document, Immeuble, Lot, Locataire, Revenu, Depense
from .models import Document, Immeuble, Lot, Locataire, Revenu, Depense, Tag
class DuplicateDocumentError(Exception):
@@ -94,19 +94,27 @@ class DatabaseService:
except (ValueError, TypeError):
return None
def save_document(self, data: dict[str, Any], source_file: str = None) -> Document:
def save_document(
self,
data: dict[str, Any],
source_file: str = None,
depenses_tags: list[dict] = None,
overwrite: bool = False,
) -> Document:
"""Save extracted JSON data to database.
Args:
data: The 'data' portion of the extracted JSON (contains metadata,
situation_locataires, recapitulatif_operations)
source_file: Original PDF filename
depenses_tags: List of tags for expenses
overwrite: If True, delete existing document and recreate it
Returns:
The created Document instance
Raises:
DuplicateDocumentError: If document already exists
DuplicateDocumentError: If document already exists and overwrite=False
"""
metadata = data.get("metadata", {})
doc_info = metadata.get("document", {})
@@ -124,7 +132,12 @@ class DatabaseService:
# Check for duplicates
existing = self.check_duplicate(reference, doc_date)
if existing:
raise DuplicateDocumentError(reference, doc_date)
if overwrite:
# Delete existing document (cascade will delete related data)
self.session.delete(existing)
self.session.flush()
else:
raise DuplicateDocumentError(reference, doc_date)
# Get or create immeuble
immeuble = self.get_or_create_immeuble(
@@ -156,8 +169,15 @@ class DatabaseService:
self._save_situation_locataire(document, immeuble, situation)
# Process recapitulatif_operations (depenses)
for operation in data.get("recapitulatif_operations", []):
self._save_operation(document, immeuble, operation)
# Créer un mapping index -> tag_id si des tags sont fournis
tag_mapping = {}
if depenses_tags:
for item in depenses_tags:
tag_mapping[item.get("index")] = item.get("tag_id")
for idx, operation in enumerate(data.get("recapitulatif_operations", [])):
tag_id = tag_mapping.get(idx)
self._save_operation(document, immeuble, operation, tag_id=tag_id)
self.session.commit()
return document
@@ -205,7 +225,11 @@ class DatabaseService:
self.session.add(revenu)
def _save_operation(
self, document: Document, immeuble: Immeuble, operation: dict
self,
document: Document,
immeuble: Immeuble,
operation: dict,
tag_id: int = None,
) -> None:
"""Save operation (depense)."""
montants = operation.get("montants", {})
@@ -221,6 +245,7 @@ class DatabaseService:
document_id=document.id,
immeuble_id=immeuble.id,
lot_id=lot_id, # Can be NULL for immeuble-level expenses
tag_id=tag_id, # Tag assigné manuellement
categorie=operation.get("categorie"),
sous_categorie=operation.get("sous_categorie"),
fournisseur=operation.get("fournisseur"),
@@ -312,3 +337,13 @@ class DatabaseService:
"total_credit": total_credit,
"count": len(depenses),
}
def list_tags(self) -> list[Tag]:
"""List all available tags."""
stmt = select(Tag).order_by(Tag.nom)
result = self.session.execute(stmt)
return list(result.scalars().all())
def get_tag_by_id(self, tag_id: int) -> Tag | None:
"""Get a tag by ID."""
return self.session.get(Tag, tag_id)