feat(fahrzeug): Fahrzeug-Spezialfelder + 5-stufiger Status (Roadmap Phase 3)
Neue Tabelle fahrzeugdetails (1:1 zu Objekt, additiv/optional): Kennzeichen, Funkrufname, Baujahr, Kilometerstand, Betriebsstunden, nächste HU/UVV. objekt_status um 'in_wartung' erweitert (Nutzer-Vorgabe: einsatzbereit/ eingeschränkt/nicht einsatzbereit/in Wartung/außer Dienst - die ersten drei bleiben weiterhin rein rechnerisch über dashboard.einsatzbereitschaft(), in_wartung/ausser_dienst sind die beiden manuell gesetzten Zustände). dashboard.einsatzbereitschaft(): ausser_dienst-Objekte zählen nicht mehr mit, in_wartung und überfällige HU/UVV sind neue Gründe für "nicht einsatzbereit". dashboard.bevorstehende_prueftermine() (Kachel 2) zeigt jetzt Geräte- UND Fahrzeugprüfungen (HU/UVV) in einer Liste (Nutzer-Vorgabe: ein Konzept für "welche Prüfungen bald fällig sind") - Schema dafür verallgemeinert (typ/bezeichnung/faelligkeitsdatum statt geräte-spezifischer Felder). Frontend: Status-Dropdown + Fahrzeugdetails-Formular in ObjektSection (nur für Objekttypen mit ist_zugfahrzeug=True), Dashboard-Kachel 2 zeigt gemischte Liste. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KC8HYvv6UkCVYheYiTw9DD
This commit is contained in:
@@ -3,11 +3,12 @@ from datetime import date, datetime, timedelta, timezone
|
||||
from sqlalchemy import func, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.models.fahrzeugdetails import Fahrzeugdetails
|
||||
from app.models.fehlbestand import Fehlbestand, FehlbestandStatus
|
||||
from app.models.geraet_instanz import GeraetInstanz
|
||||
from app.models.kontrolle import Kontrolle, KontrollStatus
|
||||
from app.models.mindermenge import MindermengeStatus, MindermengenGenehmigung
|
||||
from app.models.objekt import Objekt
|
||||
from app.models.objekt import Objekt, ObjektStatus
|
||||
from app.models.objektposition import Objektposition
|
||||
from app.models.stammdaten import Material
|
||||
from app.services.mangel import objekt_ids_mit_kritischem_offenem_mangel
|
||||
@@ -104,7 +105,12 @@ async def einsatzbereitschaft(db: AsyncSession) -> dict:
|
||||
(Leitplanke U2/Karte 08) - das Objekt bleibt daher "eingeschränkt
|
||||
einsatzbereit", nicht "einsatzbereit"."""
|
||||
heute = datetime.now(timezone.utc).date()
|
||||
objekte = (await db.execute(select(Objekt))).scalars().all()
|
||||
# ausser_dienst-Objekte zählen fachlich gar nicht mehr mit (Roadmap Phase 3,
|
||||
# Modul Fahrzeuge: 5-stufiger Status) - anders als in_wartung, das als
|
||||
# aktiver Grund weiter unten erscheint statt das Objekt komplett auszublenden.
|
||||
objekte = (
|
||||
(await db.execute(select(Objekt).where(Objekt.status != ObjektStatus.ausser_dienst))).scalars().all()
|
||||
)
|
||||
|
||||
offene_fehlbestaende = (
|
||||
await db.execute(
|
||||
@@ -163,6 +169,25 @@ async def einsatzbereitschaft(db: AsyncSession) -> dict:
|
||||
|
||||
objekt_ids_kritischer_mangel = await objekt_ids_mit_kritischem_offenem_mangel(db)
|
||||
|
||||
objekt_ids_hu_uvv_ueberfaellig = set(
|
||||
(
|
||||
await db.execute(
|
||||
select(func.distinct(Fahrzeugdetails.objekt_id)).where(
|
||||
(
|
||||
(Fahrzeugdetails.naechste_hu.is_not(None))
|
||||
& (Fahrzeugdetails.naechste_hu < heute)
|
||||
)
|
||||
| (
|
||||
(Fahrzeugdetails.naechste_uvv.is_not(None))
|
||||
& (Fahrzeugdetails.naechste_uvv < heute)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
.scalars()
|
||||
.all()
|
||||
)
|
||||
|
||||
bereit = eingeschraenkt = 0
|
||||
details: list[dict] = []
|
||||
for objekt in objekte:
|
||||
@@ -179,6 +204,10 @@ async def einsatzbereitschaft(db: AsyncSession) -> dict:
|
||||
gruende.append("kontrolle_laeuft")
|
||||
if objekt.id in objekt_ids_kritischer_mangel:
|
||||
gruende.append("mangel_kritisch_offen")
|
||||
if objekt.status == ObjektStatus.in_wartung:
|
||||
gruende.append("in_wartung")
|
||||
if objekt.id in objekt_ids_hu_uvv_ueberfaellig:
|
||||
gruende.append("hu_uvv_ueberfaellig")
|
||||
|
||||
if not gruende and fehlbestand_ids and fehlbestand_ids <= fehlbestand_ids_genehmigt:
|
||||
eingeschraenkt += 1
|
||||
@@ -205,30 +234,67 @@ async def bevorstehende_prueftermine(db: AsyncSession, warnzeitraum_tage: int =
|
||||
Endpoint /dashboard/ablaufdaten) - Prüfung ist Wartungsintervall, Ablauf ist
|
||||
Verbrauchsgut-Verfall, fachlich unterschiedlich (Nutzer-Entscheidung)."""
|
||||
heute = datetime.now(timezone.utc).date()
|
||||
eintraege = []
|
||||
|
||||
result = await db.execute(
|
||||
select(GeraetInstanz, Objektposition, Material)
|
||||
.join(Objektposition, GeraetInstanz.objektposition_id == Objektposition.id)
|
||||
.join(Material, Objektposition.material_id == Material.id)
|
||||
.where(GeraetInstanz.naechste_pruefung.is_not(None))
|
||||
)
|
||||
eintraege = []
|
||||
for geraet, objektposition, material in result.all():
|
||||
status_wert, verbleibende_tage = _ablauf_status(geraet.naechste_pruefung, warnzeitraum_tage, heute)
|
||||
if status_wert == "gueltig":
|
||||
continue
|
||||
eintraege.append(
|
||||
{
|
||||
"geraet_instanz_id": geraet.id,
|
||||
"typ": "geraet",
|
||||
"objekt_id": objektposition.objekt_id,
|
||||
"objektposition_id": objektposition.id,
|
||||
"material_id": material.id,
|
||||
"material_name": material.name,
|
||||
"seriennummer": geraet.seriennummer,
|
||||
"geraet_status": geraet.status,
|
||||
"naechste_pruefung": geraet.naechste_pruefung,
|
||||
"bezeichnung": f"{material.name} (SN {geraet.seriennummer})",
|
||||
"faelligkeitsdatum": geraet.naechste_pruefung,
|
||||
"verbleibende_tage": verbleibende_tage,
|
||||
"status": status_wert,
|
||||
"geraet_instanz_id": geraet.id,
|
||||
"objektposition_id": objektposition.id,
|
||||
"material_id": material.id,
|
||||
"seriennummer": geraet.seriennummer,
|
||||
}
|
||||
)
|
||||
|
||||
# Fahrzeug-HU/UVV (Roadmap Phase 3, Modul Fahrzeuge) - gleiche Kachel wie
|
||||
# Geräteprüfungen (Nutzer-Vorgabe: "welche Prüfungen bald fällig sind" ist
|
||||
# EIN Konzept, egal ob Gerät oder Fahrzeug).
|
||||
result = await db.execute(
|
||||
select(Fahrzeugdetails, Objekt)
|
||||
.join(Objekt, Fahrzeugdetails.objekt_id == Objekt.id)
|
||||
.where(
|
||||
(Fahrzeugdetails.naechste_hu.is_not(None)) | (Fahrzeugdetails.naechste_uvv.is_not(None))
|
||||
)
|
||||
)
|
||||
for details, objekt in result.all():
|
||||
for feld, typ, label in (
|
||||
(details.naechste_hu, "fahrzeug_hu", "HU"),
|
||||
(details.naechste_uvv, "fahrzeug_uvv", "UVV"),
|
||||
):
|
||||
if feld is None:
|
||||
continue
|
||||
status_wert, verbleibende_tage = _ablauf_status(feld, warnzeitraum_tage, heute)
|
||||
if status_wert == "gueltig":
|
||||
continue
|
||||
eintraege.append(
|
||||
{
|
||||
"typ": typ,
|
||||
"objekt_id": objekt.id,
|
||||
"bezeichnung": f"{label} {objekt.name} ({details.kennzeichen or objekt.code})",
|
||||
"faelligkeitsdatum": feld,
|
||||
"verbleibende_tage": verbleibende_tage,
|
||||
"status": status_wert,
|
||||
"geraet_instanz_id": None,
|
||||
"objektposition_id": None,
|
||||
"material_id": None,
|
||||
"seriennummer": None,
|
||||
}
|
||||
)
|
||||
|
||||
eintraege.sort(key=lambda e: e["verbleibende_tage"])
|
||||
return eintraege
|
||||
|
||||
Reference in New Issue
Block a user