"""Tests de la fiche d'un lot. Cette page restitue un lot tel que les comptes rendus le portent. Les tests protègent donc ce qui la rendrait fausse ou trompeuse : cumuler un report de solde, ventiler des charges d'immeuble qu'aucun document n'attribue, ou regrouper des lignes que le compte rendu a émises séparément. """ import pytest from plesna_gerance.database.models import Immeuble, Lot from plesna_gerance.database.service import DatabaseService @pytest.fixture def donnees(db_session, sample_data): """Deux comptes rendus sur un lot : un loyer réglé, puis un report impayé. Le second document facture un loyer resté impayé et reporte le solde du premier — la configuration exacte où un cumul naïf compterait deux fois la même dette. """ service = DatabaseService(db_session) service.save_document(data=sample_data) suivant = { **sample_data, "metadata": { **sample_data["metadata"], "document": { "reference": "REF002", "date": "2024-02-15", "type": "COMPTE RENDU DE GESTION", }, }, "situation_locataires": [ { "lot": {"numero": "01", "type": "Appartement"}, "locataire": {"nom": "DUPONT"}, "lignes": [ { "type": "solde_anterieur", "total": 300.0, "regles": 0.0, "impayes": 300.0, }, { "type": "loyer", "periode": {"debut": "2024-02-01", "fin": "2024-02-29"}, "loyers": 500.0, "total": 500.0, "regles": 200.0, "impayes": 300.0, }, ], } ], "recapitulatif_operations": [ { "categorie": "DEPENSES_NON_RECUPERABLES", "sous_categorie": "Travaux divers", "fournisseur": "PLOMBERIE", "description": "S01 ACOMPTE 40% remplacement chaudière", # Le parser déduit ce numéro du préfixe de la description ; la # fixture le fournit tel qu'il arrive en base. "lot_numero": "01", "montants": {"debit": 400.0, "deductible": 400.0}, }, { "categorie": "DEPENSES_NON_RECUPERABLES", "sous_categorie": "Travaux divers", "fournisseur": "PLOMBERIE", "description": "S01 SOLDE remplacement chaudière", "lot_numero": "01", "montants": {"debit": 600.0, "deductible": 600.0}, }, { "categorie": "HONORAIRES_DE_GESTION", "sous_categorie": "Frais d'expert", "fournisseur": "EXPERTISE", "description": "S01 - Remise état des lieux sortie", "lot_numero": "01", # Un avoir : la ligne rend au propriétaire au lieu de lui coûter. "montants": {"debit": 0.0, "credit": 35.4}, }, ], } service.save_document(data=suivant) immeuble = db_session.query(Immeuble).filter(Immeuble.code == "IMM1").one() lot = db_session.query(Lot).filter(Lot.immeuble_id == immeuble.id).one() return immeuble, lot def test_lot_inconnu_donne_404(api_client, donnees): assert api_client.get("/api/lots/999999/analyse").status_code == 404 def test_identite_montre_les_trous_de_la_fiche(api_client, donnees): """Une caractéristique non saisie reste nulle : la page doit le montrer.""" _, lot = donnees identite = api_client.get(f"/api/lots/{lot.id}/analyse").json()["identite"] assert identite["numero"] == "01" assert identite["immeuble_code"] == "IMM1" assert identite["type_effectif"] == "Appartement" assert identite["surface"] is None assert identite["dpe_classe"] is None assert identite["locataires"] == ["DUPONT"] def test_le_report_de_solde_ne_gonfle_pas_le_facture(api_client, donnees): """Le facturé ne retient que les loyers, jamais la dette reportée. Deux loyers de 500 € : cumuler en plus le report de 300 € afficherait 1300 € facturés pour un lot qui n'a jamais rien facturé de tel. """ _, lot = donnees chiffres = api_client.get(f"/api/lots/{lot.id}/analyse").json()["chiffres"] assert chiffres["facture"] == 1000.0 assert chiffres["encaisse"] == 700.0 #: Photo du dernier compte rendu (300 de report + 300 de loyer), pas un cumul. assert chiffres["restant_du"] == 600.0 assert chiffres["taux_recouvrement"] == 70.0 def test_les_charges_d_immeuble_restent_hors_du_lot(api_client, donnees): """Le nettoyage de l'immeuble ne doit pas atterrir dans un lot. Aucune donnée ne dit quelle part revient à quel lot : la ventiler inventerait des montants. Elle est exposée à part, comme contexte. """ _, lot = donnees chiffres = api_client.get(f"/api/lots/{lot.id}/analyse").json()["chiffres"] # 400 + 600 de travaux imputés au lot, sans le nettoyage de l'immeuble. assert chiffres["depenses_debit"] == 1000.0 assert chiffres["depenses_credit"] == 35.4 assert chiffres["nb_operations"] == 3 # Le nettoyage du premier compte rendu, resté sans lot. assert chiffres["depenses_immeuble_non_reparties"] == 50.0 # Encaissé (700) moins décaissé (1000), avoir rendu (35,40) : charges # communes exclues. assert chiffres["solde"] == -264.6 def test_la_chronologie_garde_les_lignes_telles_qu_extraites(api_client, donnees): """Acompte et solde restent deux lignes : le compte rendu les porte ainsi.""" _, lot = donnees chronologie = api_client.get(f"/api/lots/{lot.id}/analyse").json()["chronologie"] travaux = [ligne for ligne in chronologie if ligne["fournisseur"] == "PLOMBERIE"] assert len(travaux) == 2 assert {ligne["montant"] for ligne in travaux} == {400.0, 600.0} def test_la_chronologie_est_ordonnee_et_signale_les_reports(api_client, donnees): """L'ordre des comptes rendus est l'ordre de lecture de la page.""" _, lot = donnees chronologie = api_client.get(f"/api/lots/{lot.id}/analyse").json()["chronologie"] dates = [ligne["date"] for ligne in chronologie] assert dates == sorted(dates) assert dates[0] == "2024-01-15" reports = [ligne for ligne in chronologie if ligne["est_report"]] assert len(reports) == 1 assert reports[0]["montant"] == 300.0 def test_les_intervenants_agregent_le_fournisseur_du_compte_rendu(api_client, donnees): """Une entreprise, ses interventions et son montant — rien de déduit.""" _, lot = donnees intervenants = api_client.get(f"/api/lots/{lot.id}/analyse").json()["intervenants"] par_nom = {ligne["fournisseur"]: ligne for ligne in intervenants} assert set(par_nom) == {"PLOMBERIE", "EXPERTISE"} assert par_nom["PLOMBERIE"]["nb_interventions"] == 2 assert par_nom["PLOMBERIE"]["montant"] == 1000.0 assert par_nom["PLOMBERIE"]["derniere_date"] == "2024-02-15" def test_un_avoir_rend_le_montant_de_l_intervenant_negatif(api_client, donnees): """Le crédit est déduit : une remise ne doit pas s'afficher comme un coût.""" _, lot = donnees intervenants = api_client.get(f"/api/lots/{lot.id}/analyse").json()["intervenants"] expertise = next( ligne for ligne in intervenants if ligne["fournisseur"] == "EXPERTISE" ) assert expertise["montant"] == -35.4 # La plus engagée en tête : un avoir se classe donc en dernier. assert intervenants[-1]["fournisseur"] == "EXPERTISE" def test_le_total_d_un_intervenant_est_celui_de_ses_lignes(api_client, donnees): """Invariant du dépliage : le détail doit retrouver le total affiché. Les deux chiffres viennent de calculs séparés (agrégat SQL d'un côté, lignes de la chronologie de l'autre) ; les laisser diverger ferait mentir la ligne qu'on vient d'ouvrir. """ _, lot = donnees analyse = api_client.get(f"/api/lots/{lot.id}/analyse").json() for intervenant in analyse["intervenants"]: lignes = [ ligne for ligne in analyse["chronologie"] if ligne["fournisseur"] == intervenant["fournisseur"] ] assert len(lignes) == intervenant["nb_interventions"] assert ( round(sum(ligne["montant"] for ligne in lignes), 2) == intervenant["montant"] )