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
This commit is contained in:
@@ -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")
|
||||
|
||||
@@ -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])
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user