"""Le dashboard et la page Revenus doivent annoncer les memes chiffres. Les deux ecrans calculaient leurs totaux chacun de leur cote, avec des regles differentes : l'accueil lisait le dernier compte rendu, la page Revenus cumulait tout. Ils affichaient donc deux montants d'impayes pour la meme notion. Ces tests verrouillent leur accord. L'accueil resume desormais une annee civile entiere. S'y ajoute donc ce que cette bascule engage : la periode couverte suit les donnees et non l'horloge, et le net reverse se rapproche du solde annonce par les comptes rendus. """ import copy from datetime import date import pytest from plesna_gerance.database.service import DatabaseService @pytest.fixture def deux_comptes_rendus(db_session, sample_data): """Un impaye de 300 ne en janvier, reporte et non regle en fevrier.""" service = DatabaseService(db_session) premier = copy.deepcopy(sample_data) premier["metadata"]["document"]["reference"] = "M1" premier["metadata"]["document"]["date"] = "2024-01-15" premier["metadata"]["solde"] = { "montant": 450.0, "type": "crediteur", "date_arrete": "2024-01-15", } premier["situation_locataires"][0]["lignes"] = [ { "type": "loyer", "periode": {"debut": None, "fin": None}, "loyers": 800.0, "total": 800.0, "regles": 500.0, "impayes": 300.0, } ] service.save_document(data=premier) second = copy.deepcopy(sample_data) second["metadata"]["document"]["reference"] = "M2" second["metadata"]["document"]["date"] = "2024-02-15" second["metadata"]["solde"] = { "montant": 750.0, "type": "crediteur", "date_arrete": "2024-02-15", } second["situation_locataires"][0]["lignes"] = [ { "type": "solde_anterieur", "periode": {"debut": None, "fin": None}, "loyers": 300.0, "total": 300.0, "regles": 0.0, "impayes": 300.0, }, { "type": "loyer", "periode": {"debut": None, "fin": None}, "loyers": 800.0, "total": 800.0, "regles": 800.0, "impayes": 0.0, }, ] service.save_document(data=second) return db_session def _resume(api_client, **params): return api_client.get("/api/dashboard/resume-annuel", params=params).json() def test_les_deux_ecrans_annoncent_le_meme_impaye(api_client, db_session, sample_data): """300 dus, vus depuis l'accueil comme depuis la page Revenus. Le compte rendu est date d'aujourd'hui a dessein : les deux ecrans ne cadrent plus la meme periode — annee civile ici, douze mois glissants la-bas — et seule une donnee recente tombe dans les deux fenetres. Leur accord porte sur la regle, pas sur le cadrage. """ document = copy.deepcopy(sample_data) document["metadata"]["document"]["date"] = date.today().isoformat() document["situation_locataires"][0]["lignes"] = [ { "type": "loyer", "periode": {"debut": None, "fin": None}, "loyers": 800.0, "total": 800.0, "regles": 500.0, "impayes": 300.0, } ] DatabaseService(db_session).save_document(data=document) accueil = _resume(api_client) revenus = api_client.get("/api/revenus/summary").json() assert accueil["restant_du"] == 300.0 assert revenus["kpis"]["total_impayes"] == 300.0 def test_le_facture_de_l_annee_exclut_le_report(api_client, deux_comptes_rendus): """L'annee vaut ses deux loyers de 800, pas 1900 report compris.""" assert _resume(api_client)["facture"] == 1600.0 def test_l_encaisse_retient_les_reglements_de_dettes_anciennes( api_client, db_session, sample_data ): """Un vieil impaye rattrape est un encaissement du mois, pas un report.""" service = DatabaseService(db_session) document = copy.deepcopy(sample_data) document["metadata"]["document"]["date"] = "2024-03-15" document["situation_locataires"][0]["lignes"] = [ { "type": "solde_anterieur", "periode": {"debut": None, "fin": None}, "loyers": 300.0, "total": 300.0, "regles": 300.0, "impayes": 0.0, } ] service.save_document(data=document) resume = _resume(api_client) assert resume["facture"] == 0.0 assert resume["encaisse"] == 300.0 def test_la_tendance_mensuelle_reste_comparable(api_client, deux_comptes_rendus): """Chaque mois pese son loyer, sinon le second parait meilleur qu'il n'est.""" par_mois = _resume(api_client)["par_mois"] assert [point["mois"] for point in par_mois] == ["2024-01", "2024-02"] assert [point["facture"] for point in par_mois] == [800.0, 800.0] def test_les_raccourcis_immeubles_ne_cumulent_pas_la_dette( api_client, deux_comptes_rendus ): """Le raccourci montre la dette en cours, pas sa somme mois apres mois.""" (raccourci,) = api_client.get( "/api/dashboard/immeubles-shortcuts", params={"annee": 2024} ).json() assert raccourci["total_impayes"] == 300.0 assert raccourci["total_revenus"] == 1600.0 # 800 + 800, report exclu def test_le_net_reverse_se_rapproche_du_solde_annonce(api_client, deux_comptes_rendus): """Encaisse moins depenses doit retomber sur les soldes des comptes rendus. C'est le seul controle de bout en bout sur la qualite d'une extraction : 1300 encaisses, 100 de depenses sur les deux documents, contre 450 + 750 annonces. """ resume = _resume(api_client) assert resume["encaisse"] == 1300.0 assert resume["depenses_debit"] == 100.0 assert resume["net_reverse"] == 1200.0 assert resume["soldes_annonces"] == 1200.0 assert resume["ecart_soldes"] == 0.0 def test_un_ecart_de_solde_est_annonce_et_non_lisse( api_client, db_session, sample_data ): """Quand le recalcul et le solde du PDF divergent, l'ecart doit se voir.""" document = copy.deepcopy(sample_data) document["metadata"]["document"]["date"] = "2024-01-15" document["metadata"]["solde"] = { "montant": 1000.0, "type": "crediteur", "date_arrete": "2024-01-15", } DatabaseService(db_session).save_document(data=document) resume = _resume(api_client) # 500 encaisses - 50 de depenses = 450, la ou le document annonce 1000. assert resume["net_reverse"] == 450.0 assert resume["soldes_annonces"] == 1000.0 assert resume["ecart_soldes"] == -550.0 def test_un_solde_debiteur_se_soustrait(api_client, db_session, sample_data): """Un mois debiteur est reclame au proprietaire, il ne s'ajoute pas.""" document = copy.deepcopy(sample_data) document["metadata"]["document"]["date"] = "2024-01-15" document["metadata"]["solde"] = { "montant": 200.0, "type": "débiteur", "date_arrete": "2024-01-15", } DatabaseService(db_session).save_document(data=document) assert _resume(api_client)["soldes_annonces"] == -200.0 def test_l_annee_par_defaut_suit_les_donnees_pas_l_horloge( api_client, deux_comptes_rendus ): """Sans import recent, l'accueil montre la derniere annee pleine, pas du vide.""" resume = _resume(api_client) assert resume["annee"] == 2024 assert resume["annee"] != date.today().year assert resume["annees_disponibles"] == [2024] assert resume["perimetre"]["nb_comptes_rendus"] == 2 def test_une_annee_sans_compte_rendu_reste_lisible(api_client, deux_comptes_rendus): """Choisir une annee vide donne des zeros, pas une erreur.""" resume = _resume(api_client, annee=2023) assert resume["annee"] == 2023 assert resume["perimetre"]["nb_comptes_rendus"] == 0 assert resume["facture"] == 0.0 assert resume["restant_du"] == 0.0 assert resume["par_mois"] == [] def test_les_annees_ne_se_melangent_pas(api_client, db_session, sample_data): """Le total d'une annee ne doit rien emprunter a la suivante.""" service = DatabaseService(db_session) for reference, jour in (("A", "2024-06-15"), ("B", "2025-06-15")): document = copy.deepcopy(sample_data) document["metadata"]["document"]["reference"] = reference document["metadata"]["document"]["date"] = jour service.save_document(data=document) assert _resume(api_client, annee=2024)["facture"] == 500.0 assert _resume(api_client, annee=2025)["facture"] == 500.0 assert _resume(api_client)["annees_disponibles"] == [2025, 2024] def test_le_perimetre_ne_compte_que_les_lots_factures(api_client, deux_comptes_rendus): """La table `lots` retient d'anciennes ecritures de numeros, sans revenu.""" assert _resume(api_client)["perimetre"]["nb_lots_factures"] == 1 def test_une_base_vide_ne_casse_pas_l_accueil(api_client): """Premiere ouverture de l'application : aucun document, aucune erreur.""" resume = _resume(api_client) assert resume["annees_disponibles"] == [] assert resume["annee"] == date.today().year assert resume["facture"] == 0.0 assert resume["taux_recouvrement"] == 100.0 assert resume["par_mois"] == [] assert api_client.get("/api/dashboard/immeubles-shortcuts").json() == []