feat: décrit les logements dans un référentiel saisi à la main
Les lots n'étaient connus que par l'extraction PDF : un numéro, un type souvent vide, et rien sur le bien lui-même. Cette table de caractéristiques (surface, étage, bâtiment, chauffage, DPE, rapprochement impôts) donne au référentiel une source de vérité indépendante des comptes rendus. Table séparée de `lots` à dessein : une ré-extraction ne peut alors pas écraser la saisie, et le désaccord sur le type de lot reste visible au lieu d'être arbitré en silence. La fiche gagne, le PDF comble les trous. Échéance du DPE et écart de surface ne sont pas stockés mais calculés : une colonne dérivée finirait par mentir après une correction. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
57
tests/test_logements.py
Normal file
57
tests/test_logements.py
Normal file
@@ -0,0 +1,57 @@
|
||||
"""Tests des valeurs derivees des caracteristiques d'un logement.
|
||||
|
||||
Echeance de DPE et ecart de surface ne sont pas stockes : ils se recalculent a
|
||||
chaque lecture. Ces tests figent les regles de calcul, seul endroit ou une
|
||||
erreur passerait inapercue puisqu'aucune donnee saisie ne la contredirait.
|
||||
"""
|
||||
|
||||
from datetime import date
|
||||
|
||||
from plesna_gerance.utils.logements import (
|
||||
delta_surface,
|
||||
dpe_echeance,
|
||||
type_en_ecart,
|
||||
)
|
||||
|
||||
|
||||
def test_un_dpe_vaut_dix_ans():
|
||||
assert dpe_echeance(date(2021, 2, 8)) == date(2031, 2, 8)
|
||||
|
||||
|
||||
def test_un_dpe_du_29_fevrier_expire_le_28():
|
||||
"""2020 est bissextile, 2030 non : la date doit reculer, pas exploser."""
|
||||
assert dpe_echeance(date(2020, 2, 29)) == date(2030, 2, 28)
|
||||
|
||||
|
||||
def test_sans_date_de_dpe_pas_d_echeance():
|
||||
assert dpe_echeance(None) is None
|
||||
|
||||
|
||||
def test_ecart_de_surface_positif_quand_les_impots_en_retiennent_plus():
|
||||
assert delta_surface(100.0, 119.0) == 19.0
|
||||
|
||||
|
||||
def test_ecart_de_surface_negatif_quand_la_mesure_est_plus_grande():
|
||||
assert delta_surface(148.0, 146.0) == -2.0
|
||||
|
||||
|
||||
def test_une_seule_surface_ne_donne_aucun_ecart():
|
||||
"""Un ecart de 0 affirmerait que les deux sources concordent."""
|
||||
assert delta_surface(148.0, None) is None
|
||||
assert delta_surface(None, 146.0) is None
|
||||
|
||||
|
||||
def test_meme_type_ecrit_autrement_n_est_pas_un_ecart():
|
||||
assert not type_en_ecart("Loc. Commercial", "Loc, Commercial")
|
||||
assert not type_en_ecart("Appartement T3", "appartement t3")
|
||||
|
||||
|
||||
def test_deux_types_differents_sont_un_ecart():
|
||||
assert type_en_ecart("Appartement T3", "Appartement T2")
|
||||
|
||||
|
||||
def test_un_type_absent_n_est_pas_un_ecart():
|
||||
"""Le PDF laisse souvent le type vide : ce n'est pas une contradiction."""
|
||||
assert not type_en_ecart(None, "Appartement T3")
|
||||
assert not type_en_ecart("Appartement T3", None)
|
||||
assert not type_en_ecart("", "")
|
||||
205
tests/test_referentiel_api.py
Normal file
205
tests/test_referentiel_api.py
Normal file
@@ -0,0 +1,205 @@
|
||||
"""Tests du referentiel des logements.
|
||||
|
||||
Ces caracteristiques sont saisies a la main : elles n'ont aucune autre source
|
||||
que l'utilisateur, donc rien ne les reconstituerait si un enregistrement les
|
||||
perdait. Les tests portent sur ce qui menace cette saisie : l'ecraser depuis un
|
||||
PDF, la vider a moitie, ou la laisser contredire l'extraction en silence.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
from plesna_gerance.database.models import Depense, Immeuble, Lot
|
||||
from plesna_gerance.database.service import DatabaseService
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def immeuble_et_lot(db_session, sample_data):
|
||||
"""Un immeuble et son lot 01, tels que l'extraction les cree."""
|
||||
DatabaseService(db_session).save_document(data=sample_data)
|
||||
|
||||
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
|
||||
|
||||
|
||||
FICHE = {
|
||||
"bat": "Rue",
|
||||
"etage": "RC",
|
||||
"type": "Loc. Commercial",
|
||||
"surface": 148.0,
|
||||
"surface_date_diag": "2019-06-01",
|
||||
"chauffage": "Electrique",
|
||||
"dpe_classe": "C",
|
||||
"dpe_date_realisation": "2021-02-08",
|
||||
"numero_fiscal": "690123456789",
|
||||
"surface_impots": 146.0,
|
||||
"note_impots": "Surface relevee sur l'avis 2024",
|
||||
}
|
||||
|
||||
|
||||
def test_liste_les_lots_sans_fiche(api_client, immeuble_et_lot):
|
||||
"""Un lot jamais decrit doit apparaitre, sinon il n'est pas saisissable."""
|
||||
immeuble, lot = immeuble_et_lot
|
||||
|
||||
response = api_client.get(f"/api/immeubles/{immeuble.id}/lots/referentiel")
|
||||
|
||||
assert response.status_code == 200
|
||||
lignes = response.json()
|
||||
assert len(lignes) == 1
|
||||
assert lignes[0]["id"] == lot.id
|
||||
assert lignes[0]["numero"] == "01"
|
||||
assert lignes[0]["caracteristiques"] is None
|
||||
assert lignes[0]["type_extrait"] == "Appartement"
|
||||
assert lignes[0]["type_effectif"] == "Appartement"
|
||||
assert lignes[0]["type_ecart"] is False
|
||||
|
||||
|
||||
def test_liste_un_immeuble_inconnu_donne_404(api_client):
|
||||
assert api_client.get("/api/immeubles/99999/lots/referentiel").status_code == 404
|
||||
|
||||
|
||||
def test_enregistre_puis_relit_une_fiche(api_client, immeuble_et_lot):
|
||||
immeuble, lot = immeuble_et_lot
|
||||
|
||||
response = api_client.put(f"/api/lots/{lot.id}/caracteristiques", json=FICHE)
|
||||
|
||||
assert response.status_code == 200
|
||||
fiche = response.json()["caracteristiques"]
|
||||
assert fiche["surface"] == 148.0
|
||||
assert fiche["etage"] == "RC"
|
||||
assert fiche["numero_fiscal"] == "690123456789"
|
||||
|
||||
relu = api_client.get(f"/api/immeubles/{immeuble.id}/lots/referentiel").json()[0]
|
||||
assert relu["caracteristiques"]["surface"] == 148.0
|
||||
assert relu["caracteristiques"]["dpe_classe"] == "C"
|
||||
|
||||
|
||||
def test_expose_les_valeurs_derivees(api_client, immeuble_et_lot):
|
||||
"""L'echeance du DPE et l'ecart de surface arrivent calcules."""
|
||||
_, lot = immeuble_et_lot
|
||||
|
||||
fiche = api_client.put(f"/api/lots/{lot.id}/caracteristiques", json=FICHE).json()[
|
||||
"caracteristiques"
|
||||
]
|
||||
|
||||
assert fiche["dpe_echeance"] == "2031-02-08"
|
||||
assert fiche["delta_surface"] == -2.0
|
||||
|
||||
|
||||
def test_une_seconde_ecriture_met_a_jour_sans_dupliquer(api_client, immeuble_et_lot):
|
||||
immeuble, lot = immeuble_et_lot
|
||||
api_client.put(f"/api/lots/{lot.id}/caracteristiques", json=FICHE)
|
||||
|
||||
api_client.put(
|
||||
f"/api/lots/{lot.id}/caracteristiques", json={**FICHE, "surface": 150.0}
|
||||
)
|
||||
|
||||
lignes = api_client.get(f"/api/immeubles/{immeuble.id}/lots/referentiel").json()
|
||||
assert len(lignes) == 1
|
||||
assert lignes[0]["caracteristiques"]["surface"] == 150.0
|
||||
|
||||
|
||||
def test_un_champ_vide_efface_la_valeur(api_client, immeuble_et_lot):
|
||||
"""Corriger une erreur de saisie doit pouvoir revenir a « inconnu »."""
|
||||
_, lot = immeuble_et_lot
|
||||
api_client.put(f"/api/lots/{lot.id}/caracteristiques", json=FICHE)
|
||||
|
||||
fiche = api_client.put(
|
||||
f"/api/lots/{lot.id}/caracteristiques",
|
||||
json={**FICHE, "etage": " ", "surface": None},
|
||||
).json()["caracteristiques"]
|
||||
|
||||
assert fiche["etage"] is None
|
||||
assert fiche["surface"] is None
|
||||
assert fiche["delta_surface"] is None
|
||||
|
||||
|
||||
def test_refuse_une_classe_dpe_inconnue(api_client, immeuble_et_lot):
|
||||
_, lot = immeuble_et_lot
|
||||
|
||||
response = api_client.put(
|
||||
f"/api/lots/{lot.id}/caracteristiques", json={**FICHE, "dpe_classe": "Z"}
|
||||
)
|
||||
|
||||
assert response.status_code == 422
|
||||
|
||||
|
||||
def test_refuse_une_surface_negative(api_client, immeuble_et_lot):
|
||||
_, lot = immeuble_et_lot
|
||||
|
||||
response = api_client.put(
|
||||
f"/api/lots/{lot.id}/caracteristiques", json={**FICHE, "surface": -10}
|
||||
)
|
||||
|
||||
assert response.status_code == 422
|
||||
|
||||
|
||||
def test_ecrire_sur_un_lot_inconnu_donne_404(api_client):
|
||||
assert (
|
||||
api_client.put("/api/lots/99999/caracteristiques", json=FICHE).status_code
|
||||
== 404
|
||||
)
|
||||
|
||||
|
||||
def test_signale_un_desaccord_de_type_sans_effacer_le_pdf(api_client, immeuble_et_lot):
|
||||
"""Les deux types restent lisibles : la fiche tranche, le PDF reste visible."""
|
||||
_, lot = immeuble_et_lot
|
||||
|
||||
ligne = api_client.put(
|
||||
f"/api/lots/{lot.id}/caracteristiques", json={**FICHE, "type": "Appartement T3"}
|
||||
).json()
|
||||
|
||||
assert ligne["type_extrait"] == "Appartement"
|
||||
assert ligne["type_effectif"] == "Appartement T3"
|
||||
assert ligne["type_ecart"] is True
|
||||
|
||||
|
||||
def test_le_type_saisi_prime_dans_la_liste_des_lots(api_client, immeuble_et_lot):
|
||||
"""La priorite du referentiel vaut partout, pas seulement sur sa page."""
|
||||
_, lot = immeuble_et_lot
|
||||
api_client.put(
|
||||
f"/api/lots/{lot.id}/caracteristiques", json={**FICHE, "type": "Appartement T3"}
|
||||
)
|
||||
|
||||
lots = api_client.get("/api/lots").json()
|
||||
|
||||
assert [ligne["type"] for ligne in lots] == ["Appartement T3"]
|
||||
|
||||
|
||||
def test_une_re_extraction_ne_touche_pas_la_fiche(
|
||||
api_client, immeuble_et_lot, db_session, sample_data
|
||||
):
|
||||
"""Le point critique : reimporter le PDF ne doit rien perdre de la saisie."""
|
||||
immeuble, lot = immeuble_et_lot
|
||||
api_client.put(f"/api/lots/{lot.id}/caracteristiques", json=FICHE)
|
||||
|
||||
DatabaseService(db_session).save_document(data=sample_data, overwrite=True)
|
||||
|
||||
lignes = api_client.get(f"/api/immeubles/{immeuble.id}/lots/referentiel").json()
|
||||
fiche = next(ligne for ligne in lignes if ligne["id"] == lot.id)["caracteristiques"]
|
||||
assert fiche is not None, "la re-extraction a perdu la fiche du lot"
|
||||
assert fiche["surface"] == 148.0
|
||||
|
||||
|
||||
def test_compte_les_rattachements_du_lot(api_client, immeuble_et_lot):
|
||||
"""Un lot avec des revenus n'est pas un orphelin : la liste doit le dire."""
|
||||
immeuble, lot = immeuble_et_lot
|
||||
|
||||
ligne = api_client.get(f"/api/immeubles/{immeuble.id}/lots/referentiel").json()[0]
|
||||
|
||||
assert ligne["nb_revenus"] == 1
|
||||
# La depense de la fixture porte sur l'immeuble (lot_id NULL). La compter
|
||||
# ici rendrait tout lot de l'immeuble faussement non supprimable.
|
||||
assert ligne["nb_depenses"] == 0
|
||||
|
||||
|
||||
def test_compte_les_depenses_propres_au_lot(api_client, immeuble_et_lot, db_session):
|
||||
"""Une depense rattachee au lot, elle, doit bien remonter sur sa ligne."""
|
||||
immeuble, lot = immeuble_et_lot
|
||||
depense = db_session.query(Depense).one()
|
||||
depense.lot_id = lot.id
|
||||
db_session.commit()
|
||||
|
||||
ligne = api_client.get(f"/api/immeubles/{immeuble.id}/lots/referentiel").json()[0]
|
||||
|
||||
assert ligne["nb_depenses"] == 1
|
||||
Reference in New Issue
Block a user