Files
MABEA/backend/app/services/fehlbestand.py
T
patrickandClaude Sonnet 5 1e9012cba3
CI / backend-tests (push) Successful in 47s
Sprint 5: Mindermengen-Genehmigung + automatischer Ablauf, Historie für Kernkette
- 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
2026-09-03 23:53:14 +02:00

121 lines
4.8 KiB
Python

from datetime import datetime, timezone
from decimal import Decimal
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.models.fehlbestand import Fehlbestand, FehlbestandStatus
from app.models.mindermenge import MindermengeStatus, MindermengenGenehmigung
from app.models.nachfuellung import Nachfuellung
from app.models.objektposition import Objektposition
from app.services import historie as historie_service
class FehlbestandBereitsErledigtError(Exception):
"""Ein bereits erledigter Fehlbestand kann nicht erneut nachgefüllt werden."""
async def nachfuellen(
db: AsyncSession,
*,
fehlbestand: Fehlbestand,
menge: Decimal,
benutzer_id: int,
zustaendiger_server_id: int,
) -> tuple[Nachfuellung, Decimal]:
"""Vier-Kernbegriffe (Prompt 02.9): Nachfüllung ist die einzige Aktion, die
Objektposition.istmenge tatsächlich ändert. Deckt sowohl Sofort-Nachfüllung
während der Kontrolle (Karte 07) als auch spätere/externe Nachfüllung
(Prompt 02.5) ab - technisch derselbe Vorgang, nur anderer Zeitpunkt.
Überbestand durch Nachfüllung ist erlaubt (Prompt 21.9/E4): kein Fehler, nur
als Info im Rückgabewert `ueberbestand` gekennzeichnet.
Wichtig: `fehlbestand.istmenge` ist die zuletzt GEZÄHLTE Wahrheit (aus der
Kontrolle, die den Fehlbestand ausgelöst hat) - Objektposition.istmenge wird
hier auf den neuen Wert GESETZT, nicht unabhängig davon inkrementiert. Sonst
bliebe Objektposition.istmenge bei ihrem alten (ggf. 0/"nicht kontrolliert")
Stand stehen, obwohl die Kontrolle bereits einen realen Zählwert kennt -
"Ist-Menge wird auf Soll korrigiert" (Prompt 02.3) meint genau dieses Setzen.
"""
if fehlbestand.status == FehlbestandStatus.erledigt:
raise FehlbestandBereitsErledigtError()
neue_istmenge = fehlbestand.istmenge + menge
ueberbestand = max(Decimal(0), neue_istmenge - fehlbestand.sollmenge)
neue_fehlmenge = max(Decimal(0), fehlbestand.sollmenge - neue_istmenge)
result = await db.execute(
select(Objektposition).where(
Objektposition.objekt_id == fehlbestand.objekt_id,
Objektposition.material_id == fehlbestand.material_id,
)
)
objektposition = result.scalar_one_or_none()
if objektposition is not None:
objektposition.istmenge = neue_istmenge
fehlbestand.istmenge = neue_istmenge
fehlbestand.fehlmenge = neue_fehlmenge
wird_erledigt = neue_fehlmenge <= 0
if wird_erledigt:
fehlbestand.status = FehlbestandStatus.erledigt
fehlbestand.erledigt_am = datetime.now(timezone.utc)
else:
fehlbestand.status = FehlbestandStatus.nachgefuellt_teilweise
nachfuellung = Nachfuellung(
fehlbestand_id=fehlbestand.id,
objekt_id=fehlbestand.objekt_id,
material_id=fehlbestand.material_id,
menge=menge,
benutzer_id=benutzer_id,
zeitpunkt=datetime.now(timezone.utc),
)
db.add(nachfuellung)
await db.flush()
await historie_service.log(
db,
zustaendiger_server_id=zustaendiger_server_id,
benutzer_id=benutzer_id,
ereignistyp="nachfuellung_erfasst",
entitaet_typ="fehlbestand",
entitaet_id=fehlbestand.id,
neuer_wert={"menge": str(menge), "neue_istmenge": str(neue_istmenge), "ueberbestand": str(ueberbestand)},
)
if wird_erledigt:
await historie_service.log(
db,
zustaendiger_server_id=zustaendiger_server_id,
benutzer_id=benutzer_id,
ereignistyp="fehlbestand_erledigt",
entitaet_typ="fehlbestand",
entitaet_id=fehlbestand.id,
neuer_wert={"status": "erledigt"},
)
# U7 (Prompt 04.4): wird die Abweichung behoben, erledigt sich eine noch
# aktive Mindermengen-Genehmigung gemeinsam mit dem Fehlbestand.
result = await db.execute(
select(MindermengenGenehmigung).where(
MindermengenGenehmigung.fehlbestand_id == fehlbestand.id,
MindermengenGenehmigung.status == MindermengeStatus.aktiv,
)
)
genehmigung = result.scalar_one_or_none()
if genehmigung is not None:
genehmigung.status = MindermengeStatus.beendet_durch_erledigung
genehmigung.beendet_am = datetime.now(timezone.utc)
await historie_service.log(
db,
zustaendiger_server_id=zustaendiger_server_id,
benutzer_id=benutzer_id,
ereignistyp="mindermenge_beendet_durch_erledigung",
entitaet_typ="mindermengen_genehmigung",
entitaet_id=genehmigung.id,
neuer_wert={"status": "beendet_durch_erledigung"},
)
await db.flush()
return nachfuellung, ueberbestand