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
217 lines
7.9 KiB
Python
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
|