- 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>
1454 lines
57 KiB
Python
1454 lines
57 KiB
Python
# 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} m²",
|
||
}
|
||
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} m²",
|
||
}
|
||
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)
|
||
|
||
|