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.mindermenge import MindermengeStatus, MindermengenGenehmigung 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 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: Kontrolle bucht die gezählte Menge als neuen Ist-Bestand der Objektposition - vorher blieb dieser bei Ist=Soll unverändert (nur Nachfüllung schrieb ihn), wodurch ein frisch angelegtes, aber vollständig bestücktes Objekt dauerhaft auf 0 stehen blieb. 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.""" 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) if objektposition else Decimal(0) if objektposition is not None: objektposition.istmenge = istmenge 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.objekt_id == objekt.id, Fehlbestand.material_id == material_id, Fehlbestand.status == FehlbestandStatus.offen, ) ) bestehender = result.scalar_one_or_none() von_dieser_kontrolle = bestehender is not None and bestehender.kontrolle_id == kontrolle.id 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: # kontrolle_id bleibt unangetastet, sonst würde ein Abbruch dieser # Kontrolle einen fremden Fehlbestand mitlöschen (_verwirf_kontrolldaten). bestehender.sollmenge = sollmenge bestehender.istmenge = istmenge bestehender.fehlmenge = unterdeckung elif bestehender is not None: if von_dieser_kontrolle: await db.delete(bestehender) bestehender = None else: bestehender.istmenge = istmenge bestehender.fehlmenge = Decimal(0) bestehender.status = FehlbestandStatus.erledigt bestehender.erledigt_am = datetime.now(timezone.utc) await historie_service.log( db, zustaendiger_server_id=zustaendiger_server_id, benutzer_id=kontrolle.benutzer_id, ereignistyp="fehlbestand_erledigt", entitaet_typ="fehlbestand", entitaet_id=bestehender.id, neuer_wert={"status": "erledigt"}, ) genehmigung_result = await db.execute( select(MindermengenGenehmigung).where( MindermengenGenehmigung.fehlbestand_id == bestehender.id, MindermengenGenehmigung.status == MindermengeStatus.aktiv, ) ) genehmigung = 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=kontrolle.benutzer_id, ereignistyp="mindermenge_beendet_durch_erledigung", entitaet_typ="mindermengen_genehmigung", entitaet_id=genehmigung.id, neuer_wert={"status": "beendet_durch_erledigung"}, ) bestehender = None await db.flush() return bestehender.id if bestehender is not None else None