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:
2026-07-26 18:00:42 +02:00
parent ac63922896
commit 5a08b0c7e5
13 changed files with 720 additions and 31 deletions

View File

@@ -0,0 +1,34 @@
"""Arbitrage entre ce que dit le PDF d'un lot et ce que sa fiche en dit.
Le type d'un lot est connu de deux sources : l'extraction PDF, qui le remplit
parfois mal et souvent pas du tout, et la fiche saisie à la main, qui fait foi.
La règle est donc « la fiche gagne, le PDF comble les trous » — et elle vit ici,
en un seul endroit, sous ses deux formes : une expression SQL pour les
agrégats, une fonction Python pour l'ORM. Les faire diverger reviendrait à
afficher deux types différents pour un même lot selon la page consultée.
Le désaccord entre les deux sources n'est jamais résolu en silence : il reste
visible via `type_en_ecart` (voir `utils.logements`).
"""
from sqlalchemy import Select, func
from ..database.models import Lot, LotCaracteristiques
#: Type de lot retenu, en SQL. Requiert la jointure de `joindre_fiche`.
TYPE_LOT_EFFECTIF = func.coalesce(LotCaracteristiques.type, Lot.type)
def joindre_fiche(stmt: Select) -> Select:
"""Ajoute à une requête sur `Lot` la jointure vers sa fiche.
La relation est 1↔1 : la jointure ne multiplie aucune ligne, elle peut donc
s'ajouter à une requête agrégée sans fausser les totaux.
"""
return stmt.outerjoin(LotCaracteristiques, LotCaracteristiques.lot_id == Lot.id)
def type_effectif(lot: Lot) -> str | None:
"""Type de lot retenu, depuis un objet chargé par l'ORM."""
fiche = lot.caracteristiques
return (fiche.type if fiche else None) or lot.type