# models/dokumente.py # Phase 3: Django-natives DMS – ersetzt Paperless-NGX-Integration import uuid import os from django.contrib.postgres.indexes import GinIndex from django.contrib.postgres.search import SearchVector, SearchVectorField from django.db import models from django.utils import timezone def dokument_upload_path(instance, filename): """Speichert Dateien in MEDIA_ROOT/dokumente/YYYY/MM//""" ext = os.path.splitext(filename)[1].lower() safe_name = os.path.basename(filename)[:100] return f"dokumente/{timezone.now().strftime('%Y/%m')}/{instance.id}/{safe_name}" class DokumentDatei(models.Model): """Nativ gespeicherte Datei im Django-DMS – ersetzt Paperless-Referenzen.""" KONTEXT_CHOICES = [ ("pachtvertrag", "Pachtvertrag"), ("antrag", "Antrag / Förderantrag"), ("verwendungsnachweis", "Verwendungsnachweis"), ("studiennachweis", "Studiennachweis"), ("rechnung", "Rechnung"), ("vertrag", "Vertrag"), ("bericht", "Bericht"), ("landkarte", "Landkarte / Kataster"), ("korrespondenz", "Korrespondenz / Brief"), ("bescheid", "Bescheid / Behörde"), ("stiftungsgeschichte", "Stiftungsgeschichte / Archiv"), ("email", "E-Mail-Nachricht"), ("anderes", "Sonstiges"), ] id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) titel = models.CharField(max_length=255, verbose_name="Titel") beschreibung = models.TextField(blank=True, verbose_name="Beschreibung") kontext = models.CharField( max_length=30, choices=KONTEXT_CHOICES, default="anderes", verbose_name="Dokumententyp", ) datei = models.FileField( upload_to=dokument_upload_path, verbose_name="Datei", ) dateiname_original = models.CharField( max_length=255, blank=True, verbose_name="Originaldateiname" ) dateityp = models.CharField( max_length=100, blank=True, verbose_name="MIME-Typ" ) dateigroesse = models.PositiveIntegerField( default=0, verbose_name="Dateigröße (Bytes)" ) # Volltext-Index (PostgreSQL FTS, befüllt per Signal) inhaltstext = models.TextField( blank=True, verbose_name="Extrahierter Textinhalt", help_text="Automatisch befüllt für PDF/TXT-Dateien. Basis für Volltextsuche.", ) suchvektor = SearchVectorField( null=True, blank=True, verbose_name="Such-Vektor (FTS)" ) # Zuordnungsfelder – optional, ein Dokument kann mehreren Entitäten gehören land = models.ForeignKey( "Land", on_delete=models.SET_NULL, null=True, blank=True, related_name="dms_dokumente", verbose_name="Länderei", ) paechter = models.ForeignKey( "Paechter", on_delete=models.SET_NULL, null=True, blank=True, related_name="dms_dokumente", verbose_name="Pächter", ) verpachtung = models.ForeignKey( "LandVerpachtung", on_delete=models.SET_NULL, null=True, blank=True, related_name="dms_dokumente", verbose_name="Verpachtung", ) destinataer = models.ForeignKey( "Destinataer", on_delete=models.SET_NULL, null=True, blank=True, related_name="dms_dokumente", verbose_name="Destinatär", ) foerderung = models.ForeignKey( "Foerderung", on_delete=models.SET_NULL, null=True, blank=True, related_name="dms_dokumente", verbose_name="Förderung", ) rentmeister = models.ForeignKey( "Rentmeister", on_delete=models.SET_NULL, null=True, blank=True, related_name="dms_dokumente", verbose_name="Rentmeister", ) verwaltungskosten = models.ForeignKey( "Verwaltungskosten", on_delete=models.SET_NULL, null=True, blank=True, related_name="dms_dokumente", verbose_name="Verwaltungskosten / Rechnung", ) # Herkunft (optional: Verweis auf altes Paperless-Dokument zur Rückverfolgung) paperless_dokument_id = models.IntegerField( null=True, blank=True, verbose_name="Paperless-ID (Migration)", help_text="Wird nach vollständiger Migration entfernt.", ) # Audit erstellt_von = models.ForeignKey( "auth.User", on_delete=models.SET_NULL, null=True, blank=True, related_name="hochgeladene_dokumente", verbose_name="Erstellt von", ) erstellt_am = models.DateTimeField(auto_now_add=True, verbose_name="Erstellt am") aktualisiert_am = models.DateTimeField(auto_now=True, verbose_name="Aktualisiert am") class Meta: verbose_name = "Dokument" verbose_name_plural = "Dokumente (DMS)" ordering = ["-erstellt_am"] indexes = [ # PostgreSQL GIN-Index für Volltextsuche GinIndex(fields=["suchvektor"], name="dms_suchvektor_gin_idx"), models.Index(fields=["kontext"]), models.Index(fields=["destinataer", "kontext"]), models.Index(fields=["land", "kontext"]), models.Index(fields=["paechter", "kontext"]), ] def __str__(self): return self.titel or self.dateiname_original or str(self.id) def save(self, *args, **kwargs): # Originaldateiname aus FileField ableiten if self.datei and not self.dateiname_original: self.dateiname_original = os.path.basename(self.datei.name) super().save(*args, **kwargs) def update_suchvektor(self): """Aktualisiert den Such-Vektor aus Titel, Beschreibung und Inhaltstext.""" DokumentDatei.objects.filter(pk=self.pk).update( suchvektor=SearchVector("titel", weight="A") + SearchVector("beschreibung", weight="B") + SearchVector("inhaltstext", weight="C"), ) def get_datei_url(self): """Gibt die Download-URL zurück.""" if self.datei: return self.datei.url return None def is_pdf(self): return self.dateityp == "application/pdf" or ( self.dateiname_original and self.dateiname_original.lower().endswith(".pdf") ) def get_human_size(self): """Gibt die Dateigröße leserlich zurück.""" size = self.dateigroesse if size < 1024: return f"{size} B" elif size < 1024 * 1024: return f"{size / 1024:.1f} KB" else: return f"{size / (1024 * 1024):.1f} MB"