import calendar 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.geraet_instanz import GeraetInstanz, GeraetStatus 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 def _plus_monate(stichtag: date, monate: int) -> date: """Kein python-dateutil als Dependency für eine einzelne Monatsaddition - Tag wird auf das Monatsende gekappt, falls der Zielmonat kürzer ist (z.B. 31. Januar + 1 Monat -> 28./29. Februar).""" monats_index = stichtag.month - 1 + monate jahr = stichtag.year + monats_index // 12 monat = monats_index % 12 + 1 letzter_tag = calendar.monthrange(jahr, monat)[1] return date(jahr, monat, min(stichtag.day, letzter_tag)) async def liste_fuer_position(db: AsyncSession, objektposition_id) -> list[GeraetInstanz]: result = await db.execute( select(GeraetInstanz).where(GeraetInstanz.objektposition_id == objektposition_id) ) return list(result.scalars().all()) class SeriennummerBereitsVergebenError(Exception): pass async def erstelle(db: AsyncSession, *, objektposition_id, seriennummer: str) -> GeraetInstanz: # Explizit vorab prüfen statt sich auf den UNIQUE-Constraint zu verlassen: # ein Flush-Fehlschlag hier hinterlässt die Session im Zustand DEACTIVE # (SQLAlchemy 2.0), was in einer über mehrere Requests geteilten Session # (z.B. Testsuite) jeden folgenden Request mit PendingRollbackError zum # Absturz bringt (Fund 2026-09-05). result = await db.execute( select(GeraetInstanz).where( GeraetInstanz.objektposition_id == objektposition_id, GeraetInstanz.seriennummer == seriennummer, ) ) if result.scalar_one_or_none() is not None: raise SeriennummerBereitsVergebenError instanz = GeraetInstanz(objektposition_id=objektposition_id, seriennummer=seriennummer) db.add(instanz) await db.flush() return instanz async def loesche(db: AsyncSession, instanz: GeraetInstanz) -> None: await db.delete(instanz) async def aendere_status( db: AsyncSession, *, instanz: GeraetInstanz, status: GeraetStatus, pruefdatum: date | None, bemerkung: str | None, erzeuge_fehlbestand: bool, benutzer_id: int, zustaendiger_server_id: int, ) -> tuple[GeraetInstanz, uuid.UUID | None]: """Karte 14: Status ändern (einsatzbereit/defekt/in_reparatur), optional Prüfung erfassen (naechste_pruefung wird neu berechnet, sofern die Objektposition ein pruefintervall_monate hat), optional einen Fehlbestand erzeugen (pro Gerät entscheidbar, nutzt bestehende Fehlbestand- Infrastruktur statt eigenem Parallel-Konzept).""" alter_status = instanz.status instanz.status = status instanz.bemerkung = bemerkung if pruefdatum is not None: instanz.pruefdatum = pruefdatum objektposition = await db.get(Objektposition, instanz.objektposition_id) if objektposition is not None and objektposition.pruefintervall_monate: instanz.naechste_pruefung = _plus_monate(pruefdatum, objektposition.pruefintervall_monate) await db.flush() await historie_service.log( db, zustaendiger_server_id=zustaendiger_server_id, benutzer_id=benutzer_id, ereignistyp="geraet_status_geaendert", entitaet_typ="geraet_instanz", entitaet_id=instanz.id, alter_wert={"status": alter_status.value}, neuer_wert={"status": status.value, "seriennummer": instanz.seriennummer}, begruendung=bemerkung, ) fehlbestand_id = None if erzeuge_fehlbestand and status != GeraetStatus.einsatzbereit: fehlbestand_id = await _erzeuge_fehlbestand( db, instanz=instanz, benutzer_id=benutzer_id, zustaendiger_server_id=zustaendiger_server_id, ) return instanz, fehlbestand_id async def _erzeuge_fehlbestand( db: AsyncSession, *, instanz: GeraetInstanz, benutzer_id: int, zustaendiger_server_id: int ): objektposition = await db.get(Objektposition, instanz.objektposition_id) objekt = await db.get(Objekt, objektposition.objekt_id) fehlbestand = Fehlbestand( erzeugt_von_server_id=zustaendiger_server_id, objekt_id=objektposition.objekt_id, material_id=objektposition.material_id, standort_id=objekt.standort_id, sollmenge=Decimal(1), istmenge=Decimal(0), fehlmenge=Decimal(1), entstanden_am=datetime.now(timezone.utc), festgestellt_von=benutzer_id, ursache=f"Gerät {instanz.seriennummer} ({instanz.status.value})", status=FehlbestandStatus.offen, ) db.add(fehlbestand) await db.flush() await historie_service.log( db, zustaendiger_server_id=zustaendiger_server_id, benutzer_id=benutzer_id, ereignistyp="fehlbestand_entstanden", entitaet_typ="fehlbestand", entitaet_id=fehlbestand.id, neuer_wert={"ursache": fehlbestand.ursache, "geraet_instanz_id": str(instanz.id)}, ) await benachrichtigung_service.benachrichtige_neuer_fehlbestand(db, fehlbestand=fehlbestand) return fehlbestand.id