import uuid from datetime import date, 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.kontrolle import Kontrolle, Kontrollposition from app.models.objekt import Objekt from app.models.objektposition import Objektposition from app.services import benachrichtigung as benachrichtigung_service from app.services import historie as historie_service from app.services.objekte import sollmenge_effektiv, sollmenge_map async def erfasse_position( db: AsyncSession, *, kontrolle: Kontrolle, objekt: Objekt, material_id: int, istmenge: Decimal, zustaendiger_server_id: int, ablaufdatum: date | None = None, chargennummer: str | None = None, seriennummer: str | None = None, ) -> 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). 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. Ablaufdatum/Chargennummer/Seriennummer sind KEINE Kernbegriffe-Verletzung: sie beschreiben das physische Exemplar, nicht die Ist-Menge, und werden daher hier - auf Wunsch als Teil der normalen Kontroll-Erfassung, nicht nur als separate Admin-Pflege - direkt mitgepflegt.""" vorlagen_sollmengen = await sollmenge_map(db, objekt.vorlage_id) result = await db.execute( select(Objektposition).where( Objektposition.objekt_id == objekt.id, Objektposition.material_id == material_id ) ) objektposition = result.scalar_one_or_none() sollmenge = sollmenge_effektiv(objektposition, vorlagen_sollmengen) if objektposition else Decimal(0) if objektposition is not None: if ablaufdatum is not None: objektposition.ablaufdatum = ablaufdatum if chargennummer is not None: objektposition.chargennummer = chargennummer if seriennummer is not None: objektposition.seriennummer = seriennummer abweichung = istmenge != sollmenge result = await db.execute( select(Kontrollposition).where( Kontrollposition.kontrolle_id == kontrolle.id, Kontrollposition.material_id == material_id ) ) kontrollposition = result.scalar_one_or_none() if kontrollposition is None: kontrollposition = Kontrollposition( kontrolle_id=kontrolle.id, material_id=material_id, sollmenge_snapshot=sollmenge, istmenge_erfasst=istmenge, abweichung=abweichung, ) db.add(kontrollposition) else: kontrollposition.sollmenge_snapshot = sollmenge kontrollposition.istmenge_erfasst = istmenge 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, objekt=objekt, material_id=material_id, sollmenge=sollmenge, istmenge=istmenge, zustaendiger_server_id=zustaendiger_server_id, ) return kontrollposition, fehlbestand_id async def _synchronisiere_fehlbestand( db: AsyncSession, *, kontrolle: Kontrolle, objekt: Objekt, material_id: int, sollmenge: Decimal, istmenge: Decimal, zustaendiger_server_id: int, ) -> uuid.UUID | None: result = await db.execute( select(Fehlbestand).where( Fehlbestand.kontrolle_id == kontrolle.id, Fehlbestand.material_id == material_id, Fehlbestand.status == FehlbestandStatus.offen, ) ) bestehender = result.scalar_one_or_none() unterdeckung = sollmenge - istmenge if unterdeckung > 0: # Ist < Soll (Prompt 02.3) - Überbestand (Ist > Soll) erzeugt laut Prompt 02.4 # ausdrücklich KEINEN Fehlbestand. if bestehender is None: 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) 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), }, ) await benachrichtigung_service.benachrichtige_neuer_fehlbestand(db, fehlbestand=bestehender) else: # Korrektur vor Abschluss (z. B. Tippfehler nochmal erfasst) - solange die # Kontrolle noch läuft, ist der von IHR erzeugte Fehlbestand noch nicht # final und darf berichtigt werden. bestehender.sollmenge = sollmenge bestehender.istmenge = istmenge bestehender.fehlmenge = unterdeckung elif bestehender is not None: # 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