feat: add homepage
This commit is contained in:
21
src/plesna_gerance/database/__init__.py
Normal file
21
src/plesna_gerance/database/__init__.py
Normal file
@@ -0,0 +1,21 @@
|
||||
"""Database module for plesna-gerance."""
|
||||
|
||||
from .connection import get_engine, get_session, get_session_factory, init_db
|
||||
from .models import Base, Document, Immeuble, Lot, Locataire, Revenu, Depense
|
||||
from .service import DatabaseService, DuplicateDocumentError
|
||||
|
||||
__all__ = [
|
||||
"get_engine",
|
||||
"get_session",
|
||||
"get_session_factory",
|
||||
"init_db",
|
||||
"Base",
|
||||
"Document",
|
||||
"Immeuble",
|
||||
"Lot",
|
||||
"Locataire",
|
||||
"Revenu",
|
||||
"Depense",
|
||||
"DatabaseService",
|
||||
"DuplicateDocumentError",
|
||||
]
|
||||
118
src/plesna_gerance/database/connection.py
Normal file
118
src/plesna_gerance/database/connection.py
Normal file
@@ -0,0 +1,118 @@
|
||||
"""Database connection management for SQLite."""
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Generator
|
||||
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import Session, sessionmaker
|
||||
|
||||
from .models import Base
|
||||
|
||||
|
||||
# Default database path - relative to project root
|
||||
def _get_project_root() -> Path:
|
||||
"""Find project root by looking for pyproject.toml."""
|
||||
current = Path(__file__).resolve()
|
||||
for parent in current.parents:
|
||||
if (parent / "pyproject.toml").exists():
|
||||
return parent
|
||||
# Fallback to home directory if not found
|
||||
return Path.home() / ".plesna_gerance"
|
||||
|
||||
|
||||
DEFAULT_DB_PATH = _get_project_root() / "data" / "database.sqlite"
|
||||
|
||||
# Global engine instance
|
||||
_engine = None
|
||||
_SessionLocal = None
|
||||
|
||||
|
||||
def get_db_path() -> Path:
|
||||
"""Get database path from environment or default."""
|
||||
env_path = os.environ.get("PLESNA_DB_PATH")
|
||||
if env_path:
|
||||
return Path(env_path)
|
||||
return DEFAULT_DB_PATH
|
||||
|
||||
|
||||
def get_engine(db_path: Path | None = None):
|
||||
"""Get or create SQLAlchemy engine (singleton pattern)."""
|
||||
global _engine
|
||||
|
||||
if _engine is None:
|
||||
if db_path is None:
|
||||
db_path = get_db_path()
|
||||
|
||||
# Create parent directory if needed
|
||||
db_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Create engine with SQLite
|
||||
_engine = create_engine(
|
||||
f"sqlite:///{db_path}",
|
||||
echo=False, # Set to True for SQL debugging
|
||||
connect_args={"check_same_thread": False}, # Required for FastAPI
|
||||
)
|
||||
|
||||
return _engine
|
||||
|
||||
|
||||
def get_session_factory(engine=None):
|
||||
"""Get or create session factory."""
|
||||
global _SessionLocal
|
||||
|
||||
if _SessionLocal is None:
|
||||
if engine is None:
|
||||
engine = get_engine()
|
||||
_SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||
|
||||
return _SessionLocal
|
||||
|
||||
|
||||
def get_session() -> Generator[Session, None, None]:
|
||||
"""Dependency for FastAPI to get database session."""
|
||||
SessionLocal = get_session_factory()
|
||||
session = SessionLocal()
|
||||
try:
|
||||
yield session
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
|
||||
def init_db(db_path: Path | None = None) -> Path:
|
||||
"""Initialize database: create all tables.
|
||||
|
||||
Returns the path to the database file.
|
||||
"""
|
||||
if db_path is None:
|
||||
db_path = get_db_path()
|
||||
|
||||
# Reset globals to use new path
|
||||
global _engine, _SessionLocal
|
||||
_engine = None
|
||||
_SessionLocal = None
|
||||
|
||||
# Create parent directory
|
||||
db_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Create engine and tables
|
||||
engine = create_engine(
|
||||
f"sqlite:///{db_path}", echo=False, connect_args={"check_same_thread": False}
|
||||
)
|
||||
|
||||
Base.metadata.create_all(bind=engine)
|
||||
|
||||
# Update globals
|
||||
_engine = engine
|
||||
_SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||
|
||||
return db_path
|
||||
|
||||
|
||||
def reset_connection():
|
||||
"""Reset global connection (useful for testing)."""
|
||||
global _engine, _SessionLocal
|
||||
if _engine is not None:
|
||||
_engine.dispose()
|
||||
_engine = None
|
||||
_SessionLocal = None
|
||||
232
src/plesna_gerance/database/models.py
Normal file
232
src/plesna_gerance/database/models.py
Normal file
@@ -0,0 +1,232 @@
|
||||
"""SQLAlchemy models for plesna-gerance database."""
|
||||
|
||||
from datetime import date, datetime
|
||||
from typing import Optional
|
||||
|
||||
from sqlalchemy import (
|
||||
Column,
|
||||
Integer,
|
||||
String,
|
||||
Float,
|
||||
Date,
|
||||
DateTime,
|
||||
Text,
|
||||
ForeignKey,
|
||||
UniqueConstraint,
|
||||
Index,
|
||||
)
|
||||
from sqlalchemy.orm import DeclarativeBase, relationship
|
||||
|
||||
|
||||
class Base(DeclarativeBase):
|
||||
"""Base class for all models."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class Immeuble(Base):
|
||||
"""Table des immeubles gérés."""
|
||||
|
||||
__tablename__ = "immeubles"
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
code = Column(String(20), unique=True, nullable=False, index=True)
|
||||
adresse = Column(String(255), nullable=True)
|
||||
ville = Column(String(100), nullable=True)
|
||||
code_postal = Column(String(10), nullable=True)
|
||||
created_at = Column(DateTime, default=datetime.utcnow)
|
||||
|
||||
# Relations
|
||||
lots = relationship("Lot", back_populates="immeuble", cascade="all, delete-orphan")
|
||||
documents = relationship("Document", back_populates="immeuble")
|
||||
depenses = relationship("Depense", back_populates="immeuble")
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<Immeuble(code={self.code}, adresse={self.adresse})>"
|
||||
|
||||
|
||||
class Lot(Base):
|
||||
"""Table des lots (appartements, locaux commerciaux, etc.)."""
|
||||
|
||||
__tablename__ = "lots"
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
immeuble_id = Column(Integer, ForeignKey("immeubles.id"), nullable=False)
|
||||
numero = Column(String(10), nullable=False)
|
||||
type = Column(String(50), nullable=True) # "Loc. Commercial", "Appartement", etc.
|
||||
created_at = Column(DateTime, default=datetime.utcnow)
|
||||
|
||||
# Contrainte unique: un numéro de lot par immeuble
|
||||
__table_args__ = (
|
||||
UniqueConstraint("immeuble_id", "numero", name="uq_lot_immeuble_numero"),
|
||||
Index("ix_lot_immeuble_numero", "immeuble_id", "numero"),
|
||||
)
|
||||
|
||||
# Relations
|
||||
immeuble = relationship("Immeuble", back_populates="lots")
|
||||
locataires = relationship(
|
||||
"Locataire", back_populates="lot", cascade="all, delete-orphan"
|
||||
)
|
||||
revenus = relationship("Revenu", back_populates="lot")
|
||||
depenses = relationship("Depense", back_populates="lot")
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<Lot(numero={self.numero}, type={self.type})>"
|
||||
|
||||
|
||||
class Locataire(Base):
|
||||
"""Table des locataires avec historique."""
|
||||
|
||||
__tablename__ = "locataires"
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
lot_id = Column(Integer, ForeignKey("lots.id"), nullable=False)
|
||||
nom = Column(String(255), nullable=False)
|
||||
date_debut = Column(Date, nullable=True) # Date d'entrée dans le lot
|
||||
date_fin = Column(Date, nullable=True) # Date de sortie (NULL si actif)
|
||||
created_at = Column(DateTime, default=datetime.utcnow)
|
||||
|
||||
# Contrainte unique: un locataire par lot et période
|
||||
__table_args__ = (
|
||||
UniqueConstraint(
|
||||
"lot_id", "nom", "date_debut", name="uq_locataire_lot_nom_debut"
|
||||
),
|
||||
Index("ix_locataire_nom", "nom"),
|
||||
)
|
||||
|
||||
# Relations
|
||||
lot = relationship("Lot", back_populates="locataires")
|
||||
revenus = relationship("Revenu", back_populates="locataire")
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<Locataire(nom={self.nom})>"
|
||||
|
||||
|
||||
class Document(Base):
|
||||
"""Table des documents PDF importés avec traçabilité."""
|
||||
|
||||
__tablename__ = "documents"
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
reference = Column(String(50), nullable=False, index=True)
|
||||
date = Column(Date, nullable=False, index=True)
|
||||
type = Column(String(100), nullable=True) # "COMPTE RENDU DE GESTION"
|
||||
source_file = Column(String(255), nullable=True) # Nom du fichier PDF original
|
||||
immeuble_id = Column(Integer, ForeignKey("immeubles.id"), nullable=False)
|
||||
|
||||
# JSON brut pour traçabilité complète
|
||||
json_data = Column(Text, nullable=False)
|
||||
|
||||
# Métadonnées éditeur
|
||||
editeur_nom = Column(String(255), nullable=True)
|
||||
editeur_siret = Column(String(20), nullable=True)
|
||||
|
||||
# Solde à la date du document
|
||||
solde_montant = Column(Float, nullable=True)
|
||||
solde_type = Column(String(20), nullable=True) # "crediteur" ou "debiteur"
|
||||
solde_date_arrete = Column(Date, nullable=True)
|
||||
|
||||
created_at = Column(DateTime, default=datetime.utcnow)
|
||||
|
||||
# Clé unique: référence + date (évite les doublons)
|
||||
__table_args__ = (
|
||||
UniqueConstraint("reference", "date", name="uq_document_reference_date"),
|
||||
)
|
||||
|
||||
# Relations
|
||||
immeuble = relationship("Immeuble", back_populates="documents")
|
||||
revenus = relationship(
|
||||
"Revenu", back_populates="document", cascade="all, delete-orphan"
|
||||
)
|
||||
depenses = relationship(
|
||||
"Depense", back_populates="document", cascade="all, delete-orphan"
|
||||
)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<Document(reference={self.reference}, date={self.date})>"
|
||||
|
||||
|
||||
class Revenu(Base):
|
||||
"""Table des revenus locatifs (lignes détaillées)."""
|
||||
|
||||
__tablename__ = "revenus"
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
document_id = Column(Integer, ForeignKey("documents.id"), nullable=False)
|
||||
lot_id = Column(Integer, ForeignKey("lots.id"), nullable=False)
|
||||
locataire_id = Column(Integer, ForeignKey("locataires.id"), nullable=False)
|
||||
|
||||
# Type de ligne: loyer, solde_anterieur, rappel_loyer, divers
|
||||
type_ligne = Column(String(50), nullable=False)
|
||||
|
||||
# Période concernée
|
||||
periode_debut = Column(Date, nullable=True)
|
||||
periode_fin = Column(Date, nullable=True)
|
||||
|
||||
# Montants détaillés
|
||||
loyers = Column(Float, default=0.0)
|
||||
taxes = Column(Float, default=0.0)
|
||||
provisions = Column(Float, default=0.0)
|
||||
divers_montant = Column(Float, default=0.0)
|
||||
divers_libelle = Column(String(255), nullable=True)
|
||||
total = Column(Float, default=0.0)
|
||||
regles = Column(Float, default=0.0) # Montant réglé
|
||||
impayes = Column(Float, default=0.0)
|
||||
|
||||
created_at = Column(DateTime, default=datetime.utcnow)
|
||||
|
||||
__table_args__ = (
|
||||
Index("ix_revenu_document", "document_id"),
|
||||
Index("ix_revenu_lot", "lot_id"),
|
||||
Index("ix_revenu_periode", "periode_debut", "periode_fin"),
|
||||
)
|
||||
|
||||
# Relations
|
||||
document = relationship("Document", back_populates="revenus")
|
||||
lot = relationship("Lot", back_populates="revenus")
|
||||
locataire = relationship("Locataire", back_populates="revenus")
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<Revenu(type={self.type_ligne}, total={self.total})>"
|
||||
|
||||
|
||||
class Depense(Base):
|
||||
"""Table des dépenses/opérations."""
|
||||
|
||||
__tablename__ = "depenses"
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
document_id = Column(Integer, ForeignKey("documents.id"), nullable=False)
|
||||
immeuble_id = Column(Integer, ForeignKey("immeubles.id"), nullable=False)
|
||||
lot_id = Column(
|
||||
Integer, ForeignKey("lots.id"), nullable=True
|
||||
) # NULL si dépense immeuble
|
||||
|
||||
# Catégorisation
|
||||
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)
|
||||
description = Column(String(500), nullable=True)
|
||||
|
||||
# Montants
|
||||
debit = Column(Float, default=0.0)
|
||||
credit = Column(Float, default=0.0)
|
||||
tva = Column(Float, default=0.0)
|
||||
locatif = Column(Float, default=0.0) # Part locative
|
||||
deductible = Column(Float, default=0.0) # Part déductible
|
||||
|
||||
created_at = Column(DateTime, default=datetime.utcnow)
|
||||
|
||||
__table_args__ = (
|
||||
Index("ix_depense_document", "document_id"),
|
||||
Index("ix_depense_immeuble", "immeuble_id"),
|
||||
Index("ix_depense_categorie", "categorie"),
|
||||
)
|
||||
|
||||
# Relations
|
||||
document = relationship("Document", back_populates="depenses")
|
||||
immeuble = relationship("Immeuble", back_populates="depenses")
|
||||
lot = relationship("Lot", back_populates="depenses")
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<Depense(categorie={self.categorie}, debit={self.debit})>"
|
||||
314
src/plesna_gerance/database/service.py
Normal file
314
src/plesna_gerance/database/service.py
Normal file
@@ -0,0 +1,314 @@
|
||||
"""Database service for saving and querying extracted data."""
|
||||
|
||||
import json
|
||||
from datetime import date, datetime
|
||||
from typing import Any
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
|
||||
from .models import Document, Immeuble, Lot, Locataire, Revenu, Depense
|
||||
|
||||
|
||||
class DuplicateDocumentError(Exception):
|
||||
"""Raised when attempting to save a document that already exists."""
|
||||
|
||||
def __init__(self, reference: str, doc_date: date):
|
||||
self.reference = reference
|
||||
self.date = doc_date
|
||||
super().__init__(
|
||||
f"Document avec reference={reference} et date={doc_date} existe deja"
|
||||
)
|
||||
|
||||
|
||||
class DatabaseService:
|
||||
"""Service for database operations."""
|
||||
|
||||
def __init__(self, session: Session):
|
||||
self.session = session
|
||||
|
||||
def check_duplicate(self, reference: str, doc_date: date) -> Document | None:
|
||||
"""Check if a document with this reference and date already exists."""
|
||||
stmt = select(Document).where(
|
||||
Document.reference == reference, Document.date == doc_date
|
||||
)
|
||||
return self.session.execute(stmt).scalar_one_or_none()
|
||||
|
||||
def get_or_create_immeuble(
|
||||
self, code: str, adresse: str = None, ville: str = None, code_postal: str = None
|
||||
) -> Immeuble:
|
||||
"""Get existing immeuble or create new one."""
|
||||
stmt = select(Immeuble).where(Immeuble.code == code)
|
||||
immeuble = self.session.execute(stmt).scalar_one_or_none()
|
||||
|
||||
if immeuble is None:
|
||||
immeuble = Immeuble(
|
||||
code=code, adresse=adresse, ville=ville, code_postal=code_postal
|
||||
)
|
||||
self.session.add(immeuble)
|
||||
self.session.flush() # Get the ID
|
||||
|
||||
return immeuble
|
||||
|
||||
def get_or_create_lot(
|
||||
self, immeuble_id: int, numero: str, lot_type: str = None
|
||||
) -> Lot:
|
||||
"""Get existing lot or create new one."""
|
||||
stmt = select(Lot).where(Lot.immeuble_id == immeuble_id, Lot.numero == numero)
|
||||
lot = self.session.execute(stmt).scalar_one_or_none()
|
||||
|
||||
if lot is None:
|
||||
lot = Lot(immeuble_id=immeuble_id, numero=numero, type=lot_type)
|
||||
self.session.add(lot)
|
||||
self.session.flush()
|
||||
|
||||
return lot
|
||||
|
||||
def get_or_create_locataire(
|
||||
self, lot_id: int, nom: str, date_debut: date = None
|
||||
) -> Locataire:
|
||||
"""Get existing locataire or create new one."""
|
||||
stmt = select(Locataire).where(Locataire.lot_id == lot_id, Locataire.nom == nom)
|
||||
# If date_debut is provided, include it in the search
|
||||
if date_debut:
|
||||
stmt = stmt.where(Locataire.date_debut == date_debut)
|
||||
else:
|
||||
stmt = stmt.where(Locataire.date_debut.is_(None))
|
||||
|
||||
locataire = self.session.execute(stmt).scalar_one_or_none()
|
||||
|
||||
if locataire is None:
|
||||
locataire = Locataire(lot_id=lot_id, nom=nom, date_debut=date_debut)
|
||||
self.session.add(locataire)
|
||||
self.session.flush()
|
||||
|
||||
return locataire
|
||||
|
||||
def _parse_date(self, date_str: str | None) -> date | None:
|
||||
"""Parse ISO date string to date object."""
|
||||
if not date_str:
|
||||
return None
|
||||
try:
|
||||
return datetime.strptime(date_str, "%Y-%m-%d").date()
|
||||
except (ValueError, TypeError):
|
||||
return None
|
||||
|
||||
def save_document(self, data: dict[str, Any], source_file: str = None) -> 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
|
||||
|
||||
Returns:
|
||||
The created Document instance
|
||||
|
||||
Raises:
|
||||
DuplicateDocumentError: If document already exists
|
||||
"""
|
||||
metadata = data.get("metadata", {})
|
||||
doc_info = metadata.get("document", {})
|
||||
immeuble_info = metadata.get("immeuble", {})
|
||||
editeur_info = metadata.get("editeur", {})
|
||||
solde_info = metadata.get("solde", {})
|
||||
|
||||
# Extract key fields
|
||||
reference = doc_info.get("reference", "")
|
||||
doc_date = self._parse_date(doc_info.get("date"))
|
||||
|
||||
if not reference or not doc_date:
|
||||
raise ValueError("Document must have reference and date")
|
||||
|
||||
# Check for duplicates
|
||||
existing = self.check_duplicate(reference, doc_date)
|
||||
if existing:
|
||||
raise DuplicateDocumentError(reference, doc_date)
|
||||
|
||||
# Get or create immeuble
|
||||
immeuble = self.get_or_create_immeuble(
|
||||
code=immeuble_info.get("code", "UNKNOWN"),
|
||||
adresse=immeuble_info.get("adresse"),
|
||||
ville=immeuble_info.get("ville"),
|
||||
code_postal=immeuble_info.get("code_postal"),
|
||||
)
|
||||
|
||||
# Create document
|
||||
document = Document(
|
||||
reference=reference,
|
||||
date=doc_date,
|
||||
type=doc_info.get("type"),
|
||||
source_file=source_file,
|
||||
immeuble_id=immeuble.id,
|
||||
json_data=json.dumps(data, ensure_ascii=False, default=str),
|
||||
editeur_nom=editeur_info.get("nom"),
|
||||
editeur_siret=editeur_info.get("siret"),
|
||||
solde_montant=solde_info.get("montant"),
|
||||
solde_type=solde_info.get("type"),
|
||||
solde_date_arrete=self._parse_date(solde_info.get("date_arrete")),
|
||||
)
|
||||
self.session.add(document)
|
||||
self.session.flush()
|
||||
|
||||
# Process situation_locataires (revenus)
|
||||
for situation in data.get("situation_locataires", []):
|
||||
self._save_situation_locataire(document, immeuble, situation)
|
||||
|
||||
# Process recapitulatif_operations (depenses)
|
||||
for operation in data.get("recapitulatif_operations", []):
|
||||
self._save_operation(document, immeuble, operation)
|
||||
|
||||
self.session.commit()
|
||||
return document
|
||||
|
||||
def _save_situation_locataire(
|
||||
self, document: Document, immeuble: Immeuble, situation: dict
|
||||
) -> None:
|
||||
"""Save locataire situation (revenus lines)."""
|
||||
lot_info = situation.get("lot", {})
|
||||
locataire_info = situation.get("locataire", {})
|
||||
|
||||
# Get or create lot
|
||||
lot = self.get_or_create_lot(
|
||||
immeuble_id=immeuble.id,
|
||||
numero=lot_info.get("numero", "0000"),
|
||||
lot_type=lot_info.get("type"),
|
||||
)
|
||||
|
||||
# Get or create locataire
|
||||
locataire = self.get_or_create_locataire(
|
||||
lot_id=lot.id, nom=locataire_info.get("nom", "INCONNU")
|
||||
)
|
||||
|
||||
# Save each revenue line
|
||||
for ligne in situation.get("lignes", []):
|
||||
periode = ligne.get("periode", {})
|
||||
divers = ligne.get("divers", {})
|
||||
|
||||
revenu = Revenu(
|
||||
document_id=document.id,
|
||||
lot_id=lot.id,
|
||||
locataire_id=locataire.id,
|
||||
type_ligne=ligne.get("type", "loyer"),
|
||||
periode_debut=self._parse_date(periode.get("debut")),
|
||||
periode_fin=self._parse_date(periode.get("fin")),
|
||||
loyers=ligne.get("loyers", 0.0) or 0.0,
|
||||
taxes=ligne.get("taxes", 0.0) or 0.0,
|
||||
provisions=ligne.get("provisions", 0.0) or 0.0,
|
||||
divers_montant=divers.get("montant", 0.0) or 0.0 if divers else 0.0,
|
||||
divers_libelle=divers.get("libelle") if divers else None,
|
||||
total=ligne.get("total", 0.0) or 0.0,
|
||||
regles=ligne.get("regles", 0.0) or 0.0,
|
||||
impayes=ligne.get("impayes", 0.0) or 0.0,
|
||||
)
|
||||
self.session.add(revenu)
|
||||
|
||||
def _save_operation(
|
||||
self, document: Document, immeuble: Immeuble, operation: dict
|
||||
) -> None:
|
||||
"""Save operation (depense)."""
|
||||
montants = operation.get("montants", {})
|
||||
|
||||
# Check if operation is linked to a specific lot
|
||||
lot_id = None
|
||||
lot_numero = operation.get("lot_numero")
|
||||
if lot_numero:
|
||||
lot = self.get_or_create_lot(immeuble_id=immeuble.id, numero=lot_numero)
|
||||
lot_id = lot.id
|
||||
|
||||
depense = Depense(
|
||||
document_id=document.id,
|
||||
immeuble_id=immeuble.id,
|
||||
lot_id=lot_id, # Can be NULL for immeuble-level expenses
|
||||
categorie=operation.get("categorie"),
|
||||
sous_categorie=operation.get("sous_categorie"),
|
||||
fournisseur=operation.get("fournisseur"),
|
||||
description=operation.get("description"),
|
||||
debit=montants.get("debit", 0.0) or 0.0,
|
||||
credit=montants.get("credit", 0.0) or 0.0,
|
||||
tva=montants.get("tva", 0.0) or 0.0,
|
||||
locatif=montants.get("locatif", 0.0) or 0.0,
|
||||
deductible=montants.get("deductible", 0.0) or 0.0,
|
||||
)
|
||||
self.session.add(depense)
|
||||
|
||||
def list_documents(self, limit: int = 100, offset: int = 0) -> list[Document]:
|
||||
"""List all imported documents."""
|
||||
stmt = (
|
||||
select(Document)
|
||||
.order_by(Document.date.desc(), Document.created_at.desc())
|
||||
.limit(limit)
|
||||
.offset(offset)
|
||||
)
|
||||
result = self.session.execute(stmt)
|
||||
return list(result.scalars().all())
|
||||
|
||||
def get_document_by_id(self, doc_id: int) -> Document | None:
|
||||
"""Get a document by ID."""
|
||||
return self.session.get(Document, doc_id)
|
||||
|
||||
def get_revenus_summary(
|
||||
self, immeuble_id: int = None, year: int = None
|
||||
) -> list[dict]:
|
||||
"""Get revenue summary grouped by period."""
|
||||
stmt = select(Revenu)
|
||||
|
||||
if immeuble_id:
|
||||
stmt = stmt.join(Lot).where(Lot.immeuble_id == immeuble_id)
|
||||
|
||||
if year:
|
||||
stmt = stmt.where(
|
||||
Revenu.periode_debut >= date(year, 1, 1),
|
||||
Revenu.periode_debut <= date(year, 12, 31),
|
||||
)
|
||||
|
||||
result = self.session.execute(stmt)
|
||||
revenus = result.scalars().all()
|
||||
|
||||
# Aggregate
|
||||
total_loyers = sum(r.loyers for r in revenus)
|
||||
total_regles = sum(r.regles for r in revenus)
|
||||
total_impayes = sum(r.impayes for r in revenus)
|
||||
|
||||
return {
|
||||
"total_loyers": total_loyers,
|
||||
"total_regles": total_regles,
|
||||
"total_impayes": total_impayes,
|
||||
"count": len(revenus),
|
||||
}
|
||||
|
||||
def get_depenses_summary(self, immeuble_id: int = None, year: int = None) -> dict:
|
||||
"""Get expenses summary grouped by category."""
|
||||
stmt = select(Depense)
|
||||
|
||||
if immeuble_id:
|
||||
stmt = stmt.where(Depense.immeuble_id == immeuble_id)
|
||||
|
||||
if year:
|
||||
stmt = stmt.join(Document).where(
|
||||
Document.date >= date(year, 1, 1), Document.date <= date(year, 12, 31)
|
||||
)
|
||||
|
||||
result = self.session.execute(stmt)
|
||||
depenses = result.scalars().all()
|
||||
|
||||
# Aggregate by category
|
||||
by_category = {}
|
||||
for d in depenses:
|
||||
cat = d.categorie or "AUTRE"
|
||||
if cat not in by_category:
|
||||
by_category[cat] = {"debit": 0.0, "credit": 0.0, "count": 0}
|
||||
by_category[cat]["debit"] += d.debit
|
||||
by_category[cat]["credit"] += d.credit
|
||||
by_category[cat]["count"] += 1
|
||||
|
||||
total_debit = sum(d.debit for d in depenses)
|
||||
total_credit = sum(d.credit for d in depenses)
|
||||
|
||||
return {
|
||||
"by_category": by_category,
|
||||
"total_debit": total_debit,
|
||||
"total_credit": total_credit,
|
||||
"count": len(depenses),
|
||||
}
|
||||
Reference in New Issue
Block a user