Files
patrickandClaude Sonnet 5 977d81ddcd
CI / backend-tests (push) Failing after 2m15s
CI / frontend-build (push) Successful in 28s
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
2026-09-08 11:03:46 +02:00

100 lines
3.6 KiB
Python

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
# Karte 10: Code128 kameratauglich drucken - Mindesthöhe ~10mm, Ruhezone
# (quiet zone) >= 10 Strichbreiten links/rechts ist laut Karte die häufigste
# Ursache für Scan-Fehler bei selbstgedruckten Etiketten. module_width=0.3mm
# * 10 = 3mm Mindest-Ruhezone, quiet_zone hier bewusst höher (5mm) angesetzt.
_BARCODE_OPTIONEN = {
"module_width": 0.3,
"module_height": 10.0,
"quiet_zone": 5.0,
"font_size": 10,
"text_distance": 3.0,
"write_text": True,
"dpi": 300,
}
_RAND = 5 * mm
_MINDESTBREITE = 60 * mm
def generiere_code128_label_pdf(code: str, klartext_zusatz: str | None = None) -> bytes:
"""Druckfertiges Vektor-Label: Code128-Barcode + Klartext-Zeile (Code selbst,
via write_text der Bibliothek) + optionale zweite Klartext-Zeile (z. B.
Objekt-/Materialname) als Scan-Ausfall-Fallback (Karte 10)."""
barcode_puffer = io.BytesIO()
Code128(code, writer=ImageWriter()).write(barcode_puffer, options=_BARCODE_OPTIONEN)
barcode_puffer.seek(0)
bild = Image.open(barcode_puffer)
dpi = _BARCODE_OPTIONEN["dpi"]
breite_pt = bild.width / dpi * 72
hoehe_pt = bild.height / dpi * 72
label_breite = max(breite_pt + 2 * _RAND, _MINDESTBREITE)
zusatz_hoehe = 14 if klartext_zusatz else 0
label_hoehe = hoehe_pt + zusatz_hoehe + 2 * _RAND
pdf_puffer = io.BytesIO()
c = canvas.Canvas(pdf_puffer, pagesize=(label_breite, label_hoehe))
barcode_puffer.seek(0)
bild_x = (label_breite - breite_pt) / 2
bild_y = label_hoehe - hoehe_pt - _RAND
c.drawImage(ImageReader(barcode_puffer), bild_x, bild_y, width=breite_pt, height=hoehe_pt)
if klartext_zusatz:
c.setFont("Helvetica", 9)
c.drawCentredString(label_breite / 2, _RAND - 2, klartext_zusatz)
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()