Files
MABEA/backend/app/services/wartung.py
T
patrickandClaude Sonnet 5 847c303a97
CI / backend-tests (push) Successful in 2m22s
CI / frontend-build (push) Successful in 25s
feat(wartung): MAINT-005 Ersatzteile/Kosten je Wartungsauftrag
Neue Tabelle wartungsauftrag_teil (Migration 0033), CRUD-Endpunkte
/wartungsauftraege/{id}/teile, UI-Panel in WartungSection.tsx mit
Summenanzeige. Backlog Epic 22 (UI-Redesign Runde 2, Kacheln UI2-001..005)
als geplant ergänzt, Umsetzung folgt separat.

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

239 lines
8.7 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,
WartungsauftragTeil,
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 erstelle_wartungsauftrag_teil(
db: AsyncSession, *, auftrag_id, bezeichnung: str, menge: Decimal, kosten: Decimal
) -> WartungsauftragTeil:
teil = WartungsauftragTeil(auftrag_id=auftrag_id, bezeichnung=bezeichnung, menge=menge, kosten=kosten)
db.add(teil)
await db.flush()
return teil
async def liste_wartungsauftrag_teile(db: AsyncSession, *, auftrag_id) -> list[WartungsauftragTeil]:
result = await db.execute(
select(WartungsauftragTeil).where(WartungsauftragTeil.auftrag_id == auftrag_id)
)
return list(result.scalars().all())
async def entferne_wartungsauftrag_teil(db: AsyncSession, *, teil: WartungsauftragTeil) -> None:
await db.delete(teil)
await db.flush()
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