feat(dokumente,identity): DOC-003 Versionierung + QR-Code-Etiketten
CI / backend-tests (push) Failing after 2m15s
CI / frontend-build (push) Successful in 28s

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:
2026-09-08 11:03:46 +02:00
co-authored by Claude Sonnet 5
parent d5d8c72d9a
commit 977d81ddcd
14 changed files with 543 additions and 54 deletions
+23 -7
View File
@@ -1,4 +1,5 @@
import uuid
from typing import Literal
from fastapi import APIRouter, Depends, HTTPException, Response, status
from sqlalchemy import select
@@ -14,7 +15,9 @@ from app.models.objektposition import Objektposition
from app.models.stammdaten import Material, Objekttyp
from app.models.vorlage import Beladungsvorlage
from app.services import historie as historie_service
from app.services.label import generiere_code128_label_pdf
from app.services.label import generiere_code128_label_pdf, generiere_qr_label_pdf
LabelFormat = Literal["code128", "qr"]
from app.schemas.objekt import (
FahrzeugdetailsRead,
FahrzeugdetailsUpsert,
@@ -157,16 +160,26 @@ async def hole_objekt(
return _objekt_read(objekt, letzte_map.get(objekt.id))
def _erzeuge_label_pdf(code: str, klartext_zusatz: str | None, format: LabelFormat) -> bytes:
if format == "qr":
return generiere_qr_label_pdf(code, klartext_zusatz)
return generiere_code128_label_pdf(code, klartext_zusatz)
@router.get("/objekte/{objekt_id}/label.pdf")
async def objekt_label(
objekt_id: int, db: AsyncSession = Depends(get_db), _=Depends(get_current_user)
objekt_id: int,
format: LabelFormat = "code128",
db: AsyncSession = Depends(get_db),
_=Depends(get_current_user),
) -> Response:
"""Druckfertiges Code128-Label (Karte 10): Code + Objektname als Klartext-
Fallback. Vektor-PDF, skaliert verlustfrei auf jede Labelgröße beim Druck."""
"""Druckfertiges Etikett (Karte 10, QR-Variante Nutzer-Nachtrag): Code128
(Standard) oder QR-Code + Objektname als Klartext-Fallback. Vektor-PDF,
skaliert verlustfrei auf jede Labelgröße beim Druck."""
objekt = await db.get(Objekt, objekt_id)
if objekt is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Objekt nicht gefunden")
pdf = generiere_code128_label_pdf(objekt.code, objekt.name)
pdf = _erzeuge_label_pdf(objekt.code, objekt.name, format)
return Response(
content=pdf,
media_type="application/pdf",
@@ -176,7 +189,10 @@ async def objekt_label(
@router.get("/objektpositionen/{position_id}/label.pdf")
async def objektposition_label(
position_id: uuid.UUID, db: AsyncSession = Depends(get_db), _=Depends(get_current_user)
position_id: uuid.UUID,
format: LabelFormat = "code128",
db: AsyncSession = Depends(get_db),
_=Depends(get_current_user),
) -> Response:
"""Geräte-Etikett (Karte 10): eigener Code je Exemplar, Klartext-Zusatz ist
der Materialname (Objektname wäre bei mehreren Geräten am selben Objekt
@@ -185,7 +201,7 @@ async def objektposition_label(
if position is None or position.code is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Kein Geräte-Code vergeben")
material = await db.get(Material, position.material_id)
pdf = generiere_code128_label_pdf(position.code, material.name if material else None)
pdf = _erzeuge_label_pdf(position.code, material.name if material else None, format)
return Response(
content=pdf,
media_type="application/pdf",