feat(identity): IDENT-005 Hersteller/Modell-Stammdaten
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:
@@ -137,7 +137,7 @@ vergeben statt geraten.
|
|||||||
| IDENT-002 | Identity | Inventarnummer manuell | P0 | S | LOW | IDENT-001 | ✅ (Objektposition-Code, `objektposition.py`) |
|
| IDENT-002 | Identity | Inventarnummer manuell | P0 | S | LOW | IDENT-001 | ✅ (Objektposition-Code, `objektposition.py`) |
|
||||||
| IDENT-003 | Identity | Inventarnummer-Nummernschemata | P1 | M | MEDIUM | IDENT-002 | ⬜ (kein konfigurierbares Schema gefunden, nur fester Code-Zähler) |
|
| IDENT-003 | Identity | Inventarnummer-Nummernschemata | P1 | M | MEDIUM | IDENT-002 | ⬜ (kein konfigurierbares Schema gefunden, nur fester Code-Zähler) |
|
||||||
| IDENT-004 | Identity | Seriennummer + Eindeutigkeitsprüfung | P0 | S | LOW | IDENT-001 | ✅ (`geraet_instanz.py` Seriennummer-Feld/Unique) |
|
| IDENT-004 | Identity | Seriennummer + Eindeutigkeitsprüfung | P0 | S | LOW | IDENT-001 | ✅ (`geraet_instanz.py` Seriennummer-Feld/Unique) |
|
||||||
| IDENT-005 | Identity | Hersteller/Modell-Stammdaten | P1 | S | LOW | IDENT-004 | ⬜ (kein eigenes Hersteller/Modell-Stammdatenmodell gefunden) |
|
| IDENT-005 | Identity | Hersteller/Modell-Stammdaten | P1 | S | LOW | IDENT-004 | ✅ (`Hersteller`/`Modell`-Tabellen, Migration 0030, `geraet_instanz.modell_id` optional; kein eigenes Admin-UI, nur API) |
|
||||||
| IDENT-006 | Identity | QR-Code-Erzeugung | P0 | S | LOW | IDENT-001 | ✅ (`label.pdf`-Endpunkte für Objekt/Objektposition) |
|
| IDENT-006 | Identity | QR-Code-Erzeugung | P0 | S | LOW | IDENT-001 | ✅ (`label.pdf`-Endpunkte für Objekt/Objektposition) |
|
||||||
| IDENT-007 | Identity | QR-Code-Druck (Label) | P0 | S | LOW | IDENT-006 | ✅ (`/objekte/{id}/label.pdf`, `/objektpositionen/{id}/label.pdf`) |
|
| IDENT-007 | Identity | QR-Code-Druck (Label) | P0 | S | LOW | IDENT-006 | ✅ (`/objekte/{id}/label.pdf`, `/objektpositionen/{id}/label.pdf`) |
|
||||||
| IDENT-008 | Identity | QR-Scan → Akte öffnen | P0 | M | MEDIUM | IDENT-006, FILE-006 | ✅ (`BarcodeScanner.tsx`, Akte-Route `/akte/objekt/{id}`) |
|
| IDENT-008 | Identity | QR-Scan → Akte öffnen | P0 | M | MEDIUM | IDENT-006, FILE-006 | ✅ (`BarcodeScanner.tsx`, Akte-Route `/akte/objekt/{id}`) |
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
"""IDENT-005: Hersteller/Modell-Stammdaten, geraet_instanz.modell_id.
|
||||||
|
|
||||||
|
Revision ID: 0030_hersteller_modell
|
||||||
|
Revises: 0029_dokument_original
|
||||||
|
Create Date: 2026-09-08
|
||||||
|
"""
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
|
||||||
|
revision: str = "0030_hersteller_modell"
|
||||||
|
down_revision: Union[str, None] = "0029_dokument_original"
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
op.execute(
|
||||||
|
"""
|
||||||
|
CREATE TABLE hersteller (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
name TEXT NOT NULL UNIQUE
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
op.execute(
|
||||||
|
"""
|
||||||
|
CREATE TABLE modell (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
hersteller_id INTEGER NOT NULL REFERENCES hersteller(id),
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
UNIQUE (hersteller_id, name)
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
op.execute("CREATE INDEX ix_modell_hersteller_id ON modell (hersteller_id)")
|
||||||
|
op.execute("ALTER TABLE geraet_instanz ADD COLUMN modell_id INTEGER REFERENCES modell(id)")
|
||||||
|
op.execute("CREATE INDEX ix_geraet_instanz_modell_id ON geraet_instanz (modell_id)")
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
op.execute("DROP INDEX IF EXISTS ix_geraet_instanz_modell_id")
|
||||||
|
op.execute("ALTER TABLE geraet_instanz DROP COLUMN modell_id")
|
||||||
|
op.execute("DROP INDEX IF EXISTS ix_modell_hersteller_id")
|
||||||
|
op.execute("DROP TABLE modell")
|
||||||
|
op.execute("DROP TABLE hersteller")
|
||||||
@@ -62,7 +62,9 @@ async def erstelle_geraet(
|
|||||||
if position is None:
|
if position is None:
|
||||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Objektposition nicht gefunden")
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Objektposition nicht gefunden")
|
||||||
try:
|
try:
|
||||||
return await erstelle(db, objektposition_id=position_id, seriennummer=payload.seriennummer)
|
return await erstelle(
|
||||||
|
db, objektposition_id=position_id, seriennummer=payload.seriennummer, modell_id=payload.modell_id
|
||||||
|
)
|
||||||
except SeriennummerBereitsVergebenError as exc:
|
except SeriennummerBereitsVergebenError as exc:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_409_CONFLICT, detail="Seriennummer an dieser Position bereits vergeben"
|
status_code=status.HTTP_409_CONFLICT, detail="Seriennummer an dieser Position bereits vergeben"
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
from fastapi import APIRouter, Depends, HTTPException, status
|
from fastapi import APIRouter, Depends, HTTPException, status
|
||||||
from sqlalchemy import func, select
|
from sqlalchemy import func, select
|
||||||
|
from sqlalchemy.exc import IntegrityError
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from app.api.deps import get_current_user, require_roles, require_roles_or_permission
|
from app.api.deps import get_current_user, require_roles, require_roles_or_permission
|
||||||
from app.db.session import get_db
|
from app.db.session import get_db
|
||||||
from app.models.auth import RolleTyp
|
from app.models.auth import RolleTyp
|
||||||
from app.models.objekt import Objekt
|
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.models.vorlage import Beladungsvorlage, Vorlagenposition
|
||||||
from app.services.material_merge import finde_konflikte, fuehre_zusammen, hole_verwendung
|
from app.services.material_merge import finde_konflikte, fuehre_zusammen, hole_verwendung
|
||||||
from app.schemas.stammdaten import (
|
from app.schemas.stammdaten import (
|
||||||
@@ -18,9 +19,13 @@ from app.schemas.stammdaten import (
|
|||||||
FachRead,
|
FachRead,
|
||||||
FachUpdate,
|
FachUpdate,
|
||||||
FachVerwendungObjekt,
|
FachVerwendungObjekt,
|
||||||
|
HerstellerCreate,
|
||||||
|
HerstellerRead,
|
||||||
KategorieCreate,
|
KategorieCreate,
|
||||||
KategorieRead,
|
KategorieRead,
|
||||||
KategorieUpdate,
|
KategorieUpdate,
|
||||||
|
ModellCreate,
|
||||||
|
ModellRead,
|
||||||
MaterialCreate,
|
MaterialCreate,
|
||||||
MaterialRead,
|
MaterialRead,
|
||||||
MaterialUpdate,
|
MaterialUpdate,
|
||||||
@@ -75,6 +80,61 @@ async def aendere_bereich(
|
|||||||
return 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 ------------------------------------------------------------------
|
# -- Kategorie ------------------------------------------------------------------
|
||||||
|
|
||||||
@router.get("/kategorien", response_model=list[KategorieRead])
|
@router.get("/kategorien", response_model=list[KategorieRead])
|
||||||
|
|||||||
@@ -32,6 +32,9 @@ class GeraetInstanz(Base):
|
|||||||
UUID(as_uuid=True), ForeignKey("objektposition.id"), nullable=False
|
UUID(as_uuid=True), ForeignKey("objektposition.id"), nullable=False
|
||||||
)
|
)
|
||||||
seriennummer: Mapped[str] = mapped_column(String, nullable=False)
|
seriennummer: Mapped[str] = mapped_column(String, nullable=False)
|
||||||
|
# IDENT-005: welches Hersteller-Modell dieses konkrete Gerät ist - optional,
|
||||||
|
# nicht jede Geräteinstanz muss das pflegen.
|
||||||
|
modell_id: Mapped[int | None] = mapped_column(ForeignKey("modell.id"))
|
||||||
pruefdatum: Mapped[date | None] = mapped_column(Date)
|
pruefdatum: Mapped[date | None] = mapped_column(Date)
|
||||||
naechste_pruefung: Mapped[date | None] = mapped_column(Date)
|
naechste_pruefung: Mapped[date | None] = mapped_column(Date)
|
||||||
status: Mapped[GeraetStatus] = mapped_column(
|
status: Mapped[GeraetStatus] = mapped_column(
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import enum
|
import enum
|
||||||
|
|
||||||
from sqlalchemy import Boolean, ForeignKey, String, Text
|
from sqlalchemy import Boolean, ForeignKey, String, Text, UniqueConstraint
|
||||||
from sqlalchemy.dialects.postgresql import ENUM as PgEnum
|
from sqlalchemy.dialects.postgresql import ENUM as PgEnum
|
||||||
from sqlalchemy.orm import Mapped, mapped_column
|
from sqlalchemy.orm import Mapped, mapped_column
|
||||||
|
|
||||||
@@ -67,6 +67,28 @@ class Fach(Base):
|
|||||||
sortierung: Mapped[int] = mapped_column(default=0)
|
sortierung: Mapped[int] = mapped_column(default=0)
|
||||||
|
|
||||||
|
|
||||||
|
class Hersteller(Base):
|
||||||
|
"""IDENT-005: normalisierte Hersteller-Stammdaten statt Freitext - macht
|
||||||
|
Geräte/Fahrzeuge herstellerübergreifend auswertbar/filterbar (z.B. "alle
|
||||||
|
Weinmann-Geräte prüfpflichtig"). Ergänzt Material.hersteller (Freitext,
|
||||||
|
bleibt unverändert), betrifft hier gezielt Geräteinstanzen mit
|
||||||
|
Seriennummer (IDENT-004)."""
|
||||||
|
|
||||||
|
__tablename__ = "hersteller"
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(primary_key=True)
|
||||||
|
name: Mapped[str] = mapped_column(String, unique=True, nullable=False)
|
||||||
|
|
||||||
|
|
||||||
|
class Modell(Base):
|
||||||
|
__tablename__ = "modell"
|
||||||
|
__table_args__ = (UniqueConstraint("hersteller_id", "name"),)
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(primary_key=True)
|
||||||
|
hersteller_id: Mapped[int] = mapped_column(ForeignKey("hersteller.id"), nullable=False)
|
||||||
|
name: Mapped[str] = mapped_column(String, nullable=False)
|
||||||
|
|
||||||
|
|
||||||
class Material(Base):
|
class Material(Base):
|
||||||
__tablename__ = "material"
|
__tablename__ = "material"
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ from app.models.geraet_instanz import GeraetStatus
|
|||||||
|
|
||||||
class GeraetInstanzCreate(BaseModel):
|
class GeraetInstanzCreate(BaseModel):
|
||||||
seriennummer: str
|
seriennummer: str
|
||||||
|
modell_id: int | None = None
|
||||||
|
|
||||||
|
|
||||||
class GeraetInstanzRead(BaseModel):
|
class GeraetInstanzRead(BaseModel):
|
||||||
@@ -15,6 +16,7 @@ class GeraetInstanzRead(BaseModel):
|
|||||||
id: uuid.UUID
|
id: uuid.UUID
|
||||||
objektposition_id: uuid.UUID
|
objektposition_id: uuid.UUID
|
||||||
seriennummer: str
|
seriennummer: str
|
||||||
|
modell_id: int | None
|
||||||
pruefdatum: date | None
|
pruefdatum: date | None
|
||||||
naechste_pruefung: date | None
|
naechste_pruefung: date | None
|
||||||
status: GeraetStatus
|
status: GeraetStatus
|
||||||
|
|||||||
@@ -35,6 +35,25 @@ class KategorieUpdate(BaseModel):
|
|||||||
ueberkategorie_id: int | None = None
|
ueberkategorie_id: int | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class HerstellerCreate(BaseModel):
|
||||||
|
name: str
|
||||||
|
|
||||||
|
|
||||||
|
class HerstellerRead(HerstellerCreate):
|
||||||
|
model_config = ConfigDict(from_attributes=True)
|
||||||
|
id: int
|
||||||
|
|
||||||
|
|
||||||
|
class ModellCreate(BaseModel):
|
||||||
|
hersteller_id: int
|
||||||
|
name: str
|
||||||
|
|
||||||
|
|
||||||
|
class ModellRead(ModellCreate):
|
||||||
|
model_config = ConfigDict(from_attributes=True)
|
||||||
|
id: int
|
||||||
|
|
||||||
|
|
||||||
class StandortCreate(BaseModel):
|
class StandortCreate(BaseModel):
|
||||||
name: str
|
name: str
|
||||||
adresse: str | None = None
|
adresse: str | None = None
|
||||||
|
|||||||
@@ -25,7 +25,9 @@ class SeriennummerBereitsVergebenError(Exception):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
async def erstelle(db: AsyncSession, *, objektposition_id, seriennummer: str) -> GeraetInstanz:
|
async def erstelle(
|
||||||
|
db: AsyncSession, *, objektposition_id, seriennummer: str, modell_id: int | None = None
|
||||||
|
) -> GeraetInstanz:
|
||||||
# Explizit vorab prüfen statt sich auf den UNIQUE-Constraint zu verlassen:
|
# Explizit vorab prüfen statt sich auf den UNIQUE-Constraint zu verlassen:
|
||||||
# ein Flush-Fehlschlag hier hinterlässt die Session im Zustand DEACTIVE
|
# ein Flush-Fehlschlag hier hinterlässt die Session im Zustand DEACTIVE
|
||||||
# (SQLAlchemy 2.0), was in einer über mehrere Requests geteilten Session
|
# (SQLAlchemy 2.0), was in einer über mehrere Requests geteilten Session
|
||||||
@@ -39,7 +41,7 @@ async def erstelle(db: AsyncSession, *, objektposition_id, seriennummer: str) ->
|
|||||||
)
|
)
|
||||||
if result.scalar_one_or_none() is not None:
|
if result.scalar_one_or_none() is not None:
|
||||||
raise SeriennummerBereitsVergebenError
|
raise SeriennummerBereitsVergebenError
|
||||||
instanz = GeraetInstanz(objektposition_id=objektposition_id, seriennummer=seriennummer)
|
instanz = GeraetInstanz(objektposition_id=objektposition_id, seriennummer=seriennummer, modell_id=modell_id)
|
||||||
db.add(instanz)
|
db.add(instanz)
|
||||||
await db.flush()
|
await db.flush()
|
||||||
return instanz
|
return instanz
|
||||||
|
|||||||
@@ -187,3 +187,34 @@ async def test_defekt_kann_fehlbestand_erzeugen(client, objekt_mit_position, mat
|
|||||||
"/api/v1/fehlbestaende", params={"objekt_id": objekt.id}, headers=auth_header(token)
|
"/api/v1/fehlbestaende", params={"objekt_id": objekt.id}, headers=auth_header(token)
|
||||||
)
|
)
|
||||||
assert len(nachher.json()) == anzahl_vorher + 1
|
assert len(nachher.json()) == anzahl_vorher + 1
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_geraet_kann_mit_hersteller_modell_verknuepft_werden(
|
||||||
|
client, objekt_mit_position, materialverantwortlicher_user, admin_user
|
||||||
|
):
|
||||||
|
"""IDENT-005: optionales modell_id-Feld verweist auf Hersteller/Modell-Stammdaten."""
|
||||||
|
objekt, _material = objekt_mit_position
|
||||||
|
admin_token = await login(client, "admin1")
|
||||||
|
token = await login(client, "materialverantwortlicher1")
|
||||||
|
|
||||||
|
hersteller = await client.post(
|
||||||
|
"/api/v1/hersteller", json={"name": "Weinmann"}, headers=auth_header(admin_token)
|
||||||
|
)
|
||||||
|
modell = await client.post(
|
||||||
|
"/api/v1/modelle",
|
||||||
|
json={"hersteller_id": hersteller.json()["id"], "name": "MEDUMAT Standard2"},
|
||||||
|
headers=auth_header(admin_token),
|
||||||
|
)
|
||||||
|
modell_id = modell.json()["id"]
|
||||||
|
|
||||||
|
positionen = await client.get(f"/api/v1/objekte/{objekt.id}/positionen", headers=auth_header(token))
|
||||||
|
position_id = positionen.json()[0]["id"]
|
||||||
|
|
||||||
|
erstellt = await client.post(
|
||||||
|
f"/api/v1/objektpositionen/{position_id}/geraete",
|
||||||
|
json={"seriennummer": "SN-MODELL-1", "modell_id": modell_id},
|
||||||
|
headers=auth_header(token),
|
||||||
|
)
|
||||||
|
assert erstellt.status_code == 201
|
||||||
|
assert erstellt.json()["modell_id"] == modell_id
|
||||||
|
|||||||
@@ -369,3 +369,47 @@ async def test_mitarbeiter_cannot_create_fach(client, mitarbeiter_user, admin_us
|
|||||||
headers=auth_header(token),
|
headers=auth_header(token),
|
||||||
)
|
)
|
||||||
assert response.status_code == 403
|
assert response.status_code == 403
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_hersteller_modell_lifecycle(client, admin_user, mitarbeiter_user):
|
||||||
|
"""IDENT-005: Hersteller/Modell-Stammdaten anlegen, per hersteller_id filtern."""
|
||||||
|
admin_token = await login(client, "admin1")
|
||||||
|
|
||||||
|
hersteller = await client.post(
|
||||||
|
"/api/v1/hersteller", json={"name": "Weinmann"}, headers=auth_header(admin_token)
|
||||||
|
)
|
||||||
|
assert hersteller.status_code == 201
|
||||||
|
hersteller_id = hersteller.json()["id"]
|
||||||
|
|
||||||
|
modell = await client.post(
|
||||||
|
"/api/v1/modelle",
|
||||||
|
json={"hersteller_id": hersteller_id, "name": "MEDUMAT Standard2"},
|
||||||
|
headers=auth_header(admin_token),
|
||||||
|
)
|
||||||
|
assert modell.status_code == 201
|
||||||
|
assert modell.json()["hersteller_id"] == hersteller_id
|
||||||
|
|
||||||
|
anderer_hersteller = await client.post(
|
||||||
|
"/api/v1/hersteller", json={"name": "Weinmann"}, headers=auth_header(admin_token)
|
||||||
|
)
|
||||||
|
assert anderer_hersteller.status_code == 409
|
||||||
|
|
||||||
|
unbekannter_hersteller = await client.post(
|
||||||
|
"/api/v1/modelle",
|
||||||
|
json={"hersteller_id": 999999, "name": "Irgendwas"},
|
||||||
|
headers=auth_header(admin_token),
|
||||||
|
)
|
||||||
|
assert unbekannter_hersteller.status_code == 404
|
||||||
|
|
||||||
|
mitarbeiter_token = await login(client, "mitarbeiter1")
|
||||||
|
verweigert = await client.post(
|
||||||
|
"/api/v1/hersteller", json={"name": "Dräger"}, headers=auth_header(mitarbeiter_token)
|
||||||
|
)
|
||||||
|
assert verweigert.status_code == 403
|
||||||
|
|
||||||
|
liste = await client.get(
|
||||||
|
f"/api/v1/modelle?hersteller_id={hersteller_id}", headers=auth_header(mitarbeiter_token)
|
||||||
|
)
|
||||||
|
assert len(liste.json()) == 1
|
||||||
|
assert liste.json()[0]["name"] == "MEDUMAT Standard2"
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ export interface GeraetInstanz {
|
|||||||
id: string;
|
id: string;
|
||||||
objektposition_id: string;
|
objektposition_id: string;
|
||||||
seriennummer: string;
|
seriennummer: string;
|
||||||
|
modell_id: number | null;
|
||||||
pruefdatum: string | null;
|
pruefdatum: string | null;
|
||||||
naechste_pruefung: string | null;
|
naechste_pruefung: string | null;
|
||||||
status: "einsatzbereit" | "defekt" | "in_reparatur";
|
status: "einsatzbereit" | "defekt" | "in_reparatur";
|
||||||
@@ -282,6 +283,17 @@ export interface Kategorie {
|
|||||||
ueberkategorie_id: number | null;
|
ueberkategorie_id: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface Hersteller {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Modell {
|
||||||
|
id: number;
|
||||||
|
hersteller_id: number;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface Objekttyp {
|
export interface Objekttyp {
|
||||||
id: number;
|
id: number;
|
||||||
bereich_id: number;
|
bereich_id: number;
|
||||||
|
|||||||
Reference in New Issue
Block a user