feat(identity): IDENT-003 konfigurierbare Nummernschemata je Objekttyp
CI / backend-tests (push) Successful in 2m24s
CI / frontend-build (push) Successful in 27s

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
This commit is contained in:
2026-09-08 20:23:14 +02:00
co-authored by Claude Sonnet 5
parent 15f783fb5e
commit 3b5cf8feab
12 changed files with 380 additions and 53 deletions
+25 -4
View File
@@ -12,7 +12,7 @@ from app.models.auth import KnotenTyp, RolleTyp, Systemknoten
from app.models.fahrzeugdetails import Fahrzeugdetails
from app.models.objekt import Objekt
from app.models.objektposition import Objektposition
from app.models.stammdaten import Material, Objekttyp
from app.models.stammdaten import Material, Nummernschema, Objekttyp
from app.models.vorlage import Beladungsvorlage
from app.services import historie as historie_service
from app.services.label import generiere_code128_label_pdf, generiere_qr_label_pdf
@@ -105,11 +105,32 @@ async def hole_objekt_per_code(
@router.get("/objekte/naechster-code")
async def naechster_objekt_code(
praefix: str, db: AsyncSession = Depends(get_db), _=Depends(get_current_user)
praefix: str | None = None,
objekttyp_id: int | None = None,
db: AsyncSession = Depends(get_db),
_=Depends(get_current_user),
) -> dict[str, str]:
"""Code-Vorschlag beim Anlegen/Duplizieren (Konzept-Ergänzung Karte 10/Prompt 09).
Vorschlag ist überschreibbar, Eindeutigkeit erzwingt weiterhin objekt.code UNIQUE."""
return {"code": await naechster_freier_code(db, Objekt.code, praefix)}
Vorschlag ist überschreibbar, Eindeutigkeit erzwingt weiterhin objekt.code UNIQUE.
IDENT-003: `objekttyp_id` statt `praefix` nutzt das konfigurierte
Nummernschema (Präfix + Mindest-Stellenzahl) - kein manuelles Eintippen/
Merken des richtigen Präfix je Objekttyp mehr nötig. `praefix` bleibt für
Objekttypen ohne konfiguriertes Schema nutzbar (Rückwärtskompatibilität)."""
stellenzahl = 5
if objekttyp_id is not None:
result = await db.execute(select(Nummernschema).where(Nummernschema.objekttyp_id == objekttyp_id))
schema = result.scalar_one_or_none()
if schema is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Kein Nummernschema für diesen Objekttyp konfiguriert - Präfix manuell angeben",
)
praefix = schema.praefix
stellenzahl = schema.stellenzahl
if not praefix:
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail="Präfix oder Objekttyp angeben")
return {"code": await naechster_freier_code(db, Objekt.code, praefix, stellenzahl)}
@router.get("/objektpositionen/naechster-code")
+51 -1
View File
@@ -6,7 +6,7 @@ from app.api.deps import get_current_user, require_roles, require_roles_or_permi
from app.db.session import get_db
from app.models.auth import RolleTyp
from app.models.objekt import Objekt
from app.models.stammdaten import Bereich, Fach, Hersteller, Kategorie, Material, Modell, Objekttyp, Standort
from app.models.stammdaten import Bereich, Fach, Hersteller, Kategorie, Material, Modell, Nummernschema, Objekttyp, Standort
from app.models.vorlage import Beladungsvorlage, Vorlagenposition
from app.services.material_merge import finde_konflikte, fuehre_zusammen, hole_verwendung
from app.schemas.stammdaten import (
@@ -25,6 +25,9 @@ from app.schemas.stammdaten import (
KategorieUpdate,
ModellCreate,
ModellRead,
NummernschemaCreate,
NummernschemaRead,
NummernschemaUpdate,
MaterialCreate,
MaterialRead,
MaterialUpdate,
@@ -141,6 +144,53 @@ async def erstelle_modell(
return modell
# -- Nummernschema (IDENT-003) ---------------------------------------------------
@router.get("/nummernschemata", response_model=list[NummernschemaRead])
async def liste_nummernschemata(
db: AsyncSession = Depends(get_db), _=Depends(get_current_user)
) -> list[Nummernschema]:
result = await db.execute(select(Nummernschema))
return list(result.scalars().all())
@router.post("/nummernschemata", response_model=NummernschemaRead, status_code=status.HTTP_201_CREATED)
async def erstelle_nummernschema(
payload: NummernschemaCreate,
db: AsyncSession = Depends(get_db),
_=Depends(require_roles(RolleTyp.administration)),
) -> Nummernschema:
if await db.get(Objekttyp, payload.objekttyp_id) is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Objekttyp nicht gefunden")
bestehendes = await db.execute(
select(Nummernschema).where(Nummernschema.objekttyp_id == payload.objekttyp_id)
)
if bestehendes.scalar_one_or_none() is not None:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT, detail="Für diesen Objekttyp existiert bereits ein Nummernschema"
)
schema = Nummernschema(**payload.model_dump())
db.add(schema)
await db.flush()
return schema
@router.patch("/nummernschemata/{schema_id}", response_model=NummernschemaRead)
async def aendere_nummernschema(
schema_id: int,
payload: NummernschemaUpdate,
db: AsyncSession = Depends(get_db),
_=Depends(require_roles(RolleTyp.administration)),
) -> Nummernschema:
schema = await db.get(Nummernschema, schema_id)
if schema is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Nummernschema nicht gefunden")
for feld, wert in payload.model_dump(exclude_unset=True).items():
setattr(schema, feld, wert)
await db.flush()
return schema
# -- Kategorie ------------------------------------------------------------------
@router.get("/kategorien", response_model=list[KategorieRead])
+19 -1
View File
@@ -1,6 +1,6 @@
import enum
from sqlalchemy import Boolean, ForeignKey, String, Text, UniqueConstraint
from sqlalchemy import Boolean, ForeignKey, Integer, String, Text, UniqueConstraint
from sqlalchemy.dialects.postgresql import ENUM as PgEnum
from sqlalchemy.orm import Mapped, mapped_column
@@ -54,6 +54,24 @@ class Objekttyp(Base):
ist_zugfahrzeug: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
class Nummernschema(Base):
"""IDENT-003: statt bei jeder Objekt-Anlage das Präfix manuell einzutippen
(Fehlerquelle - Tippfehler, uneinheitliche Groß-/Kleinschreibung, falsches
Präfix für den Objekttyp) legt der Admin je Objekttyp einmal Präfix +
Stellenzahl fest. `naechster_freier_code` schlägt darauf aufbauend den
nächsten Code vor (PRAEFIX-NNN...), wie bisher, nur nicht mehr frei
eingebbar. Ein Objekttyp OHNE Schema kann weiterhin mit explizitem Präfix
angelegt werden (Rückwärtskompatibilität, kein Zwang zur Migration
bestehender Objekte)."""
__tablename__ = "nummernschema"
id: Mapped[int] = mapped_column(primary_key=True)
objekttyp_id: Mapped[int] = mapped_column(ForeignKey("objekttyp.id"), unique=True, nullable=False)
praefix: Mapped[str] = mapped_column(String, nullable=False)
stellenzahl: Mapped[int] = mapped_column(Integer, nullable=False, default=5)
class Fach(Base):
"""Nutzer-Vorgabe: feste Fächer-Liste je Objekttyp, damit beim Anlegen einer
Vorlagenposition nicht jedes Mal Freitext getippt werden muss. `sortierung`
+16
View File
@@ -54,6 +54,22 @@ class ModellRead(ModellCreate):
id: int
class NummernschemaCreate(BaseModel):
objekttyp_id: int
praefix: str
stellenzahl: int = 5
class NummernschemaRead(NummernschemaCreate):
model_config = ConfigDict(from_attributes=True)
id: int
class NummernschemaUpdate(BaseModel):
praefix: str | None = None
stellenzahl: int | None = None
class StandortCreate(BaseModel):
name: str
adresse: str | None = None
+6 -4
View File
@@ -13,14 +13,16 @@ from app.models.vorlage import Beladungsvorlage, Vorlagenposition
_CODE_MUSTER = re.compile(r"^-(\d+)$")
async def naechster_freier_code(db: AsyncSession, spalte, praefix: str) -> str:
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 (mind. 5 Stellen bei Erstvergabe), damit die Sortierung im
Regal stabil bleibt, solange nicht mehr als 10^Breite Objekte existieren."""
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 = 5
breite = mindestbreite
for (code,) in result.all():
treffer = _CODE_MUSTER.match(code[len(praefix) :])
if treffer is None: