Files
stiftung-management-system/app/stiftung/views/dokumente.py
SysAdmin Agent 3ca2706e5d Phase 0: forms.py, admin.py und views.py in Domain-Packages aufteilen
- forms.py → forms/ Package (8 Domänen: destinataere, land, finanzen,
  foerderung, dokumente, veranstaltung, system, geschichte)
- admin.py → admin/ Package (7 Domänen, alle 22 @admin.register dekoriert)
- views.py (8845 Zeilen) → views/ Package (10 Domänen: dashboard, destinataere,
  land, paechter, finanzen, foerderung, dokumente, unterstuetzungen,
  veranstaltung, geschichte, system)
- __init__.py in jedem Package re-exportiert alle Symbole für Rückwärtskompatibilität
- urls.py bleibt unverändert (funktioniert durch Re-Exports)
- Django system check: 0 Fehler, alle URL-Auflösungen funktionieren

Keine funktionalen Änderungen – reine Strukturverbesserung für Vision 2026.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-11 09:55:15 +00:00

1454 lines
57 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# views/dokumente.py
# Phase 0: Vision 2026 Code-Refactoring
import csv
import io
import json
import os
import time
from datetime import datetime, timedelta, date
from decimal import Decimal
import qrcode
import qrcode.image.svg
import requests
from django.conf import settings
from django.contrib import messages
from django.contrib.auth.decorators import login_required
from django.core.paginator import Paginator
from django.db.models import (Avg, Count, DecimalField, F, IntegerField, Q,
Sum, Value)
from django.db.models.functions import Cast, Coalesce, NullIf, Replace
from django.http import HttpResponse, JsonResponse
from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse
from django.utils import timezone
from django.views.decorators.csrf import csrf_exempt
from django_otp.decorators import otp_required
from django_otp.plugins.otp_totp.models import TOTPDevice
from django_otp.plugins.otp_static.models import StaticDevice, StaticToken
from django_otp.util import random_hex
from rest_framework.decorators import api_view
from rest_framework.response import Response
from stiftung.models import (AppConfiguration, AuditLog, BackupJob, BankTransaction,
BriefVorlage, CSVImport, Destinataer,
DestinataerEmailEingang, DestinataerNotiz,
DestinataerUnterstuetzung,
DokumentLink, Foerderung, GeschichteBild, GeschichteSeite,
Land, LandAbrechnung, LandVerpachtung, Paechter, Person,
Rentmeister, StiftungsKalenderEintrag, StiftungsKonto,
UnterstuetzungWiederkehrend, Veranstaltung,
Veranstaltungsteilnehmer, Verwaltungskosten,
VierteljahresNachweis)
from stiftung.forms import (
DestinataerForm, DestinataerUnterstuetzungForm, DestinataerNotizForm,
FoerderungForm, GeschichteBildForm, GeschichteSeiteForm,
LandForm, LandVerpachtungForm, LandAbrechnungForm,
PaechterForm, DokumentLinkForm,
RentmeisterForm, StiftungsKontoForm, VerwaltungskostenForm,
BankTransactionForm, BankImportForm,
UnterstuetzungForm, UnterstuetzungWiederkehrendForm,
UnterstuetzungMarkAsPaidForm, VierteljahresNachweisForm,
UserCreationForm, UserUpdateForm, PasswordChangeForm, UserPermissionForm,
TwoFactorSetupForm, TwoFactorVerifyForm, TwoFactorDisableForm,
BackupTokenRegenerateForm, PersonForm,
VeranstaltungForm, VeranstaltungsteilnehmerForm,
)
@login_required
def dokument_management(request):
"""Dokumentenverwaltung: zeigt Paperless-Dokumente (mit Stiftung-Tags) und bestehende Verknüpfungen.
Bietet Filter und ermöglicht Re-Linking.
"""
return render(request, "stiftung/dokument_management.html")
@api_view(["GET"])
def paperless_document_redirect(_request, doc_id: int):
"""Redirects to the Paperless UI document URL and supports thumbnails if needed later."""
from stiftung.utils.config import get_paperless_config
config = get_paperless_config()
url = config["api_url"]
if not url:
return Response({"error": "Paperless API not configured"}, status=400)
# Remove /api suffix if present, then construct the document URL
base_url = url[:-4] if url.endswith("/api") else url
# For external Paperless (already includes /paperless/ in base URL)
return redirect(f"{base_url}/documents/{doc_id}/details/")
@login_required
def dokument_list(request):
"""Zeigt alle verknüpften Dokumente an"""
# Alle verknüpften Dokumente laden
dokumente = DokumentLink.objects.all().order_by("-id")
# Paperless-API-Konfiguration für verfügbare Dokumente
import requests
from stiftung.utils.config import get_paperless_config
config = get_paperless_config()
url = config["api_url"]
token = config["api_token"]
available_dokumente = []
if url and token:
try:
base_url = url[:-4] if url.endswith("/api") else url
headers = {"Authorization": f"Token {token}"}
# Alle verfügbaren Dokumente abrufen (mit Paginierung)
all_dokumente = []
page = 1
page_size = 100
while True:
response = requests.get(
f"{base_url}/api/documents/?page={page}&page_size={page_size}",
headers=headers,
timeout=10,
)
response.raise_for_status()
data = response.json()
all_dokumente.extend(data.get("results", []))
if not data.get("next"):
break
page += 1
# Stiftung-Dokumente filtern
for doc in all_dokumente:
try:
tags = []
doc_tags = doc.get("tags", [])
if isinstance(doc_tags, list):
for tag in doc_tags:
if isinstance(tag, dict) and "name" in tag:
tags.append(tag["name"])
elif isinstance(tag, str):
tags.append(tag)
elif isinstance(tag, int):
tags.append(f"Tag_{tag}")
elif isinstance(doc_tags, str):
tags = [tag.strip() for tag in doc_tags.split(",")]
if any(
tag
in [
config["destinataere_tag"],
config["land_tag"],
config["admin_tag"],
]
for tag in tags
):
bereits_verknuepft = DokumentLink.objects.filter(
paperless_document_id=doc["id"]
).exists()
if not bereits_verknuepft:
available_dokumente.append(
{
"id": doc["id"],
"title": doc.get("title", f'Dokument {doc["id"]}'),
"created_date": doc.get("created_date", ""),
"tags": tags,
"thumbnail_url": f"{base_url}/api/documents/{doc['id']}/thumb/",
"document_url": f"{base_url}/documents/{doc['id']}/",
}
)
except Exception:
continue
# Nach Erstellungsdatum sortieren
available_dokumente.sort(key=lambda x: x["created_date"], reverse=True)
except Exception:
pass
context = {
"dokumente": dokumente,
"available_dokumente": available_dokumente,
"title": "Alle verknüpften Dokumente",
}
return render(request, "stiftung/dokument_list.html", context)
@login_required
def dokument_detail(request, pk):
"""Show details of a specific document link"""
dokument = get_object_or_404(DokumentLink, pk=pk)
context = {
"dokument": dokument,
"title": f"Dokument: {dokument}",
}
return render(request, "stiftung/dokument_detail.html", context)
@login_required
def dokument_create(request):
"""Create a new document link"""
if request.method == "POST":
form = DokumentLinkForm(request.POST)
if form.is_valid():
dokument = form.save()
messages.success(
request, f'Dokument "{dokument.titel}" wurde erfolgreich verknüpft.'
)
# Zurück zur verknüpften Entität leiten
if dokument.land_verpachtung_id:
return redirect(
"stiftung:verpachtung_detail", pk=dokument.land_verpachtung_id
)
elif dokument.verpachtung_id:
return redirect(
"stiftung:verpachtung_detail", pk=dokument.verpachtung_id
)
elif dokument.land_id:
return redirect("stiftung:land_detail", pk=dokument.land_id)
elif dokument.paechter_id:
return redirect("stiftung:paechter_detail", pk=dokument.paechter_id)
elif dokument.destinataer_id:
return redirect(
"stiftung:destinataer_detail", pk=dokument.destinataer_id
)
elif dokument.foerderung_id:
return redirect("stiftung:foerderung_detail", pk=dokument.foerderung_id)
else:
return redirect("stiftung:dokument_detail", pk=dokument.pk)
else:
# Initial-Werte aus GET-Parametern setzen
initial_data = {}
if request.GET.get("land_verpachtung_id"):
initial_data["land_verpachtung_id"] = request.GET.get("land_verpachtung_id")
if request.GET.get("verpachtung"):
initial_data["verpachtung_id"] = request.GET.get("verpachtung")
if request.GET.get("land"):
initial_data["land_id"] = request.GET.get("land")
if request.GET.get("paechter"):
initial_data["paechter_id"] = request.GET.get("paechter")
if request.GET.get("destinataer"):
initial_data["destinataer_id"] = request.GET.get("destinataer")
if request.GET.get("foerderung"):
initial_data["foerderung_id"] = request.GET.get("foerderung")
form = DokumentLinkForm(initial=initial_data)
context = {
"form": form,
"title": "Neues Dokument verknüpfen",
}
return render(request, "stiftung/dokument_form.html", context)
@login_required
def dokument_update(request, pk):
"""Update an existing document link"""
dokument = get_object_or_404(DokumentLink, pk=pk)
if request.method == "POST":
form = DokumentLinkForm(request.POST, instance=dokument)
if form.is_valid():
form.save()
messages.success(
request, f'Dokument "{dokument.titel}" wurde erfolgreich aktualisiert.'
)
return redirect("stiftung:dokument_detail", pk=dokument.pk)
else:
form = DokumentLinkForm(instance=dokument)
context = {
"form": form,
"dokument": dokument,
"title": f"Dokument bearbeiten: {dokument}",
}
return render(request, "stiftung/dokument_form.html", context)
@login_required
def dokument_delete(request, pk):
"""Delete a document link"""
dokument = get_object_or_404(DokumentLink, pk=pk)
if request.method == "POST":
dokument.delete()
messages.success(
request, f'Dokument "{dokument.titel}" wurde erfolgreich gelöscht.'
)
return redirect("stiftung:dokument_list")
context = {
"dokument": dokument,
"title": f"Dokument löschen: {dokument}",
}
return render(request, "stiftung/dokument_confirm_delete.html", context)
# Legacy document views removed - use dokument_management instead
# Jahresbericht Views
@api_view(["GET"])
def paperless_ping(_request):
from stiftung.utils.config import get_paperless_config
config = get_paperless_config()
url = config["api_url"]
token = config["api_token"]
if not url or not token:
return Response(
{"ok": False, "reason": "Paperless API not configured"}, status=400
)
try:
# Entferne /api vom Ende der URL falls vorhanden
base_url = url[:-4] if url.endswith("/api") else url
r = requests.get(
f"{base_url}/api/tags/",
headers={"Authorization": f"Token {token}"},
timeout=5,
)
return Response({"ok": r.ok, "status_code": r.status_code})
except Exception as e:
return Response({"ok": False, "error": str(e)}, status=500)
@api_view(["GET"])
def paperless_documents(request):
"""Holt Dokumente aus Paperless mit den erforderlichen Tags.
Optionales Polling: ?poll=1 wartet kurz und versucht erneut, damit frisch ingestete
Dokumente in Paperless erscheinen, bevor die Liste zurückgegeben wird.
"""
from stiftung.utils.config import get_paperless_config
config = get_paperless_config()
url = config["api_url"]
token = config["api_token"]
required_tag = config["destinataere_tag"]
land_tag = config["land_tag"]
admin_tag = config["admin_tag"]
destinaere_tag_id = config["destinataere_tag_id"]
land_tag_id = config["land_tag_id"]
admin_tag_id = config["admin_tag_id"]
if not url or not token:
return Response(
{
"error": "Paperless API not configured",
"message": "Please set PAPERLESS_API_URL and PAPERLESS_API_TOKEN in your environment variables",
"documents": [],
"total_destinaere": 0,
"total_land": 0,
"total_admin": 0,
"total_all": 0,
},
status=400,
)
try:
# Entferne /api vom Ende der URL falls vorhanden
base_url = url[:-4] if url.endswith("/api") else url
headers = {"Authorization": f"Token {token}"}
def fetch_tagged():
# mit ordering=-created neueste zuerst
dest_resp = requests.get(
f"{base_url}/api/documents/?tags__id={destinaere_tag_id}&ordering=-created",
headers=headers,
timeout=10,
)
dest_resp.raise_for_status()
dest_docs = dest_resp.json()
land_resp = requests.get(
f"{base_url}/api/documents/?tags__id={land_tag_id}&ordering=-created",
headers=headers,
timeout=10,
)
land_resp.raise_for_status()
land_docs = land_resp.json()
admin_resp = requests.get(
f"{base_url}/api/documents/?tags__id={admin_tag_id}&ordering=-created",
headers=headers,
timeout=10,
)
admin_resp.raise_for_status()
admin_docs = admin_resp.json()
return dest_docs, land_docs, admin_docs
dest_docs, land_docs, admin_docs = fetch_tagged()
# Optionales kurzes Polling, wenn angefordert
if request.GET.get("poll") in ("1", "true", "yes"):
start_total = sum(
[
dest_docs.get("count", 0),
land_docs.get("count", 0),
admin_docs.get("count", 0),
]
)
deadline = time.time() + 6.0 # bis zu 6 Sekunden warten
while time.time() < deadline:
time.sleep(1.0)
d2, l2, a2 = fetch_tagged()
new_total = sum(
[d2.get("count", 0), l2.get("count", 0), a2.get("count", 0)]
)
if new_total > start_total:
dest_docs, land_docs, admin_docs = d2, l2, a2
break
# Alle Dokumente zusammenfassen
all_documents = []
for doc in dest_docs.get("results", []):
doc["tag_category"] = "destinaere"
all_documents.append(doc)
for doc in land_docs.get("results", []):
doc["tag_category"] = "land"
all_documents.append(doc)
for doc in admin_docs.get("results", []):
doc["tag_category"] = "admin"
all_documents.append(doc)
return Response(
{
"documents": all_documents,
"total_destinaere": dest_docs.get("count", 0),
"total_land": land_docs.get("count", 0),
"total_admin": admin_docs.get("count", 0),
"total_all": len(all_documents),
}
)
except requests.exceptions.RequestException as e:
import logging
logger = logging.getLogger(__name__)
logger.error(f"Paperless API request failed: {e}")
logger.error(f"Paperless API URL: {base_url}")
logger.error(f"Token configured: {'Yes' if token else 'No'}")
return Response(
{
"error": f"API-Fehler: {e}",
"message": f"Could not connect to Paperless API at {base_url}. Please check your configuration.",
"debug_info": {
"api_url": base_url,
"has_token": bool(token),
"error_type": type(e).__name__
},
"documents": [],
"total_destinaere": 0,
"total_land": 0,
"total_admin": 0,
"total_all": 0,
},
status=500,
)
except Exception as e:
return Response(
{
"error": f"Unerwarteter Fehler: {e}",
"message": "An unexpected error occurred while fetching documents.",
"documents": [],
"total_destinaere": 0,
"total_land": 0,
"total_admin": 0,
"total_all": 0,
},
status=500,
)
# Legacy dokument_integration view removed - use dokument_management instead
@api_view(["GET"])
def paperless_debug(request):
"""Debug-View für Paperless-Integration"""
from stiftung.utils.config import get_paperless_config
config = get_paperless_config()
url = config["api_url"]
token = config["api_token"]
required_tag = config["destinataere_tag"]
land_tag = config["land_tag"]
admin_tag = config["admin_tag"]
destinaere_tag_id = config["destinataere_tag_id"]
land_tag_id = config["land_tag_id"]
admin_tag_id = config["admin_tag_id"]
if not url or not token:
return Response({"error": "Paperless API not configured"}, status=400)
try:
# Entferne /api vom Ende der URL falls vorhanden
base_url = url[:-4] if url.endswith("/api") else url
headers = {"Authorization": f"Token {token}"}
# Alle Tags abrufen
tags_response = requests.get(
f"{base_url}/api/tags/?page_size=1000", headers=headers, timeout=10
)
tags_response.raise_for_status()
tags_data = tags_response.json()
# Alle Tags durchsuchen
all_tags = tags_data.get("results", [])
exact_match_destinaere = None
exact_match_land = None
exact_match_admin = None
similar_tags = []
# Nach den neuen Tag-Namen suchen (mit Unterstrichen)
for tag in all_tags:
tag_name = tag.get("name", "")
tag_id = tag.get("id")
# Suche nach den neuen Tag-Namen
if tag_name == "Stiftung_Destinatäre":
exact_match_destinaere = {"id": tag_id, "name": tag_name}
elif tag_name == "Stiftung_Land_und_Pächter":
exact_match_land = {"id": tag_id, "name": tag_name}
elif tag_name == "Stiftung_Administration":
exact_match_admin = {"id": tag_id, "name": tag_name}
# Ähnliche Tags finden
if (
"stiftung" in tag_name.lower()
or "destinat" in tag_name.lower()
or "land" in tag_name.lower()
or "admin" in tag_name.lower()
):
similar_tags.append({"id": tag_id, "name": tag_name})
# Alle Tag-Namen sammeln
all_tag_names = [tag.get("name", "") for tag in all_tags]
# Dokumente abrufen
documents_response = requests.get(
f"{base_url}/api/documents/?page_size=10", headers=headers, timeout=10
)
documents_response.raise_for_status()
documents_data = documents_response.json()
# Stiftung-Dokumente finden (mit Tag 21 "Stiftung")
stiftung_documents = []
for doc in documents_data.get("results", []):
doc_tags = doc.get("tags", [])
if 21 in doc_tags: # Tag 21 ist "Stiftung"
stiftung_documents.append(doc)
# Sample-Dokumente mit Tag-Namen anreichern
sample_documents = documents_data.get("results", [])[:5]
enriched_documents = []
for doc in sample_documents:
doc_copy = doc.copy()
tag_names = []
for tag_id in doc.get("tags", []):
# Tag-Namen aus der Tag-Liste finden
tag_name = next(
(
tag.get("name", f"Unknown({tag_id})")
for tag in all_tags
if tag.get("id") == tag_id
),
f"Unknown({tag_id})",
)
tag_names.append(tag_name)
doc_copy["tag_names"] = tag_names
enriched_documents.append(doc_copy)
return Response(
{
"paperless_url": url,
"base_url": base_url,
"required_tag": required_tag,
"land_tag": land_tag,
"admin_tag": admin_tag,
"destinaere_tag_id": destinaere_tag_id,
"land_tag_id": land_tag_id,
"admin_tag_id": admin_tag_id,
"exact_match_destinaere": exact_match_destinaere,
"exact_match_land": exact_match_land,
"exact_match_admin": exact_match_admin,
"similar_tags": similar_tags,
"all_tag_names": all_tag_names,
"total_tags": len(all_tags),
"total_documents": documents_data.get("count", 0),
"sample_documents": sample_documents,
"api_token_length": len(token) if token else 0,
"enriched_documents": enriched_documents,
"stiftung_documents": stiftung_documents,
}
)
except requests.exceptions.RequestException as e:
return Response({"error": f"API-Fehler: {e}"}, status=500)
except Exception as e:
return Response({"error": f"Unerwarteter Fehler: {e}"}, status=500)
@api_view(["GET"])
def paperless_tags_only(request):
"""Holt nur die Tag-Liste aus Paperless - ohne Dokumente"""
from stiftung.utils.config import get_paperless_config
config = get_paperless_config()
url = config["api_url"]
token = config["api_token"]
if not url or not token:
return Response({"error": "Paperless API not configured"}, status=400)
try:
# Entferne /api vom Ende der URL falls vorhanden
base_url = url[:-4] if url.endswith("/api") else url
# Alle Tags abrufen (mit großer page_size)
headers = {"Authorization": f"Token {token}"}
# Erste Anfrage mit großer page_size
tags_response = requests.get(
f"{base_url}/api/tags/?page_size=1000", headers=headers, timeout=10
)
tags_response.raise_for_status()
tags_data = tags_response.json()
all_tags = []
# Erste Seite verarbeiten
for tag in tags_data.get("results", []):
tag_detail = {
"id": tag.get("id"),
"name": tag.get("name", ""),
"slug": tag.get("slug", ""),
"color": tag.get("color", ""),
"text_color": tag.get("text_color", ""),
"match": tag.get("match", ""),
"matching_algorithm": tag.get("matching_algorithm"),
"is_inbox_tag": tag.get("is_inbox_tag"),
"document_count": tag.get("document_count", 0),
}
all_tags.append(tag_detail)
# Weitere Seiten abrufen falls vorhanden
next_url = tags_data.get("next")
while next_url:
next_response = requests.get(next_url, headers=headers, timeout=10)
next_response.raise_for_status()
next_data = next_response.json()
for tag in next_data.get("results", []):
tag_detail = {
"id": tag.get("id"),
"name": tag.get("name", ""),
"slug": tag.get("slug", ""),
"color": tag.get("color", ""),
"text_color": tag.get("text_color", ""),
"match": tag.get("match", ""),
"matching_algorithm": tag.get("matching_algorithm"),
"is_inbox_tag": tag.get("is_inbox_tag"),
"document_count": tag.get("document_count", 0),
}
all_tags.append(tag_detail)
next_url = next_data.get("next")
# Nach ID sortieren
all_tags.sort(key=lambda x: x["id"])
return Response(
{
"total_tags": len(all_tags),
"tags": all_tags,
"tag_ids": [tag["id"] for tag in all_tags],
"tag_names": [tag["name"] for tag in all_tags],
"api_info": {
"page_size_used": 1000,
"total_count_from_api": tags_data.get("count", 0),
},
}
)
except requests.exceptions.RequestException as e:
return Response({"error": f"API-Fehler: {e}"}, status=500)
except Exception as e:
return Response({"error": f"Unerwarteter Fehler: {e}"}, status=500)
@api_view(["GET"])
def link_document_search(request):
"""Sucht nach Datensätzen für die Dokument-Verknüpfung"""
from django.db.models import Q
query = request.GET.get("q", "")
category = request.GET.get("category", "all")
results = {}
if category in ["all", "destinataer"]:
# Suche nach Destinatären
destinataer_query = Q()
if query and query != "all":
destinataer_query = (
Q(nachname__icontains=query)
| Q(vorname__icontains=query)
| Q(email__icontains=query)
| Q(telefon__icontains=query)
| Q(strasse__icontains=query)
| Q(ort__icontains=query)
| Q(plz__icontains=query)
| Q(institution__icontains=query)
| Q(familienzweig__icontains=query)
| Q(notizen__icontains=query)
)
destinataer_results = Destinataer.objects.filter(destinataer_query)[:25]
results["destinataer"] = [
{
"id": d.id,
"name": (
f"{d.vorname} {d.nachname}".strip()
if d.vorname
else (d.institution or d.nachname)
),
"type": "Destinatär",
"details": f"{d.strasse or ''}{(', ' + d.plz + ' ' + d.ort) if d.plz and d.ort else (' ' + d.ort) if d.ort else ''} {('' + d.email) if d.email else ''} {('' + d.institution) if d.institution else ''} {('' + d.familienzweig) if d.familienzweig else ''}".strip(),
}
for d in destinataer_results
]
if category in ["all", "land"]:
# Suche nach Ländereien
land_query = Q()
if query and query != "all":
# Extract numbers from search terms like "Flur 9" or "Flurstück 11"
import re
flur_match = re.search(r"flur\s*(\d+)", query, re.IGNORECASE)
flurstuck_match = re.search(r"flurstück\s*(\d+)", query, re.IGNORECASE)
land_query = (
Q(gemarkung__icontains=query)
| Q(gemeinde__icontains=query)
| Q(flur__icontains=query)
| Q(flurstueck__icontains=query)
| Q(lfd_nr__icontains=query)
| Q(ew_nummer__icontains=query)
| Q(notizen__icontains=query)
)
# Add specific searches for extracted numbers
if flur_match:
land_query |= Q(flur__exact=flur_match.group(1))
if flurstuck_match:
land_query |= Q(flurstueck__exact=flurstuck_match.group(1))
land_results = Land.objects.filter(land_query)[:25]
results["land"] = [
{
"id": l.id,
"name": f"{l.gemarkung} - {l.gemeinde}, Flur {l.flur}, Flurstück {l.flurstueck or 'N/A'}",
"type": "Land",
"details": f"Lfd.Nr: {l.lfd_nr or 'N/A'} • EW-Nr: {l.ew_nummer or 'N/A'} • Größe: {l.groesse_qm or 0}",
}
for l in land_results
]
if category in ["all", "verpachtung"]:
# Suche nach Verpachtungen (using new LandVerpachtung model)
verpachtung_query = Q()
if query and query != "all":
verpachtung_query = (
Q(paechter__nachname__icontains=query)
| Q(paechter__vorname__icontains=query)
| Q(paechter__ort__icontains=query)
| Q(paechter__email__icontains=query)
| Q(paechter__pachtnummer__icontains=query)
| Q(land__gemarkung__icontains=query)
| Q(land__gemeinde__icontains=query)
| Q(land__flur__icontains=query)
| Q(land__flurstueck__icontains=query)
| Q(land__lfd_nr__icontains=query)
| Q(vertragsnummer__icontains=query)
| Q(pachtzins_pauschal__icontains=query)
| Q(bemerkungen__icontains=query)
)
verpachtung_results = LandVerpachtung.objects.filter(
verpachtung_query
).select_related("paechter", "land")[:25]
results["verpachtung"] = [
{
"id": v.id,
"name": f"{(v.paechter.vorname + ' ') if v.paechter and v.paechter.vorname else ''}{v.paechter.nachname if v.paechter else 'Unbekannt'}{v.land.gemarkung}, Flur {v.land.flur}",
"type": "Verpachtung",
"details": f"Vertrag: {v.vertragsnummer} • Flurstück: {v.land.flurstueck or 'N/A'} • Pachtzins: {v.pachtzins_pauschal or 'N/A'} €/Jahr • Zeitraum: {v.pachtbeginn.strftime('%Y') if v.pachtbeginn else '?'}-{v.pachtende.strftime('%Y') if v.pachtende else 'laufend'}",
}
for v in verpachtung_results
]
if category in ["all", "paechter"]:
# Suche nach Pächtern
paechter_query = Q()
if query and query != "all":
paechter_query = (
Q(nachname__icontains=query)
| Q(vorname__icontains=query)
| Q(ort__icontains=query)
| Q(email__icontains=query)
| Q(telefon__icontains=query)
| Q(strasse__icontains=query)
| Q(pachtnummer__icontains=query)
| Q(plz__icontains=query)
| Q(notizen__icontains=query)
)
paechter_results = Paechter.objects.filter(paechter_query)[:25]
results["paechter"] = [
{
"id": p.id,
"name": f"{(p.vorname + ' ') if p.vorname else ''}{p.nachname}"
+ (f" (#{p.pachtnummer})" if p.pachtnummer else ""),
"type": "Pächter",
"details": f"{p.strasse or ''}{(', ' + p.plz + ' ' + p.ort) if p.plz and p.ort else (' ' + p.ort) if p.ort else ''} {('' + p.email) if p.email else ''} {('• Tel: ' + p.telefon) if p.telefon else ''}".strip(),
}
for p in paechter_results
]
if category in ["all", "rentmeister"]:
# Suche nach Rentmeistern
from stiftung.models import Rentmeister
rentmeister_query = Q()
if query and query != "all":
rentmeister_query = (
Q(nachname__icontains=query)
| Q(vorname__icontains=query)
| Q(ort__icontains=query)
| Q(email__icontains=query)
| Q(telefon__icontains=query)
| Q(strasse__icontains=query)
| Q(plz__icontains=query)
| Q(notizen__icontains=query)
| Q(titel__icontains=query)
| Q(mobil__icontains=query)
)
rentmeister_results = Rentmeister.objects.filter(rentmeister_query)[:25]
results["rentmeister"] = [
{
"id": r.id,
"name": f"{(r.anrede + ' ') if r.anrede else ''}{(r.vorname + ' ') if r.vorname else ''}{r.nachname}"
+ (f" ({r.titel})" if r.titel else ""),
"type": "Rentmeister",
"details": f"{r.strasse or ''}{(', ' + r.plz + ' ' + r.ort) if r.plz and r.ort else (' ' + r.ort) if r.ort else ''} {('' + r.email) if r.email else ''} {('• Tel: ' + r.telefon) if r.telefon else ''} {('• Mobil: ' + r.mobil) if r.mobil else ''}".strip(),
}
for r in rentmeister_results
]
if category in ["all", "abrechnung"]:
# Suche nach Abrechnungen
abrechnung_query = Q()
if query and query != "all":
abrechnung_query = (
Q(land__gemarkung__icontains=query)
| Q(land__gemeinde__icontains=query)
| Q(land__flur__icontains=query)
| Q(land__flurstueck__icontains=query)
| Q(land__lfd_nr__icontains=query)
| Q(abrechnungsjahr__icontains=query)
| Q(bemerkungen__icontains=query)
)
abrechnung_results = LandAbrechnung.objects.filter(
abrechnung_query
).select_related("land")[:25]
results["abrechnung"] = [
{
"id": a.id,
"name": f"Abrechnung {a.abrechnungsjahr} - {a.land.gemarkung}, Flur {a.land.flur}",
"type": "Abrechnung",
"details": f"Flurstück: {a.land.flurstueck or 'N/A'} • Jahr: {a.abrechnungsjahr} • Grundsteuer: {a.grundsteuer_betrag or 0} € • Versicherung: {a.versicherungen_betrag or 0}",
}
for a in abrechnung_results
]
if category in ["all", "foerderung"]:
# Suche nach Förderungen
foerderung_query = Q()
if query and query != "all":
foerderung_query = (
Q(destinataer__nachname__icontains=query)
| Q(destinataer__vorname__icontains=query)
| Q(destinataer__institution__icontains=query)
| Q(destinataer__email__icontains=query)
| Q(jahr__icontains=query)
| Q(betrag__icontains=query)
| Q(kategorie__icontains=query)
| Q(status__icontains=query)
| Q(bemerkungen__icontains=query)
)
foerderung_results = Foerderung.objects.filter(foerderung_query).select_related(
"destinataer"
)[:25]
results["foerderung"] = [
{
"id": str(f.id), # Convert UUID to string for JSON serialization
"name": f"{f.destinataer.get_full_name() if f.destinataer else 'Unbekannt'} - {f.jahr}",
"type": "Förderung",
"details": f"Betrag: {f.betrag} € • Kategorie: {f.get_kategorie_display()} • Status: {f.get_status_display()} • Jahr: {f.jahr}",
}
for f in foerderung_results
]
return Response(results)
def create_paechter_link_for_verpachtung(paperless_id, paperless_title, verpachtung_id):
"""Hilfsfunktion: Erstellt automatisch eine Pächter-Verknüpfung für eine Verpachtung"""
try:
# Hole die LandVerpachtung und den zugehörigen Pächter
verpachtung = LandVerpachtung.objects.select_related("paechter").get(
id=verpachtung_id
)
if verpachtung.paechter:
# Prüfe, ob bereits eine Verknüpfung für dieses Dokument und diesen Pächter existiert
existing_link = DokumentLink.objects.filter(
paperless_document_id=paperless_id, paechter_id=verpachtung.paechter.id
).first()
if not existing_link:
# Erstelle automatische Pächter-Verknüpfung
DokumentLink.objects.create(
paperless_document_id=paperless_id,
titel=paperless_title,
kontext="paechter",
paechter_id=verpachtung.paechter.id,
)
return True
except (LandVerpachtung.DoesNotExist, Exception):
pass
return False
@csrf_exempt
@api_view(["POST"])
def link_document_create(request):
"""Erstellt eine Verknüpfung zwischen einem Paperless-Dokument und einem Datensatz"""
from django.db import transaction
try:
# Robust JSON parsing to tolerate Latin-1 encoded payloads (e.g., umlauts)
try:
payload = request.data
except Exception:
raw = request.body
try:
payload = json.loads(raw.decode("utf-8"))
except UnicodeDecodeError:
payload = json.loads(raw.decode("latin-1"))
paperless_id = payload.get("paperless_id")
paperless_title = payload.get("paperless_title")
paperless_url = payload.get("paperless_url")
link_type = payload.get(
"link_type"
) # 'destinataer', 'land', 'verpachtung', 'paechter', 'abrechnung'
link_id = payload.get("link_id")
if not all([paperless_id, paperless_title, paperless_url, link_type, link_id]):
return Response({"error": "Alle Felder sind erforderlich"}, status=400)
with transaction.atomic():
# Erstelle den DokumentLink
dokument_link = DokumentLink.objects.create(
paperless_document_id=paperless_id, # Korrigiert: 'paperless_document_id' statt 'paperless_id'
titel=paperless_title, # Korrigiert: 'titel' statt 'title'
kontext="anderes",
)
# Setze die entsprechenden UUID-Felder basierend auf dem Link-Typ
if link_type == "destinataer":
dokument_link.destinataer_id = link_id
elif link_type == "land":
dokument_link.land_id = link_id
elif link_type == "verpachtung":
# Use new LandVerpachtung field instead of legacy
dokument_link.land_verpachtung_id = link_id
elif link_type == "paechter":
dokument_link.paechter_id = link_id
elif link_type == "foerderung":
dokument_link.foerderung_id = link_id
elif link_type == "rentmeister":
dokument_link.rentmeister_id = link_id
elif link_type == "abrechnung":
dokument_link.abrechnung_id = link_id
dokument_link.save()
# Log the document linking action
from stiftung.audit import log_link
try:
# Get the linked entity name for logging
entity_name = paperless_title
if link_type == "destinataer":
from stiftung.models import Destinataer
entity = Destinataer.objects.get(id=link_id)
target_name = entity.get_full_name()
elif link_type == "land":
from stiftung.models import Land
entity = Land.objects.get(id=link_id)
target_name = str(entity)
elif link_type == "paechter":
from stiftung.models import Paechter
entity = Paechter.objects.get(id=link_id)
target_name = f"{entity.vorname} {entity.nachname}".strip()
elif link_type == "foerderung":
from stiftung.models import Foerderung
entity = Foerderung.objects.get(id=link_id)
target_name = f"{entity.destinataer.get_full_name() if entity.destinataer else 'Unbekannt'} - {entity.jahr}"
elif link_type == "verpachtung":
entity = LandVerpachtung.objects.get(id=link_id)
target_name = str(entity)
elif link_type == "rentmeister":
from stiftung.models import Rentmeister
entity = Rentmeister.objects.get(id=link_id)
target_name = entity.get_full_name()
else:
target_name = f"ID {link_id}"
log_link(
request=request,
entity_type="dokumentlink",
entity_id=str(dokument_link.id),
entity_name=entity_name,
target_type=link_type,
target_name=target_name,
)
except Exception as e:
# Don't fail the main operation if logging fails
print(f"Audit logging failed: {e}")
# Automatische Pächter-Verknüpfung NACH der Haupttransaktion
paechter_linked = False
if link_type == "verpachtung":
paechter_linked = create_paechter_link_for_verpachtung(
paperless_id, paperless_title, link_id
)
message = f"Dokument erfolgreich mit {link_type} verknüpft"
if paechter_linked:
message += " (automatisch auch mit Pächter verknüpft)"
return Response(
{"success": True, "message": message, "dokument_id": dokument_link.id}
)
except Exception as e:
return Response(
{"error": f"Fehler beim Erstellen der Verknüpfung: {str(e)}"}, status=500
)
# Legacy dokument_verknuepfung view removed - use dokument_management instead
@api_view(["GET"])
def link_document_list(request):
"""Listet alle bestehenden Dokument-Verknüpfungen auf, gruppiert nach Paperless-Document-ID"""
try:
dokument_links = DokumentLink.objects.all().order_by("-id")
# Group links by paperless_document_id to show multiple links per document
links_by_document = {}
for link in dokument_links:
paperless_id = link.paperless_document_id
if paperless_id not in links_by_document:
links_by_document[paperless_id] = {
"paperless_id": paperless_id,
"title": link.titel,
"paperless_url": f"/api/paperless/documents/{paperless_id}/",
"links": [],
}
# Create link info
link_info = {
"id": str(link.id), # Ensure UUID is stringified
"kontext": link.kontext,
"link_type": None,
"linked_object": None,
}
# Determine link type and get linked object details
if link.destinataer_id:
link_info["link_type"] = "destinataer"
try:
dest = Destinataer.objects.get(id=link.destinataer_id)
link_info["linked_object"] = {
"id": str(dest.id),
"type": "Destinatär",
"name": (
f"{dest.vorname} {dest.nachname}".strip()
if dest.vorname
else dest.institution
),
"details": (
f"Institution: {dest.institution}"
if dest.institution
else f"Person: {dest.vorname} {dest.nachname}".strip()
),
}
except Destinataer.DoesNotExist:
link_info["linked_object"] = {
"type": "Destinatär",
"name": "Gelöscht",
"details": "Datensatz nicht mehr verfügbar",
}
elif link.land_id:
link_info["link_type"] = "land"
try:
land = Land.objects.get(id=link.land_id)
link_info["linked_object"] = {
"id": str(land.id),
"type": "Land",
"name": f"{land.gemarkung} - {land.gemeinde}",
"details": f"Flur: {land.flur}, Größe: {land.groesse_qm or 0}",
}
except Land.DoesNotExist:
link_info["linked_object"] = {
"type": "Land",
"name": "Gelöscht",
"details": "Datensatz nicht mehr verfügbar",
}
elif link.paechter_id:
link_info["link_type"] = "paechter"
try:
p = Paechter.objects.get(id=link.paechter_id)
link_info["linked_object"] = {
"id": str(p.id),
"type": "Pächter",
"name": f"{(p.vorname + ' ') if p.vorname else ''}{p.nachname}",
"details": f"{p.ort or ''}",
}
except Paechter.DoesNotExist:
link_info["linked_object"] = {
"type": "Pächter",
"name": "Gelöscht",
"details": "Datensatz nicht mehr verfügbar",
}
elif link.land_verpachtung_id:
link_info["link_type"] = "verpachtung"
try:
from stiftung.models import LandVerpachtung
verp = LandVerpachtung.objects.select_related(
"paechter", "land"
).get(id=link.land_verpachtung_id)
link_info["linked_object"] = {
"id": str(verp.id),
"type": "Verpachtung",
"name": f"Verpachtung {(verp.paechter.vorname + ' ') if verp.paechter and verp.paechter.vorname else ''}{verp.paechter.nachname if verp.paechter else 'Unbekannt'}",
"details": f"Land: {verp.land.gemarkung} - {verp.land.gemeinde}, Pächter: {(verp.paechter.vorname + ' ') if verp.paechter and verp.paechter.vorname else ''}{verp.paechter.nachname if verp.paechter else 'Unbekannt'}, Vertrag: {verp.vertragsnummer}",
}
except LandVerpachtung.DoesNotExist:
link_info["linked_object"] = {
"type": "Verpachtung",
"name": "Gelöscht",
"details": "Datensatz nicht mehr verfügbar",
}
elif link.rentmeister_id:
link_info["link_type"] = "rentmeister"
try:
from stiftung.models import Rentmeister
rentmeister = Rentmeister.objects.get(id=link.rentmeister_id)
link_info["linked_object"] = {
"id": str(rentmeister.id),
"type": "Rentmeister",
"name": f"{(rentmeister.anrede + ' ') if rentmeister.anrede else ''}{(rentmeister.vorname + ' ') if rentmeister.vorname else ''}{rentmeister.nachname}"
+ (f" ({rentmeister.titel})" if rentmeister.titel else ""),
"details": f"Seit: {rentmeister.seit_datum.strftime('%d.%m.%Y') if rentmeister.seit_datum else 'Unbekannt'}"
+ (
f", Tel: {rentmeister.telefon}"
if rentmeister.telefon
else ""
)
+ (f", {rentmeister.email}" if rentmeister.email else ""),
"url": f"/geschaeftsfuehrung/rentmeister/{rentmeister.id}/",
}
except Rentmeister.DoesNotExist:
link_info["linked_object"] = {
"type": "Rentmeister",
"name": "Gelöscht",
"details": "Datensatz nicht mehr verfügbar",
}
elif link.abrechnung_id:
link_info["link_type"] = "abrechnung"
try:
abrechnung = LandAbrechnung.objects.select_related("land").get(
id=link.abrechnung_id
)
link_info["linked_object"] = {
"id": str(abrechnung.id),
"type": "Abrechnung",
"name": f"Abrechnung {abrechnung.abrechnungsjahr} - {abrechnung.land.gemarkung}",
"details": f"Land: {abrechnung.land.gemarkung} - {abrechnung.land.gemeinde}, Flur {abrechnung.land.flur}, Jahr {abrechnung.abrechnungsjahr}",
"url": f"/laendereien/abrechnungen/{abrechnung.id}/",
}
except LandAbrechnung.DoesNotExist:
link_info["linked_object"] = {
"type": "Abrechnung",
"name": "Gelöscht",
"details": "Datensatz nicht mehr verfügbar",
}
links_by_document[paperless_id]["links"].append(link_info)
# Convert to list format for frontend
results = list(links_by_document.values())
return Response(
{
"total_documents": len(results),
"total_links": sum(len(doc["links"]) for doc in results),
"links": results,
}
)
except Exception as e:
return Response(
{"error": f"Fehler beim Abrufen der Verknüpfungen: {str(e)}"}, status=500
)
@csrf_exempt
@api_view(["POST"])
def link_document_update(request):
"""Aktualisiert die Verknüpfung eines Dokuments (Re-Linking auf anderen Datensatz/Kontext)."""
from django.db import transaction
try:
# Robust JSON parsing to tolerate Latin-1 encoded payloads (e.g., umlauts)
try:
payload = request.data
except Exception:
raw = request.body
try:
payload = json.loads(raw.decode("utf-8"))
except UnicodeDecodeError:
payload = json.loads(raw.decode("latin-1"))
link_id = payload.get("link_id")
link_type = payload.get(
"link_type"
) # 'destinataer' | 'land' | 'verpachtung' | 'paechter' | 'rentmeister'
link_target_id = payload.get("link_id_target")
if not all([link_id, link_type, link_target_id]):
return Response(
{"error": "link_id, link_type und link_id_target sind erforderlich"},
status=400,
)
with transaction.atomic():
link = DokumentLink.objects.get(id=link_id)
old_verpachtung_id = (
link.verpachtung_id
) # Merke alte Verpachtung für Cleanup
paperless_id_for_cleanup = link.paperless_document_id
titel_for_new_link = link.titel
# Reset all associations first
link.destinataer_id = None
link.land_id = None
link.verpachtung_id = None
link.paechter_id = None
link.foerderung_id = None
link.rentmeister_id = None
link.kontext = link_type
if link_type == "destinataer":
link.destinataer_id = link_target_id
elif link_type == "land":
link.land_id = link_target_id
elif link_type == "verpachtung":
link.verpachtung_id = link_target_id
elif link_type == "paechter":
link.paechter_id = link_target_id
elif link_type == "foerderung":
link.foerderung_id = link_target_id
elif link_type == "rentmeister":
link.rentmeister_id = link_target_id
else:
return Response({"error": "Ungültiger link_type"}, status=400)
link.save()
# Automatische Pächter-Verknüpfung und Cleanup NACH der Haupttransaktion
paechter_linked = False
if link_type == "verpachtung":
paechter_linked = create_paechter_link_for_verpachtung(
paperless_id_for_cleanup, titel_for_new_link, link_target_id
)
# Cleanup: Entferne verwaiste Pächter-Verknüpfungen wenn von Verpachtung weg geändert
if old_verpachtung_id and link_type != "verpachtung":
try:
old_verpachtung = LandVerpachtung.objects.select_related(
"paechter"
).get(id=old_verpachtung_id)
if old_verpachtung.paechter:
# Prüfe ob noch andere Verpachtungs-Links für diesen Pächter existieren
other_verpachtung_links = DokumentLink.objects.filter(
paperless_document_id=paperless_id_for_cleanup,
verpachtung__paechter_id=old_verpachtung.paechter.id,
).exists()
if not other_verpachtung_links:
# Entferne automatisch erstellte Pächter-Verknüpfung
DokumentLink.objects.filter(
paperless_document_id=paperless_id_for_cleanup,
paechter_id=old_verpachtung.paechter.id,
kontext="paechter",
).delete()
except (LandVerpachtung.DoesNotExist, Exception):
pass
message = "Verknüpfung aktualisiert"
if paechter_linked:
message += " (automatisch auch mit Pächter verknüpft)"
return Response({"success": True, "message": message})
except DokumentLink.DoesNotExist:
return Response({"error": "Verknüpfung nicht gefunden"}, status=404)
except Exception as e:
return Response({"error": f"Unerwarteter Fehler: {e}"}, status=500)
@csrf_exempt
@api_view(["DELETE"])
def link_document_delete(request, link_id):
"""Löscht eine bestehende Verknüpfung."""
from django.db import transaction
try:
with transaction.atomic():
link = DokumentLink.objects.get(id=link_id)
verpachtung_id_for_cleanup = link.verpachtung_id
paperless_id_for_cleanup = link.paperless_document_id
# Log the unlinking action before deletion
from stiftung.audit import log_unlink
try:
# Determine what entity this was linked to
target_type = "unknown"
target_name = "Unknown"
if link.destinataer_id:
target_type = "destinataer"
try:
entity = Destinataer.objects.get(id=link.destinataer_id)
target_name = entity.get_full_name()
except Destinataer.DoesNotExist:
target_name = f"Destinatär ID {link.destinataer_id}"
elif link.land_id:
target_type = "land"
try:
entity = Land.objects.get(id=link.land_id)
target_name = str(entity)
except Land.DoesNotExist:
target_name = f"Land ID {link.land_id}"
elif link.paechter_id:
target_type = "paechter"
try:
entity = Paechter.objects.get(id=link.paechter_id)
target_name = f"{entity.vorname} {entity.nachname}".strip()
except Paechter.DoesNotExist:
target_name = f"Pächter ID {link.paechter_id}"
elif link.verpachtung_id:
target_type = "verpachtung"
try:
entity = LandVerpachtung.objects.get(id=link.verpachtung_id)
target_name = str(entity)
except LandVerpachtung.DoesNotExist:
target_name = f"Verpachtung ID {link.verpachtung_id}"
elif link.rentmeister_id:
target_type = "rentmeister"
try:
from stiftung.models import Rentmeister
entity = Rentmeister.objects.get(id=link.rentmeister_id)
target_name = entity.get_full_name()
except Rentmeister.DoesNotExist:
target_name = f"Rentmeister ID {link.rentmeister_id}"
log_unlink(
request=request,
entity_type="dokumentlink",
entity_id=str(link.id),
entity_name=link.titel,
target_type=target_type,
target_name=target_name,
)
except Exception as e:
# Don't fail the main operation if logging fails
print(f"Audit logging failed: {e}")
link.delete()
# Cleanup NACH der Haupttransaktion: Wenn eine Verpachtungs-Verknüpfung gelöscht wurde, prüfe Pächter-Links
if verpachtung_id_for_cleanup:
try:
verpachtung = LandVerpachtung.objects.select_related("paechter").get(
id=verpachtung_id_for_cleanup
)
if verpachtung.paechter:
# Prüfe ob noch andere Verpachtungs-Links für diesen Pächter existieren
other_verpachtung_links = DokumentLink.objects.filter(
paperless_document_id=paperless_id_for_cleanup,
verpachtung__paechter_id=verpachtung.paechter.id,
).exists()
if not other_verpachtung_links:
# Entferne automatisch erstellte Pächter-Verknüpfung
DokumentLink.objects.filter(
paperless_document_id=paperless_id_for_cleanup,
paechter_id=verpachtung.paechter.id,
kontext="paechter",
).delete()
except (LandVerpachtung.DoesNotExist, Exception):
pass
return Response({"success": True})
except DokumentLink.DoesNotExist:
return Response({"error": "Verknüpfung nicht gefunden"}, status=404)
except Exception as e:
return Response({"error": f"Unerwarteter Fehler: {e}"}, status=500)