Files
MABEA/backend/app/services/wartung.py
T
patrickandClaude Sonnet 5 2a4d391711
CI / backend-tests (push) Failing after 2m4s
CI / frontend-build (push) Successful in 18s
feat(maintenance): MAINT-Epic (Wartungspläne/-aufträge, Werkstatt) + Akte-Redesign
Neues Wartungskonzept: Wartungsplan+Position+Intervall (zeit/km/
Betriebsstunden, kombinierbar - fällig sobald ein Kriterium erreicht
ist), Wartungsauftrag mit intern/externer Werkstatt, Status offen/
in_arbeit/erledigt, Kosten, Historie, Dokument-Anhang. Ad-hoc-Aufträge
ohne Plan möglich (z.B. kleinere Reparatur). Admin-Tab "Wartungspläne".

Objektakte-Redesign (Epic 21, Nutzer-Feedback "wirkt nicht modern"):
CSS-Grid statt Karten-Stapel, einspaltig mobil, zweispaltig ab 900px;
Fahrzeug-Objekte zeigen Wartung/Fahrzeugdaten prominent, Geräte/
Rucksäcke die Beladung. Dabei gefundene Lücke: Beladung (Soll/Ist je
Position) fehlte in der Akte komplett, nur über eine laufende
Kontrolle sichtbar - jetzt eigene Karte.

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

217 lines
7.9 KiB
Python

from datetime import date, datetime, timezone
from decimal import Decimal
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.models.fahrzeugdetails import Fahrzeugdetails
from app.models.objekt import Objekt
from app.models.wartung import (
IntervallTyp,
Wartungsauftrag,
WartungsauftragStatus,
Wartungsintervall,
Wartungsplan,
WartungsplanPosition,
)
from app.schemas.wartung import WartungsplanPositionCreate
from app.services.datumshilfen import plus_monate
async def erstelle_wartungsplan(
db: AsyncSession, *, objekttyp_id: int, name: str, positionen: list[WartungsplanPositionCreate]
) -> Wartungsplan:
plan = Wartungsplan(objekttyp_id=objekttyp_id, name=name)
db.add(plan)
await db.flush()
for pos in positionen:
position = WartungsplanPosition(
wartungsplan_id=plan.id, wartungsart=pos.wartungsart, beschreibung=pos.beschreibung
)
db.add(position)
await db.flush()
for intervall in pos.intervalle:
db.add(
Wartungsintervall(
wartungsplan_position_id=position.id, typ=intervall.typ, wert=intervall.wert
)
)
await db.flush()
return plan
async def hole_positionen_mit_intervallen(db: AsyncSession, wartungsplan_id: int) -> list[WartungsplanPosition]:
result = await db.execute(
select(WartungsplanPosition).where(WartungsplanPosition.wartungsplan_id == wartungsplan_id)
)
return list(result.scalars().all())
async def hole_intervalle(db: AsyncSession, wartungsplan_position_id: int) -> list[Wartungsintervall]:
result = await db.execute(
select(Wartungsintervall).where(Wartungsintervall.wartungsplan_position_id == wartungsplan_position_id)
)
return list(result.scalars().all())
async def erstelle_wartungsauftrag(
db: AsyncSession,
*,
objekt_id: int,
geraet_instanz_id,
wartungsplan_position_id: int | None,
wartungsart: str,
beschreibung: str | None,
werkstatt_intern: bool,
werkstatt_name: str | None,
faelligkeit_am: date | None,
) -> Wartungsauftrag:
auftrag = Wartungsauftrag(
objekt_id=objekt_id,
geraet_instanz_id=geraet_instanz_id,
wartungsplan_position_id=wartungsplan_position_id,
wartungsart=wartungsart,
beschreibung=beschreibung,
status=WartungsauftragStatus.offen,
werkstatt_intern=werkstatt_intern,
werkstatt_name=werkstatt_name,
faelligkeit_am=faelligkeit_am,
erstellt_am=datetime.now(timezone.utc),
)
db.add(auftrag)
await db.flush()
return auftrag
async def aendere_status(db: AsyncSession, *, auftrag: Wartungsauftrag, status: WartungsauftragStatus) -> Wartungsauftrag:
auftrag.status = status
await db.flush()
return auftrag
async def erledige_wartungsauftrag(
db: AsyncSession,
*,
auftrag: Wartungsauftrag,
benutzer_id: int,
erledigt_am: date | None,
kosten: Decimal | None,
kilometerstand_bei_erledigung: int | None,
betriebsstunden_bei_erledigung: Decimal | None,
) -> Wartungsauftrag:
auftrag.status = WartungsauftragStatus.erledigt
auftrag.erledigt_am = (
datetime.combine(erledigt_am, datetime.min.time(), tzinfo=timezone.utc)
if erledigt_am
else datetime.now(timezone.utc)
)
auftrag.durchgefuehrt_von = benutzer_id
auftrag.kosten = kosten
if kilometerstand_bei_erledigung is None or betriebsstunden_bei_erledigung is None:
fahrzeugdetails = await db.get(Fahrzeugdetails, auftrag.objekt_id)
if fahrzeugdetails is not None:
if kilometerstand_bei_erledigung is None:
kilometerstand_bei_erledigung = fahrzeugdetails.kilometerstand
if betriebsstunden_bei_erledigung is None:
betriebsstunden_bei_erledigung = fahrzeugdetails.betriebsstunden
auftrag.kilometerstand_bei_erledigung = kilometerstand_bei_erledigung
auftrag.betriebsstunden_bei_erledigung = betriebsstunden_bei_erledigung
await db.flush()
return auftrag
async def liste_wartungsauftraege(
db: AsyncSession, *, objekt_id: int | None = None, status: WartungsauftragStatus | None = None
) -> list[Wartungsauftrag]:
stmt = select(Wartungsauftrag)
if objekt_id is not None:
stmt = stmt.where(Wartungsauftrag.objekt_id == objekt_id)
if status is not None:
stmt = stmt.where(Wartungsauftrag.status == status)
stmt = stmt.order_by(Wartungsauftrag.erstellt_am.desc())
result = await db.execute(stmt)
return list(result.scalars().all())
async def faellige_wartungen_fuer_objekt(db: AsyncSession, objekt: Objekt) -> list[dict]:
"""MAINT-002: kombiniert Zeit-/km-/Betriebsstunden-Intervalle je Position -
fällig, sobald das erste Kriterium erreicht ist. Nie durchgeführt gilt
immer als fällig (wie "nie kontrolliert" bei der Einsatzbereitschaft)."""
plaene = (
await db.execute(select(Wartungsplan).where(Wartungsplan.objekttyp_id == objekt.objekttyp_id))
).scalars().all()
if not plaene:
return []
fahrzeugdetails = await db.get(Fahrzeugdetails, objekt.id)
heute = datetime.now(timezone.utc).date()
ergebnisse: list[dict] = []
for plan in plaene:
positionen = await hole_positionen_mit_intervallen(db, plan.id)
for position in positionen:
intervalle = await hole_intervalle(db, position.id)
letzter = (
await db.execute(
select(Wartungsauftrag)
.where(
Wartungsauftrag.objekt_id == objekt.id,
Wartungsauftrag.wartungsplan_position_id == position.id,
Wartungsauftrag.status == WartungsauftragStatus.erledigt,
)
.order_by(Wartungsauftrag.erledigt_am.desc())
.limit(1)
)
).scalar_one_or_none()
if letzter is None:
ergebnisse.append(
{
"wartungsplan_position_id": position.id,
"wartungsart": position.wartungsart,
"objekt_id": objekt.id,
"faellig": True,
"faellig_am": None,
"faellig_bei_km": None,
"faellig_bei_betriebsstunden": None,
}
)
continue
faellig = False
faellig_am = None
faellig_bei_km = None
faellig_bei_bs = None
for intervall in intervalle:
if intervall.typ == IntervallTyp.zeit:
faellig_am = plus_monate(letzter.erledigt_am.date(), intervall.wert)
if heute >= faellig_am:
faellig = True
elif intervall.typ == IntervallTyp.km and fahrzeugdetails is not None:
basis_km = letzter.kilometerstand_bei_erledigung or 0
faellig_bei_km = basis_km + intervall.wert
if (fahrzeugdetails.kilometerstand or 0) >= faellig_bei_km:
faellig = True
elif intervall.typ == IntervallTyp.betriebsstunden and fahrzeugdetails is not None:
basis_bs = letzter.betriebsstunden_bei_erledigung or Decimal(0)
faellig_bei_bs = basis_bs + intervall.wert
if (fahrzeugdetails.betriebsstunden or Decimal(0)) >= faellig_bei_bs:
faellig = True
ergebnisse.append(
{
"wartungsplan_position_id": position.id,
"wartungsart": position.wartungsart,
"objekt_id": objekt.id,
"faellig": faellig,
"faellig_am": faellig_am,
"faellig_bei_km": faellig_bei_km,
"faellig_bei_betriebsstunden": faellig_bei_bs,
}
)
return ergebnisse