_synchronisiere_fehlbestand suchte nur nach Fehlbeständen der eigenen
Kontroll-Sitzung. Ein bereits offener Fehlbestand aus einer früheren
Kontrolle für dasselbe Objekt+Material wurde nicht wiedergefunden -
es entstand ein zweiter, paralleler Fehlbestand mit eigener (neuerer)
Sollmenge, der alte blieb mit veralteter Sollmenge offen liegen.
Lookup jetzt über objekt_id+material_id+status=offen statt
kontrolle_id; kontrolle_id am gefundenen Datensatz bleibt unverändert,
damit ein Abbruch der aktuellen Kontrolle ihn nicht fälschlich
mitlöscht. Löst sich die Abweichung über eine andere Kontrolle als
die erzeugende auf, wird der Fehlbestand jetzt sauber als erledigt
markiert (inkl. Mindermenge-Abschluss) statt stillschweigend gelöscht.
Nebenbei: Kommentare mit Kachel-/Datums-Referenzen ("Bug-Fund
2026-09-06", "UI-00X (Epic 20)") aus dem Code dieser Session entfernt.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CVgbozhYmuEhiEJHffRXCV
478 lines
19 KiB
Python
478 lines
19 KiB
Python
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.auth import Benutzer
|
|
from app.models.objekt import Objekt, ObjektStatus
|
|
from app.models.objektposition import Objektposition
|
|
from app.models.personal import BenutzerQualifikation, Qualifikationstyp
|
|
from app.models.stammdaten import Material
|
|
from app.services.mangel import objekt_ids_mit_kritischem_offenem_mangel
|
|
from app.services.objekte import letzte_kontrollen_map
|
|
|
|
# Prompt 12.2: offen/in_bearbeitung/nachgefuellt_teilweise zählen gemeinsam als "offen"
|
|
# (technischer Substatus ist keine Erledigung, Prompt 03/20).
|
|
OFFEN_STATI = (FehlbestandStatus.offen, FehlbestandStatus.in_bearbeitung, FehlbestandStatus.nachgefuellt_teilweise)
|
|
|
|
STANDARD_WARNZEITRAUM_TAGE = 30
|
|
|
|
|
|
async def kennzahlen(db: AsyncSession) -> dict:
|
|
jetzt = datetime.now(timezone.utc)
|
|
|
|
result = await db.execute(select(func.count()).select_from(Fehlbestand).where(Fehlbestand.status.in_(OFFEN_STATI)))
|
|
anzahl_offen = result.scalar_one()
|
|
|
|
result = await db.execute(
|
|
select(func.count(func.distinct(MindermengenGenehmigung.fehlbestand_id))).where(
|
|
MindermengenGenehmigung.status == MindermengeStatus.aktiv
|
|
)
|
|
)
|
|
anzahl_genehmigt = result.scalar_one()
|
|
|
|
result = await db.execute(
|
|
select(func.count())
|
|
.select_from(Fehlbestand)
|
|
.where(Fehlbestand.status == FehlbestandStatus.erledigt, Fehlbestand.erledigt_am >= jetzt - timedelta(days=7))
|
|
)
|
|
anzahl_kuerzlich_erledigt = result.scalar_one()
|
|
|
|
result = await db.execute(select(func.count(func.distinct(Fehlbestand.objekt_id))).where(Fehlbestand.status.in_(OFFEN_STATI)))
|
|
anzahl_problematische_objekte = result.scalar_one()
|
|
|
|
result = await db.execute(select(func.count()).select_from(Objekt))
|
|
anzahl_objekte_gesamt = result.scalar_one()
|
|
|
|
ablaufdaten_liste = await bevorstehende_ablaufdaten(db)
|
|
|
|
return {
|
|
"anzahl_offener_fehlbestaende": anzahl_offen,
|
|
"anzahl_genehmigter_mindermengen": anzahl_genehmigt,
|
|
"anzahl_kuerzlich_erledigter_fehlbestaende": anzahl_kuerzlich_erledigt,
|
|
"anzahl_bevorstehender_ablaufdaten": len(ablaufdaten_liste),
|
|
"anzahl_problematischer_objekte": anzahl_problematische_objekte,
|
|
"anzahl_objekte_gesamt": anzahl_objekte_gesamt,
|
|
}
|
|
|
|
|
|
def _ablauf_status(ablaufdatum: date, warnzeitraum_tage: int, heute: date) -> tuple[str, int]:
|
|
verbleibende_tage = (ablaufdatum - heute).days
|
|
if verbleibende_tage < 0:
|
|
return "abgelaufen", verbleibende_tage
|
|
if verbleibende_tage <= warnzeitraum_tage:
|
|
return "bald_ablaufend", verbleibende_tage
|
|
return "gueltig", verbleibende_tage
|
|
|
|
|
|
async def bevorstehende_ablaufdaten(db: AsyncSession) -> list[dict]:
|
|
"""Prompt 14.5: nur bald_ablaufend/abgelaufen, sortiert nach verbleibenden Tagen
|
|
aufsteigend (dringendste zuerst). "gueltig" gehört nicht in diese Liste."""
|
|
heute = datetime.now(timezone.utc).date()
|
|
result = await db.execute(
|
|
select(Objektposition, Material)
|
|
.join(Material, Objektposition.material_id == Material.id)
|
|
.where(Objektposition.ablaufdatum.is_not(None))
|
|
)
|
|
eintraege = []
|
|
for objektposition, material in result.all():
|
|
warnzeitraum = material.warnzeitraum_tage or STANDARD_WARNZEITRAUM_TAGE
|
|
status_wert, verbleibende_tage = _ablauf_status(objektposition.ablaufdatum, warnzeitraum, heute)
|
|
if status_wert == "gueltig":
|
|
continue
|
|
eintraege.append(
|
|
{
|
|
"objekt_id": objektposition.objekt_id,
|
|
"material_id": objektposition.material_id,
|
|
"ablaufdatum": objektposition.ablaufdatum,
|
|
"verbleibende_tage": verbleibende_tage,
|
|
"status": status_wert,
|
|
}
|
|
)
|
|
eintraege.sort(key=lambda e: e["verbleibende_tage"])
|
|
return eintraege
|
|
|
|
|
|
async def einsatzbereitschaft(db: AsyncSession) -> dict:
|
|
"""Karte-Nachtrag (Leitungs-Dashboard Kachel 1): ein Objekt ist einsatzbereit,
|
|
wenn kein offener Fehlbestand (deckt auch Geräte-Instanzen ab, da
|
|
Statuswechsel defekt/in_reparatur automatisch einen Fehlbestand erzeugt,
|
|
siehe app/services/geraet_instanz.py), kein abgelaufenes ablaufdatum, keine
|
|
überfällige Geräteprüfung und keine laufende Kontrolle (Sperre) vorliegt.
|
|
Aktive Mindermengen-Genehmigung erledigt den Fehlbestand fachlich NICHT
|
|
(Leitplanke U2/Karte 08) - das Objekt bleibt daher "eingeschränkt
|
|
einsatzbereit", nicht "einsatzbereit"."""
|
|
heute = datetime.now(timezone.utc).date()
|
|
# 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(
|
|
select(Fehlbestand.objekt_id, Fehlbestand.id).where(Fehlbestand.status.in_(OFFEN_STATI))
|
|
)
|
|
).all()
|
|
objekt_ids_mit_fehlbestand: dict[int, set] = {}
|
|
for objekt_id, fehlbestand_id in offene_fehlbestaende:
|
|
objekt_ids_mit_fehlbestand.setdefault(objekt_id, set()).add(fehlbestand_id)
|
|
|
|
fehlbestand_ids_genehmigt = set(
|
|
(
|
|
await db.execute(
|
|
select(func.distinct(MindermengenGenehmigung.fehlbestand_id)).where(
|
|
MindermengenGenehmigung.status == MindermengeStatus.aktiv
|
|
)
|
|
)
|
|
)
|
|
.scalars()
|
|
.all()
|
|
)
|
|
|
|
objekt_ids_abgelaufen = set(
|
|
(
|
|
await db.execute(
|
|
select(func.distinct(Objektposition.objekt_id)).where(
|
|
Objektposition.ablaufdatum.is_not(None), Objektposition.ablaufdatum < heute
|
|
)
|
|
)
|
|
)
|
|
.scalars()
|
|
.all()
|
|
)
|
|
|
|
objekt_ids_pruefung_ueberfaellig = set(
|
|
(
|
|
await db.execute(
|
|
select(func.distinct(Objektposition.objekt_id))
|
|
.join(GeraetInstanz, GeraetInstanz.objektposition_id == Objektposition.id)
|
|
.where(GeraetInstanz.naechste_pruefung.is_not(None), GeraetInstanz.naechste_pruefung < heute)
|
|
)
|
|
)
|
|
.scalars()
|
|
.all()
|
|
)
|
|
|
|
objekt_ids_gesperrt = set(
|
|
(
|
|
await db.execute(
|
|
select(func.distinct(Kontrolle.objekt_id)).where(Kontrolle.status == KontrollStatus.in_bearbeitung)
|
|
)
|
|
)
|
|
.scalars()
|
|
.all()
|
|
)
|
|
|
|
objekt_ids_kritischer_mangel = await objekt_ids_mit_kritischem_offenem_mangel(db)
|
|
|
|
# Nutzer-Fund (Readiness-Review): ein nie kontrolliertes Objekt hatte bisher als
|
|
# "einsatzbereit" gegolten, weil keine Kriterien verletzt waren - fachlich falsch,
|
|
# "nie geprüft" ist nicht dasselbe wie "geprüft und in Ordnung".
|
|
letzte_kontrolle_map = await letzte_kontrollen_map(db, [o.id for o in objekte])
|
|
|
|
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 = unbekannt = 0
|
|
nie_kontrolliert_gesamt = 0
|
|
details: list[dict] = []
|
|
for objekt in objekte:
|
|
kategorie, gruende = _kategorie_und_gruende(
|
|
objekt,
|
|
letzte_kontrolle_map.get(objekt.id),
|
|
objekt_ids_mit_fehlbestand.get(objekt.id, set()),
|
|
fehlbestand_ids_genehmigt,
|
|
objekt_ids_abgelaufen,
|
|
objekt_ids_pruefung_ueberfaellig,
|
|
objekt_ids_gesperrt,
|
|
objekt_ids_kritischer_mangel,
|
|
objekt_ids_hu_uvv_ueberfaellig,
|
|
)
|
|
if "noch_nie_kontrolliert" in gruende:
|
|
nie_kontrolliert_gesamt += 1
|
|
if kategorie == "ready":
|
|
bereit += 1
|
|
else:
|
|
if kategorie == "limited":
|
|
eingeschraenkt += 1
|
|
elif kategorie == "unknown":
|
|
unbekannt += 1
|
|
details.append({"objekt_id": objekt.id, "code": objekt.code, "name": objekt.name, "gruende": gruende})
|
|
|
|
return {
|
|
"einsatzbereit": bereit,
|
|
"eingeschraenkt_einsatzbereit": eingeschraenkt,
|
|
"unbekannt": unbekannt,
|
|
"nicht_einsatzbereit": len(objekte) - bereit - eingeschraenkt - unbekannt,
|
|
"gesamt": len(objekte),
|
|
# "unbekannt" zählt nur Objekte ohne jeden anderen Grund - diese Zahl
|
|
# zählt "nie kontrolliert" unabhängig von sonstigen Problemen mit.
|
|
"nie_kontrolliert_gesamt": nie_kontrolliert_gesamt,
|
|
"details": details,
|
|
}
|
|
|
|
|
|
def _kategorie_und_gruende(
|
|
objekt: Objekt,
|
|
letzte_kontrolle,
|
|
fehlbestand_ids: set,
|
|
fehlbestand_ids_genehmigt: set,
|
|
objekt_ids_abgelaufen: set,
|
|
objekt_ids_pruefung_ueberfaellig: set,
|
|
objekt_ids_gesperrt: set,
|
|
objekt_ids_kritischer_mangel: set,
|
|
objekt_ids_hu_uvv_ueberfaellig: set,
|
|
) -> tuple[str, list[str]]:
|
|
"""aus der `einsatzbereitschaft()`-Schleife extrahiert, damit
|
|
`objekt_readiness()` (Begründung auf der Objektakte) dieselbe Logik nutzt statt
|
|
sie zu duplizieren."""
|
|
gruende: list[str] = []
|
|
ungenehmigt = fehlbestand_ids - fehlbestand_ids_genehmigt
|
|
if ungenehmigt:
|
|
gruende.append("fehlbestand_offen")
|
|
if objekt.id in objekt_ids_abgelaufen:
|
|
gruende.append("ablaufdatum_abgelaufen")
|
|
if objekt.id in objekt_ids_pruefung_ueberfaellig:
|
|
gruende.append("pruefung_ueberfaellig")
|
|
if objekt.id in objekt_ids_gesperrt:
|
|
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")
|
|
|
|
# "noch nie kontrolliert" gilt zusätzlich zu jedem anderen Grund, nicht nur
|
|
# wenn es der einzige ist.
|
|
nie_kontrolliert = letzte_kontrolle is None
|
|
|
|
if not gruende and fehlbestand_ids and fehlbestand_ids <= fehlbestand_ids_genehmigt:
|
|
zusatz = ["noch_nie_kontrolliert"] if nie_kontrolliert else []
|
|
return "limited", ["mindermenge_genehmigt"] + zusatz
|
|
if not gruende and nie_kontrolliert:
|
|
return "unknown", ["noch_nie_kontrolliert"]
|
|
if not gruende:
|
|
return "ready", []
|
|
if nie_kontrolliert:
|
|
gruende = gruende + ["noch_nie_kontrolliert"]
|
|
return "not-ready", gruende
|
|
|
|
|
|
async def objekt_readiness(db: AsyncSession, objekt_id: int) -> dict:
|
|
"""begründete Einsatzbereitschaft für EIN Objekt (Objektakte),
|
|
dieselbe Fachlogik wie `einsatzbereitschaft()` (Dashboard-Aggregat), nur für
|
|
einen einzelnen Datensatz statt aller Objekte. `ausser_dienst` zählt hier
|
|
bewusst nicht als eigener Fall - Objektakte zeigt das Objekt so oder so an,
|
|
der Objekt-Lebenszyklus-Status wird separat dargestellt."""
|
|
heute = datetime.now(timezone.utc).date()
|
|
objekt = await db.get(Objekt, objekt_id)
|
|
if objekt is None:
|
|
return {"status": "unknown", "gruende": []}
|
|
|
|
fehlbestand_ids = set(
|
|
(
|
|
await db.execute(
|
|
select(Fehlbestand.id).where(
|
|
Fehlbestand.objekt_id == objekt_id, Fehlbestand.status.in_(OFFEN_STATI)
|
|
)
|
|
)
|
|
)
|
|
.scalars()
|
|
.all()
|
|
)
|
|
fehlbestand_ids_genehmigt = set(
|
|
(
|
|
await db.execute(
|
|
select(func.distinct(MindermengenGenehmigung.fehlbestand_id)).where(
|
|
MindermengenGenehmigung.fehlbestand_id.in_(fehlbestand_ids),
|
|
MindermengenGenehmigung.status == MindermengeStatus.aktiv,
|
|
)
|
|
)
|
|
)
|
|
.scalars()
|
|
.all()
|
|
)
|
|
hat_ablaufdatum = (
|
|
await db.execute(
|
|
select(Objektposition.id).where(
|
|
Objektposition.objekt_id == objekt_id,
|
|
Objektposition.ablaufdatum.is_not(None),
|
|
Objektposition.ablaufdatum < heute,
|
|
).limit(1)
|
|
)
|
|
).first() is not None
|
|
hat_ueberfaellige_pruefung = (
|
|
await db.execute(
|
|
select(GeraetInstanz.id)
|
|
.join(Objektposition, GeraetInstanz.objektposition_id == Objektposition.id)
|
|
.where(
|
|
Objektposition.objekt_id == objekt_id,
|
|
GeraetInstanz.naechste_pruefung.is_not(None),
|
|
GeraetInstanz.naechste_pruefung < heute,
|
|
)
|
|
.limit(1)
|
|
)
|
|
).first() is not None
|
|
ist_gesperrt = (
|
|
await db.execute(
|
|
select(Kontrolle.id)
|
|
.where(Kontrolle.objekt_id == objekt_id, Kontrolle.status == KontrollStatus.in_bearbeitung)
|
|
.limit(1)
|
|
)
|
|
).first() is not None
|
|
hat_kritischen_mangel = objekt_id in await objekt_ids_mit_kritischem_offenem_mangel(db)
|
|
hat_ueberfaellige_hu_uvv = (
|
|
await db.execute(
|
|
select(Fahrzeugdetails.objekt_id).where(
|
|
Fahrzeugdetails.objekt_id == objekt_id,
|
|
(
|
|
(Fahrzeugdetails.naechste_hu.is_not(None)) & (Fahrzeugdetails.naechste_hu < heute)
|
|
)
|
|
| ((Fahrzeugdetails.naechste_uvv.is_not(None)) & (Fahrzeugdetails.naechste_uvv < heute)),
|
|
)
|
|
)
|
|
).first() is not None
|
|
letzte_kontrolle_map = await letzte_kontrollen_map(db, [objekt_id])
|
|
|
|
kategorie, gruende = _kategorie_und_gruende(
|
|
objekt,
|
|
letzte_kontrolle_map.get(objekt_id),
|
|
fehlbestand_ids,
|
|
fehlbestand_ids_genehmigt,
|
|
{objekt_id} if hat_ablaufdatum else set(),
|
|
{objekt_id} if hat_ueberfaellige_pruefung else set(),
|
|
{objekt_id} if ist_gesperrt else set(),
|
|
{objekt_id} if hat_kritischen_mangel else set(),
|
|
{objekt_id} if hat_ueberfaellige_hu_uvv else set(),
|
|
)
|
|
return {"status": kategorie, "gruende": gruende}
|
|
|
|
|
|
async def bevorstehende_prueftermine(db: AsyncSession, warnzeitraum_tage: int = 30) -> list[dict]:
|
|
"""Karte-Nachtrag (Leitungs-Dashboard Kachel 2): Geräteprüfungen (Karte 14),
|
|
nicht Ablaufdaten von Verbrauchsmaterial (bleibt eigene, bestehende Kachel/
|
|
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))
|
|
)
|
|
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(
|
|
{
|
|
"typ": "geraet",
|
|
"objekt_id": objektposition.objekt_id,
|
|
"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
|
|
|
|
|
|
async def bevorstehende_qualifikationsablaeufe(db: AsyncSession, warnzeitraum_tage: int = 60) -> list[dict]:
|
|
"""Personal-Modul-Kachel (Nutzer-Vorgabe: "wer verliert bald Führerschein/
|
|
Lehrgang-Gültigkeit"). Analog bevorstehende_prueftermine, aber 60 statt 30
|
|
Tage Standard-Vorlauf (Nachweise wie Führerscheine brauchen typischerweise
|
|
mehr Vorlaufzeit zur Erneuerung als eine Geräteprüfung)."""
|
|
heute = datetime.now(timezone.utc).date()
|
|
result = await db.execute(
|
|
select(BenutzerQualifikation, Benutzer, Qualifikationstyp)
|
|
.join(Benutzer, BenutzerQualifikation.benutzer_id == Benutzer.id)
|
|
.join(Qualifikationstyp, BenutzerQualifikation.qualifikationstyp_id == Qualifikationstyp.id)
|
|
.where(BenutzerQualifikation.gueltig_bis.is_not(None))
|
|
)
|
|
eintraege = []
|
|
for qualifikation, benutzer, qualifikationstyp in result.all():
|
|
status_wert, verbleibende_tage = _ablauf_status(qualifikation.gueltig_bis, warnzeitraum_tage, heute)
|
|
if status_wert == "gueltig":
|
|
continue
|
|
eintraege.append(
|
|
{
|
|
"benutzer_qualifikation_id": qualifikation.id,
|
|
"benutzer_id": benutzer.id,
|
|
"benutzer_name": benutzer.name,
|
|
"qualifikationstyp_id": qualifikationstyp.id,
|
|
"qualifikationstyp_name": qualifikationstyp.name,
|
|
"gueltig_bis": qualifikation.gueltig_bis,
|
|
"verbleibende_tage": verbleibende_tage,
|
|
"status": status_wert,
|
|
}
|
|
)
|
|
eintraege.sort(key=lambda e: e["verbleibende_tage"])
|
|
return eintraege
|