Atomare Aus+Ein-Buchung in einer Materialbewegung-Zeile statt zwei Einzelschritten. NOTIF-001 war bereits produktiv umgesetzt, Backlog war nur veraltet - korrigiert. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CVgbozhYmuEhiEJHffRXCV
114 lines
3.6 KiB
Python
114 lines
3.6 KiB
Python
from datetime import datetime, timezone
|
|
from decimal import Decimal
|
|
|
|
from sqlalchemy import select
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from app.models.lager import Bestand, Materialbewegung
|
|
|
|
|
|
class NichtGenugBestandError(Exception):
|
|
pass
|
|
|
|
|
|
async def _bestand_zeile(db: AsyncSession, lagerplatz_id: int, material_id: int) -> Bestand | None:
|
|
result = await db.execute(
|
|
select(Bestand).where(Bestand.lagerplatz_id == lagerplatz_id, Bestand.material_id == material_id)
|
|
)
|
|
return result.scalar_one_or_none()
|
|
|
|
|
|
async def einlagern(
|
|
db: AsyncSession, *, lagerplatz_id: int, material_id: int, menge: Decimal, benutzer_id: int, grund: str | None
|
|
) -> Materialbewegung:
|
|
"""WH-004: Bestand zubuchen (Upsert, falls noch keine Zeile existiert)."""
|
|
bestand = await _bestand_zeile(db, lagerplatz_id, material_id)
|
|
if bestand is None:
|
|
bestand = Bestand(lagerplatz_id=lagerplatz_id, material_id=material_id, menge=Decimal(0))
|
|
db.add(bestand)
|
|
bestand.menge += menge
|
|
|
|
bewegung = Materialbewegung(
|
|
material_id=material_id,
|
|
menge=menge,
|
|
von_lagerplatz_id=None,
|
|
nach_lagerplatz_id=lagerplatz_id,
|
|
benutzer_id=benutzer_id,
|
|
zeitpunkt=datetime.now(timezone.utc),
|
|
grund=grund,
|
|
)
|
|
db.add(bewegung)
|
|
await db.flush()
|
|
return bewegung
|
|
|
|
|
|
class GleicherLagerplatzError(Exception):
|
|
pass
|
|
|
|
|
|
async def umlagern(
|
|
db: AsyncSession,
|
|
*,
|
|
von_lagerplatz_id: int,
|
|
nach_lagerplatz_id: int,
|
|
material_id: int,
|
|
menge: Decimal,
|
|
benutzer_id: int,
|
|
grund: str | None,
|
|
) -> Materialbewegung:
|
|
"""WH-005: Aus- und Einlagerung atomar in einer Transaktion (get_db committet
|
|
erst nach Request-Ende), eine einzige Materialbewegung-Zeile mit Von+Nach
|
|
statt zwei Einzelbuchungen wie bei WH-004."""
|
|
if von_lagerplatz_id == nach_lagerplatz_id:
|
|
raise GleicherLagerplatzError
|
|
|
|
quelle = await _bestand_zeile(db, von_lagerplatz_id, material_id)
|
|
if quelle is None or quelle.menge < menge:
|
|
raise NichtGenugBestandError
|
|
quelle.menge -= menge
|
|
|
|
ziel = await _bestand_zeile(db, nach_lagerplatz_id, material_id)
|
|
if ziel is None:
|
|
ziel = Bestand(lagerplatz_id=nach_lagerplatz_id, material_id=material_id, menge=Decimal(0))
|
|
db.add(ziel)
|
|
ziel.menge += menge
|
|
|
|
bewegung = Materialbewegung(
|
|
material_id=material_id,
|
|
menge=menge,
|
|
von_lagerplatz_id=von_lagerplatz_id,
|
|
nach_lagerplatz_id=nach_lagerplatz_id,
|
|
benutzer_id=benutzer_id,
|
|
zeitpunkt=datetime.now(timezone.utc),
|
|
grund=grund,
|
|
)
|
|
db.add(bewegung)
|
|
await db.flush()
|
|
return bewegung
|
|
|
|
|
|
async def auslagern(
|
|
db: AsyncSession, *, lagerplatz_id: int, material_id: int, menge: Decimal, benutzer_id: int, grund: str | None
|
|
) -> Materialbewegung:
|
|
"""WH-004: Bestand abbuchen - negativer Bestand ist ausgeschlossen (Akzeptanz-
|
|
kriterium der Kachel), vorab geprüft statt DB-Constraint (gleiches Muster
|
|
wie objekte.py Selbstbezug-Check, Fund 2026-09-05: DB-Exception mitten in
|
|
einer geteilten Session hinterlässt die Session im Zustand DEACTIVE)."""
|
|
bestand = await _bestand_zeile(db, lagerplatz_id, material_id)
|
|
if bestand is None or bestand.menge < menge:
|
|
raise NichtGenugBestandError
|
|
bestand.menge -= menge
|
|
|
|
bewegung = Materialbewegung(
|
|
material_id=material_id,
|
|
menge=menge,
|
|
von_lagerplatz_id=lagerplatz_id,
|
|
nach_lagerplatz_id=None,
|
|
benutzer_id=benutzer_id,
|
|
zeitpunkt=datetime.now(timezone.utc),
|
|
grund=grund,
|
|
)
|
|
db.add(bewegung)
|
|
await db.flush()
|
|
return bewegung
|