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>
132 lines
4.5 KiB
Python
132 lines
4.5 KiB
Python
"""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)
|
|
|