Files
MABEA/backend/app/models/dokument.py
T
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

70 lines
3.3 KiB
Python

import enum
import uuid
from datetime import datetime
from sqlalchemy import ForeignKey, Integer, String
from sqlalchemy.dialects.postgresql import ENUM as PgEnum, TIMESTAMP, UUID
from sqlalchemy.orm import Mapped, mapped_column
from app.db.base import Base
class DokumentTyp(str, enum.Enum):
"""DOC-002: feste Kategorie statt Freitext - macht Dokumente filter-/
wiederfindbar (Nutzer-Vorgabe Modul 10: Prüfprotokolle/Wartungsberichte/
Bedienungsanleitungen/Rechnungen/Zulassungsdokumente)."""
pruefprotokoll = "pruefprotokoll"
wartungsbericht = "wartungsbericht"
bedienungsanleitung = "bedienungsanleitung"
rechnung = "rechnung"
zulassungsdokument = "zulassungsdokument"
sonstiges = "sonstiges"
dokument_typ_pg = PgEnum(DokumentTyp, name="dokument_typ", create_type=False)
# DOC-005: Rechnungen sind finanziell sensibel - nur Materialverantwortliche/
# Leitungsverantwortliche/Administration dürfen sie sehen/herunterladen,
# Mitarbeiter nicht (dürfen sie aber weiterhin hochladen, z.B. direkt beim
# Wareneingang scannen). Andere Dokumenttypen (Prüfprotokolle, Anleitungen,
# ...) bleiben für alle mit Zugriff auf die Akte sichtbar - die werden im
# Feld gebraucht.
EINGESCHRAENKTE_DOKUMENTTYPEN = {DokumentTyp.rechnung}
class Dokument(Base):
"""Roadmap Phase 5 (Modul Dokumente): polymorpher Datei-Anhang an beliebige
Ressource (Objekt, Objektposition, Geräteinstanz, Mangel, Fahrzeugdetails,
Benutzer, ...) - gleiches entitaet_typ/entitaet_id-Muster wie Historie/
Mangel. Datei liegt lokal auf Platte (self-hosted, kein Cloud-Zwang),
speicherpfad ist relativ zu settings.upload_dir."""
__tablename__ = "dokument"
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
entitaet_typ: Mapped[str] = mapped_column(String, nullable=False)
entitaet_id: Mapped[str] = mapped_column(String, nullable=False)
dateiname: Mapped[str] = mapped_column(String, nullable=False)
speicherpfad: Mapped[str] = mapped_column(String, nullable=False)
mime_type: Mapped[str] = mapped_column(String, nullable=False)
groesse_bytes: Mapped[int] = mapped_column(Integer, nullable=False)
# SHA-256 des Dateiinhalts (DOC-001: Duplikat-Erkennung) - hex-codiert, 64
# Zeichen. Vergleich nur innerhalb derselben entitaet_typ/entitaet_id, nicht
# global (gleiche Datei an zwei verschiedenen Objekten ist kein Duplikat).
# Nullable wegen Altbestand vor dieser Migration (Hash nachträglich ohne
# Dateizugriff nicht sinnvoll befüllbar); jeder neue Upload setzt ihn.
dateihash: Mapped[str | None] = mapped_column(String(64))
dokumenttyp: Mapped[DokumentTyp] = mapped_column(
dokument_typ_pg, nullable=False, default=DokumentTyp.sonstiges
)
beschreibung: Mapped[str | None] = mapped_column(String)
hochgeladen_von: Mapped[int] = mapped_column(ForeignKey("benutzer.id"), nullable=False)
hochgeladen_am: Mapped[datetime] = mapped_column(TIMESTAMP(timezone=True), nullable=False)
# DOC-003: Versionierung - zeigt auf das Dokument, das diese Version ersetzt.
# Alte Version bleibt als eigene Zeile erhalten (kein Überschreiben/Löschen),
# "aktuell" ist jedes Dokument, auf das kein anderes per vorgaenger_id zeigt.
vorgaenger_id: Mapped[uuid.UUID | None] = mapped_column(
UUID(as_uuid=True), ForeignKey("dokument.id"), nullable=True
)