from datetime import datetime, timezone from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from app.models.fehlbestand import Fehlbestand, FehlbestandStatus from app.models.kontrolle import Kontrolle, KontrollStatus, Kontrollposition from app.models.mindermenge import MindermengeStatus, MindermengenGenehmigung from app.models.objektposition import Objektposition, ObjektpositionStatus from app.services import historie as historie_service from .exceptions import ObjektGesperrtError, UnvollstaendigError async def aktive_kontrolle(db: AsyncSession, objekt_id: int) -> Kontrolle | None: result = await db.execute( select(Kontrolle).where( Kontrolle.objekt_id == objekt_id, Kontrolle.status == KontrollStatus.in_bearbeitung ) ) return result.scalar_one_or_none() async def starte_kontrolle( db: AsyncSession, *, objekt_id: int, benutzer_id: int, uebernehmen: bool, zustaendiger_server_id: int, ) -> Kontrolle: laufende = await aktive_kontrolle(db, objekt_id) if laufende is not None: if not uebernehmen: raise ObjektGesperrtError(laufende) # Übernahme (Prompt 02.8): alte Kontrolle wird abgebrochen, nicht gelöscht - # bleibt als Datensatz nachvollziehbar (Prompt 16.6-Analogie). laufende.status = KontrollStatus.abgebrochen laufende.abbruch_grund = f"Übernommen durch Benutzer {benutzer_id}" laufende.beendet_am = datetime.now(timezone.utc) # Vom vorherigen Inhaber während der abgebrochenen Kontrolle erzeugte # Fehlbestände dürfen nicht stehen bleiben (Prompt 16.6: kein Fehlbestand # durch eine abgebrochene Kontrolle). await _verwirf_kontrolldaten(db, laufende.id) await historie_service.log( db, zustaendiger_server_id=zustaendiger_server_id, benutzer_id=benutzer_id, ereignistyp="kontrolle_uebernommen", entitaet_typ="kontrolle", entitaet_id=laufende.id, alter_wert={"benutzer_id": laufende.benutzer_id}, neuer_wert={"benutzer_id": benutzer_id}, ) neue_kontrolle = Kontrolle( erzeugt_von_server_id=zustaendiger_server_id, objekt_id=objekt_id, benutzer_id=benutzer_id, status=KontrollStatus.in_bearbeitung, gestartet_am=datetime.now(timezone.utc), ) db.add(neue_kontrolle) await db.flush() await _lasse_mindermengen_ablaufen( db, objekt_id=objekt_id, neue_kontrolle=neue_kontrolle, zustaendiger_server_id=zustaendiger_server_id ) await historie_service.log( db, zustaendiger_server_id=zustaendiger_server_id, benutzer_id=benutzer_id, ereignistyp="kontrolle_gestartet", entitaet_typ="kontrolle", entitaet_id=neue_kontrolle.id, neuer_wert={"objekt_id": objekt_id, "status": "in_bearbeitung"}, ) return neue_kontrolle async def _lasse_mindermengen_ablaufen( db: AsyncSession, *, objekt_id: int, neue_kontrolle: Kontrolle, zustaendiger_server_id: int ) -> None: """Karte 08 / Prompt 04.4 / Sprintplan U6: Genehmigung gilt automatisch nur bis zur nächsten Kontrolle desselben Objekts - kein Zeit-Trigger nötig, der Start der nächsten Kontrolle IST der Ablauf-Zeitpunkt.""" result = await db.execute( select(MindermengenGenehmigung) .join(Fehlbestand, MindermengenGenehmigung.fehlbestand_id == Fehlbestand.id) .where(Fehlbestand.objekt_id == objekt_id, MindermengenGenehmigung.status == MindermengeStatus.aktiv) ) for genehmigung in result.scalars().all(): genehmigung.status = MindermengeStatus.abgelaufen genehmigung.beendet_am = datetime.now(timezone.utc) genehmigung.beendende_kontrolle_id = neue_kontrolle.id await historie_service.log( db, zustaendiger_server_id=zustaendiger_server_id, benutzer_id=None, ereignistyp="mindermenge_abgelaufen", entitaet_typ="mindermengen_genehmigung", entitaet_id=genehmigung.id, alter_wert={"status": "aktiv"}, neuer_wert={"status": "abgelaufen", "beendende_kontrolle_id": str(neue_kontrolle.id)}, ) await db.flush() async def _verwirf_kontrolldaten(db: AsyncSession, kontrolle_id) -> None: fehlbestaende = await db.execute(select(Fehlbestand).where(Fehlbestand.kontrolle_id == kontrolle_id)) for fehlbestand in fehlbestaende.scalars().all(): if fehlbestand.status != FehlbestandStatus.offen: # Bereits (teilweise) nachgefüllt (Karte 07 Sofort-Nachfüllung während # der Kontrolle) - das ist eine echte, dokumentierte Bestandsbewegung # und darf beim Verwerfen/Abbruch der Kontrolle NICHT gelöscht werden # (FK-Verweis aus nachfuellung + Historie müssen erhalten bleiben). continue await db.delete(fehlbestand) positionen = await db.execute(select(Kontrollposition).where(Kontrollposition.kontrolle_id == kontrolle_id)) for position in positionen.scalars().all(): await db.delete(position) await db.flush() async def fehlende_material_ids(db: AsyncSession, kontrolle: Kontrolle) -> list[int]: result = await db.execute( select(Objektposition.material_id).where( Objektposition.objekt_id == kontrolle.objekt_id, Objektposition.ist_status == ObjektpositionStatus.aktiv, ) ) erwartete = {m for (m,) in result.all()} result = await db.execute( select(Kontrollposition.material_id).where(Kontrollposition.kontrolle_id == kontrolle.id) ) erfasste = {m for (m,) in result.all()} return sorted(erwartete - erfasste) async def abschliesse_kontrolle(db: AsyncSession, *, kontrolle: Kontrolle, zustaendiger_server_id: int) -> None: fehlende = await fehlende_material_ids(db, kontrolle) if fehlende: raise UnvollstaendigError(fehlende) kontrolle.status = KontrollStatus.abgeschlossen kontrolle.beendet_am = datetime.now(timezone.utc) await db.flush() await historie_service.log( db, zustaendiger_server_id=zustaendiger_server_id, benutzer_id=kontrolle.benutzer_id, ereignistyp="kontrolle_abgeschlossen", entitaet_typ="kontrolle", entitaet_id=kontrolle.id, neuer_wert={"status": "abgeschlossen"}, ) async def abbreche_kontrolle( db: AsyncSession, *, kontrolle: Kontrolle, grund: str | None, zustaendiger_server_id: int ) -> None: """Prompt 16.6: keine Ist-Übernahme (Kontrolle rührt Objektposition ohnehin nie an), kein Fehlbestand aus dieser Kontrolle bleibt bestehen. Kontrolle selbst bleibt als Datensatz erhalten (Status "abgebrochen"), nicht gelöscht.""" await _verwirf_kontrolldaten(db, kontrolle.id) kontrolle.status = KontrollStatus.abgebrochen kontrolle.abbruch_grund = grund kontrolle.beendet_am = datetime.now(timezone.utc) await db.flush() await historie_service.log( db, zustaendiger_server_id=zustaendiger_server_id, benutzer_id=kontrolle.benutzer_id, ereignistyp="kontrolle_abgebrochen", entitaet_typ="kontrolle", entitaet_id=kontrolle.id, neuer_wert={"status": "abgebrochen", "grund": grund}, )