Sprint 4: Fehlbestand-Statusmaschine komplett, Nachfüllung sofort/später/teilweise
CI / backend-tests (push) Successful in 40s
CI / backend-tests (push) Successful in 40s
- Nachfuellung-Modell: einzige Aktion, die Objektposition.istmenge tatsächlich ändert (Vier-Kernbegriffe). Deckt Sofort-Nachfüllung während Kontrolle (Karte 07) und spätere/externe Nachfüllung (Prompt 02.5) mit derselben Logik ab - Wichtiger Fix vor dem ersten Testlauf: Objektposition.istmenge wird bei Nachfüllung auf den neuen Wert GESETZT (fehlbestand.istmenge + menge), nicht unabhängig inkrementiert - sonst bliebe sie beim initialen "nicht kontrolliert"-Stand (0) stehen, obwohl die Kontrolle bereits einen realen Zählwert kennt - Statusübergänge: offen -> nachgefuellt_teilweise (Fehlmenge>0) -> erledigt (Fehlmenge=0), ausschließlich automatisch bei Ist=Soll (U3) - Überbestand durch Nachfüllung erlaubt, als Info gekennzeichnet, kein Fehler (E4) - KontrollpositionRead liefert jetzt fehlbestand_id mit, damit die UI direkt eine Sofort-Nachfüllung anbieten kann, ohne separat nachzufragen - Tests: U3, U4 (Sofort-Nachfüllung ohne Zwischenstatus), U5 (Teilnachfüllung), Überbestand-Fall, Nachfüllung auf bereits erledigten Fehlbestand -> 409 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L85hmKbvX7Cqkq47KnQhFt
This commit is contained in:
@@ -0,0 +1,69 @@
|
||||
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.nachfuellung import Nachfuellung
|
||||
from app.models.objektposition import Objektposition
|
||||
|
||||
|
||||
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
|
||||
) -> 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
|
||||
if neue_fehlmenge <= 0:
|
||||
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()
|
||||
return nachfuellung, ueberbestand
|
||||
@@ -1,3 +1,4 @@
|
||||
import uuid
|
||||
from datetime import datetime, timezone
|
||||
from decimal import Decimal
|
||||
|
||||
@@ -102,10 +103,12 @@ async def erfasse_position(
|
||||
material_id: int,
|
||||
istmenge: Decimal,
|
||||
zustaendiger_server_id: int,
|
||||
) -> Kontrollposition:
|
||||
) -> tuple[Kontrollposition, uuid.UUID | None]:
|
||||
"""Prompt 02.3/09 Vier-Kernbegriffe: Kontrolle erfasst nur, ändert nie die
|
||||
persistente Objektposition.istmenge. Bei Ist < Soll entsteht automatisch ein
|
||||
Fehlbestand (nie bei Überbestand, Prompt 02.4)."""
|
||||
Fehlbestand (nie bei Überbestand, Prompt 02.4). Gibt zusätzlich die
|
||||
Fehlbestand-ID zurück (falls einer aktiv ist), damit die UI direkt eine
|
||||
Sofort-Nachfüllung (Karte 07) anbieten kann, ohne separat nachzufragen."""
|
||||
vorlagen_sollmengen = await sollmenge_map(db, objekt.vorlage_id)
|
||||
result = await db.execute(
|
||||
select(Objektposition).where(
|
||||
@@ -138,7 +141,7 @@ async def erfasse_position(
|
||||
kontrollposition.abweichung = abweichung
|
||||
await db.flush()
|
||||
|
||||
await _synchronisiere_fehlbestand(
|
||||
fehlbestand_id = await _synchronisiere_fehlbestand(
|
||||
db,
|
||||
kontrolle=kontrolle,
|
||||
objekt=objekt,
|
||||
@@ -147,7 +150,7 @@ async def erfasse_position(
|
||||
istmenge=istmenge,
|
||||
zustaendiger_server_id=zustaendiger_server_id,
|
||||
)
|
||||
return kontrollposition
|
||||
return kontrollposition, fehlbestand_id
|
||||
|
||||
|
||||
async def _synchronisiere_fehlbestand(
|
||||
@@ -159,7 +162,7 @@ async def _synchronisiere_fehlbestand(
|
||||
sollmenge: Decimal,
|
||||
istmenge: Decimal,
|
||||
zustaendiger_server_id: int,
|
||||
) -> None:
|
||||
) -> uuid.UUID | None:
|
||||
result = await db.execute(
|
||||
select(Fehlbestand).where(
|
||||
Fehlbestand.kontrolle_id == kontrolle.id,
|
||||
@@ -174,21 +177,20 @@ async def _synchronisiere_fehlbestand(
|
||||
# Ist < Soll (Prompt 02.3) - Überbestand (Ist > Soll) erzeugt laut Prompt 02.4
|
||||
# ausdrücklich KEINEN Fehlbestand.
|
||||
if bestehender is None:
|
||||
db.add(
|
||||
Fehlbestand(
|
||||
erzeugt_von_server_id=zustaendiger_server_id,
|
||||
objekt_id=objekt.id,
|
||||
material_id=material_id,
|
||||
standort_id=objekt.standort_id,
|
||||
sollmenge=sollmenge,
|
||||
istmenge=istmenge,
|
||||
fehlmenge=unterdeckung,
|
||||
entstanden_am=datetime.now(timezone.utc),
|
||||
festgestellt_von=kontrolle.benutzer_id,
|
||||
kontrolle_id=kontrolle.id,
|
||||
status=FehlbestandStatus.offen,
|
||||
)
|
||||
bestehender = Fehlbestand(
|
||||
erzeugt_von_server_id=zustaendiger_server_id,
|
||||
objekt_id=objekt.id,
|
||||
material_id=material_id,
|
||||
standort_id=objekt.standort_id,
|
||||
sollmenge=sollmenge,
|
||||
istmenge=istmenge,
|
||||
fehlmenge=unterdeckung,
|
||||
entstanden_am=datetime.now(timezone.utc),
|
||||
festgestellt_von=kontrolle.benutzer_id,
|
||||
kontrolle_id=kontrolle.id,
|
||||
status=FehlbestandStatus.offen,
|
||||
)
|
||||
db.add(bestehender)
|
||||
else:
|
||||
# Korrektur vor Abschluss (z. B. Tippfehler nochmal erfasst) - solange die
|
||||
# Kontrolle noch läuft, ist der von IHR erzeugte Fehlbestand noch nicht
|
||||
@@ -200,7 +202,9 @@ async def _synchronisiere_fehlbestand(
|
||||
# Abweichung wurde vor Abschluss korrigiert - der von dieser Kontrolle
|
||||
# erzeugte Fehlbestand hat sich erledigt, bevor er je "real" wurde.
|
||||
await db.delete(bestehender)
|
||||
bestehender = None
|
||||
await db.flush()
|
||||
return bestehender.id if bestehender is not None else None
|
||||
|
||||
|
||||
async def fehlende_material_ids(db: AsyncSession, kontrolle: Kontrolle) -> list[int]:
|
||||
|
||||
Reference in New Issue
Block a user