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
+42
View File
@@ -3,6 +3,9 @@ import io
from barcode import Code128
from barcode.writer import ImageWriter
from PIL import Image
from reportlab.graphics import renderPDF
from reportlab.graphics.barcode import qr
from reportlab.graphics.shapes import Drawing
from reportlab.lib.units import mm
from reportlab.lib.utils import ImageReader
from reportlab.pdfgen import canvas
@@ -55,3 +58,42 @@ def generiere_code128_label_pdf(code: str, klartext_zusatz: str | None = None) -
c.showPage()
c.save()
return pdf_puffer.getvalue()
_QR_ZIELGROESSE = 25 * mm
_QR_MINDESTBREITE = 30 * mm
def generiere_qr_label_pdf(code: str, klartext_zusatz: str | None = None) -> bytes:
"""Alternative zum Code128-Label (Nutzer-Wunsch): QR-Code statt 1D-Barcode -
robuster bei teilbeschädigtem Etikett (Fehlerkorrektur), von praktisch jeder
Handy-Kamera nativ lesbar, kompakter am Objekt anbringbar. Gleicher
Klartext-Code als Inhalt wie beim Code128-Label, damit beide Varianten
denselben Scan-Lookup (/objekte/code/{code}) treffen."""
qr_code = qr.QrCodeWidget(code)
qr_code.barWidth = _QR_ZIELGROESSE
qr_code.barHeight = _QR_ZIELGROESSE
bounds = qr_code.getBounds()
qr_breite_pt = bounds[2] - bounds[0]
qr_hoehe_pt = bounds[3] - bounds[1]
zeichnung = Drawing(qr_breite_pt, qr_hoehe_pt, transform=[1, 0, 0, 1, -bounds[0], -bounds[1]])
zeichnung.add(qr_code)
label_breite = max(qr_breite_pt + 2 * _RAND, _QR_MINDESTBREITE)
zusatz_hoehe = 14 if klartext_zusatz else 0
label_hoehe = qr_hoehe_pt + zusatz_hoehe + 2 * _RAND
pdf_puffer = io.BytesIO()
c = canvas.Canvas(pdf_puffer, pagesize=(label_breite, label_hoehe))
zeichnung_x = (label_breite - qr_breite_pt) / 2
zeichnung_y = label_hoehe - qr_hoehe_pt - _RAND
renderPDF.draw(zeichnung, c, zeichnung_x, zeichnung_y)
if klartext_zusatz:
c.setFont("Helvetica", 9)
c.drawCentredString(label_breite / 2, _RAND - 2, klartext_zusatz)
c.showPage()
c.save()
return pdf_puffer.getvalue()