feat(identity): IDENT-005 Hersteller/Modell-Stammdaten
CI / backend-tests (push) Failing after 2m16s
CI / frontend-build (push) Successful in 26s

Neue Tabellen hersteller/modell (Migration 0030), CRUD-Endpunkte
(/hersteller, /modelle) nach bestehendem Kategorie-Muster, inkl. 409 bei
Duplikaten (IntegrityError abgefangen wie in personal.py). geraet_instanz
bekommt optionales modell_id-Feld - verknüpft eine konkrete Geräteinstanz
(mit Seriennummer, IDENT-004) mit ihrem Hersteller/Modell.

Ergänzt Material.hersteller (bleibt unverändert als Freitext) um eine
normalisierte Variante gezielt für Geräte mit Seriennummer - keine
Datenmigration bestehender Freitext-Werte (Scope dieser Kachel).

Bewusst kein neues Admin-UI-Screen für die Pflege (Scope-Grenze der
Kachel) - Hersteller/Modell aktuell nur über die API verwaltbar.

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 14:51:15 +02:00
co-authored by Claude Sonnet 5
parent ae09433c11
commit 2dd7e574e6
12 changed files with 249 additions and 6 deletions
+61 -1
View File
@@ -1,12 +1,13 @@
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy import func, select
from sqlalchemy.exc import IntegrityError
from sqlalchemy.ext.asyncio import AsyncSession
from app.api.deps import get_current_user, require_roles, require_roles_or_permission
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, Kategorie, Material, Objekttyp, Standort
from app.models.stammdaten import Bereich, Fach, Hersteller, Kategorie, Material, Modell, 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 (
@@ -18,9 +19,13 @@ from app.schemas.stammdaten import (
FachRead,
FachUpdate,
FachVerwendungObjekt,
HerstellerCreate,
HerstellerRead,
KategorieCreate,
KategorieRead,
KategorieUpdate,
ModellCreate,
ModellRead,
MaterialCreate,
MaterialRead,
MaterialUpdate,
@@ -75,6 +80,61 @@ async def aendere_bereich(
return bereich
# -- Hersteller/Modell (IDENT-005) ---------------------------------------------
@router.get("/hersteller", response_model=list[HerstellerRead])
async def liste_hersteller(
db: AsyncSession = Depends(get_db), _=Depends(get_current_user)
) -> list[Hersteller]:
result = await db.execute(select(Hersteller))
return list(result.scalars().all())
@router.post("/hersteller", response_model=HerstellerRead, status_code=status.HTTP_201_CREATED)
async def erstelle_hersteller(
payload: HerstellerCreate,
db: AsyncSession = Depends(get_db),
_=Depends(require_roles(RolleTyp.administration)),
) -> Hersteller:
hersteller = Hersteller(**payload.model_dump())
db.add(hersteller)
try:
await db.flush()
except IntegrityError as exc:
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="Hersteller existiert bereits") from exc
return hersteller
@router.get("/modelle", response_model=list[ModellRead])
async def liste_modelle(
hersteller_id: int | None = None,
db: AsyncSession = Depends(get_db),
_=Depends(get_current_user),
) -> list[Modell]:
stmt = select(Modell)
if hersteller_id is not None:
stmt = stmt.where(Modell.hersteller_id == hersteller_id)
result = await db.execute(stmt)
return list(result.scalars().all())
@router.post("/modelle", response_model=ModellRead, status_code=status.HTTP_201_CREATED)
async def erstelle_modell(
payload: ModellCreate,
db: AsyncSession = Depends(get_db),
_=Depends(require_roles(RolleTyp.administration)),
) -> Modell:
if await db.get(Hersteller, payload.hersteller_id) is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Hersteller nicht gefunden")
modell = Modell(**payload.model_dump())
db.add(modell)
try:
await db.flush()
except IntegrityError as exc:
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="Modell existiert bereits") from exc
return modell
# -- Kategorie ------------------------------------------------------------------
@router.get("/kategorien", response_model=list[KategorieRead])