feat(dokumente,identity): DOC-003 Versionierung + QR-Code-Etiketten
Dokument-Versionierung: vorgaenger_id verkettet Versionen (Migration 0027),
POST /dokumente/{id}/ersetzen legt neue Version an statt zu überschreiben,
alte Fassung bleibt als Historie erhalten. GET /dokumente/{id}/versionen
liefert die Kette (neueste zuerst), respektiert DOC-005-Zugriffsrechte.
Liste zeigt nur die jeweils aktuelle Version. Löschen der aktuellen Version
gibt automatisch den Vorgänger als neue "aktuelle" frei. Frontend:
"Ersetzen"-Button + aufklappbare Versionshistorie im DokumentePanel.
QR-Code-Etiketten: bisher nur Code128-Barcode möglich. generiere_qr_label_pdf
(reportlab-eigenes QR-Widget, keine neue Abhängigkeit) als Alternative,
?format=qr|code128 an /objekte/{id}/label.pdf und /objektpositionen/{id}/
label.pdf. AktePage bekommt "Etikett (Barcode)"/"Etikett (QR)"-Buttons
(vorher gab es dafür noch gar keinen UI-Zugang im Web-Frontend).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CVgbozhYmuEhiEJHffRXCV
This commit is contained in:
@@ -259,3 +259,104 @@ async def test_mitarbeiter_kann_rechnung_nicht_direkt_herunterladen(
|
||||
f"/api/v1/dokumente/{dokument_id}/download", headers=auth_header(verantwortlicher_token)
|
||||
)
|
||||
assert erlaubt.status_code == 200
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_dokument_ersetzen_legt_neue_version_an_alte_bleibt_erhalten(
|
||||
client, objekt_mit_position, mitarbeiter_user
|
||||
):
|
||||
"""DOC-003: alte Version bleibt in der Versionshistorie, Liste zeigt nur
|
||||
die neue als aktuell."""
|
||||
objekt, _material = objekt_mit_position
|
||||
token = await login(client, "mitarbeiter1")
|
||||
|
||||
v1 = await client.post(
|
||||
"/api/v1/dokumente",
|
||||
data={
|
||||
"entitaet_typ": "objekt",
|
||||
"entitaet_id": str(objekt.id),
|
||||
"dokumenttyp": "pruefprotokoll",
|
||||
"beschreibung": "Erste Fassung",
|
||||
},
|
||||
files={"datei": ("protokoll_v1.pdf", b"%PDF-1.4 v1", "application/pdf")},
|
||||
headers=auth_header(token),
|
||||
)
|
||||
v1_id = v1.json()["id"]
|
||||
|
||||
v2 = await client.post(
|
||||
f"/api/v1/dokumente/{v1_id}/ersetzen",
|
||||
data={"beschreibung": "Zweite Fassung"},
|
||||
files={"datei": ("protokoll_v2.pdf", b"%PDF-1.4 v2", "application/pdf")},
|
||||
headers=auth_header(token),
|
||||
)
|
||||
assert v2.status_code == 201
|
||||
v2_body = v2.json()
|
||||
assert v2_body["dateiname"] == "protokoll_v2.pdf"
|
||||
assert v2_body["dokumenttyp"] == "pruefprotokoll"
|
||||
assert v2_body["vorgaenger_id"] == v1_id
|
||||
|
||||
liste = await client.get(
|
||||
f"/api/v1/dokumente?entitaet_typ=objekt&entitaet_id={objekt.id}", headers=auth_header(token)
|
||||
)
|
||||
body = liste.json()
|
||||
assert len(body) == 1
|
||||
assert body[0]["dateiname"] == "protokoll_v2.pdf"
|
||||
|
||||
historie = await client.get(f"/api/v1/dokumente/{v2_body['id']}/versionen", headers=auth_header(token))
|
||||
dateinamen = [d["dateiname"] for d in historie.json()]
|
||||
assert dateinamen == ["protokoll_v2.pdf", "protokoll_v1.pdf"]
|
||||
|
||||
alte_datei_noch_da = await client.get(f"/api/v1/dokumente/{v1_id}/download", headers=auth_header(token))
|
||||
assert alte_datei_noch_da.status_code == 200
|
||||
assert alte_datei_noch_da.content == b"%PDF-1.4 v1"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_loeschen_der_aktuellen_version_gibt_vorgaenger_frei(client, objekt_mit_position, mitarbeiter_user):
|
||||
"""Löscht man die aktuelle Version, wird die vorherige wieder als aktuell
|
||||
gelistet (nichts zeigt mehr per vorgaenger_id auf sie)."""
|
||||
objekt, _material = objekt_mit_position
|
||||
token = await login(client, "mitarbeiter1")
|
||||
|
||||
v1 = await client.post(
|
||||
"/api/v1/dokumente",
|
||||
data={"entitaet_typ": "objekt", "entitaet_id": str(objekt.id), "dokumenttyp": "sonstiges"},
|
||||
files={"datei": ("v1.pdf", b"%PDF-1.4 v1", "application/pdf")},
|
||||
headers=auth_header(token),
|
||||
)
|
||||
v1_id = v1.json()["id"]
|
||||
v2 = await client.post(
|
||||
f"/api/v1/dokumente/{v1_id}/ersetzen",
|
||||
data={},
|
||||
files={"datei": ("v2.pdf", b"%PDF-1.4 v2", "application/pdf")},
|
||||
headers=auth_header(token),
|
||||
)
|
||||
v2_id = v2.json()["id"]
|
||||
|
||||
await client.delete(f"/api/v1/dokumente/{v2_id}", headers=auth_header(token))
|
||||
|
||||
liste = await client.get(
|
||||
f"/api/v1/dokumente?entitaet_typ=objekt&entitaet_id={objekt.id}", headers=auth_header(token)
|
||||
)
|
||||
body = liste.json()
|
||||
assert len(body) == 1
|
||||
assert body[0]["id"] == v1_id
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_versionshistorie_respektiert_zugriffsrechte(
|
||||
client, objekt_mit_position, mitarbeiter_user, materialverantwortlicher_user
|
||||
):
|
||||
objekt, _material = objekt_mit_position
|
||||
verantwortlicher_token = await login(client, "materialverantwortlicher1")
|
||||
v1 = await client.post(
|
||||
"/api/v1/dokumente",
|
||||
data={"entitaet_typ": "objekt", "entitaet_id": str(objekt.id), "dokumenttyp": "rechnung"},
|
||||
files={"datei": ("rechnung_v1.pdf", b"%PDF-1.4 v1", "application/pdf")},
|
||||
headers=auth_header(verantwortlicher_token),
|
||||
)
|
||||
v1_id = v1.json()["id"]
|
||||
|
||||
mitarbeiter_token = await login(client, "mitarbeiter1")
|
||||
verweigert = await client.get(f"/api/v1/dokumente/{v1_id}/versionen", headers=auth_header(mitarbeiter_token))
|
||||
assert verweigert.status_code == 403
|
||||
|
||||
Reference in New Issue
Block a user