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:
131
tests/test_analytics_filtres.py
Normal file
131
tests/test_analytics_filtres.py
Normal file
@@ -0,0 +1,131 @@
|
||||
"""Filtres de la page Depenses : plusieurs fournisseurs, et l'absence de tag.
|
||||
|
||||
Le filtre fournisseur cherchait une sous-chaine, ce qui interdisait d'en
|
||||
comparer deux et retenait au passage leurs homonymes. Il retient desormais une
|
||||
liste de noms exacts. Le filtre tag, lui, ne savait pas demander « ce qui n'est
|
||||
pas encore tague » - c'est pourtant la question qui amorce le travail de
|
||||
tagging.
|
||||
|
||||
Ces tests verrouillent aussi l'accord entre la table et le resume : les deux
|
||||
endpoints doivent voir le meme perimetre, sinon les totaux affiches ne sont pas
|
||||
ceux des lignes listees.
|
||||
"""
|
||||
|
||||
import copy
|
||||
|
||||
import pytest
|
||||
|
||||
from plesna_gerance.database.models import Depense, Tag
|
||||
from plesna_gerance.database.service import DatabaseService
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def depenses_de_trois_fournisseurs(db_session, sample_data):
|
||||
"""Quatre depenses : ACME (2), ACME SUD (1), BOREAL (1).
|
||||
|
||||
« ACME SUD » est la pour verifier qu'une selection sur « ACME » ne
|
||||
l'emporte pas au passage, ce que faisait l'ancienne recherche.
|
||||
"""
|
||||
donnees = copy.deepcopy(sample_data)
|
||||
donnees["recapitulatif_operations"] = [
|
||||
{
|
||||
"categorie": "DEPENSES_LOCATIVES",
|
||||
"fournisseur": "ACME",
|
||||
"description": "Nettoyage janvier",
|
||||
"montants": {"debit": 50.0},
|
||||
},
|
||||
{
|
||||
"categorie": "DEPENSES_LOCATIVES",
|
||||
"fournisseur": "ACME",
|
||||
"description": "Nettoyage fevrier",
|
||||
"montants": {"debit": 60.0},
|
||||
},
|
||||
{
|
||||
"categorie": "TRAVAUX",
|
||||
"fournisseur": "ACME SUD",
|
||||
"description": "Reprise peinture",
|
||||
"montants": {"debit": 200.0},
|
||||
},
|
||||
{
|
||||
"categorie": "TRAVAUX",
|
||||
"fournisseur": "BOREAL",
|
||||
"description": "Toiture",
|
||||
"montants": {"debit": 300.0},
|
||||
},
|
||||
]
|
||||
DatabaseService(db_session).save_document(data=donnees)
|
||||
return donnees
|
||||
|
||||
|
||||
def _noms(lignes):
|
||||
"""Les fournisseurs des lignes retournees, tries pour comparer sans l'ordre."""
|
||||
return sorted(ligne["fournisseur"] for ligne in lignes)
|
||||
|
||||
|
||||
def test_retient_les_fournisseurs_demandes(api_client, depenses_de_trois_fournisseurs):
|
||||
reponse = api_client.get(
|
||||
"/api/analytics/depenses?fournisseur=ACME&fournisseur=BOREAL"
|
||||
)
|
||||
|
||||
assert reponse.status_code == 200
|
||||
assert _noms(reponse.json()) == ["ACME", "ACME", "BOREAL"]
|
||||
|
||||
|
||||
def test_un_fournisseur_selectionne_exclut_ses_homonymes(
|
||||
api_client, depenses_de_trois_fournisseurs
|
||||
):
|
||||
"""« ACME » ne doit plus ramener « ACME SUD » : la selection est exacte."""
|
||||
reponse = api_client.get("/api/analytics/depenses?fournisseur=ACME")
|
||||
|
||||
assert _noms(reponse.json()) == ["ACME", "ACME"]
|
||||
|
||||
|
||||
def test_sans_fournisseur_demande_tout_reste_visible(
|
||||
api_client, depenses_de_trois_fournisseurs
|
||||
):
|
||||
assert len(api_client.get("/api/analytics/depenses").json()) == 4
|
||||
|
||||
|
||||
def test_le_resume_porte_sur_les_memes_lignes_que_la_table(
|
||||
api_client, depenses_de_trois_fournisseurs
|
||||
):
|
||||
requete = "fournisseur=ACME&fournisseur=BOREAL"
|
||||
lignes = api_client.get(f"/api/analytics/depenses?{requete}").json()
|
||||
resume = api_client.get(f"/api/analytics/depenses/summary?{requete}").json()
|
||||
|
||||
assert resume["total_count"] == len(lignes)
|
||||
assert resume["total_debit"] == pytest.approx(
|
||||
sum(ligne["debit"] for ligne in lignes)
|
||||
)
|
||||
assert resume["total_debit"] == pytest.approx(410.0)
|
||||
|
||||
|
||||
def test_filtre_les_depenses_sans_tag(
|
||||
api_client, db_session, depenses_de_trois_fournisseurs
|
||||
):
|
||||
"""tag_id=0 demande les depenses non taggees, la ou l'omettre les prend toutes."""
|
||||
tag = db_session.query(Tag).order_by(Tag.id).first()
|
||||
taggee = db_session.query(Depense).filter(Depense.fournisseur == "BOREAL").one()
|
||||
taggee.tag_id = tag.id
|
||||
db_session.flush()
|
||||
|
||||
sans_tag = api_client.get("/api/analytics/depenses?tag_id=0").json()
|
||||
avec_ce_tag = api_client.get(f"/api/analytics/depenses?tag_id={tag.id}").json()
|
||||
toutes = api_client.get("/api/analytics/depenses").json()
|
||||
|
||||
assert _noms(sans_tag) == ["ACME", "ACME", "ACME SUD"]
|
||||
assert _noms(avec_ce_tag) == ["BOREAL"]
|
||||
assert len(toutes) == len(sans_tag) + len(avec_ce_tag)
|
||||
|
||||
|
||||
def test_le_resume_par_tag_montre_les_non_taggees(
|
||||
api_client, depenses_de_trois_fournisseurs
|
||||
):
|
||||
"""La masse non taggee reste une part du camembert, pas un trou."""
|
||||
resume = api_client.get("/api/analytics/depenses/summary").json()
|
||||
|
||||
non_taggees = [part for part in resume["by_tag"] if part["tag_id"] is None]
|
||||
assert len(non_taggees) == 1
|
||||
assert non_taggees[0]["count"] == 4
|
||||
assert non_taggees[0]["total_debit"] == pytest.approx(610.0)
|
||||
|
||||
Reference in New Issue
Block a user