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