Files
MABEA/backend/app/services/objekte.py
T
patrickandClaude Sonnet 5 3b5cf8feab
CI / backend-tests (push) Successful in 2m24s
CI / frontend-build (push) Successful in 27s
feat(identity): IDENT-003 konfigurierbare Nummernschemata je Objekttyp
Neue Stammdaten-Tabelle nummernschema (Migration 0032): Präfix + Stellenzahl
einmal je Objekttyp festlegen statt bei jeder Objekt-Anlage manuell
einzutippen (Fehlerquelle - Tippfehler, uneinheitliche Schreibweise,
falsches Präfix für den Typ). GET /objekte/naechster-code akzeptiert jetzt
objekttyp_id als Alternative zu praefix; ohne konfiguriertes Schema bleibt
der bisherige manuelle Präfix-Weg nutzbar (Rückwärtskompatibilität).

Admin-UI: Nummernschema-Pflege direkt in der Objekttyp-Verwaltung
(ObjekttypSection). ObjektAnlegenFormular versucht zuerst das Schema des
gewählten Objekttyps, fällt bei 404 auf manuelles Präfix zurück.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CVgbozhYmuEhiEJHffRXCV
2026-09-08 20:23:14 +02:00

142 lines
5.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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, mindestbreite: int = 5) -> 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 (mindestens `mindestbreite` bei Erstvergabe - IDENT-003:
kommt aus dem konfigurierten Nummernschema, falls vorhanden, sonst 5 wie
bisher), 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 = mindestbreite
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