Files
MABEA/backend/app/services/akte.py
T
patrickandClaude Sonnet 5 cc016f09d3
CI / backend-tests (push) Failing after 2m6s
CI / frontend-build (push) Successful in 18s
feat(akte): Ablaufdatum in der Beladung-Tabelle
Bevorstehende Ablaufdaten waren nur im Dashboard und als
Einsatzbereitschafts-Grund sichtbar, nicht direkt an der jeweiligen
Beladungsposition in der Akte. Neue Spalte "Ablauf", rot bei
überfälligem, gelb bei <30 Tagen verbleibender Zeit.

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

158 lines
6.6 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, GeraetStatus
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 Material, MaterialTyp, Objekttyp, Standort
from app.models.vorlage import Vorlagenposition
from app.models.zustaendigkeit import Kontrollverantwortung, Zustaendigkeit
from app.schemas.akte import AkteRead, BeladungsPosition, 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.dashboard import objekt_readiness
from app.services.objekte import letzte_kontrollen_map, sollmenge_effektiv
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))
fach_by_material_id: dict[int, str | None] = {}
if objekt.vorlage_id is not None:
vorlagenpositionen_result = await db.execute(
select(Vorlagenposition).where(Vorlagenposition.vorlage_id == objekt.vorlage_id)
)
fach_by_material_id = {vp.material_id: vp.fach for vp in vorlagenpositionen_result.scalars().all()}
einsatzbereite_geraete_je_position: dict = {}
for geraet in geraete:
if geraet.status == GeraetStatus.einsatzbereit:
einsatzbereite_geraete_je_position[geraet.objektposition_id] = (
einsatzbereite_geraete_je_position.get(geraet.objektposition_id, 0) + 1
)
beladung_result = await db.execute(
select(Objektposition, Material)
.join(Material, Objektposition.material_id == Material.id)
.where(Objektposition.objekt_id == objekt_id)
)
beladung = [
BeladungsPosition(
objektposition_id=position.id,
material_id=material.id,
material_name=material.name,
einheit=material.einheit,
fach=fach_by_material_id.get(material.id),
sollmenge=sollmenge_effektiv(position),
istmenge=(
einsatzbereite_geraete_je_position.get(position.id, 0)
if material.materialtyp == MaterialTyp.geraet_sn
else position.istmenge
),
ablaufdatum=position.ablaufdatum,
)
for position, material in beladung_result.all()
]
readiness = await objekt_readiness(db, objekt_id)
objekt_read = ObjektRead.model_validate(objekt).model_copy(
update={"letzte_kontrolle_am": letzte_map.get(objekt_id)}
)
return AkteRead(
objekt=objekt_read,
einsatzbereitschaft_status=readiness["status"],
einsatzbereitschaft_gruende=readiness["gruende"],
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()],
beladung=beladung,
)