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:
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