# 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)