feat: étend le référentiel au parc entier et à la suppression des lots
Trois manques que la saisie a fait apparaître : Le tableau porte une colonne immeuble, la liste ne peut donc plus être suspendue à un immeuble choisi d'avance : elle renvoie tout le parc, chaque ligne emportant de quoi nommer son immeuble sans requête de plus. Un lot qu'aucune ligne de compte rendu ne mentionne ne décrit rien : il encombre la saisie et doit pouvoir disparaître. La garde est côté serveur — un lot porteur de revenus ou de dépenses est refusé, ses montants partiraient avec lui. Le nom d'usage s'enregistre, et la réponse recalcule les compteurs de l'immeuble plutôt que de les laisser à zéro : elle remplace l'immeuble dans les listes du client, qui le croirait vide de lots. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -5,7 +5,7 @@ DPE, chauffage) et donnent au référentiel une source de vérité indépendante
|
||||
l'extraction.
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from sqlalchemy import func, select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
@@ -16,6 +16,8 @@ from ...utils.logements import delta_surface, dpe_echeance, type_en_ecart
|
||||
from ..schemas.models import (
|
||||
CaracteristiquesBody,
|
||||
CaracteristiquesResponse,
|
||||
ImmeubleBody,
|
||||
ImmeubleResponse,
|
||||
LotReferentielResponse,
|
||||
)
|
||||
|
||||
@@ -44,8 +46,9 @@ def _caracteristiques_response(
|
||||
def _lot_response(
|
||||
lot: Lot,
|
||||
immeuble_code: str | None,
|
||||
nb_revenus: int = 0,
|
||||
nb_depenses: int = 0,
|
||||
nb_revenus: int,
|
||||
nb_depenses: int,
|
||||
immeuble_denomination: str | None = None,
|
||||
) -> LotReferentielResponse:
|
||||
"""Assemble la ligne de référentiel d'un lot."""
|
||||
fiche = lot.caracteristiques
|
||||
@@ -55,6 +58,7 @@ def _lot_response(
|
||||
numero=lot.numero,
|
||||
immeuble_id=lot.immeuble_id,
|
||||
immeuble_code=immeuble_code,
|
||||
immeuble_denomination=immeuble_denomination,
|
||||
type_extrait=lot.type,
|
||||
type_effectif=type_effectif(lot),
|
||||
type_ecart=type_en_ecart(lot.type, fiche.type if fiche else None),
|
||||
@@ -64,27 +68,16 @@ def _lot_response(
|
||||
)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/immeubles/{immeuble_id}/lots/referentiel",
|
||||
response_model=list[LotReferentielResponse],
|
||||
)
|
||||
async def list_lots_referentiel(
|
||||
immeuble_id: int,
|
||||
session: Session = Depends(get_session),
|
||||
) -> list[LotReferentielResponse]:
|
||||
"""Liste les lots d'un immeuble avec leur fiche de caractéristiques.
|
||||
def _requete_lignes():
|
||||
"""Requête des lots avec le nombre de revenus et de dépenses rattachés.
|
||||
|
||||
- **immeuble_id**: ID de l'immeuble
|
||||
Partagée par la liste et l'enregistrement : une ligne renvoyée après
|
||||
écriture doit être comptée comme celle du tableau, sinon un lot bien occupé
|
||||
se met à passer pour inutilisé dès qu'on le décrit.
|
||||
|
||||
Les lots sans fiche sont renvoyés avec `caracteristiques` à `null` : le
|
||||
tableau de saisie doit montrer les lignes vides autant que les remplies.
|
||||
Sous-requêtes corrélées plutôt que des jointures : compter revenus et
|
||||
dépenses dans la même jointure multiplierait les lignes entre elles.
|
||||
"""
|
||||
immeuble = session.get(Immeuble, immeuble_id)
|
||||
if immeuble is None:
|
||||
raise HTTPException(status_code=404, detail="Immeuble introuvable.")
|
||||
|
||||
# Sous-requêtes corrélées plutôt que des jointures : compter revenus et
|
||||
# dépenses dans la même jointure multiplierait les lignes entre elles.
|
||||
nb_revenus = (
|
||||
select(func.count(Revenu.id))
|
||||
.where(Revenu.lot_id == Lot.id)
|
||||
@@ -98,16 +91,41 @@ async def list_lots_referentiel(
|
||||
.scalar_subquery()
|
||||
)
|
||||
|
||||
return select(Lot, nb_revenus.label("nb_revenus"), nb_depenses.label("nb_depenses"))
|
||||
|
||||
|
||||
@router.get("/lots/referentiel", response_model=list[LotReferentielResponse])
|
||||
async def list_lots_referentiel(
|
||||
immeuble_id: int | None = Query(None, description="Filtrer par immeuble"),
|
||||
session: Session = Depends(get_session),
|
||||
) -> list[LotReferentielResponse]:
|
||||
"""Liste les lots avec leur fiche de caractéristiques.
|
||||
|
||||
- **immeuble_id**: ID de l'immeuble pour restreindre la liste (optionnel)
|
||||
|
||||
Tout le parc par défaut : le tableau de saisie porte une colonne immeuble,
|
||||
et comparer deux immeubles au m² n'a de sens que s'ils s'affichent ensemble.
|
||||
|
||||
Les lots sans fiche sont renvoyés avec `caracteristiques` à `null` : le
|
||||
tableau doit montrer les lignes vides autant que les remplies.
|
||||
"""
|
||||
if immeuble_id is not None and session.get(Immeuble, immeuble_id) is None:
|
||||
raise HTTPException(status_code=404, detail="Immeuble introuvable.")
|
||||
|
||||
stmt = (
|
||||
select(Lot, nb_revenus.label("nb_revenus"), nb_depenses.label("nb_depenses"))
|
||||
.where(Lot.immeuble_id == immeuble_id)
|
||||
.order_by(Lot.numero)
|
||||
_requete_lignes()
|
||||
.add_columns(Immeuble.code, Immeuble.denomination)
|
||||
.join(Immeuble, Lot.immeuble_id == Immeuble.id)
|
||||
.order_by(Immeuble.code, Lot.numero)
|
||||
)
|
||||
if immeuble_id is not None:
|
||||
stmt = stmt.where(Lot.immeuble_id == immeuble_id)
|
||||
|
||||
return [
|
||||
_lot_response(
|
||||
row.Lot,
|
||||
immeuble.code,
|
||||
row.code,
|
||||
immeuble_denomination=row.denomination,
|
||||
nb_revenus=row.nb_revenus or 0,
|
||||
nb_depenses=row.nb_depenses or 0,
|
||||
)
|
||||
@@ -142,7 +160,89 @@ async def upsert_caracteristiques(
|
||||
setattr(fiche, champ, getattr(body, champ))
|
||||
|
||||
session.commit()
|
||||
session.refresh(lot)
|
||||
|
||||
row = session.execute(_requete_lignes().where(Lot.id == lot_id)).one()
|
||||
immeuble = session.get(Immeuble, lot.immeuble_id)
|
||||
return _lot_response(lot, immeuble.code if immeuble else None)
|
||||
return _lot_response(
|
||||
row.Lot,
|
||||
immeuble.code if immeuble else None,
|
||||
immeuble_denomination=immeuble.denomination if immeuble else None,
|
||||
nb_revenus=row.nb_revenus or 0,
|
||||
nb_depenses=row.nb_depenses or 0,
|
||||
)
|
||||
|
||||
|
||||
@router.delete("/lots/{lot_id}", status_code=204)
|
||||
async def supprimer_lot(
|
||||
lot_id: int,
|
||||
session: Session = Depends(get_session),
|
||||
) -> None:
|
||||
"""Supprime un lot que rien ne rattache à un document.
|
||||
|
||||
- **lot_id**: ID du lot
|
||||
|
||||
Sert à nettoyer les lots laissés par un ancien format de numérotation, qui
|
||||
encombrent le tableau de saisie sans rien décrire. Un lot qui porte des
|
||||
revenus ou des dépenses est refusé : le supprimer emporterait des montants
|
||||
du compte rendu, et un compte faux est pire qu'une ligne en trop.
|
||||
"""
|
||||
lot = session.get(Lot, lot_id)
|
||||
if lot is None:
|
||||
raise HTTPException(status_code=404, detail="Lot introuvable.")
|
||||
|
||||
row = session.execute(_requete_lignes().where(Lot.id == lot_id)).one()
|
||||
if row.nb_revenus or row.nb_depenses:
|
||||
raise HTTPException(
|
||||
status_code=409,
|
||||
detail=(
|
||||
f"Le lot {lot.numero} porte {row.nb_revenus} revenu(s) et "
|
||||
f"{row.nb_depenses} dépense(s) : il ne peut pas être supprimé."
|
||||
),
|
||||
)
|
||||
|
||||
# Les locataires du lot partent avec lui (cascade). Un locataire qui aurait
|
||||
# encore des revenus les rattacherait au lot, deja refuse ci-dessus.
|
||||
session.delete(lot)
|
||||
session.commit()
|
||||
|
||||
|
||||
@router.put("/immeubles/{immeuble_id}", response_model=ImmeubleResponse)
|
||||
async def renommer_immeuble(
|
||||
immeuble_id: int,
|
||||
body: ImmeubleBody,
|
||||
session: Session = Depends(get_session),
|
||||
) -> ImmeubleResponse:
|
||||
"""Donne à l'immeuble son nom d'usage.
|
||||
|
||||
- **immeuble_id**: ID de l'immeuble
|
||||
|
||||
Le code de gestion ("33689020") vient des PDF et ne se remplace pas ; la
|
||||
dénomination ("Servient") s'affiche à sa place partout où l'immeuble est
|
||||
cité.
|
||||
"""
|
||||
immeuble = session.get(Immeuble, immeuble_id)
|
||||
if immeuble is None:
|
||||
raise HTTPException(status_code=404, detail="Immeuble introuvable.")
|
||||
|
||||
immeuble.denomination = body.denomination
|
||||
session.commit()
|
||||
|
||||
# Compteurs recalcules plutot que laisses a zero : la reponse remplace
|
||||
# l'immeuble dans les listes du client, qui le croirait vide de lots.
|
||||
nb_lots = session.execute(
|
||||
select(func.count(Lot.id)).where(Lot.immeuble_id == immeuble.id)
|
||||
).scalar_one()
|
||||
nb_depenses = session.execute(
|
||||
select(func.count(Depense.id)).where(Depense.immeuble_id == immeuble.id)
|
||||
).scalar_one()
|
||||
|
||||
return ImmeubleResponse(
|
||||
id=immeuble.id,
|
||||
code=immeuble.code,
|
||||
denomination=immeuble.denomination,
|
||||
adresse=immeuble.adresse,
|
||||
ville=immeuble.ville,
|
||||
code_postal=immeuble.code_postal,
|
||||
nb_lots=nb_lots,
|
||||
nb_depenses=nb_depenses,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user