Sprint 5: Mindermengen-Genehmigung + automatischer Ablauf, Historie für Kernkette
CI / backend-tests (push) Successful in 47s
CI / backend-tests (push) Successful in 47s
- MindermengenGenehmigung-Modell/Service (Prompt 04): reine fachliche Bewertung, ändert NIEMALS Ist-Menge/Fehlmenge/Fehlbestand-Status (Leitplanke, U2) - Automatischer Ablauf bei Start der nächsten Kontrolle desselben Objekts (Karte 08, U6) - kein Zeit-Trigger, der Kontrollstart selbst ist der Ablauf-Zeitpunkt - Genehmigung endet gemeinsam mit dem Fehlbestand, wenn dieser durch Nachfüllung erledigt wird (U7), unabhängig davon ob vorher eine neue Kontrolle stattfand - Historie-Service (app/services/historie.py) + Verdrahtung in die komplette Kernkette: kontrolle_gestartet/-abgeschlossen/-abgebrochen/-uebernommen, istmenge_erfasst, fehlbestand_entstanden/-erledigt, nachfuellung_erfasst, mindermenge_genehmigt/-abgelaufen/-beendet_durch_erledigung (U14) Hinweis: Stammdaten-/Vorlagen-/Benutzerverwaltung noch nicht retrofittet - Sprint 5 deckt bewusst die im Testkonzept referenzierte Kernkette ab, keine flächendeckende Audit-Abdeckung aller CRUD-Endpunkte. - Tests: U2, U6, U7, vollständige Historie-Kette nach Prompt-13.3-Beispiel (U14) Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L85hmKbvX7Cqkq47KnQhFt
This commit is contained in:
@@ -6,10 +6,11 @@ from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.models.fehlbestand import Fehlbestand, FehlbestandStatus
|
||||
from app.models.historie import Historie
|
||||
from app.models.kontrolle import Kontrolle, KontrollStatus, Kontrollposition
|
||||
from app.models.mindermenge import MindermengeStatus, MindermengenGenehmigung
|
||||
from app.models.objekt import Objekt
|
||||
from app.models.objektposition import Objektposition, ObjektpositionStatus
|
||||
from app.services import historie as historie_service
|
||||
from app.services.objekte import sollmenge_effektiv, sollmenge_map
|
||||
|
||||
|
||||
@@ -59,19 +60,16 @@ async def starte_kontrolle(
|
||||
# Fehlbestände dürfen nicht stehen bleiben (Prompt 16.6: kein Fehlbestand
|
||||
# durch eine abgebrochene Kontrolle).
|
||||
await _verwirf_kontrolldaten(db, laufende.id)
|
||||
db.add(
|
||||
Historie(
|
||||
erzeugt_von_server_id=zustaendiger_server_id,
|
||||
zeitpunkt=datetime.now(timezone.utc),
|
||||
benutzer_id=benutzer_id,
|
||||
ereignistyp="kontrolle_uebernommen",
|
||||
entitaet_typ="kontrolle",
|
||||
entitaet_id=str(laufende.id),
|
||||
alter_wert={"benutzer_id": laufende.benutzer_id},
|
||||
neuer_wert={"benutzer_id": benutzer_id},
|
||||
)
|
||||
await historie_service.log(
|
||||
db,
|
||||
zustaendiger_server_id=zustaendiger_server_id,
|
||||
benutzer_id=benutzer_id,
|
||||
ereignistyp="kontrolle_uebernommen",
|
||||
entitaet_typ="kontrolle",
|
||||
entitaet_id=laufende.id,
|
||||
alter_wert={"benutzer_id": laufende.benutzer_id},
|
||||
neuer_wert={"benutzer_id": benutzer_id},
|
||||
)
|
||||
await db.flush()
|
||||
|
||||
neue_kontrolle = Kontrolle(
|
||||
erzeugt_von_server_id=zustaendiger_server_id,
|
||||
@@ -82,9 +80,51 @@ async def starte_kontrolle(
|
||||
)
|
||||
db.add(neue_kontrolle)
|
||||
await db.flush()
|
||||
|
||||
await _lasse_mindermengen_ablaufen(
|
||||
db, objekt_id=objekt_id, neue_kontrolle=neue_kontrolle, zustaendiger_server_id=zustaendiger_server_id
|
||||
)
|
||||
|
||||
await historie_service.log(
|
||||
db,
|
||||
zustaendiger_server_id=zustaendiger_server_id,
|
||||
benutzer_id=benutzer_id,
|
||||
ereignistyp="kontrolle_gestartet",
|
||||
entitaet_typ="kontrolle",
|
||||
entitaet_id=neue_kontrolle.id,
|
||||
neuer_wert={"objekt_id": objekt_id, "status": "in_bearbeitung"},
|
||||
)
|
||||
return neue_kontrolle
|
||||
|
||||
|
||||
async def _lasse_mindermengen_ablaufen(
|
||||
db: AsyncSession, *, objekt_id: int, neue_kontrolle: Kontrolle, zustaendiger_server_id: int
|
||||
) -> None:
|
||||
"""Karte 08 / Prompt 04.4 / Sprintplan U6: Genehmigung gilt automatisch nur bis
|
||||
zur nächsten Kontrolle desselben Objekts - kein Zeit-Trigger nötig, der Start
|
||||
der nächsten Kontrolle IST der Ablauf-Zeitpunkt."""
|
||||
result = await db.execute(
|
||||
select(MindermengenGenehmigung)
|
||||
.join(Fehlbestand, MindermengenGenehmigung.fehlbestand_id == Fehlbestand.id)
|
||||
.where(Fehlbestand.objekt_id == objekt_id, MindermengenGenehmigung.status == MindermengeStatus.aktiv)
|
||||
)
|
||||
for genehmigung in result.scalars().all():
|
||||
genehmigung.status = MindermengeStatus.abgelaufen
|
||||
genehmigung.beendet_am = datetime.now(timezone.utc)
|
||||
genehmigung.beendende_kontrolle_id = neue_kontrolle.id
|
||||
await historie_service.log(
|
||||
db,
|
||||
zustaendiger_server_id=zustaendiger_server_id,
|
||||
benutzer_id=None,
|
||||
ereignistyp="mindermenge_abgelaufen",
|
||||
entitaet_typ="mindermengen_genehmigung",
|
||||
entitaet_id=genehmigung.id,
|
||||
alter_wert={"status": "aktiv"},
|
||||
neuer_wert={"status": "abgelaufen", "beendende_kontrolle_id": str(neue_kontrolle.id)},
|
||||
)
|
||||
await db.flush()
|
||||
|
||||
|
||||
async def _verwirf_kontrolldaten(db: AsyncSession, kontrolle_id) -> None:
|
||||
fehlbestaende = await db.execute(select(Fehlbestand).where(Fehlbestand.kontrolle_id == kontrolle_id))
|
||||
for fehlbestand in fehlbestaende.scalars().all():
|
||||
@@ -141,6 +181,16 @@ async def erfasse_position(
|
||||
kontrollposition.abweichung = abweichung
|
||||
await db.flush()
|
||||
|
||||
await historie_service.log(
|
||||
db,
|
||||
zustaendiger_server_id=zustaendiger_server_id,
|
||||
benutzer_id=kontrolle.benutzer_id,
|
||||
ereignistyp="istmenge_erfasst",
|
||||
entitaet_typ="kontrollposition",
|
||||
entitaet_id=kontrollposition.id,
|
||||
neuer_wert={"material_id": material_id, "sollmenge": str(sollmenge), "istmenge": str(istmenge)},
|
||||
)
|
||||
|
||||
fehlbestand_id = await _synchronisiere_fehlbestand(
|
||||
db,
|
||||
kontrolle=kontrolle,
|
||||
@@ -191,6 +241,22 @@ async def _synchronisiere_fehlbestand(
|
||||
status=FehlbestandStatus.offen,
|
||||
)
|
||||
db.add(bestehender)
|
||||
await db.flush()
|
||||
await historie_service.log(
|
||||
db,
|
||||
zustaendiger_server_id=zustaendiger_server_id,
|
||||
benutzer_id=kontrolle.benutzer_id,
|
||||
ereignistyp="fehlbestand_entstanden",
|
||||
entitaet_typ="fehlbestand",
|
||||
entitaet_id=bestehender.id,
|
||||
neuer_wert={
|
||||
"objekt_id": objekt.id,
|
||||
"material_id": material_id,
|
||||
"sollmenge": str(sollmenge),
|
||||
"istmenge": str(istmenge),
|
||||
"fehlmenge": str(unterdeckung),
|
||||
},
|
||||
)
|
||||
else:
|
||||
# Korrektur vor Abschluss (z. B. Tippfehler nochmal erfasst) - solange die
|
||||
# Kontrolle noch läuft, ist der von IHR erzeugte Fehlbestand noch nicht
|
||||
@@ -224,16 +290,27 @@ async def fehlende_material_ids(db: AsyncSession, kontrolle: Kontrolle) -> list[
|
||||
return sorted(erwartete - erfasste)
|
||||
|
||||
|
||||
async def abschliesse_kontrolle(db: AsyncSession, *, kontrolle: Kontrolle) -> None:
|
||||
async def abschliesse_kontrolle(db: AsyncSession, *, kontrolle: Kontrolle, zustaendiger_server_id: int) -> None:
|
||||
fehlende = await fehlende_material_ids(db, kontrolle)
|
||||
if fehlende:
|
||||
raise UnvollstaendigError(fehlende)
|
||||
kontrolle.status = KontrollStatus.abgeschlossen
|
||||
kontrolle.beendet_am = datetime.now(timezone.utc)
|
||||
await db.flush()
|
||||
await historie_service.log(
|
||||
db,
|
||||
zustaendiger_server_id=zustaendiger_server_id,
|
||||
benutzer_id=kontrolle.benutzer_id,
|
||||
ereignistyp="kontrolle_abgeschlossen",
|
||||
entitaet_typ="kontrolle",
|
||||
entitaet_id=kontrolle.id,
|
||||
neuer_wert={"status": "abgeschlossen"},
|
||||
)
|
||||
|
||||
|
||||
async def abbreche_kontrolle(db: AsyncSession, *, kontrolle: Kontrolle, grund: str | None) -> None:
|
||||
async def abbreche_kontrolle(
|
||||
db: AsyncSession, *, kontrolle: Kontrolle, grund: str | None, zustaendiger_server_id: int
|
||||
) -> None:
|
||||
"""Prompt 16.6: keine Ist-Übernahme (Kontrolle rührt Objektposition ohnehin nie
|
||||
an), kein Fehlbestand aus dieser Kontrolle bleibt bestehen. Kontrolle selbst
|
||||
bleibt als Datensatz erhalten (Status "abgebrochen"), nicht gelöscht."""
|
||||
@@ -242,3 +319,12 @@ async def abbreche_kontrolle(db: AsyncSession, *, kontrolle: Kontrolle, grund: s
|
||||
kontrolle.abbruch_grund = grund
|
||||
kontrolle.beendet_am = datetime.now(timezone.utc)
|
||||
await db.flush()
|
||||
await historie_service.log(
|
||||
db,
|
||||
zustaendiger_server_id=zustaendiger_server_id,
|
||||
benutzer_id=kontrolle.benutzer_id,
|
||||
ereignistyp="kontrolle_abgebrochen",
|
||||
entitaet_typ="kontrolle",
|
||||
entitaet_id=kontrolle.id,
|
||||
neuer_wert={"status": "abgebrochen", "grund": grund},
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user