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()} def sollmenge_effektiv(position: Objektposition) -> Decimal: """Nutzer-Vorgabe 2026-09-05: Vorlagen sollen direkt editierbar sein, ohne dass das rückwirkend alle Objekte verändert, die auf sie zeigen (bisher: Sollmenge wurde live aus der Vorlage gelesen, keine Versionierung mehr nötig heißt aber auch keine implizite Live-Kopplung mehr). sollmenge_vorlage wird daher bei Objekt-Anlage/Duplizieren FEST kopiert (siehe objekt_aus_vorlage/ dupliziere_objekt) statt bei jeder Anzeige neu aus der Vorlage gelesen.""" if position.sollmenge_override is not None: return position.sollmenge_override return position.sollmenge_vorlage or 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, sollmenge_vorlage=vorlagenposition.sollmenge, 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, sollmenge_vorlage=quellposition.sollmenge_vorlage, ist_status=quellposition.ist_status, istmenge=Decimal(0), ) ) await db.flush() return neues_objekt