import re from datetime import datetime from decimal import Decimal from sqlalchemy import func, select from sqlalchemy.ext.asyncio import AsyncSession from app.models.kontrolle import Kontrolle, KontrollStatus from app.models.objekt import Objekt from app.models.objektposition import Objektposition, ObjektpositionStatus from app.models.vorlage import Beladungsvorlage, Vorlagenposition _CODE_MUSTER = re.compile(r"^-(\d+)$") async def naechster_freier_code(db: AsyncSession, spalte, praefix: str) -> str: """Konzept-Ergänzung Karte 10/Prompt 09: Code-Vorschlag PRAEFIX-NNN mit fortlaufender Nummer je Präfix. Zahlenbreite folgt der bisher größten vergebenen Nummer (mind. 5 Stellen bei Erstvergabe), damit die Sortierung im Regal stabil bleibt, solange nicht mehr als 10^Breite Objekte existieren.""" result = await db.execute(select(spalte).where(spalte.like(f"{praefix}-%"))) hoechste = 0 breite = 5 for (code,) in result.all(): treffer = _CODE_MUSTER.match(code[len(praefix) :]) if treffer is None: continue zahl = treffer.group(1) breite = max(breite, len(zahl)) hoechste = max(hoechste, int(zahl)) return f"{praefix}-{hoechste + 1:0{breite}d}" async def letzte_kontrollen_map(db: AsyncSession, objekt_ids: list[int]) -> dict[int, datetime]: """Nutzer-Frage: "wann war die letzte komplette Kontrolle vom Objekt" - bisher nirgends vermerkt (kein Cache-Feld am Objekt, nur ableitbar aus kontrolle. beendet_am). Liefert je Objekt den jüngsten Abschluss-Zeitpunkt einer Kontrolle mit status=abgeschlossen; Objekte ohne abgeschlossene Kontrolle fehlen im Ergebnis (Aufrufer behandelt das als "noch nie kontrolliert").""" if not objekt_ids: return {} result = await db.execute( select(Kontrolle.objekt_id, func.max(Kontrolle.beendet_am)) .where(Kontrolle.objekt_id.in_(objekt_ids), Kontrolle.status == KontrollStatus.abgeschlossen) .group_by(Kontrolle.objekt_id) ) return {objekt_id: beendet_am for objekt_id, beendet_am in result.all()} async def sollmenge_map(db: AsyncSession, vorlage_id: int | None) -> dict[int, Decimal]: """material_id -> Sollmenge laut Vorlage (Prompt 10: Basis für die dynamische Auflösung, wenn eine Objektposition keinen Override hat).""" if vorlage_id is None: return {} result = await db.execute( select(Vorlagenposition.material_id, Vorlagenposition.sollmenge).where( Vorlagenposition.vorlage_id == vorlage_id ) ) return {material_id: sollmenge for material_id, sollmenge in result.all()} def sollmenge_effektiv(position: Objektposition, vorlagen_sollmengen: dict[int, Decimal]) -> Decimal: if position.sollmenge_override is not None: return position.sollmenge_override return vorlagen_sollmengen.get(position.material_id, Decimal(0)) async def objekt_aus_vorlage( db: AsyncSession, *, code: str, name: str, standort_id: int, vorlage: Beladungsvorlage, zustaendiger_server_id: int, fahrzeug_id: int | None = None, ) -> Objekt: """Prompt 09 (Neuanlage, nicht Duplizieren): Objektpositionen werden aus den Vorlagenpositionen initialisiert, Ist-Menge startet bei 0 ("nicht kontrolliert"). fahrzeug_id: optionale Zuordnung zu einem Fahrzeug (1:n, Nutzer-Vorgabe).""" objekt = Objekt( code=code, name=name, objekttyp_id=vorlage.objekttyp_id, vorlage_id=vorlage.id, standort_id=standort_id, zustaendiger_server_id=zustaendiger_server_id, fahrzeug_id=fahrzeug_id, ) db.add(objekt) await db.flush() result = await db.execute(select(Vorlagenposition).where(Vorlagenposition.vorlage_id == vorlage.id)) for vorlagenposition in result.scalars().all(): db.add( Objektposition( objekt_id=objekt.id, material_id=vorlagenposition.material_id, ist_status=ObjektpositionStatus.aktiv, istmenge=Decimal(0), ) ) await db.flush() return objekt async def dupliziere_objekt( db: AsyncSession, *, quelle: Objekt, neuer_code: str, neuer_name: str, neuer_standort_id: int, zustaendiger_server_id: int, ) -> Objekt: """Prompt 09: Struktur/Sollmengen (inkl. individueller Overrides) werden kopiert, Zustandsdaten (Ist-Menge, SN, Ablauf, Charge) NICHT – neues Objekt startet "nicht kontrolliert". Fehlbestände/Historie existieren für ein neues Objekt ohnehin nicht (kein Kopierschritt nötig).""" neues_objekt = Objekt( code=neuer_code, name=neuer_name, objekttyp_id=quelle.objekttyp_id, vorlage_id=quelle.vorlage_id, standort_id=neuer_standort_id, zustaendiger_server_id=zustaendiger_server_id, ) db.add(neues_objekt) await db.flush() result = await db.execute(select(Objektposition).where(Objektposition.objekt_id == quelle.id)) for quellposition in result.scalars().all(): db.add( Objektposition( objekt_id=neues_objekt.id, material_id=quellposition.material_id, sollmenge_override=quellposition.sollmenge_override, ist_status=quellposition.ist_status, istmenge=Decimal(0), ) ) await db.flush() return neues_objekt