feat(identity): IDENT-009 QR-/Code-Neuvergabe mit Historie
POST /objekte/{id}/code-neu-vergeben und /objektpositionen/{id}/code-neu-
vergeben (admin-only) - z.B. bei beschädigtem/verlorenem Etikett. Alter
Code bleibt über die bestehende Audit-Infrastruktur (Historie-Tabelle,
Prompt 13) nachvollziehbar statt eine eigene Versions-Tabelle zu brauchen:
GET /historie?entitaet_typ=objekt&entitaet_id=... zeigt alter_wert/
neuer_wert des Codes. Duplikat-Prüfung explizit vorab (Projekt-Konvention,
siehe geraet_instanz.py::erstelle).
Frontend: "Code neu vergeben"-Aktion in der Objektakte (nur Admin), inline
statt window.prompt (Accessibility-Vorgabe aus dieser Session).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CVgbozhYmuEhiEJHffRXCV
This commit is contained in:
@@ -525,3 +525,113 @@ async def test_fahrzeug_zuordnung_lehnt_nicht_zugfahrzeug_typ_ab(
|
||||
headers=auth_header(token),
|
||||
)
|
||||
assert abgelehnt_patch.status_code == 422
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_objekt_code_neu_vergeben_mit_historie(client, objekt_mit_position, admin_user):
|
||||
"""IDENT-009: neuer Code wird gesetzt, alter Code bleibt über die
|
||||
Historie nachvollziehbar."""
|
||||
objekt, _material = objekt_mit_position
|
||||
token = await login(client, "admin1")
|
||||
alter_code = objekt.code
|
||||
|
||||
neu = await client.post(
|
||||
f"/api/v1/objekte/{objekt.id}/code-neu-vergeben",
|
||||
json={"neuer_code": "KT-NEU", "begruendung": "Etikett beschädigt"},
|
||||
headers=auth_header(token),
|
||||
)
|
||||
assert neu.status_code == 200
|
||||
assert neu.json()["code"] == "KT-NEU"
|
||||
|
||||
historie = await client.get(
|
||||
f"/api/v1/historie?entitaet_typ=objekt&entitaet_id={objekt.id}", headers=auth_header(token)
|
||||
)
|
||||
eintraege = [h for h in historie.json() if h["ereignistyp"] == "objekt_code_neu_vergeben"]
|
||||
assert len(eintraege) == 1
|
||||
assert eintraege[0]["alter_wert"] == {"code": alter_code}
|
||||
assert eintraege[0]["neuer_wert"] == {"code": "KT-NEU"}
|
||||
assert eintraege[0]["begruendung"] == "Etikett beschädigt"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_objekt_code_neu_vergeben_duplikat_abgelehnt(
|
||||
client, db_session, admin_user, standort_factory, objekttyp_factory, hauptserver_id
|
||||
):
|
||||
"""Neuer Code darf nicht mit dem Code eines ANDEREN Objekts kollidieren."""
|
||||
from app.models.objekt import Objekt
|
||||
|
||||
objekttyp = await objekttyp_factory("Typ-CodeDupe")
|
||||
standort = await standort_factory("Standort-CodeDupe")
|
||||
|
||||
erstes = Objekt(
|
||||
code="KT-A",
|
||||
name="Objekt A",
|
||||
objekttyp_id=objekttyp.id,
|
||||
standort_id=standort.id,
|
||||
zustaendiger_server_id=hauptserver_id,
|
||||
)
|
||||
zweites = Objekt(
|
||||
code="KT-B",
|
||||
name="Objekt B",
|
||||
objekttyp_id=objekttyp.id,
|
||||
standort_id=standort.id,
|
||||
zustaendiger_server_id=hauptserver_id,
|
||||
)
|
||||
db_session.add_all([erstes, zweites])
|
||||
await db_session.flush()
|
||||
|
||||
token = await login(client, "admin1")
|
||||
konflikt = await client.post(
|
||||
f"/api/v1/objekte/{zweites.id}/code-neu-vergeben",
|
||||
json={"neuer_code": "KT-A"},
|
||||
headers=auth_header(token),
|
||||
)
|
||||
assert konflikt.status_code == 409
|
||||
|
||||
eigener_code_ist_ok = await client.post(
|
||||
f"/api/v1/objekte/{zweites.id}/code-neu-vergeben",
|
||||
json={"neuer_code": "KT-B"},
|
||||
headers=auth_header(token),
|
||||
)
|
||||
assert eigener_code_ist_ok.status_code == 200
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_mitarbeiter_darf_code_nicht_neu_vergeben(client, objekt_mit_position, mitarbeiter_user):
|
||||
objekt, _material = objekt_mit_position
|
||||
token = await login(client, "mitarbeiter1")
|
||||
response = await client.post(
|
||||
f"/api/v1/objekte/{objekt.id}/code-neu-vergeben",
|
||||
json={"neuer_code": "KT-XYZ"},
|
||||
headers=auth_header(token),
|
||||
)
|
||||
assert response.status_code == 403
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_objektposition_code_neu_vergeben(client, db_session, objekt_mit_position, admin_user):
|
||||
objekt, material = objekt_mit_position
|
||||
token = await login(client, "admin1")
|
||||
|
||||
positionen = await client.get(f"/api/v1/objekte/{objekt.id}/positionen", headers=auth_header(token))
|
||||
position_id = positionen.json()[0]["id"]
|
||||
await client.patch(
|
||||
f"/api/v1/objekte/{objekt.id}/positionen/{position_id}",
|
||||
json={"code": "GR-ALT"},
|
||||
headers=auth_header(token),
|
||||
)
|
||||
|
||||
neu = await client.post(
|
||||
f"/api/v1/objektpositionen/{position_id}/code-neu-vergeben",
|
||||
json={"neuer_code": "GR-NEU", "begruendung": "Etikett verloren"},
|
||||
headers=auth_header(token),
|
||||
)
|
||||
assert neu.status_code == 200
|
||||
assert neu.json()["code"] == "GR-NEU"
|
||||
|
||||
historie = await client.get(
|
||||
f"/api/v1/historie?entitaet_typ=objektposition&entitaet_id={position_id}", headers=auth_header(token)
|
||||
)
|
||||
eintraege = [h for h in historie.json() if h["ereignistyp"] == "objektposition_code_neu_vergeben"]
|
||||
assert len(eintraege) == 1
|
||||
assert eintraege[0]["alter_wert"] == {"code": "GR-ALT"}
|
||||
|
||||
Reference in New Issue
Block a user