fix: distingue les montants cumulables des soldes à date
Chaque compte rendu reporte la dette du précédent dans une ligne solde_anterieur. Les agrégats sommaient ces lignes comme le reste : la même créance était recomptée à chaque document, et un remboursement ne pouvait jamais s'inscrire — une dette soldée restait affichée à vie. Sur la base réelle, la page Revenus annonçait ainsi 247 354 € d'impayés pour une dette de 49 374 €, et désignait comme deuxième et troisième débiteurs deux locataires à jour depuis avril (SURBECK 690,10 € et GUINAIS 445,81 €, tous deux soldés). Deux natures cohabitent, et c'est la colonne qui la porte, pas la ligne : une ligne de report a un `total` déjà compté le mois d'avant, mais ses `regles` sont un encaissement bien réel de la période. Écarter la ligne entière ferait disparaître de l'argent reçu (1 298,81 € ici). - flux (facturé, encaissé) : cumulés sur la période, report exclu ; - stock (restant dû) : lu dans le dernier compte rendu de chaque immeuble ; - taux de recouvrement : réglé sur facturé, report exclu des deux côtés, sans quoi rattraper une vieille dette ferait dépasser 100 %. Résultat : 85 748 € facturés, 98,2 % de recouvrement, 49 374 € encore dus. Les règles vivent dans services/revenus_query.py, pour que le dashboard s'y branche au lieu de les réinventer. /summary borne désormais tous ses blocs à la période demandée : les KPIs et by_immeuble ignoraient `months` alors que by_month le respectait. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -7,6 +7,7 @@ aucune erreur, juste des montants faux qui derivent avec l'anciennete du parc.
|
||||
"""
|
||||
|
||||
import copy
|
||||
from datetime import date, timedelta
|
||||
|
||||
import pytest
|
||||
|
||||
@@ -14,6 +15,19 @@ from plesna_gerance.database.models import Locataire, Revenu
|
||||
from plesna_gerance.database.service import DatabaseService
|
||||
|
||||
|
||||
def _mois_glissant(recul: int) -> str:
|
||||
"""Date du 15 du mois, `recul` mois en arriere.
|
||||
|
||||
Les resumes ne portent que sur les derniers mois : des dates fixes
|
||||
sortiraient de la fenetre avec le temps et videraient les tests de leur
|
||||
substance sans jamais les faire echouer.
|
||||
"""
|
||||
jour = date.today().replace(day=15)
|
||||
for _ in range(recul):
|
||||
jour = (jour.replace(day=1) - timedelta(days=1)).replace(day=15)
|
||||
return jour.isoformat()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def lot_a_deux_locataires(db_session, sample_data):
|
||||
"""Un lot occupe successivement par deux locataires, un revenu chacun.
|
||||
@@ -22,11 +36,13 @@ def lot_a_deux_locataires(db_session, sample_data):
|
||||
deux noms differents sur deux comptes rendus.
|
||||
"""
|
||||
service = DatabaseService(db_session)
|
||||
service.save_document(data=sample_data)
|
||||
premier = copy.deepcopy(sample_data)
|
||||
premier["metadata"]["document"]["date"] = _mois_glissant(2)
|
||||
service.save_document(data=premier)
|
||||
|
||||
suivant = copy.deepcopy(sample_data)
|
||||
suivant["metadata"]["document"]["reference"] = "REF002"
|
||||
suivant["metadata"]["document"]["date"] = "2024-02-15"
|
||||
suivant["metadata"]["document"]["date"] = _mois_glissant(1)
|
||||
suivant["situation_locataires"][0]["locataire"]["nom"] = "MARTIN"
|
||||
suivant["situation_locataires"][0]["lignes"][0]["loyers"] = 600.0
|
||||
suivant["situation_locataires"][0]["lignes"][0]["total"] = 600.0
|
||||
@@ -40,46 +56,40 @@ def lot_a_deux_locataires(db_session, sample_data):
|
||||
return db_session
|
||||
|
||||
|
||||
def _totaux_reels(session) -> tuple[float, float, float]:
|
||||
revenus = session.query(Revenu).all()
|
||||
return (
|
||||
sum(r.total for r in revenus),
|
||||
sum(r.regles for r in revenus),
|
||||
sum(r.impayes for r in revenus),
|
||||
)
|
||||
#: Le decor : 500 puis 600 factures, 500 puis 400 regles, 200 encore dus.
|
||||
FACTURE = 1100.0
|
||||
ENCAISSE = 900.0
|
||||
RESTANT_DU = 200.0
|
||||
|
||||
|
||||
def test_immeubles_ne_compte_pas_les_revenus_en_double(
|
||||
api_client, lot_a_deux_locataires
|
||||
):
|
||||
"""/api/revenus/immeubles somme chaque revenu une seule fois."""
|
||||
total, regles, impayes = _totaux_reels(lot_a_deux_locataires)
|
||||
|
||||
response = api_client.get("/api/revenus/immeubles")
|
||||
assert response.status_code == 200
|
||||
(immeuble,) = response.json()
|
||||
|
||||
assert immeuble["total_revenus"] == total
|
||||
assert immeuble["total_regles"] == regles
|
||||
assert immeuble["total_impayes"] == impayes
|
||||
assert immeuble["total_revenus"] == FACTURE
|
||||
assert immeuble["total_regles"] == ENCAISSE
|
||||
assert immeuble["total_impayes"] == RESTANT_DU
|
||||
|
||||
|
||||
def test_summary_ne_compte_pas_les_revenus_en_double(
|
||||
api_client, lot_a_deux_locataires
|
||||
):
|
||||
"""Le bloc by_immeuble de /api/revenus/summary somme comme les KPIs."""
|
||||
total, regles, impayes = _totaux_reels(lot_a_deux_locataires)
|
||||
|
||||
response = api_client.get("/api/revenus/summary")
|
||||
assert response.status_code == 200
|
||||
corps = response.json()
|
||||
(immeuble,) = corps["by_immeuble"]
|
||||
|
||||
assert immeuble["total_revenus"] == total
|
||||
assert immeuble["total_regles"] == regles
|
||||
assert immeuble["total_impayes"] == impayes
|
||||
# Les KPIs globaux n'ont jamais eu le defaut : les deux doivent concorder.
|
||||
assert immeuble["total_revenus"] == FACTURE
|
||||
assert immeuble["total_regles"] == ENCAISSE
|
||||
assert immeuble["total_impayes"] == RESTANT_DU
|
||||
# Les deux blocs doivent raconter la meme chose.
|
||||
assert corps["kpis"]["total_revenus"] == immeuble["total_revenus"]
|
||||
assert corps["kpis"]["total_impayes"] == immeuble["total_impayes"]
|
||||
|
||||
|
||||
def test_immeubles_compte_lots_et_locataires(api_client, lot_a_deux_locataires):
|
||||
@@ -92,8 +102,6 @@ def test_immeubles_compte_lots_et_locataires(api_client, lot_a_deux_locataires):
|
||||
|
||||
def test_taux_recouvrement_reste_coherent(api_client, lot_a_deux_locataires):
|
||||
"""Le taux se deduit des totaux : il doit suivre la meme correction."""
|
||||
total, regles, _ = _totaux_reels(lot_a_deux_locataires)
|
||||
|
||||
(immeuble,) = api_client.get("/api/revenus/immeubles").json()
|
||||
|
||||
assert immeuble["taux_recouvrement"] == round(regles / total * 100, 1)
|
||||
assert immeuble["taux_recouvrement"] == round(ENCAISSE / FACTURE * 100, 1)
|
||||
|
||||
Reference in New Issue
Block a user