Files
MABEA/backend/app/services/akte.py
T
patrickandClaude Sonnet 5 0c8f375a51
CI / backend-tests (push) Successful in 1m57s
CI / frontend-build (push) Successful in 18s
refactor: Code-Review-Fixes (Reuse/Simplification/Efficiency) aus dieser Session
4 parallele Review-Agenten (Reuse/Simplification/Efficiency/Altitude) gegen den
Diff dieser Session (c7dd47f...HEAD) laufen lassen, echte Funde angewendet:

- lager.py: 5x wiederholtes "db.get(...) or 404 raisen" durch _get_or_404()-
  Helper ersetzt
- akte.py: zwei Queries für Geräte-Instanzen (erst Positions-IDs, dann Geräte)
  zu einer Query mit Subquery zusammengefasst
- akte.py: manuelle Feld-für-Feld-Rekonstruktion von ObjektRead/HistorieRead
  durch model_validate()+model_copy() ersetzt (HistorieRead.benutzer_name
  bekommt dafür einen Default, harmlos für den bestehenden Endpunkt)
- vorlagen.py: doppelte "Positionen löschen + flush"-Logik (Vorlage-Löschen
  und Positionen-Ersetzen) in loesche_alle_positionen() zusammengeführt

Bewusst nicht angewendet:
- Zentrale Session-Rollback-Vereinheitlichung für die drei Pre-Check-Stellen
  (objekte.py Selbstbezug, geraet_instanz.py SN-Duplikat, lager.py Eltern-
  Selbstbezug) - der Altitude-Review schlug das vor, aber get_db() rollt in
  Produktion bei jeder Exception bereits korrekt zurück (jede Anfrage hat eine
  eigene Session); das PendingRollbackError-Problem trat nur in der geteilten
  Test-Session auf und wurde bereits gezielt per Pre-Check vermieden - eine
  zusätzliche zentrale Rollback-Schicht würde nichts mehr reparieren, was
  nicht schon repariert ist.
- Postgres-Cluster-Encoding (template1) auf beiden Hosts fixen, nicht nur in
  CI/für die eine migrierte DB - echter Infra-Eingriff, braucht Rücksprache.
- LagerSection.tsx "inkonsistente" Formular-Resets - beim genaueren Hinsehen
  gewolltes UX (Ort/Typ bleiben ausgewählt für mehrere Anlagen hintereinander).

140 Tests weiterhin grün.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CVgbozhYmuEhiEJHffRXCV
2026-09-05 23:49:28 +02:00

114 lines
4.7 KiB
Python

from sqlalchemy import or_, select
from sqlalchemy.ext.asyncio import AsyncSession
from app.models.auth import Benutzer
from app.models.dokument import Dokument
from app.models.fahrzeugdetails import Fahrzeugdetails
from app.models.geraet_instanz import GeraetInstanz
from app.models.historie import Historie
from app.models.mangel import Mangel
from app.models.objekt import Objekt
from app.models.objektposition import Objektposition
from app.models.stammdaten import Objekttyp, Standort
from app.models.zustaendigkeit import Kontrollverantwortung, Zustaendigkeit
from app.schemas.akte import AkteRead, ZustaendigerBenutzer
from app.schemas.dokument import DokumentRead
from app.schemas.geraet_instanz import GeraetInstanzRead
from app.schemas.historie import HistorieRead
from app.schemas.mangel import MangelRead
from app.schemas.objekt import FahrzeugdetailsRead, ObjektRead
from app.schemas.zustaendigkeit import KontrollverantwortungRead
from app.services.objekte import letzte_kontrollen_map
async def _zustaendige_benutzer(db: AsyncSession, objekt: Objekt) -> list[ZustaendigerBenutzer]:
"""Karte 04: Umkehrung von zustaendige_objekte() - wer ist für DIESES Objekt
zuständig (Vereinigung Standort-Vererbung + objektspezifische Zeile)."""
stmt = (
select(Benutzer.id, Benutzer.name, Zustaendigkeit.standort_id, Zustaendigkeit.objekt_id)
.join(
Zustaendigkeit,
or_(
Zustaendigkeit.standort_id == objekt.standort_id,
Zustaendigkeit.objekt_id == objekt.id,
),
)
.where(Benutzer.id == Zustaendigkeit.benutzer_id)
)
result = await db.execute(stmt)
gesehen: set[int] = set()
ergebnis: list[ZustaendigerBenutzer] = []
for benutzer_id, name, standort_id, objekt_id in result.all():
if benutzer_id in gesehen:
continue
gesehen.add(benutzer_id)
quelle = "objekt" if objekt_id == objekt.id else "standort"
ergebnis.append(ZustaendigerBenutzer(id=benutzer_id, name=name, quelle=quelle))
return ergebnis
async def akte_fuer_objekt(db: AsyncSession, objekt_id: int) -> AkteRead | None:
"""FILE-001: bündelt Stammdaten/Standort/Verantwortlichkeit/Historie/
Dokumente/Prüfungen/Mängel für ein Objekt zu einer Aggregations-Sicht."""
objekt = await db.get(Objekt, objekt_id)
if objekt is None:
return None
objekttyp = await db.get(Objekttyp, objekt.objekttyp_id)
standort = await db.get(Standort, objekt.standort_id)
fahrzeugdetails = await db.get(Fahrzeugdetails, objekt_id)
letzte_map = await letzte_kontrollen_map(db, [objekt_id])
zustaendige = await _zustaendige_benutzer(db, objekt)
kontrollverantwortung_result = await db.execute(
select(Kontrollverantwortung).where(Kontrollverantwortung.objekt_id == objekt_id)
)
historie_result = await db.execute(
select(Historie, Benutzer.name)
.outerjoin(Benutzer, Benutzer.id == Historie.benutzer_id)
.where(Historie.entitaet_typ == "objekt", Historie.entitaet_id == str(objekt_id))
.order_by(Historie.zeitpunkt.desc())
.limit(50)
)
dokumente_result = await db.execute(
select(Dokument).where(Dokument.entitaet_typ == "objekt", Dokument.entitaet_id == str(objekt_id))
)
geraete_result = await db.execute(
select(GeraetInstanz).where(
GeraetInstanz.objektposition_id.in_(
select(Objektposition.id).where(Objektposition.objekt_id == objekt_id)
)
)
)
geraete = list(geraete_result.scalars().all())
maengel_result = await db.execute(select(Mangel).where(Mangel.objekt_id == objekt_id))
objekt_read = ObjektRead.model_validate(objekt).model_copy(
update={"letzte_kontrolle_am": letzte_map.get(objekt_id)}
)
return AkteRead(
objekt=objekt_read,
objekttyp_name=objekttyp.name if objekttyp else "",
standort_name=standort.name if standort else "",
fahrzeugdetails=FahrzeugdetailsRead.model_validate(fahrzeugdetails) if fahrzeugdetails else None,
zustaendige_benutzer=zustaendige,
kontrollverantwortung=[
KontrollverantwortungRead.model_validate(k) for k in kontrollverantwortung_result.scalars().all()
],
letzte_kontrolle_am=letzte_map.get(objekt_id),
historie=[
HistorieRead.model_validate(eintrag).model_copy(update={"benutzer_name": benutzer_name})
for eintrag, benutzer_name in historie_result.all()
],
dokumente=[DokumentRead.model_validate(d) for d in dokumente_result.scalars().all()],
geraete=[GeraetInstanzRead.model_validate(g) for g in geraete],
maengel=[MangelRead.model_validate(m) for m in maengel_result.scalars().all()],
)