fix(zustaendigkeit): ZUST-004 Runde 7 - Lagerort-Lücken aus Runde 5 geschlossen
CI / backend-tests (push) Failing after 3m47s
CI / frontend-build (push) Failing after 1m2s

GET /lagerorte war entgegen /lagerplaetze nicht standortgefiltert - Nutzer-
Review deckte das auf. Zusätzlich: standort_id konnte bisher an jedem
Lagerort-Typ gesetzt werden, nur dokumentiert als "nur bei typ=standort
sinnvoll". Jetzt harte 409-Validierung bei POST und PATCH (PATCH prüft die
resultierende Typ/standort_id-Kombination, nicht nur einzeln übergebene
Felder).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VDMvneg6wKfXdXvTYNd3dN
This commit is contained in:
patrickandClaude Sonnet 5 committed 2026-09-15 00:33:10 +02:00
1 parent b68314a06d
commit 260e0f6133
4 files changed
+192 -4

No files matched your search

+80
View File
@@ -16183,3 +16183,83 @@ Keine Commits in dieser Session.
- frontend/src/pages/admin/LagerSection.tsx | 41 ++++++++++++++++++++++++++++---
---
## 2026-09-14 23:53 – 23:57 (3m)
**Beschreibung:** Claude Code Session
**Projekt:** asb-material
### Commits
- b68314a feat(zustaendigkeit): ZUST-004 Runde 6 - kennzahlen/ablaufdaten/wartungen
### Geänderte Dateien
- DEVLOG.md | 105 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
- arbeitskacheln/17_zustaendigkeit.md | 18 ++++++++----
- backend/app/api/v1/endpoints/dashboard.py | 30 +++++++++++++------
- backend/app/services/dashboard.py | 59 ++++++++++++++++++++++++++++---------
- backend/app/services/wartung.py | 12 ++++++--
- backend/tests/test_zust004_standort_sichtbarkeit.py | 35 ++++++++++++++++++++++
---
## 2026-09-15 00:01 – 00:01 (0m)
**Beschreibung:** Claude Code Session
**Projekt:** asb-material
### Commits
Keine Commits in dieser Session.
### Geänderte Dateien
- DEVLOG.md | 105 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
- arbeitskacheln/17_zustaendigkeit.md | 18 ++++++++----
- backend/app/api/v1/endpoints/dashboard.py | 30 +++++++++++++------
- backend/app/services/dashboard.py | 59 ++++++++++++++++++++++++++++---------
- backend/app/services/wartung.py | 12 ++++++--
- backend/tests/test_zust004_standort_sichtbarkeit.py | 35 ++++++++++++++++++++++
---
## 2026-09-15 00:06 – 00:06 (0m)
**Beschreibung:** Claude Code Session
**Projekt:** asb-material
### Commits
Keine Commits in dieser Session.
### Geänderte Dateien
- DEVLOG.md | 105 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
- arbeitskacheln/17_zustaendigkeit.md | 18 ++++++++----
- backend/app/api/v1/endpoints/dashboard.py | 30 +++++++++++++------
- backend/app/services/dashboard.py | 59 ++++++++++++++++++++++++++++---------
- backend/app/services/wartung.py | 12 ++++++--
- backend/tests/test_zust004_standort_sichtbarkeit.py | 35 ++++++++++++++++++++++
---
## 2026-09-15 00:07 – 00:07 (0m)
**Beschreibung:** Claude Code Session
**Projekt:** asb-material
### Commits
Keine Commits in dieser Session.
### Geänderte Dateien
- DEVLOG.md | 105 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
- arbeitskacheln/17_zustaendigkeit.md | 18 ++++++++----
- backend/app/api/v1/endpoints/dashboard.py | 30 +++++++++++++------
- backend/app/services/dashboard.py | 59 ++++++++++++++++++++++++++++---------
- backend/app/services/wartung.py | 12 ++++++--
- backend/tests/test_zust004_standort_sichtbarkeit.py | 35 ++++++++++++++++++++++
---
## 2026-09-15 00:22 – 00:22 (0m)
**Beschreibung:** Claude Code Session
**Projekt:** asb-material
### Commits
Keine Commits in dieser Session.
### Geänderte Dateien
- DEVLOG.md | 105 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
- arbeitskacheln/17_zustaendigkeit.md | 18 ++++++++----
- backend/app/api/v1/endpoints/dashboard.py | 30 +++++++++++++------
- backend/app/services/dashboard.py | 59 ++++++++++++++++++++++++++++---------
- backend/app/services/wartung.py | 12 ++++++--
- backend/tests/test_zust004_standort_sichtbarkeit.py | 35 ++++++++++++++++++++++
---
+13
View File
@@ -244,6 +244,19 @@ war die Vorgängerlücke, diese hier ist die zweite gefundene Lücke).
Objekt-bezogenen Endpunkte durchgezogen. Test:
`test_dashboard_kennzahlen_ablaufdaten_wartungen_standort_eingeschraenkt`
in `backend/tests/test_zust004_standort_sichtbarkeit.py`.
**Runde 7 (2026-09-15, Nutzer-Review "Lager/Lagerorte nochmal
durchgehen"): zwei Lücken in Runde 5 gefunden und geschlossen.**
(1) `GET /lagerorte` selbst war NICHT gefiltert - nur `/lagerplaetze`
war es, ein auf Standort A beschränkter Nutzer sah trotzdem den
kompletten Lagerort-Baum. Jetzt gleiches Filter-Muster wie bei
Lagerplatz. (2) `standort_id` konnte an jedem Lagerort-Typ gesetzt werden
(nur dokumentiert, nicht erzwungen) - Nutzer-Entscheidung: jetzt harte
409-Validierung, `standort_id` nur an `typ=standort` erlaubt (POST UND
PATCH, PATCH prüft die resultierende Typ/standort_id-Kombination, nicht
nur die einzeln übergebenen Felder). Tests:
`test_lagerorte_liste_standort_eingeschraenkt`,
`test_lagerort_standort_id_nur_bei_typ_standort`
in `backend/tests/test_zust004_standort_sichtbarkeit.py`.
---
+35 -4
View File
@@ -7,7 +7,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
from app.api.deps import get_current_user, require_roles
from app.db.session import get_db
from app.models.auth import RolleTyp
from app.models.lager import Bestand, Lagerort, Lagerplatz, Materialbewegung
from app.models.lager import Bestand, Lagerort, LagerortTyp, Lagerplatz, Materialbewegung
from app.schemas.lager import (
BestandRead,
EinAuslagernRequest,
@@ -47,9 +47,36 @@ async def _get_or_404(db: AsyncSession, model: type[_T], id_: int, meldung: str)
# -- Lagerort (WH-001) ---------------------------------------------------------
@router.get("/lagerorte", response_model=list[LagerortRead])
async def liste_lagerorte(db: AsyncSession = Depends(get_db), _=Depends(get_current_user)) -> list[Lagerort]:
result = await db.execute(select(Lagerort))
return list(result.scalars().all())
async def liste_lagerorte(db: AsyncSession = Depends(get_db), current_user=Depends(get_current_user)) -> list[Lagerort]:
"""ZUST-004-Nachtrag (Runde 5 hatte diese Lücke übersehen, Nutzer-Fund
2026-09-15): war bisher NICHT gefiltert, obwohl /lagerplaetze es schon
war - ein auf Standort A beschränkter Nutzer sah trotzdem den kompletten
Lagerort-Baum anderer Standorte. Gleiches Muster wie bei Lagerplatz:
ein Knoten ohne auflösbaren Standort bleibt sichtbar (unrestricted),
einer mit fremdem Standort wird ausgeblendet."""
lagerorte = list((await db.execute(select(Lagerort))).scalars().all())
sichtbare = await sichtbare_standort_ids(db, current_user.id)
if sichtbare is None:
return lagerorte
gefiltert = []
for lagerort in lagerorte:
standort_id = await standort_fuer_lagerort(db, lagerort.id)
if standort_id is None or standort_id in sichtbare:
gefiltert.append(lagerort)
return gefiltert
def _pruefe_standort_id_nur_bei_typ_standort(typ: LagerortTyp, standort_id: int | None) -> None:
"""Nutzer-Entscheidung 2026-09-15: standort_id ist fachlich nur an einem
typ=standort-Knoten sinnvoll (dessen Vorfahren/Nachfahren lösen ihren
Standort über diesen Knoten auf) - an raum/lager/gebaeude/organisation
gesetzt wäre eine irreführende Fehlbedienung, jetzt hart abgelehnt statt
nur dokumentiert."""
if standort_id is not None and typ != LagerortTyp.standort:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail="standort_id ist nur an einem Lagerort vom Typ 'standort' erlaubt",
)
@router.post("/lagerorte", response_model=LagerortRead, status_code=status.HTTP_201_CREATED)
@@ -58,6 +85,7 @@ async def erstelle_lagerort(
) -> Lagerort:
if payload.parent_id is not None:
await _get_or_404(db, Lagerort, payload.parent_id, "Übergeordneter Lagerort nicht gefunden")
_pruefe_standort_id_nur_bei_typ_standort(payload.typ, payload.standort_id)
lagerort = Lagerort(**payload.model_dump())
db.add(lagerort)
await db.flush()
@@ -72,6 +100,9 @@ async def aendere_lagerort(
daten = payload.model_dump(exclude_unset=True)
if daten.get("parent_id") == lagerort_id:
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="Lagerort kann nicht sein eigener Elternknoten sein")
neuer_typ = daten.get("typ", lagerort.typ)
neue_standort_id = daten.get("standort_id", lagerort.standort_id)
_pruefe_standort_id_nur_bei_typ_standort(neuer_typ, neue_standort_id)
for feld, wert in daten.items():
setattr(lagerort, feld, wert)
await db.flush()
@@ -443,6 +443,70 @@ async def test_dashboard_kennzahlen_ablaufdaten_wartungen_standort_eingeschraenk
assert wartungen.status_code == 200
@pytest.mark.asyncio
async def test_lagerorte_liste_standort_eingeschraenkt(
client, admin_user, materialverantwortlicher_user, standort_factory
):
"""Nutzer-Fund 2026-09-15: GET /lagerorte selbst war in Runde 5 übersehen
worden - nur /lagerplaetze war gefiltert, der Lagerort-Baum nicht."""
standort_a = await standort_factory("Standort A14")
standort_b = await standort_factory("Standort B14")
admin_token = await login(client, "admin1")
ort_a = await client.post(
"/api/v1/lagerorte",
json={"name": "Wache A14", "typ": "standort", "standort_id": standort_a.id},
headers=auth_header(admin_token),
)
ort_b = await client.post(
"/api/v1/lagerorte",
json={"name": "Wache B14", "typ": "standort", "standort_id": standort_b.id},
headers=auth_header(admin_token),
)
await client.put(
f"/api/v1/benutzer/{materialverantwortlicher_user.id}/standorte",
json={"standort_ids": [standort_a.id]},
headers=auth_header(admin_token),
)
token = await login(client, "materialverantwortlicher1")
liste = await client.get("/api/v1/lagerorte", headers=auth_header(token))
ids = {o["id"] for o in liste.json()}
assert ort_a.json()["id"] in ids
assert ort_b.json()["id"] not in ids
@pytest.mark.asyncio
async def test_lagerort_standort_id_nur_bei_typ_standort(client, admin_user, standort_factory):
"""Nutzer-Entscheidung 2026-09-15: standort_id an einem raum/lager-Knoten
ist eine Fehlbedienung, jetzt hart abgelehnt (409) statt nur dokumentiert."""
standort = await standort_factory("Standort C14")
token = await login(client, "admin1")
falsch = await client.post(
"/api/v1/lagerorte",
json={"name": "Regal falsch verknüpft", "typ": "lager", "standort_id": standort.id},
headers=auth_header(token),
)
assert falsch.status_code == 409
richtig = await client.post(
"/api/v1/lagerorte",
json={"name": "Wache richtig verknüpft", "typ": "standort", "standort_id": standort.id},
headers=auth_header(token),
)
assert richtig.status_code == 201
# PATCH auf einen anderen Typ, ohne standort_id zu löschen, muss ebenfalls
# scheitern (neue Typ/standort_id-Kombination wird geprüft, nicht nur die
# jeweils einzeln übergebenen Felder).
umbenannt = await client.patch(
f"/api/v1/lagerorte/{richtig.json()['id']}", json={"typ": "lager"}, headers=auth_header(token)
)
assert umbenannt.status_code == 409
@pytest.mark.asyncio
async def test_standort_zugriff_setzen_nur_admin(client, mitarbeiter_user, mitarbeiter2_user):
token = await login(client, "mitarbeiter1")