feat: filtre les dépenses sur plusieurs fournisseurs et sur l'absence de tag
Le filtre fournisseur était une recherche de sous-chaîne : impossible de comparer deux fournisseurs, et « PPR » ramenait ses homonymes au passage. Il devient une sélection multiple exacte, prise dans un menu déroulant filtrable au clavier (SelectionMultiple, générique et réutilisable). La liste vient de /api/fournisseurs, remise en ordre alphabétique — l'API la trie par montant, ce qui se lit bien dans un classement mais rend introuvable un fournisseur qu'on cherche à l'œil. Côté API, `fournisseur` devient répétable et s'entend comme un OU exact. Le filtre tag, lui, ne savait pas demander « ce qui n'est pas encore tagué » — c'est pourtant la question qui amorce le travail de tagging. `tag_id=0` le demande, et l'option « Sans tag » l'ouvre depuis la page. Au passage, les deux endpoints dupliquaient leurs filtres : ils partagent désormais _appliquer_filtres, pour que les totaux du résumé ne puissent plus porter sur d'autres lignes que la table. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -25,6 +25,47 @@ from ..schemas.models import (
|
||||
|
||||
router = APIRouter(prefix="/api", tags=["analytics"])
|
||||
|
||||
#: Valeur de `tag_id` demandant les depenses sans tag. C'est la meme clef que
|
||||
#: celle employee pour agreger les non taggues dans `by_tag` : « aucun tag » est
|
||||
#: un choix de filtre a part entiere, pas l'absence de filtre (`tag_id` omis).
|
||||
SANS_TAG = 0
|
||||
|
||||
|
||||
def _appliquer_filtres(
|
||||
stmt,
|
||||
*,
|
||||
immeuble_id: int | None,
|
||||
lot_id: int | None,
|
||||
tag_id: int | None,
|
||||
categorie: str | None,
|
||||
fournisseurs: list[str] | None,
|
||||
date_debut: date | None,
|
||||
date_fin: date | None,
|
||||
):
|
||||
"""Applique les filtres communs a la table et au resume.
|
||||
|
||||
Les deux endpoints doivent voir exactement le meme perimetre : les totaux du
|
||||
resume ne veulent rien dire s'ils portent sur d'autres lignes que la table.
|
||||
"""
|
||||
if immeuble_id is not None:
|
||||
stmt = stmt.where(Depense.immeuble_id == immeuble_id)
|
||||
if lot_id is not None:
|
||||
stmt = stmt.where(Depense.lot_id == lot_id)
|
||||
if tag_id is not None:
|
||||
if tag_id == SANS_TAG:
|
||||
stmt = stmt.where(Depense.tag_id.is_(None))
|
||||
else:
|
||||
stmt = stmt.where(Depense.tag_id == tag_id)
|
||||
if categorie is not None:
|
||||
stmt = stmt.where(Depense.categorie == categorie)
|
||||
if fournisseurs:
|
||||
stmt = stmt.where(Depense.fournisseur.in_(fournisseurs))
|
||||
if date_debut is not None:
|
||||
stmt = stmt.where(Document.date >= date_debut)
|
||||
if date_fin is not None:
|
||||
stmt = stmt.where(Document.date <= date_fin)
|
||||
return stmt
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Reference data endpoints (for filters)
|
||||
@@ -180,9 +221,11 @@ async def list_tags_with_stats(
|
||||
async def get_depenses(
|
||||
immeuble_id: int | None = Query(None, description="Filtrer par immeuble"),
|
||||
lot_id: int | None = Query(None, description="Filtrer par lot"),
|
||||
tag_id: int | None = Query(None, description="Filtrer par tag"),
|
||||
tag_id: int | None = Query(None, description="Filtrer par tag (0 = sans tag)"),
|
||||
categorie: str | None = Query(None, description="Filtrer par categorie"),
|
||||
fournisseur: str | None = Query(None, description="Filtrer par fournisseur"),
|
||||
fournisseur: list[str] | None = Query(
|
||||
None, description="Fournisseurs retenus (parametre repetable)"
|
||||
),
|
||||
date_debut: date | None = Query(None, description="Date de debut (YYYY-MM-DD)"),
|
||||
date_fin: date | None = Query(None, description="Date de fin (YYYY-MM-DD)"),
|
||||
limit: int = Query(500, description="Nombre maximum de resultats"),
|
||||
@@ -194,9 +237,10 @@ async def get_depenses(
|
||||
Filtres disponibles:
|
||||
- **immeuble_id**: ID de l'immeuble
|
||||
- **lot_id**: ID du lot
|
||||
- **tag_id**: ID du tag
|
||||
- **tag_id**: ID du tag, ou 0 pour les depenses sans tag
|
||||
- **categorie**: Categorie de depense
|
||||
- **fournisseur**: Nom du fournisseur
|
||||
- **fournisseur**: nom exact d'un fournisseur, repetable pour en retenir
|
||||
plusieurs (`?fournisseur=A&fournisseur=B`)
|
||||
- **date_debut**: Date de debut (incluse)
|
||||
- **date_fin**: Date de fin (incluse)
|
||||
- **limit**: Nombre max de resultats (defaut: 500)
|
||||
@@ -219,21 +263,16 @@ async def get_depenses(
|
||||
.order_by(Document.date.desc(), Depense.id.desc())
|
||||
)
|
||||
|
||||
# Apply filters
|
||||
if immeuble_id is not None:
|
||||
stmt = stmt.where(Depense.immeuble_id == immeuble_id)
|
||||
if lot_id is not None:
|
||||
stmt = stmt.where(Depense.lot_id == lot_id)
|
||||
if tag_id is not None:
|
||||
stmt = stmt.where(Depense.tag_id == tag_id)
|
||||
if categorie is not None:
|
||||
stmt = stmt.where(Depense.categorie == categorie)
|
||||
if fournisseur is not None:
|
||||
stmt = stmt.where(Depense.fournisseur.ilike(f"%{fournisseur}%"))
|
||||
if date_debut is not None:
|
||||
stmt = stmt.where(Document.date >= date_debut)
|
||||
if date_fin is not None:
|
||||
stmt = stmt.where(Document.date <= date_fin)
|
||||
stmt = _appliquer_filtres(
|
||||
stmt,
|
||||
immeuble_id=immeuble_id,
|
||||
lot_id=lot_id,
|
||||
tag_id=tag_id,
|
||||
categorie=categorie,
|
||||
fournisseurs=fournisseur,
|
||||
date_debut=date_debut,
|
||||
date_fin=date_fin,
|
||||
)
|
||||
|
||||
stmt = stmt.limit(limit).offset(offset)
|
||||
|
||||
@@ -271,9 +310,11 @@ async def get_depenses(
|
||||
async def get_depenses_summary(
|
||||
immeuble_id: int | None = Query(None, description="Filtrer par immeuble"),
|
||||
lot_id: int | None = Query(None, description="Filtrer par lot"),
|
||||
tag_id: int | None = Query(None, description="Filtrer par tag"),
|
||||
tag_id: int | None = Query(None, description="Filtrer par tag (0 = sans tag)"),
|
||||
categorie: str | None = Query(None, description="Filtrer par categorie"),
|
||||
fournisseur: str | None = Query(None, description="Filtrer par fournisseur"),
|
||||
fournisseur: list[str] | None = Query(
|
||||
None, description="Fournisseurs retenus (parametre repetable)"
|
||||
),
|
||||
date_debut: date | None = Query(None, description="Date de debut (YYYY-MM-DD)"),
|
||||
date_fin: date | None = Query(None, description="Date de fin (YYYY-MM-DD)"),
|
||||
session: Session = Depends(get_session),
|
||||
@@ -289,22 +330,16 @@ async def get_depenses_summary(
|
||||
- Top fournisseurs
|
||||
"""
|
||||
# Base query with filters
|
||||
base_stmt = select(Depense).join(Document, Depense.document_id == Document.id)
|
||||
|
||||
if immeuble_id is not None:
|
||||
base_stmt = base_stmt.where(Depense.immeuble_id == immeuble_id)
|
||||
if lot_id is not None:
|
||||
base_stmt = base_stmt.where(Depense.lot_id == lot_id)
|
||||
if tag_id is not None:
|
||||
base_stmt = base_stmt.where(Depense.tag_id == tag_id)
|
||||
if categorie is not None:
|
||||
base_stmt = base_stmt.where(Depense.categorie == categorie)
|
||||
if fournisseur is not None:
|
||||
base_stmt = base_stmt.where(Depense.fournisseur.ilike(f"%{fournisseur}%"))
|
||||
if date_debut is not None:
|
||||
base_stmt = base_stmt.where(Document.date >= date_debut)
|
||||
if date_fin is not None:
|
||||
base_stmt = base_stmt.where(Document.date <= date_fin)
|
||||
base_stmt = _appliquer_filtres(
|
||||
select(Depense).join(Document, Depense.document_id == Document.id),
|
||||
immeuble_id=immeuble_id,
|
||||
lot_id=lot_id,
|
||||
tag_id=tag_id,
|
||||
categorie=categorie,
|
||||
fournisseurs=fournisseur,
|
||||
date_debut=date_debut,
|
||||
date_fin=date_fin,
|
||||
)
|
||||
|
||||
# Get all matching depenses
|
||||
result = session.execute(base_stmt)
|
||||
|
||||
Reference in New Issue
Block a user