Files
stiftung-management-system/app/stiftung/views.py
Stiftung Development 15c798a97b Update footer version to v2.1.4 - Enhanced Destinataer Management
- Bump version from v2.1.3 to v2.1.4
- Update status badge from 'Stabil' to 'Enhanced'
- Reflect recent improvements to Destinataer inline editing and CSV import
2025-09-21 20:12:32 +02:00

7198 lines
262 KiB
Python

import csv
import io
import json
import os
import time
from datetime import datetime
from decimal import Decimal
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 JsonResponse
from django.shortcuts import get_object_or_404, redirect, render
from django.utils import timezone
from django.views.decorators.csrf import csrf_exempt
from rest_framework.decorators import api_view
from rest_framework.response import Response
from .models import (AppConfiguration, CSVImport, Destinataer,
DestinataerUnterstuetzung, DokumentLink, Foerderung, Land,
LandAbrechnung, LandVerpachtung, Paechter, Person,
StiftungsKonto, UnterstuetzungWiederkehrend)
def get_pdf_generator():
"""Lazy load PDF generator to handle missing dependencies gracefully"""
try:
from .utils.pdf_generator import pdf_generator
return pdf_generator
except ImportError as e:
# Store the error message for use in MockPDFGenerator
error_message = str(e)
# Return a mock generator if dependencies are missing
class MockPDFGenerator:
def is_available(self):
return False
def export_data_list_pdf(self, *args, **kwargs):
from django.http import HttpResponse
error_html = f"""
<!DOCTYPE html>
<html>
<head><title>PDF Not Available</title></head>
<body>
<h1>PDF Export Not Available</h1>
<p>PDF generation requires additional system dependencies that are not installed.</p>
<p>Error: {error_message}</p>
<p>Please install WeasyPrint dependencies or use CSV export instead.</p>
</body>
</html>
"""
response = HttpResponse(error_html, content_type="text/html")
response["Content-Disposition"] = (
'inline; filename="pdf_not_available.html"'
)
return response
return MockPDFGenerator()
class GrampsClient:
"""Lightweight client for Gramps Web API."""
def __init__(
self, base_url: str, token: str = "", username: str = "", password: str = ""
):
self.base_url = base_url.rstrip("/")
self.session = requests.Session()
if token:
self.session.headers.update({"Authorization": f"Bearer {token}"})
self.username = username
self.password = password
self._cached_token = token
def search_people(self, query: str, limit: int = 5):
try:
r = self.session.get(
f"{self.base_url}/api/people/",
params={"q": query, "limit": limit},
timeout=10,
)
r.raise_for_status()
return r.json()
except Exception as e:
# try login-once if unauthorized and we have credentials
if self.username and self.password and "401" in str(e):
if self._login():
return self.search_people(query, limit)
return {"error": str(e)}
def get_person(self, handle_or_id: str):
try:
r = self.session.get(
f"{self.base_url}/api/people/{handle_or_id}", timeout=10
)
r.raise_for_status()
return r.json()
except Exception as e:
if self.username and self.password and "401" in str(e):
if self._login():
return self.get_person(handle_or_id)
return {"error": str(e)}
def _login(self) -> bool:
try:
# try common endpoints
endpoints = [
(
"/api/auth/login",
{"username": self.username, "password": self.password},
"json",
),
(
"/auth/login",
{"username": self.username, "password": self.password},
"json",
),
(
"/api/token",
{"username": self.username, "password": self.password},
"form",
),
(
"/login",
{"username": self.username, "password": self.password},
"form",
),
(
"/token",
{"username": self.username, "password": self.password},
"form",
),
(
"/api/login",
{"username": self.username, "password": self.password},
"json",
),
]
for path, payload, mode in endpoints:
url = f"{self.base_url}{path}"
if mode == "json":
r = self.session.post(
url, json=payload, timeout=10, allow_redirects=False
)
else:
r = self.session.post(
url, data=payload, timeout=10, allow_redirects=False
)
# Success with token body
if r.status_code in (200, 201) and "application/json" in r.headers.get(
"Content-Type", ""
):
data = r.json()
token = (
data.get("access_token")
or data.get("token")
or data.get("access")
or data.get("jwt")
)
if token:
self._cached_token = token
self.session.headers.update(
{"Authorization": f"Bearer {token}"}
)
return True
# Success via session cookie and redirect
if r.status_code in (200, 302) and (
"set-cookie" in {k.lower(): v for k, v in r.headers.items()}
):
return True
# Basic Auth fallback (some setups protect API with Basic)
try:
self.session.auth = (self.username, self.password)
r = self.session.get(f"{self.base_url}/api/people/?limit=1", timeout=10)
if r.status_code == 200:
return True
except Exception:
pass
return False
except Exception:
return False
def get_gramps_client() -> GrampsClient:
return GrampsClient(
getattr(settings, "GRAMPS_URL", ""),
getattr(settings, "GRAMPS_API_TOKEN", ""),
getattr(settings, "GRAMPS_USERNAME", ""),
getattr(settings, "GRAMPS_PASSWORD", ""),
)
@api_view(["GET"])
def gramps_debug_api(_request):
return Response(
{
"GRAMPS_URL": getattr(settings, "GRAMPS_URL", ""),
"has_username": bool(getattr(settings, "GRAMPS_USERNAME", "")),
"has_password": bool(getattr(settings, "GRAMPS_PASSWORD", "")),
}
)
from stiftung.models import DestinataerNotiz, DestinataerUnterstuetzung
from .forms import (DestinataerForm, DestinataerNotizForm,
DestinataerUnterstuetzungForm, DokumentLinkForm,
FoerderungForm, LandForm, PaechterForm, PersonForm,
UnterstuetzungForm, UnterstuetzungMarkAsPaidForm)
def home(request):
"""Home page for the Stiftungsverwaltung application"""
return render(
request,
"stiftung/home.html",
{"title": "Stiftungsverwaltung", "description": "Foundation Management System"},
)
@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.rstrip("/api") if url.endswith("/api") else url
# For external Paperless (already includes /paperless/ in base URL)
return redirect(f"{base_url}/documents/{doc_id}/details/")
@api_view(["GET"])
def health_check(request):
"""Simple health check endpoint for deployment monitoring"""
return JsonResponse(
{
"status": "healthy",
"timestamp": timezone.now().isoformat(),
"service": "stiftung-web",
}
)
## Removed duplicate paperless_ping referencing non-existent PAPERLESS_URL
# CSV Import Views
@login_required
def csv_import_list(request):
"""List all CSV import operations"""
imports = CSVImport.objects.all().order_by("-started_at")
paginator = Paginator(imports, 20)
page_number = request.GET.get("page")
page_obj = paginator.get_page(page_number)
context = {
"page_obj": page_obj,
"import_types": CSVImport.IMPORT_TYPE_CHOICES,
"status_choices": CSVImport.STATUS_CHOICES,
}
return render(request, "stiftung/csv_import_list.html", context)
@login_required
def csv_import_create(request):
"""Show CSV import form and handle file upload"""
if request.method == "POST":
import_type = request.POST.get("import_type")
csv_file = request.FILES.get("csv_file")
if not csv_file or not import_type:
messages.error(
request, "Bitte wählen Sie einen Import-Typ und eine CSV-Datei aus."
)
return redirect("stiftung:csv_import_create")
if not csv_file.name.endswith(".csv"):
messages.error(request, "Bitte wählen Sie eine gültige CSV-Datei aus.")
return redirect("stiftung:csv_import_create")
try:
# Create import record
csv_import = CSVImport.objects.create(
import_type=import_type,
filename=csv_file.name,
file_size=csv_file.size,
created_by=(
request.user.username
if request.user.is_authenticated
else "Unknown"
),
status="processing",
)
# Process the CSV file
if import_type == "destinataere":
result = process_destinataere_csv(csv_file, csv_import)
elif import_type == "paechter":
result = process_paechter_csv(csv_file, csv_import)
elif import_type == "personen":
result = process_personen_csv(csv_file, csv_import)
elif import_type == "laendereien":
result = process_laendereien_csv(csv_file, csv_import)
else:
messages.error(request, "Unbekannter Import-Typ.")
csv_import.status = "failed"
csv_import.save()
return redirect("stiftung:csv_import_create")
# Update import record
csv_import.total_rows = result["total_rows"]
csv_import.imported_rows = result["imported_rows"]
csv_import.failed_rows = result["failed_rows"]
csv_import.error_log = result["error_log"]
csv_import.status = result["status"]
csv_import.completed_at = timezone.now()
csv_import.save()
if result["status"] == "completed":
messages.success(
request,
f'CSV-Import erfolgreich! {result["imported_rows"]} Datensätze importiert.',
)
elif result["status"] == "partial":
messages.warning(
request,
f'CSV-Import teilweise erfolgreich. {result["imported_rows"]} importiert, {result["failed_rows"]} fehlgeschlagen.',
)
else:
messages.error(
request, f'CSV-Import fehlgeschlagen. {result["error_log"]}'
)
return redirect("stiftung:csv_import_list")
except Exception as e:
messages.error(request, f"Fehler beim CSV-Import: {str(e)}")
return redirect("stiftung:csv_import_create")
context = {
"import_types": CSVImport.IMPORT_TYPE_CHOICES,
}
return render(request, "stiftung/csv_import_form.html", context)
def process_personen_csv(csv_file, csv_import):
"""Process CSV file for Personen import"""
decoded_file = csv_file.read().decode("utf-8")
# Handle both comma and semicolon separated files
if ";" in decoded_file.split("\n")[0]:
csv_data = csv.DictReader(io.StringIO(decoded_file), delimiter=";")
else:
csv_data = csv.DictReader(io.StringIO(decoded_file))
total_rows = 0
imported_rows = 0
failed_rows = 0
error_log = []
for row_num, row in enumerate(
csv_data, start=2
): # Start at 2 because row 1 is header
total_rows += 1
try:
# Map CSV columns to model fields
person_data = {
"vorname": row.get("Vorname", "").strip(),
"nachname": row.get("Nachname", "").strip(),
"familienzweig": row.get("Familienzweig", "hauptzweig").strip(),
"email": row.get("E-Mail", "").strip() or None,
"telefon": row.get("Telefon", "").strip() or None,
"iban": row.get("IBAN", "").strip() or None,
"adresse": row.get("Adresse", "").strip() or None,
"notizen": row.get("Notizen", "").strip() or None,
"aktiv": row.get("Aktiv", "true").lower() == "true",
}
# Handle date fields
if row.get("Geburtsdatum"):
try:
person_data["geburtsdatum"] = datetime.strptime(
row["Geburtsdatum"], "%d.%m.%Y"
).date()
except ValueError:
try:
person_data["geburtsdatum"] = datetime.strptime(
row["Geburtsdatum"], "%Y-%m-%d"
).date()
except ValueError:
person_data["geburtsdatum"] = None
# Validate required fields
if not person_data["vorname"] or not person_data["nachname"]:
error_log.append(
f"Zeile {row_num}: Vorname und Nachname sind erforderlich"
)
failed_rows += 1
continue
# Check if person already exists
existing_person = Person.objects.filter(
vorname__iexact=person_data["vorname"],
nachname__iexact=person_data["nachname"],
).first()
if existing_person:
# Update existing person
for field, value in person_data.items():
if value is not None:
setattr(existing_person, field, value)
existing_person.save()
else:
# Create new person
Person.objects.create(**person_data)
imported_rows += 1
except Exception as e:
error_log.append(f"Zeile {row_num}: {str(e)}")
failed_rows += 1
# Determine status
if failed_rows == 0:
status = "completed"
elif imported_rows > 0:
status = "partial"
else:
status = "failed"
return {
"total_rows": total_rows,
"imported_rows": imported_rows,
"failed_rows": failed_rows,
"error_log": "\n".join(error_log) if error_log else None,
"status": status,
}
def process_destinataere_csv(csv_file, csv_import):
"""Process CSV file for Destinatäre import"""
decoded_file = csv_file.read().decode("utf-8")
# Handle both comma and semicolon separated files
if ";" in decoded_file.split("\n")[0]:
csv_data = csv.DictReader(io.StringIO(decoded_file), delimiter=";")
else:
csv_data = csv.DictReader(io.StringIO(decoded_file))
total_rows = 0
imported_rows = 0
failed_rows = 0
error_log = []
for row_num, row in enumerate(
csv_data, start=2
): # Start at 2 because row 1 is header
total_rows += 1
try:
# Helper function to parse boolean values from CSV
def parse_boolean(value, default=False):
"""Parse boolean values from CSV with multiple accepted formats"""
if not value:
return default
value_str = str(value).strip().lower()
# Accept various true values
true_values = ['true', 'ja', 'yes', '1', 'wahr', 'x']
# Accept various false values
false_values = ['false', 'nein', 'no', '0', 'falsch', '']
if value_str in true_values:
return True
elif value_str in false_values:
return False
else:
# If unclear, return default
return default
# Map CSV columns to model fields
destinataer_data = {
"vorname": row.get("Vorname", "").strip(),
"nachname": row.get("Nachname", "").strip(),
"familienzweig": row.get("Familienzweig", "hauptzweig").strip(),
"email": row.get("E-Mail", "").strip() or None,
"telefon": row.get("Telefon", "").strip() or None,
"iban": row.get("IBAN", "").strip() or None,
"strasse": row.get("Straße", "").strip() or None,
"plz": row.get("PLZ", "").strip() or None,
"ort": row.get("Ort", "").strip() or None,
"berufsgruppe": row.get("Berufsgruppe", "andere").strip(),
"ausbildungsstand": row.get("Ausbildungsstand", "").strip() or None,
"institution": row.get("Institution", "").strip() or None,
"projekt_beschreibung": row.get("Projektbeschreibung", "").strip()
or None,
"jaehrliches_einkommen": (
float(row.get("Jährliches_Einkommen", 0))
if row.get("Jährliches_Einkommen")
else None
),
"notizen": row.get("Notizen", "").strip() or None,
# Boolean fields with improved parsing
"finanzielle_notlage": parse_boolean(row.get("Finanzielle_Notlage"), False),
"aktiv": parse_boolean(row.get("Aktiv"), True),
"ist_abkoemmling": parse_boolean(row.get("Ist_Abkömmling"), False),
"unterstuetzung_bestaetigt": parse_boolean(row.get("Unterstützung_bestätigt"), False),
"studiennachweis_erforderlich": parse_boolean(row.get("Studiennachweis_erforderlich"), False),
}
# Handle numeric fields
if row.get("Haushaltsgröße"):
try:
destinataer_data["haushaltsgroesse"] = int(row["Haushaltsgröße"])
except ValueError:
pass
if row.get("Monatliche_Bezüge"):
try:
destinataer_data["monatliche_bezuege"] = float(row["Monatliche_Bezüge"])
except ValueError:
pass
if row.get("Vermögen"):
try:
destinataer_data["vermoegen"] = float(row["Vermögen"])
except ValueError:
pass
if row.get("Vierteljährlicher_Betrag"):
try:
destinataer_data["vierteljaehrlicher_betrag"] = float(row["Vierteljährlicher_Betrag"])
except ValueError:
pass
# Handle date fields
if row.get("Geburtsdatum"):
try:
destinataer_data["geburtsdatum"] = datetime.strptime(
row["Geburtsdatum"], "%d.%m.%Y"
).date()
except ValueError:
try:
destinataer_data["geburtsdatum"] = datetime.strptime(
row["Geburtsdatum"], "%Y-%m-%d"
).date()
except ValueError:
destinataer_data["geburtsdatum"] = None
if row.get("Letzter_Studiennachweis"):
try:
destinataer_data["letzter_studiennachweis"] = datetime.strptime(
row["Letzter_Studiennachweis"], "%d.%m.%Y"
).date()
except ValueError:
try:
destinataer_data["letzter_studiennachweis"] = datetime.strptime(
row["Letzter_Studiennachweis"], "%Y-%m-%d"
).date()
except ValueError:
destinataer_data["letzter_studiennachweis"] = None
# Validate required fields
if not destinataer_data["vorname"] or not destinataer_data["nachname"]:
error_log.append(
f"Zeile {row_num}: Vorname und Nachname sind erforderlich"
)
failed_rows += 1
continue
# Check if destinataer already exists
existing_destinataer = Destinataer.objects.filter(
vorname__iexact=destinataer_data["vorname"],
nachname__iexact=destinataer_data["nachname"],
).first()
if existing_destinataer:
# Update existing destinataer
for field, value in destinataer_data.items():
if value is not None:
setattr(existing_destinataer, field, value)
existing_destinataer.save()
else:
# Create new destinataer
Destinataer.objects.create(**destinataer_data)
imported_rows += 1
except Exception as e:
error_log.append(f"Zeile {row_num}: {str(e)}")
failed_rows += 1
# Determine status
if failed_rows == 0:
status = "completed"
elif imported_rows > 0:
status = "partial"
else:
status = "failed"
return {
"total_rows": total_rows,
"imported_rows": imported_rows,
"failed_rows": failed_rows,
"error_log": "\n".join(error_log) if error_log else None,
"status": status,
}
def process_paechter_csv(csv_file, csv_import):
"""Process CSV file for Paechter import"""
decoded_file = csv_file.read().decode("utf-8")
# Handle both comma and semicolon separated files
if ";" in decoded_file.split("\n")[0]:
csv_data = csv.DictReader(io.StringIO(decoded_file), delimiter=";")
else:
csv_data = csv.DictReader(io.StringIO(decoded_file))
total_rows = 0
imported_rows = 0
failed_rows = 0
error_log = []
for row_num, row in enumerate(
csv_data, start=2
): # Start at 2 because row 1 is header
total_rows += 1
try:
# Get raw values from CSV - handle both semicolon and comma separated
# Handle BOM in column names
vorname_raw = row.get("Vorname", "") or row.get("\ufeffVorname", "")
nachname_raw = row.get("Nachname", "")
personentyp_raw = row.get("Personentyp", "")
# Clean up the values (remove extra whitespace but keep empty strings)
vorname_raw = vorname_raw.strip() if vorname_raw else ""
nachname_raw = nachname_raw.strip() if nachname_raw else ""
personentyp_raw = personentyp_raw.strip() if personentyp_raw else ""
# Debug: Log raw values and available columns
error_log.append(f"Zeile {row_num}: Available columns: {list(row.keys())}")
error_log.append(
f"Zeile {row_num}: RAW Vorname='{vorname_raw}', Nachname='{nachname_raw}', Personentyp='{personentyp_raw}'"
)
# Determine personentyp based on the data
if personentyp_raw in ["Gesellschaft", "KG", "GbR", "GmbH"]:
personentyp = "gesellschaft"
elif personentyp_raw in ["Herrn", "Frau"]:
personentyp = "natuerlich"
else:
# Fallback: analyze the Nachname to detect companies
nachname_lower = nachname_raw.lower()
if any(
keyword in nachname_lower
for keyword in [
"kg",
"gbr",
"gmbh",
"ag",
"ohg",
"e.v.",
"stiftung",
"genossenschaft",
]
):
personentyp = "gesellschaft"
else:
personentyp = "natuerlich"
# Handle Vorname - keep original value unless it's 'N/A'
vorname = vorname_raw if vorname_raw and vorname_raw != "N/A" else ""
# Debug: Log processed values
error_log.append(
f"Zeile {row_num}: PROCESSED Vorname='{vorname}', Nachname='{nachname_raw}', Personentyp='{personentyp}'"
)
paechter_data = {
"vorname": vorname,
"nachname": nachname_raw,
"email": row.get("E-Mail", "").strip() or None,
"telefon": row.get("Telefon", "").strip() or None,
"iban": row.get("IBAN", "").strip() or None,
"strasse": row.get("Straße", "").strip() or None,
"plz": row.get("PLZ", "").strip() or None,
"ort": row.get("Ort", "").strip() or None,
"personentyp": personentyp,
"pachtnummer": row.get("Pachtnummer", "").strip() or None,
"landwirtschaftliche_ausbildung": row.get(
"Landwirtschaftliche_Ausbildung", "false"
).lower()
== "true",
"berufserfahrung_jahre": (
int(row.get("Berufserfahrung_Jahre", 0))
if row.get("Berufserfahrung_Jahre")
else None
),
"spezialisierung": row.get("Spezialisierung", "").strip() or None,
"notizen": row.get("Notizen", "").strip() or None,
"aktiv": row.get("Aktiv", "true").lower()
in ["true", "wahr", "ja", "1"],
}
# Handle date fields
if row.get("Geburtsdatum"):
try:
paechter_data["geburtsdatum"] = datetime.strptime(
row["Geburtsdatum"], "%d.%m.%Y"
).date()
except ValueError:
try:
paechter_data["geburtsdatum"] = datetime.strptime(
row["Geburtsdatum"], "%Y-%m-%d"
).date()
except ValueError:
paechter_data["geburtsdatum"] = None
if row.get("Pachtbeginn_Erste"):
try:
paechter_data["pachtbeginn_erste"] = datetime.strptime(
row["Pachtbeginn_Erste"], "%d.%m.%Y"
).date()
except ValueError:
try:
paechter_data["pachtbeginn_erste"] = datetime.strptime(
row["Pachtbeginn_Erste"], "%Y-%m-%d"
).date()
except ValueError:
paechter_data["pachtbeginn_erste"] = None
if row.get("Pachtende_Letzte"):
try:
paechter_data["pachtende_letzte"] = datetime.strptime(
row["Pachtende_Letzte"], "%d.%m.%Y"
).date()
except ValueError:
try:
paechter_data["pachtende_letzte"] = datetime.strptime(
row["Pachtende_Letzte"], "%Y-%m-%d"
).date()
except ValueError:
paechter_data["pachtende_letzte"] = None
# Handle decimal fields
if row.get("Pachtzins_Aktuell"):
try:
paechter_data["pachtzins_aktuell"] = float(row["Pachtzins_Aktuell"])
except ValueError:
paechter_data["pachtzins_aktuell"] = None
# Validate required fields
if personentyp == "gesellschaft":
# For companies, only Nachname is required
if not paechter_data["nachname"]:
error_log.append(
f"Zeile {row_num}: Nachname ist für Gesellschaften erforderlich"
)
failed_rows += 1
continue
else:
# For natural persons, only Nachname is required
if not paechter_data["nachname"]:
error_log.append(
f"Zeile {row_num}: Nachname ist für natürliche Personen erforderlich"
)
failed_rows += 1
continue
# Check if paechter already exists
if personentyp == "gesellschaft":
# For companies, search by Nachname only
existing_paechter = Paechter.objects.filter(
nachname__iexact=paechter_data["nachname"],
personentyp="gesellschaft",
).first()
else:
# For natural persons, search by Nachname only (since Vorname can be empty)
existing_paechter = Paechter.objects.filter(
nachname__iexact=paechter_data["nachname"], personentyp="natuerlich"
).first()
if existing_paechter:
# Update existing paechter
for field, value in paechter_data.items():
if value is not None:
setattr(existing_paechter, field, value)
existing_paechter.save()
else:
# Create new paechter
Paechter.objects.create(**paechter_data)
imported_rows += 1
except Exception as e:
error_log.append(f"Zeile {row_num}: {str(e)}")
failed_rows += 1
# Determine status
if failed_rows == 0:
status = "completed"
elif imported_rows > 0:
status = "partial"
else:
status = "failed"
return {
"total_rows": total_rows,
"imported_rows": imported_rows,
"failed_rows": failed_rows,
"error_log": "\n".join(error_log) if error_log else None,
"status": status,
}
def process_laendereien_csv(csv_file, csv_import):
"""Process CSV file for Ländereien import"""
decoded_file = csv_file.read().decode("utf-8")
# Handle both comma and semicolon separated files
if ";" in decoded_file.split("\n")[0]:
csv_data = csv.DictReader(io.StringIO(decoded_file), delimiter=";")
else:
csv_data = csv.DictReader(io.StringIO(decoded_file))
total_rows = 0
imported_rows = 0
failed_rows = 0
error_log = []
last_gemeinde = None
for row_num, row in enumerate(csv_data, start=2):
total_rows += 1
try:
# Build case-insensitive access helpers (strip BOM, normalize separators)
def clean_key(key: str) -> str:
return (key or "").replace("\ufeff", "").replace("\ufeff", "").strip()
normalized_row = {clean_key(k): (v or "").strip() for k, v in row.items()}
lower_row = {
clean_key(k).lower(): (v or "").strip() for k, v in row.items()
}
sanitized_row = {
clean_key(k)
.lower()
.replace("-", "_")
.replace(" ", "_"): (v or "")
.strip()
for k, v in row.items()
}
def get_val(*keys):
# Try exact keys first, then case-insensitive
for key in keys:
if key in normalized_row:
return normalized_row[key]
for key in keys:
lk = key.lower()
if lk in lower_row:
return lower_row[lk]
sk = lk.replace("-", "_").replace(" ", "_")
if sk in sanitized_row:
return sanitized_row[sk]
return ""
def parse_float(value):
if not value:
return 0
# replace comma decimal if present
v = (
value.replace(".", "").replace(",", ".")
if value.count(",") == 1 and value.count(".") > 1
else value.replace(",", ".")
)
try:
return float(v)
except ValueError:
return 0
# Map CSV columns to model fields (robust to header variants)
lfd_nr_val = get_val(
"Lfd_Nr",
"lfd_nr",
"LfdNr",
"lfdnr",
"laufende_nummer",
"laufende-nummer",
)
land_data = {
"lfd_nr": lfd_nr_val,
"ew_nummer": get_val("EW_Nummer", "ew_nummer") or None,
"amtsgericht": get_val("Amtsgericht", "amtsgericht"),
"gemeinde": get_val("Gemeinde", "gemeinde"),
"gemarkung": get_val("Gemarkung", "gemarkung"),
"flur": get_val("Flur", "flur"),
"flurstueck": get_val(
"Flurstück", "Flurstueck", "flurstück", "flurstueck"
),
"groesse_qm": parse_float(
get_val("Größe_qm", "Groesse_qm", "groesse_qm", "größe_qm")
),
"gruenland_qm": parse_float(
get_val(
"Grünland_qm", "Gruenland_qm", "gruenland_qm", "grünland_qm"
)
),
"acker_qm": parse_float(get_val("Acker_qm", "acker_qm")),
"wald_qm": parse_float(get_val("Wald_qm", "wald_qm")),
"sonstiges_qm": parse_float(get_val("Sonstiges_qm", "sonstiges_qm")),
"verpachtete_gesamtflaeche": parse_float(
get_val(
"Verpachtete_Gesamtfläche_qm",
"Verpachtete_Gesamtflaeche_qm",
"verpachtete_gesamtfläche_qm",
"verpachtete_gesamtflaeche_qm",
)
),
"verp_flaeche_aktuell": parse_float(
get_val(
"Verp_Fläche_aktuell_qm",
"Verp_Flaeche_aktuell_qm",
"verp_flaeche_aktuell_qm",
"verp_fläche_aktuell_qm",
)
),
"aktiv": get_val("Aktiv", "aktiv").lower()
in ["true", "wahr", "ja", "1"],
"notizen": get_val("Notizen", "notizen") or None,
}
# Fallback for missing 'Gemeinde' -> set explicit placeholder
if not land_data["gemeinde"]:
land_data["gemeinde"] = "FEHLT"
# Validate required fields
required_fields = ["lfd_nr", "gemeinde", "gemarkung", "flur", "flurstueck"]
missing_fields = [
field for field in required_fields if not land_data[field]
]
if missing_fields:
# Log header diagnostics on first failure only to help debugging
if row_num == 2:
error_log.append(f"Erkannte Spalten: {list(normalized_row.keys())}")
error_log.append(
f"Zeile {row_num}: Fehlende Pflichtfelder: {', '.join(missing_fields)}"
)
failed_rows += 1
continue
# Check if land already exists
existing_land = Land.objects.filter(lfd_nr=land_data["lfd_nr"]).first()
if existing_land:
# Update existing land
for field, value in land_data.items():
if value is not None:
setattr(existing_land, field, value)
existing_land.save()
else:
# Create new land
Land.objects.create(**land_data)
imported_rows += 1
if land_data["gemeinde"]:
last_gemeinde = land_data["gemeinde"]
except Exception as e:
error_log.append(f"Zeile {row_num}: {str(e)}")
failed_rows += 1
# Determine status
if failed_rows == 0:
status = "completed"
elif imported_rows > 0:
status = "partial"
else:
status = "failed"
return {
"total_rows": total_rows,
"imported_rows": imported_rows,
"failed_rows": failed_rows,
"error_log": "\n".join(error_log) if error_log else None,
"status": status,
}
# Person Views
@login_required
def person_list(request):
search_query = request.GET.get("search", "")
familienzweig_filter = request.GET.get("familienzweig", "")
aktiv_filter = request.GET.get("aktiv", "")
persons = Person.objects.all()
if search_query:
persons = persons.filter(
Q(nachname__icontains=search_query)
| Q(vorname__icontains=search_query)
| Q(email__icontains=search_query)
| Q(familienzweig__icontains=search_query)
)
if familienzweig_filter:
persons = persons.filter(familienzweig=familienzweig_filter)
if aktiv_filter == "true":
persons = persons.filter(aktiv=True)
elif aktiv_filter == "false":
persons = persons.filter(aktiv=False)
# Annotate with total funding
persons = persons.annotate(total_foerderungen=Sum("foerderung__betrag"))
paginator = Paginator(persons, 20)
page_number = request.GET.get("page")
page_obj = paginator.get_page(page_number)
context = {
"page_obj": page_obj,
"search_query": search_query,
"familienzweig_filter": familienzweig_filter,
"aktiv_filter": aktiv_filter,
"familienzweig_choices": Person.FAMILIENZWIG_CHOICES,
}
return render(request, "stiftung/person_list.html", context)
@login_required
def person_detail(request, pk):
person = get_object_or_404(Person, pk=pk)
foerderungen = person.foerderung_set.all().order_by("-jahr", "-betrag")
# Get new LandVerpachtungen for this person's Paechter instances
verpachtungen = LandVerpachtung.objects.filter(paechter__person=person).order_by(
"-pachtbeginn"
)
context = {
"person": person,
"foerderungen": foerderungen,
"verpachtungen": verpachtungen,
}
return render(request, "stiftung/person_detail.html", context)
@login_required
def person_create(request):
if request.method == "POST":
form = PersonForm(request.POST)
if form.is_valid():
person = form.save()
messages.success(
request,
f'Person "{person.get_full_name()}" wurde erfolgreich erstellt.',
)
return redirect("stiftung:person_detail", pk=person.pk)
else:
form = PersonForm()
context = {"form": form, "title": "Neue Person erstellen"}
return render(request, "stiftung/person_form.html", context)
@login_required
def person_update(request, pk):
person = get_object_or_404(Person, pk=pk)
if request.method == "POST":
form = PersonForm(request.POST, instance=person)
if form.is_valid():
person = form.save()
messages.success(
request,
f'Person "{person.get_full_name()}" wurde erfolgreich aktualisiert.',
)
return redirect("stiftung:person_detail", pk=person.pk)
else:
form = PersonForm(instance=person)
context = {
"form": form,
"person": person,
"title": f"Person bearbeiten: {person.get_full_name()}",
}
return render(request, "stiftung/person_form.html", context)
@login_required
def person_delete(request, pk):
person = get_object_or_404(Person, pk=pk)
if request.method == "POST":
person.delete()
messages.success(
request, f'Person "{person.get_full_name()}" wurde erfolgreich gelöscht.'
)
return redirect("stiftung:person_list")
context = {"person": person}
return render(request, "stiftung/person_confirm_delete.html", context)
# Destinatär Views (Förderungsempfänger)
@login_required
def destinataer_list(request):
search_query = request.GET.get("search", "")
familienzweig_filter = request.GET.get("familienzweig", "")
berufsgruppe_filter = request.GET.get("berufsgruppe", "")
aktiv_filter = request.GET.get("aktiv", "")
sort = request.GET.get("sort", "")
direction = request.GET.get("dir", "asc")
destinataere = Destinataer.objects.all()
if search_query:
destinataere = destinataere.filter(
Q(nachname__icontains=search_query)
| Q(vorname__icontains=search_query)
| Q(email__icontains=search_query)
| Q(institution__icontains=search_query)
| Q(familienzweig__icontains=search_query)
)
if familienzweig_filter:
destinataere = destinataere.filter(familienzweig=familienzweig_filter)
if berufsgruppe_filter:
destinataere = destinataere.filter(berufsgruppe=berufsgruppe_filter)
if aktiv_filter == "true":
destinataere = destinataere.filter(aktiv=True)
elif aktiv_filter == "false":
destinataere = destinataere.filter(aktiv=False)
# Annotate with total funding (coalesce nulls to Decimal for stable sorting)
destinataere = destinataere.annotate(
total_foerderungen=Coalesce(
Sum("foerderung__betrag"),
Value(
Decimal("0.00"),
output_field=DecimalField(max_digits=12, decimal_places=2),
),
output_field=DecimalField(max_digits=12, decimal_places=2),
)
)
# Sorting
sort_map = {
"vorname": ["vorname"],
"nachname": ["nachname"],
"email": ["email"],
"vierteljaehrlicher_betrag": ["vierteljaehrlicher_betrag"],
"letzter_studiennachweis": ["letzter_studiennachweis"],
"unterstuetzung_bestaetigt": ["unterstuetzung_bestaetigt"],
# Keep old mappings for backward compatibility
"name": ["nachname", "vorname"],
"familienzweig": ["familienzweig"],
"berufsgruppe": ["berufsgruppe"],
"institution": ["institution"],
"foerderungen": ["total_foerderungen"],
"status": ["aktiv"],
}
if sort in sort_map:
fields = sort_map[sort]
if direction == "desc":
order_fields = [f"-{f}" for f in fields]
else:
order_fields = fields
destinataere = destinataere.order_by(*order_fields)
paginator = Paginator(destinataere, 20)
page_number = request.GET.get("page")
page_obj = paginator.get_page(page_number)
context = {
"page_obj": page_obj,
"search_query": search_query,
"familienzweig_filter": familienzweig_filter,
"berufsgruppe_filter": berufsgruppe_filter,
"aktiv_filter": aktiv_filter,
"familienzweig_choices": Destinataer.FAMILIENZWIG_CHOICES,
"berufsgruppe_choices": Destinataer.BERUFSGRUPPE_CHOICES,
"sort": sort,
"dir": direction,
}
return render(request, "stiftung/destinataer_list.html", context)
@login_required
def destinataer_detail(request, pk):
destinataer = get_object_or_404(Destinataer, pk=pk)
# Alle mit diesem Destinatär verknüpften Dokumente laden
verknuepfte_dokumente = DokumentLink.objects.filter(
destinataer_id=destinataer.pk
).order_by("kontext", "titel")
# Förderungen für diesen Destinatär laden
foerderungen = Foerderung.objects.filter(destinataer=destinataer).order_by(
"-jahr", "-betrag"
)
# Unterstützungen für diesen Destinatär laden
unterstuetzungen = DestinataerUnterstuetzung.objects.filter(
destinataer=destinataer
).order_by("-faellig_am")
# Notizen laden
notizen_eintraege = DestinataerNotiz.objects.filter(
destinataer=destinataer
).order_by("-erstellt_am")
# Alle verfügbaren StiftungsKonten für das Select-Feld laden
stiftungskonten = StiftungsKonto.objects.all().order_by("kontoname")
context = {
"destinataer": destinataer,
"verknuepfte_dokumente": verknuepfte_dokumente,
"foerderungen": foerderungen,
"unterstuetzungen": unterstuetzungen,
"notizen_eintraege": notizen_eintraege,
"stiftungskonten": stiftungskonten,
}
return render(request, "stiftung/destinataer_detail.html", context)
@login_required
def destinataer_create(request):
if request.method == "POST":
form = DestinataerForm(request.POST)
if form.is_valid():
destinataer = form.save()
messages.success(
request,
f'Destinatär "{destinataer.get_full_name()}" wurde erfolgreich erstellt.',
)
return redirect("stiftung:destinataer_detail", pk=destinataer.pk)
else:
form = DestinataerForm()
context = {"form": form, "title": "Neuen Destinatär erstellen"}
return render(request, "stiftung/destinataer_form.html", context)
@login_required
def destinataer_update(request, pk):
destinataer = get_object_or_404(Destinataer, pk=pk)
if request.method == "POST":
form = DestinataerForm(request.POST, instance=destinataer)
# Handle AJAX requests
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
if form.is_valid():
try:
destinataer = form.save()
# Auto-create a Destinatärunterstützung if conditions are met
if (
destinataer.aktiv
and destinataer.unterstuetzung_bestaetigt
and destinataer.standard_konto
and destinataer.vierteljaehrlicher_betrag
and destinataer.vierteljaehrlicher_betrag > 0
):
from decimal import Decimal
from stiftung.models import DestinataerUnterstuetzung
heute = timezone.now().date()
beschreibung = f"Vierteljährliche Vorauszahlung für {destinataer.get_full_name()}"
# ensure only one upcoming planned entry; update if one exists
existing = (
DestinataerUnterstuetzung.objects.filter(
destinataer=destinataer, status="geplant"
)
.order_by("faellig_am")
.first()
)
if existing:
existing.konto = destinataer.standard_konto
existing.betrag = Decimal(destinataer.vierteljaehrlicher_betrag)
existing.faellig_am = heute
existing.beschreibung = beschreibung
existing.save()
else:
DestinataerUnterstuetzung.objects.create(
destinataer=destinataer,
konto=destinataer.standard_konto,
betrag=Decimal(destinataer.vierteljaehrlicher_betrag),
faellig_am=heute,
status="geplant",
beschreibung=beschreibung,
)
return JsonResponse({
'success': True,
'message': f'Destinatär "{destinataer.get_full_name()}" wurde erfolgreich aktualisiert.'
})
except Exception as e:
return JsonResponse({
'success': False,
'error': f'Fehler beim Speichern: {str(e)}'
})
else:
# Return form errors for AJAX requests
errors = []
for field, field_errors in form.errors.items():
for error in field_errors:
errors.append(f'{form[field].label}: {error}')
return JsonResponse({
'success': False,
'error': 'Formular enthält Fehler: ' + '; '.join(errors)
})
# Handle regular form submission
if form.is_valid():
destinataer = form.save()
try:
# Auto-create a Destinatärunterstützung if conditions are met
if (
destinataer.aktiv
and destinataer.unterstuetzung_bestaetigt
and destinataer.standard_konto
and destinataer.vierteljaehrlicher_betrag
and destinataer.vierteljaehrlicher_betrag > 0
):
from decimal import Decimal
from stiftung.models import DestinataerUnterstuetzung
heute = timezone.now().date()
beschreibung = f"Vierteljährliche Vorauszahlung für {destinataer.get_full_name()}"
# ensure only one upcoming planned entry; update if one exists
existing = (
DestinataerUnterstuetzung.objects.filter(
destinataer=destinataer, status="geplant"
)
.order_by("faellig_am")
.first()
)
if existing:
existing.konto = destinataer.standard_konto
existing.betrag = Decimal(destinataer.vierteljaehrlicher_betrag)
existing.faellig_am = heute
existing.beschreibung = beschreibung
existing.save()
else:
DestinataerUnterstuetzung.objects.create(
destinataer=destinataer,
konto=destinataer.standard_konto,
betrag=Decimal(destinataer.vierteljaehrlicher_betrag),
faellig_am=heute,
status="geplant",
beschreibung=beschreibung,
)
except Exception:
pass
messages.success(
request,
f'Destinatär "{destinataer.get_full_name()}" wurde erfolgreich aktualisiert.',
)
return redirect("stiftung:destinataer_detail", pk=destinataer.pk)
else:
form = DestinataerForm(instance=destinataer)
context = {
"form": form,
"destinataer": destinataer,
"title": f"Destinatär bearbeiten: {destinataer.get_full_name()}",
}
return render(request, "stiftung/destinataer_form.html", context)
@login_required
def destinataer_delete(request, pk):
destinataer = get_object_or_404(Destinataer, pk=pk)
if request.method == "POST":
destinataer.delete()
messages.success(
request,
f'Destinatär "{destinataer.get_full_name()}" wurde erfolgreich gelöscht.',
)
return redirect("stiftung:destinataer_list")
context = {"destinataer": destinataer}
return render(request, "stiftung/destinataer_confirm_delete.html", context)
# Paechter Views (Landpächter)
@login_required
def paechter_list(request):
search_query = request.GET.get("search", "")
ausbildung_filter = request.GET.get("ausbildung", "")
aktiv_filter = request.GET.get("aktiv", "")
sort = request.GET.get("sort", "")
direction = request.GET.get("dir", "asc")
paechter = Paechter.objects.all()
if search_query:
paechter = paechter.filter(
Q(nachname__icontains=search_query)
| Q(vorname__icontains=search_query)
| Q(email__icontains=search_query)
| Q(pachtnummer__icontains=search_query)
)
if ausbildung_filter == "true":
paechter = paechter.filter(landwirtschaftliche_ausbildung=True)
elif ausbildung_filter == "false":
paechter = paechter.filter(landwirtschaftliche_ausbildung=False)
if aktiv_filter == "true":
paechter = paechter.filter(aktiv=True)
elif aktiv_filter == "false":
paechter = paechter.filter(aktiv=False)
# Annotate with total leased area and rent (coalesce nulls to Decimal for stable sorting)
paechter = paechter.annotate(
gesamt_flaeche=Coalesce(
Sum("neue_verpachtungen__verpachtete_flaeche"),
Value(
Decimal("0.00"),
output_field=DecimalField(max_digits=12, decimal_places=2),
),
output_field=DecimalField(max_digits=12, decimal_places=2),
),
gesamt_pachtzins=Coalesce(
Sum("neue_verpachtungen__pachtzins_pauschal"),
Value(
Decimal("0.00"),
output_field=DecimalField(max_digits=12, decimal_places=2),
),
output_field=DecimalField(max_digits=12, decimal_places=2),
),
)
# Sorting
sort_map = {
"name": ["nachname", "vorname"],
"pachtnummer": ["pachtnummer"],
"ausbildung": ["landwirtschaftliche_ausbildung"],
"spezialisierung": ["spezialisierung"],
"flaeche": ["gesamt_flaeche"],
"pachtzins": ["gesamt_pachtzins"],
"status": ["aktiv"],
}
if sort in sort_map:
fields = sort_map[sort]
if direction == "desc":
order_fields = [f"-{f}" for f in fields]
else:
order_fields = fields
paechter = paechter.order_by(*order_fields)
paginator = Paginator(paechter, 20)
page_number = request.GET.get("page")
page_obj = paginator.get_page(page_number)
context = {
"page_obj": page_obj,
"search_query": search_query,
"ausbildung_filter": ausbildung_filter,
"aktiv_filter": aktiv_filter,
"sort": sort,
"dir": direction,
}
return render(request, "stiftung/paechter_list.html", context)
@login_required
def paechter_detail(request, pk):
paechter = get_object_or_404(Paechter, pk=pk)
# Alle mit diesem Pächter verknüpften Dokumente laden
verknuepfte_dokumente = DokumentLink.objects.filter(
paechter_id=paechter.pk
).order_by("kontext", "titel")
# Neue LandVerpachtungen für diesen Pächter laden
verpachtungen = LandVerpachtung.objects.filter(paechter=paechter).order_by(
"-pachtbeginn"
)
# Neue gepachtete Ländereien (über aktueller_paechter)
gepachtete_laendereien = paechter.gepachtete_laendereien.filter(
aktiv=True
).order_by("gemeinde", "gemarkung")
# Statistiken berechnen
total_flaeche_neu = sum(
land.verp_flaeche_aktuell or 0 for land in gepachtete_laendereien
)
total_pachtzins_neu = sum(
land.pachtzins_pauschal or 0 for land in gepachtete_laendereien
)
context = {
"paechter": paechter,
"verknuepfte_dokumente": verknuepfte_dokumente,
"verpachtungen": verpachtungen, # Now using LandVerpachtung
"gepachtete_laendereien": gepachtete_laendereien, # Neu
"total_flaeche_neu": total_flaeche_neu,
"total_pachtzins_neu": total_pachtzins_neu,
}
return render(request, "stiftung/paechter_detail.html", context)
@login_required
def paechter_create(request):
if request.method == "POST":
form = PaechterForm(request.POST)
if form.is_valid():
paechter = form.save()
messages.success(
request,
f'Pächter "{paechter.get_full_name()}" wurde erfolgreich erstellt.',
)
return redirect("stiftung:paechter_detail", pk=paechter.pk)
else:
# Debug: Log form errors and show them to user
print(f"Form errors: {form.errors}")
print(f"Form data: {request.POST}")
messages.error(request, f"Formular-Fehler: {form.errors}")
for field, errors in form.errors.items():
for error in errors:
messages.error(request, f"{field}: {error}")
else:
form = PaechterForm()
context = {"form": form, "title": "Neuen Pächter erstellen"}
return render(request, "stiftung/paechter_form.html", context)
@login_required
def paechter_update(request, pk):
paechter = get_object_or_404(Paechter, pk=pk)
if request.method == "POST":
form = PaechterForm(request.POST, instance=paechter)
if form.is_valid():
paechter = form.save()
messages.success(
request,
f'Pächter "{paechter.get_full_name()}" wurde erfolgreich aktualisiert.',
)
return redirect("stiftung:paechter_detail", pk=paechter.pk)
else:
# Debug: Log form errors and show them to user
print(f"Form errors: {form.errors}")
print(f"Form data: {request.POST}")
messages.error(request, f"Formular-Fehler: {form.errors}")
for field, errors in form.errors.items():
for error in errors:
messages.error(request, f"{field}: {error}")
else:
form = PaechterForm(instance=paechter)
context = {
"form": form,
"paechter": paechter,
"title": f"Pächter bearbeiten: {paechter.get_full_name()}",
}
return render(request, "stiftung/paechter_form.html", context)
@login_required
def paechter_delete(request, pk):
paechter = get_object_or_404(Paechter, pk=pk)
if request.method == "POST":
paechter.delete()
messages.success(
request, f'Pächter "{paechter.get_full_name()}" wurde erfolgreich gelöscht.'
)
return redirect("stiftung:paechter_list")
context = {"paechter": paechter}
return render(request, "stiftung/paechter_confirm_delete.html", context)
# Land Views
@login_required
def land_list(request):
search_query = request.GET.get("search", "")
gemeinde_filter = request.GET.get("gemeinde", "")
aktiv_filter = request.GET.get("aktiv", "")
sort = request.GET.get("sort", "")
direction = request.GET.get("dir", "asc")
lands = Land.objects.all()
if search_query:
lands = lands.filter(
Q(lfd_nr__icontains=search_query)
| Q(gemeinde__icontains=search_query)
| Q(gemarkung__icontains=search_query)
| Q(flur__icontains=search_query)
| Q(flurstueck__icontains=search_query)
)
if gemeinde_filter:
lands = lands.filter(gemeinde=gemeinde_filter)
if aktiv_filter == "true":
lands = lands.filter(aktiv=True)
elif aktiv_filter == "false":
lands = lands.filter(aktiv=False)
# Annotate with verpachtungsgrad and numeric casts for natural sorting
# Prepare numeric versions of textual fields by stripping common non-digits
def digits_only(field_expr):
expr = Replace(field_expr, Value(" "), Value(""))
expr = Replace(expr, Value("-"), Value(""))
expr = Replace(expr, Value("."), Value(""))
expr = Replace(expr, Value("/"), Value(""))
expr = Replace(expr, Value("L"), Value(""))
return expr
lands = lands.extra(
select={
"verpachtungsgrad": "CASE WHEN groesse_qm > 0 THEN (verp_flaeche_aktuell / groesse_qm) * 100 ELSE 0 END"
}
).annotate(
lfd_nr_num=Cast(NullIf(digits_only(F("lfd_nr")), Value("")), IntegerField()),
flur_num=Cast(NullIf(digits_only(F("flur")), Value("")), IntegerField()),
flurstueck_num=Cast(
NullIf(digits_only(F("flurstueck")), Value("")), IntegerField()
),
)
# Sorting
sort_map = {
"lfd_nr": ["lfd_nr_num", "lfd_nr"],
"gemeinde": ["gemeinde"],
"gemarkung": ["gemarkung"],
"flur": ["flur_num", "flur"],
"flurstueck": ["flurstueck_num", "flurstueck"],
"groesse": ["groesse_qm"],
"verp": ["verp_flaeche_aktuell"],
"grad": ["verpachtungsgrad"],
}
if sort in sort_map:
fields = sort_map[sort]
if direction == "desc":
order_fields = [f"-{f}" for f in fields]
else:
order_fields = fields
lands = lands.order_by(*order_fields)
# Aggregated statistics for current filter set
aggregates = lands.aggregate(
sum_groesse_qm=Sum("groesse_qm"),
sum_gruenland_qm=Sum("gruenland_qm"),
sum_acker_qm=Sum("acker_qm"),
sum_wald_qm=Sum("wald_qm"),
sum_sonstiges_qm=Sum("sonstiges_qm"),
)
sum_gruenland_qm = float(aggregates.get("sum_gruenland_qm") or 0)
sum_acker_qm = float(aggregates.get("sum_acker_qm") or 0)
sum_wald_qm = float(aggregates.get("sum_wald_qm") or 0)
sum_sonstiges_qm = float(aggregates.get("sum_sonstiges_qm") or 0)
sum_total_use_qm = sum_gruenland_qm + sum_acker_qm + sum_wald_qm + sum_sonstiges_qm
def pct(part, total):
return round((part / total) * 100, 1) if total and part is not None else 0.0
stats = {
"sum_gruenland_qm": sum_gruenland_qm,
"sum_acker_qm": sum_acker_qm,
"sum_wald_qm": sum_wald_qm,
"sum_sonstiges_qm": sum_sonstiges_qm,
"sum_total_use_qm": sum_total_use_qm,
"pct_gruenland": pct(sum_gruenland_qm, sum_total_use_qm),
"pct_acker": pct(sum_acker_qm, sum_total_use_qm),
"pct_wald": pct(sum_wald_qm, sum_total_use_qm),
}
# Prepare size chart data (top 30 by size)
top_sizes = list(
lands.order_by("-groesse_qm").values_list("lfd_nr", "groesse_qm")[:30]
)
size_chart_labels = [label or "" for label, _ in top_sizes]
size_chart_values = [float(val or 0) for _, val in top_sizes]
paginator = Paginator(lands, 20)
page_number = request.GET.get("page")
page_obj = paginator.get_page(page_number)
# Get unique gemeinden for filter
gemeinden = (
Land.objects.values_list("gemeinde", flat=True).distinct().order_by("gemeinde")
)
context = {
"page_obj": page_obj,
"search_query": search_query,
"gemeinde_filter": gemeinde_filter,
"aktiv_filter": aktiv_filter,
"gemeinden": gemeinden,
"stats": stats,
"size_chart_labels_json": json.dumps(size_chart_labels),
"size_chart_values_json": json.dumps(size_chart_values),
"sort": sort,
"dir": direction,
}
return render(request, "stiftung/land_list.html", context)
@login_required
def land_detail(request, pk):
land = get_object_or_404(Land, pk=pk)
# Alle mit dieser Länderei verknüpften Dokumente laden
verknuepfte_dokumente = DokumentLink.objects.filter(land_id=land.pk).order_by(
"kontext", "titel"
)
# Neue LandVerpachtungen laden (mit related data)
neue_verpachtungen = land.neue_verpachtungen.select_related("paechter").order_by(
"-pachtbeginn"
)
context = {
"land": land,
"verknuepfte_dokumente": verknuepfte_dokumente,
"verpachtungen": neue_verpachtungen, # Using only new system now
"neue_verpachtungen": neue_verpachtungen,
}
return render(request, "stiftung/land_detail.html", context)
@login_required
def land_create(request):
if request.method == "POST":
form = LandForm(request.POST)
if form.is_valid():
land = form.save()
messages.success(request, f'Länderei "{land}" wurde erfolgreich erstellt.')
return redirect("stiftung:land_detail", pk=land.pk)
else:
form = LandForm()
context = {"form": form, "title": "Neue Länderei erstellen"}
return render(request, "stiftung/land_form.html", context)
@login_required
def land_update(request, pk):
land = get_object_or_404(Land, pk=pk)
if request.method == "POST":
form = LandForm(request.POST, instance=land)
if form.is_valid():
land = form.save()
messages.success(
request, f'Länderei "{land}" wurde erfolgreich aktualisiert.'
)
return redirect("stiftung:land_detail", pk=land.pk)
else:
form = LandForm(instance=land)
context = {"form": form, "land": land, "title": f"Länderei bearbeiten: {land}"}
return render(request, "stiftung/land_form.html", context)
@login_required
def land_delete(request, pk):
land = get_object_or_404(Land, pk=pk)
if request.method == "POST":
land.delete()
messages.success(request, f'Länderei "{land}" wurde erfolgreich gelöscht.')
return redirect("stiftung:land_list")
context = {"land": land}
return render(request, "stiftung/land_confirm_delete.html", context)
# Verpachtung Views
@login_required
def verpachtung_list(request):
search_query = request.GET.get("search", "")
status_filter = request.GET.get("status", "")
gemeinde_filter = request.GET.get("gemeinde", "")
sort = request.GET.get("sort", "")
direction = request.GET.get("dir", "asc")
verpachtungen = LandVerpachtung.objects.select_related("land", "paechter").all()
if search_query:
verpachtungen = verpachtungen.filter(
Q(vertragsnummer__icontains=search_query)
| Q(land__gemeinde__icontains=search_query)
| Q(paechter__nachname__icontains=search_query)
| Q(paechter__vorname__icontains=search_query)
)
if status_filter:
verpachtungen = verpachtungen.filter(status=status_filter)
if gemeinde_filter:
verpachtungen = verpachtungen.filter(land__gemeinde=gemeinde_filter)
# Sorting
sort_map = {
"vertragsnummer": ["vertragsnummer"],
"land": ["land__gemeinde"],
"paechter": ["paechter__nachname", "paechter__vorname"],
"beginn": ["pachtbeginn"],
"ende": ["pachtende"],
"flaeche": ["verpachtete_flaeche"],
"pachtzins": ["pachtzins_pauschal"],
"status": ["status"],
}
if sort in sort_map:
fields = sort_map[sort]
if direction == "desc":
order_fields = [f"-{f}" for f in fields]
else:
order_fields = fields
verpachtungen = verpachtungen.order_by(*order_fields)
paginator = Paginator(verpachtungen, 20)
page_number = request.GET.get("page")
page_obj = paginator.get_page(page_number)
# Calculate statistics for the summary cards
# Get ALL verpachtungen (not filtered) for accurate statistics
all_verpachtungen = LandVerpachtung.objects.select_related("land", "paechter").all()
# Active verpachtungen count
aktive_verpachtungen = all_verpachtungen.filter(status="aktiv").count()
# Total leased area (only active verpachtungen)
gesamt_flaeche_result = all_verpachtungen.filter(status="aktiv").aggregate(
total=Sum("verpachtete_flaeche")
)
gesamt_flaeche = (
gesamt_flaeche_result["total"]
if gesamt_flaeche_result["total"] is not None
else 0
)
# Total annual rent (only active verpachtungen)
jaehrlicher_pachtzins_result = all_verpachtungen.filter(status="aktiv").aggregate(
total=Sum("pachtzins_pauschal")
)
jaehrlicher_pachtzins = (
jaehrlicher_pachtzins_result["total"]
if jaehrlicher_pachtzins_result["total"] is not None
else 0
)
# Total count of all verpachtungen
anzahl_verpachtungen = all_verpachtungen.count()
# Get unique gemeinden and statuses for filters
gemeinden = (
Land.objects.values_list("gemeinde", flat=True).distinct().order_by("gemeinde")
)
status_choices = LandVerpachtung.STATUS_CHOICES
context = {
"page_obj": page_obj,
"search_query": search_query,
"status_filter": status_filter,
"gemeinde_filter": gemeinde_filter,
"gemeinden": gemeinden,
"status_choices": status_choices,
# Statistics for summary cards
"aktive_verpachtungen": aktive_verpachtungen,
"gesamt_flaeche": gesamt_flaeche,
"jaehrlicher_pachtzins": jaehrlicher_pachtzins,
"anzahl_verpachtungen": anzahl_verpachtungen,
"sort": sort,
"dir": direction,
}
return render(request, "stiftung/verpachtung_list.html", context)
@login_required
@login_required
def land_verpachtung_detail(request, pk):
"""Detail view for LandVerpachtung"""
verpachtung = get_object_or_404(LandVerpachtung, pk=pk)
# Alle mit dieser Verpachtung verknüpften Dokumente laden
verknuepfte_dokumente = DokumentLink.objects.filter(
land_verpachtung_id=verpachtung.pk
).order_by("kontext", "titel")
context = {
"verpachtung": verpachtung,
"landverpachtung": verpachtung, # Template expects this variable name
"verknuepfte_dokumente": verknuepfte_dokumente,
}
return render(request, "stiftung/land_verpachtung_detail.html", context)
@login_required
def land_verpachtung_update(request, pk):
"""Update an existing LandVerpachtung by its primary key"""
verpachtung = get_object_or_404(LandVerpachtung, pk=pk)
if request.method == "POST":
# Handle the update form submission
vertragsnummer = request.POST.get("vertragsnummer")
pachtbeginn = request.POST.get("pachtbeginn")
pachtende = request.POST.get("pachtende")
pachtzins_pauschal = request.POST.get("pachtzins_pauschal")
if vertragsnummer:
verpachtung.vertragsnummer = vertragsnummer
if pachtbeginn:
verpachtung.pachtbeginn = pachtbeginn
if pachtende:
verpachtung.pachtende = pachtende
if pachtzins_pauschal:
verpachtung.pachtzins_pauschal = pachtzins_pauschal
verpachtung.save()
messages.success(request, "Verpachtung wurde erfolgreich aktualisiert.")
return redirect("stiftung:land_verpachtung_detail", pk=verpachtung.pk)
context = {
"verpachtung": verpachtung,
"landverpachtung": verpachtung, # Template expects this variable name
"is_edit": True,
"is_update": True, # Form template uses this flag
}
return render(request, "stiftung/land_verpachtung_form.html", context)
@login_required
def land_verpachtung_end_direct(request, pk):
"""End a LandVerpachtung directly by its primary key"""
verpachtung = get_object_or_404(LandVerpachtung, pk=pk)
if request.method == "POST":
verpachtung.status = "beendet"
verpachtung.pachtende = timezone.now().date()
verpachtung.save()
messages.success(request, "Verpachtung wurde erfolgreich beendet.")
return redirect("stiftung:land_detail", pk=verpachtung.land.pk)
context = {
"verpachtung": verpachtung,
}
return render(request, "stiftung/land_verpachtung_end_confirm.html", context)
# Förderung Views
@login_required
def foerderung_list(request):
"""List all funding grants with filtering and pagination"""
foerderungen = Foerderung.objects.select_related(
"destinataer", "verwendungsnachweis"
).all()
# Check for export request - handle both GET and POST
export_format = (
request.POST.get("format")
if request.method == "POST"
else request.GET.get("format", "")
)
selected_ids_param = (
request.POST.get("selected_entries", "")
if request.method == "POST"
else request.GET.get("selected_entries", "")
)
selected_ids = (
[id for id in selected_ids_param.split(",") if id] if selected_ids_param else []
)
# Filtering
jahr = request.GET.get("jahr")
kategorie = request.GET.get("kategorie")
status = request.GET.get("status")
destinataer = request.GET.get("destinataer")
if jahr:
foerderungen = foerderungen.filter(jahr=int(jahr))
if kategorie:
foerderungen = foerderungen.filter(kategorie=kategorie)
if status:
foerderungen = foerderungen.filter(status=status)
if destinataer:
foerderungen = foerderungen.filter(destinataer__nachname__icontains=destinataer)
# Handle exports
if export_format == "csv":
return export_foerderungen_csv(request, foerderungen, selected_ids)
elif export_format == "pdf":
return export_foerderungen_pdf(request, foerderungen, selected_ids)
# Pagination
paginator = Paginator(foerderungen, 25)
page_number = request.GET.get("page")
page_obj = paginator.get_page(page_number)
# Statistics
total_betrag = foerderungen.aggregate(total=Sum("betrag"))["total"] or 0
avg_betrag = foerderungen.aggregate(avg=Avg("betrag"))["avg"] or 0
# Year choices for filters
jahre = sorted(
set(list(Foerderung.objects.values_list("jahr", flat=True))), reverse=True
)
context = {
"page_obj": page_obj,
"foerderungen": foerderungen, # Add for counting
"total_betrag": total_betrag,
"avg_betrag": avg_betrag,
"kategorien": Foerderung.KATEGORIE_CHOICES,
"status_choices": Foerderung.STATUS_CHOICES,
"filter_jahr": jahr,
"filter_kategorie": kategorie,
"filter_status": status,
"filter_person": destinataer,
"jahre": jahre,
}
return render(request, "stiftung/foerderung_list.html", context)
@login_required
def foerderung_detail(request, pk):
"""Show details of a specific funding grant"""
foerderung = get_object_or_404(
Foerderung.objects.select_related("person", "verwendungsnachweis"), pk=pk
)
# Alle mit dieser Förderung verknüpften Dokumente laden
verknuepfte_dokumente = DokumentLink.objects.filter(
foerderung_id=foerderung.pk
).order_by("kontext", "titel")
context = {
"foerderung": foerderung,
"verknuepfte_dokumente": verknuepfte_dokumente,
"title": f"Förderung: {foerderung}",
}
return render(request, "stiftung/foerderung_detail.html", context)
@login_required
def foerderung_create(request):
"""Create a new funding grant"""
# Get destinataer from URL parameter if provided
destinataer_id = request.GET.get("destinataer")
initial = {}
if destinataer_id:
initial["destinataer"] = destinataer_id
if request.method == "POST":
form = FoerderungForm(request.POST)
if form.is_valid():
foerderung = form.save()
messages.success(
request,
f"Förderung für {foerderung.destinataer} wurde erfolgreich erstellt.",
)
return redirect("stiftung:foerderung_detail", pk=foerderung.pk)
else:
form = FoerderungForm(initial=initial)
context = {
"form": form,
"title": "Neue Förderung erstellen",
}
return render(request, "stiftung/foerderung_form.html", context)
@login_required
def foerderung_update(request, pk):
"""Update an existing funding grant"""
foerderung = get_object_or_404(Foerderung, pk=pk)
if request.method == "POST":
form = FoerderungForm(request.POST, instance=foerderung)
if form.is_valid():
form.save()
messages.success(
request,
f"Förderung für {foerderung.person} wurde erfolgreich aktualisiert.",
)
return redirect("stiftung:foerderung_detail", pk=foerderung.pk)
else:
form = FoerderungForm(instance=foerderung)
context = {
"form": form,
"foerderung": foerderung,
"title": f"Förderung bearbeiten: {foerderung}",
}
return render(request, "stiftung/foerderung_form.html", context)
@login_required
def foerderung_delete(request, pk):
"""Delete a funding grant"""
foerderung = get_object_or_404(Foerderung, pk=pk)
if request.method == "POST":
# Get the recipient name before deletion
recipient_name = (
foerderung.destinataer.get_full_name()
if foerderung.destinataer
else (
foerderung.person.get_full_name()
if foerderung.person
else "Unbekannter Empfänger"
)
)
foerderung.delete()
messages.success(
request, f"Förderung für {recipient_name} wurde erfolgreich gelöscht."
)
return redirect("stiftung:foerderung_list")
context = {
"foerderung": foerderung,
"title": f"Förderung löschen: {foerderung}",
}
return render(request, "stiftung/foerderung_confirm_delete.html", context)
# DokumentLink Views
@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.rstrip("/api") 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
@login_required
def bericht_list(request):
"""List available reports"""
# Get available years from data
jahre = sorted(
set(
list(Foerderung.objects.values_list("jahr", flat=True))
+ list(LandVerpachtung.objects.values_list("pachtbeginn__year", flat=True))
),
reverse=True,
)
# Statistics for overview tiles (removed legacy Person and Verpachtung)
total_destinataere = Destinataer.objects.count()
total_laendereien = Land.objects.count()
total_verpachtungen = LandVerpachtung.objects.count()
total_foerderungen = Foerderung.objects.count()
context = {
"jahre": jahre,
"title": "Berichte",
"total_destinataere": total_destinataere,
"total_laendereien": total_laendereien,
"total_verpachtungen": total_verpachtungen,
"total_foerderungen": total_foerderungen,
}
return render(request, "stiftung/bericht_list.html", context)
@login_required
def jahresbericht_generate(request, jahr):
"""Generate annual report for a specific year"""
# Get data for the year
foerderungen = Foerderung.objects.filter(jahr=jahr).select_related("person")
verpachtungen = LandVerpachtung.objects.filter(
pachtbeginn__year__lte=jahr, pachtende__year__gte=jahr
).select_related("land", "paechter")
# Calculate statistics
total_foerderungen = foerderungen.aggregate(total=Sum("betrag"))["total"] or 0
total_pachtzins = (
verpachtungen.aggregate(total=Sum("pachtzins_pauschal"))["total"] or 0
)
context = {
"jahr": jahr,
"foerderungen": foerderungen,
"verpachtungen": verpachtungen,
"total_foerderungen": total_foerderungen,
"total_pachtzins": total_pachtzins,
"title": f"Jahresbericht {jahr}",
}
return render(request, "stiftung/jahresbericht.html", context)
@login_required
def jahresbericht_generate_redirect(request):
"""Redirects the GET form without path param to the proper URL using the provided query param 'jahr'."""
jahr = request.GET.get("jahr")
if jahr and str(jahr).isdigit():
return redirect("stiftung:jahresbericht_generate", jahr=int(jahr))
messages.error(request, "Bitte wählen Sie ein gültiges Jahr aus.")
return redirect("stiftung:bericht_list")
@login_required
def jahresbericht_pdf(request, jahr):
"""Generate PDF version of annual report"""
from django.http import HttpResponse
from django.template.loader import render_to_string
from weasyprint import HTML
# Get data for the year
foerderungen = Foerderung.objects.filter(jahr=jahr).select_related("person")
verpachtungen = LandVerpachtung.objects.filter(
pachtbeginn__year__lte=jahr, pachtende__year__gte=jahr
).select_related("land", "paechter")
# Calculate statistics
total_foerderungen = foerderungen.aggregate(total=Sum("betrag"))["total"] or 0
total_pachtzins = (
verpachtungen.aggregate(total=Sum("pachtzins_pauschal"))["total"] or 0
)
context = {
"jahr": jahr,
"foerderungen": foerderungen,
"verpachtungen": verpachtungen,
"total_foerderungen": total_foerderungen,
"total_pachtzins": total_pachtzins,
}
# Render HTML
html_string = render_to_string("stiftung/jahresbericht.html", context)
# Generate PDF
pdf = HTML(string=html_string).write_pdf()
# Create response
response = HttpResponse(pdf, content_type="application/pdf")
response["Content-Disposition"] = f'attachment; filename="jahresbericht_{jahr}.pdf"'
return response
# Dashboard Views
@login_required
def dashboard(request):
# Foerderung statistics (Person statistics removed - was legacy Verpachtung system)
total_foerderungen = Foerderung.objects.aggregate(total=Sum("betrag"))["total"] or 0
# Land statistics
total_land = Land.objects.count()
active_land = Land.objects.filter(aktiv=True).count()
total_flaeche = Land.objects.aggregate(total=Sum("groesse_qm"))["total"] or 0
# Calculate total verpachtet from active verpachtungen
total_verpachtet = (
LandVerpachtung.objects.filter(status="aktiv").aggregate(
total=Sum("verpachtete_flaeche")
)["total"]
or 0
)
# Verpachtung statistics
total_verpachtungen = LandVerpachtung.objects.count()
active_verpachtungen = LandVerpachtung.objects.filter(status="aktiv").count()
total_pachtzins = (
LandVerpachtung.objects.filter(status="aktiv").aggregate(
total=Sum("pachtzins_pauschal")
)["total"]
or 0
)
# Recent activities
recent_lands = Land.objects.order_by("-erstellt_am")[:5]
recent_verpachtungen = LandVerpachtung.objects.select_related(
"land", "paechter"
).order_by("-erstellt_am")[:5]
# Dokumentenübersicht
dokumente_uebersicht = DokumentLink.objects.all().order_by("-id")[:10]
# Verfügbare Paperless-Dokumente für Dashboard
available_paperless_docs = []
from stiftung.utils.config import get_paperless_config
config = get_paperless_config()
url = config["api_url"]
token = config["api_token"]
if url and token:
try:
base_url = url.rstrip("/api") 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_paperless_docs.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 (neueste zuerst)
available_paperless_docs.sort(key=lambda x: x["created_date"], reverse=True)
except Exception:
pass
context = {
# Person statistics removed - was legacy Verpachtung system
"total_foerderungen": total_foerderungen,
"total_land": total_land,
"active_land": active_land,
"total_flaeche": total_flaeche,
"total_verpachtet": total_verpachtet,
"total_verpachtungen": total_verpachtungen,
"active_verpachtungen": active_verpachtungen,
"total_pachtzins": total_pachtzins,
"recent_lands": recent_lands,
"recent_verpachtungen": recent_verpachtungen,
"dokumente_uebersicht": dokumente_uebersicht,
"available_paperless_docs": available_paperless_docs,
}
return render(request, "stiftung/dashboard.html", context)
# API Views for AJAX
@login_required
def land_stats_api(request):
"""API endpoint for land statistics"""
if request.method == "GET":
gemeinde = request.GET.get("gemeinde", "")
if gemeinde:
lands = Land.objects.filter(gemeinde=gemeinde)
else:
lands = Land.objects.all()
stats = {
"total_count": lands.count(),
"total_flaeche": float(
lands.aggregate(total=Sum("groesse_qm"))["total"] or 0
),
"total_verpachtet": float(
LandVerpachtung.objects.filter(
status="aktiv", land__in=lands
).aggregate(total=Sum("verpachtete_flaeche"))["total"]
or 0
),
"avg_verpachtungsgrad": 0,
}
if stats["total_flaeche"] > 0:
stats["avg_verpachtungsgrad"] = (
stats["total_verpachtet"] / stats["total_flaeche"]
) * 100
return JsonResponse(stats)
return JsonResponse({"error": "Invalid request method"}, status=400)
@api_view(["GET"])
def health(_request):
return Response({"status": "ok"})
@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.rstrip("/api") 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.rstrip("/api") 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.rstrip("/api") 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.rstrip("/api") if url.endswith("/api") else url
# Alle Tags abrufen (mit großer page_size)
headers = {"Authorization": f"Token {token}"}
# Erste Anfrage mit großer page_size
tags_response = requests.get(
f"{base_url}/api/tags/?page_size=1000", headers=headers, timeout=10
)
tags_response.raise_for_status()
tags_data = tags_response.json()
all_tags = []
# Erste Seite verarbeiten
for tag in tags_data.get("results", []):
tag_detail = {
"id": tag.get("id"),
"name": tag.get("name", ""),
"slug": tag.get("slug", ""),
"color": tag.get("color", ""),
"text_color": tag.get("text_color", ""),
"match": tag.get("match", ""),
"matching_algorithm": tag.get("matching_algorithm"),
"is_inbox_tag": tag.get("is_inbox_tag"),
"document_count": tag.get("document_count", 0),
}
all_tags.append(tag_detail)
# Weitere Seiten abrufen falls vorhanden
next_url = tags_data.get("next")
while next_url:
next_response = requests.get(next_url, headers=headers, timeout=10)
next_response.raise_for_status()
next_data = next_response.json()
for tag in next_data.get("results", []):
tag_detail = {
"id": tag.get("id"),
"name": tag.get("name", ""),
"slug": tag.get("slug", ""),
"color": tag.get("color", ""),
"text_color": tag.get("text_color", ""),
"match": tag.get("match", ""),
"matching_algorithm": tag.get("matching_algorithm"),
"is_inbox_tag": tag.get("is_inbox_tag"),
"document_count": tag.get("document_count", 0),
}
all_tags.append(tag_detail)
next_url = next_data.get("next")
# Nach ID sortieren
all_tags.sort(key=lambda x: x["id"])
return Response(
{
"total_tags": len(all_tags),
"tags": all_tags,
"tag_ids": [tag["id"] for tag in all_tags],
"tag_names": [tag["name"] for tag in all_tags],
"api_info": {
"page_size_used": 1000,
"total_count_from_api": tags_data.get("count", 0),
},
}
)
except requests.exceptions.RequestException as e:
return Response({"error": f"API-Fehler: {e}"}, status=500)
except Exception as e:
return Response({"error": f"Unerwarteter Fehler: {e}"}, status=500)
@api_view(["GET"])
def link_document_search(request):
"""Sucht nach Datensätzen für die Dokument-Verknüpfung"""
from django.db.models import Q
query = request.GET.get("q", "")
category = request.GET.get("category", "all")
results = {}
if category in ["all", "destinataer"]:
# Suche nach Destinatären
destinataer_query = Q()
if query and query != "all":
destinataer_query = (
Q(nachname__icontains=query)
| Q(vorname__icontains=query)
| Q(email__icontains=query)
| Q(telefon__icontains=query)
| Q(strasse__icontains=query)
| Q(ort__icontains=query)
| Q(plz__icontains=query)
| Q(institution__icontains=query)
| Q(familienzweig__icontains=query)
| Q(notizen__icontains=query)
)
destinataer_results = Destinataer.objects.filter(destinataer_query)[:25]
results["destinataer"] = [
{
"id": d.id,
"name": (
f"{d.vorname} {d.nachname}".strip()
if d.vorname
else (d.institution or d.nachname)
),
"type": "Destinatär",
"details": f"{d.strasse or ''}{(', ' + d.plz + ' ' + d.ort) if d.plz and d.ort else (' ' + d.ort) if d.ort else ''} {('' + d.email) if d.email else ''} {('' + d.institution) if d.institution else ''} {('' + d.familienzweig) if d.familienzweig else ''}".strip(),
}
for d in destinataer_results
]
if category in ["all", "land"]:
# Suche nach Ländereien
land_query = Q()
if query and query != "all":
# Extract numbers from search terms like "Flur 9" or "Flurstück 11"
import re
flur_match = re.search(r"flur\s*(\d+)", query, re.IGNORECASE)
flurstuck_match = re.search(r"flurstück\s*(\d+)", query, re.IGNORECASE)
land_query = (
Q(gemarkung__icontains=query)
| Q(gemeinde__icontains=query)
| Q(flur__icontains=query)
| Q(flurstueck__icontains=query)
| Q(lfd_nr__icontains=query)
| Q(ew_nummer__icontains=query)
| Q(notizen__icontains=query)
)
# Add specific searches for extracted numbers
if flur_match:
land_query |= Q(flur__exact=flur_match.group(1))
if flurstuck_match:
land_query |= Q(flurstueck__exact=flurstuck_match.group(1))
land_results = Land.objects.filter(land_query)[:25]
results["land"] = [
{
"id": l.id,
"name": f"{l.gemarkung} - {l.gemeinde}, Flur {l.flur}, Flurstück {l.flurstueck or 'N/A'}",
"type": "Land",
"details": f"Lfd.Nr: {l.lfd_nr or 'N/A'} • EW-Nr: {l.ew_nummer or 'N/A'} • Größe: {l.groesse_qm or 0}",
}
for l in land_results
]
if category in ["all", "verpachtung"]:
# Suche nach Verpachtungen (using new LandVerpachtung model)
verpachtung_query = Q()
if query and query != "all":
verpachtung_query = (
Q(paechter__nachname__icontains=query)
| Q(paechter__vorname__icontains=query)
| Q(paechter__ort__icontains=query)
| Q(paechter__email__icontains=query)
| Q(paechter__pachtnummer__icontains=query)
| Q(land__gemarkung__icontains=query)
| Q(land__gemeinde__icontains=query)
| Q(land__flur__icontains=query)
| Q(land__flurstueck__icontains=query)
| Q(land__lfd_nr__icontains=query)
| Q(vertragsnummer__icontains=query)
| Q(pachtzins_pauschal__icontains=query)
| Q(bemerkungen__icontains=query)
)
verpachtung_results = LandVerpachtung.objects.filter(
verpachtung_query
).select_related("paechter", "land")[:25]
results["verpachtung"] = [
{
"id": v.id,
"name": f"{(v.paechter.vorname + ' ') if v.paechter and v.paechter.vorname else ''}{v.paechter.nachname if v.paechter else 'Unbekannt'}{v.land.gemarkung}, Flur {v.land.flur}",
"type": "Verpachtung",
"details": f"Vertrag: {v.vertragsnummer} • Flurstück: {v.land.flurstueck or 'N/A'} • Pachtzins: {v.pachtzins_pauschal or 'N/A'} €/Jahr • Zeitraum: {v.pachtbeginn.strftime('%Y') if v.pachtbeginn else '?'}-{v.pachtende.strftime('%Y') if v.pachtende else 'laufend'}",
}
for v in verpachtung_results
]
if category in ["all", "paechter"]:
# Suche nach Pächtern
paechter_query = Q()
if query and query != "all":
paechter_query = (
Q(nachname__icontains=query)
| Q(vorname__icontains=query)
| Q(ort__icontains=query)
| Q(email__icontains=query)
| Q(telefon__icontains=query)
| Q(strasse__icontains=query)
| Q(pachtnummer__icontains=query)
| Q(plz__icontains=query)
| Q(notizen__icontains=query)
)
paechter_results = Paechter.objects.filter(paechter_query)[:25]
results["paechter"] = [
{
"id": p.id,
"name": f"{(p.vorname + ' ') if p.vorname else ''}{p.nachname}"
+ (f" (#{p.pachtnummer})" if p.pachtnummer else ""),
"type": "Pächter",
"details": f"{p.strasse or ''}{(', ' + p.plz + ' ' + p.ort) if p.plz and p.ort else (' ' + p.ort) if p.ort else ''} {('' + p.email) if p.email else ''} {('• Tel: ' + p.telefon) if p.telefon else ''}".strip(),
}
for p in paechter_results
]
if category in ["all", "rentmeister"]:
# Suche nach Rentmeistern
from stiftung.models import Rentmeister
rentmeister_query = Q()
if query and query != "all":
rentmeister_query = (
Q(nachname__icontains=query)
| Q(vorname__icontains=query)
| Q(ort__icontains=query)
| Q(email__icontains=query)
| Q(telefon__icontains=query)
| Q(strasse__icontains=query)
| Q(plz__icontains=query)
| Q(notizen__icontains=query)
| Q(titel__icontains=query)
| Q(mobil__icontains=query)
)
rentmeister_results = Rentmeister.objects.filter(rentmeister_query)[:25]
results["rentmeister"] = [
{
"id": r.id,
"name": f"{(r.anrede + ' ') if r.anrede else ''}{(r.vorname + ' ') if r.vorname else ''}{r.nachname}"
+ (f" ({r.titel})" if r.titel else ""),
"type": "Rentmeister",
"details": f"{r.strasse or ''}{(', ' + r.plz + ' ' + r.ort) if r.plz and r.ort else (' ' + r.ort) if r.ort else ''} {('' + r.email) if r.email else ''} {('• Tel: ' + r.telefon) if r.telefon else ''} {('• Mobil: ' + r.mobil) if r.mobil else ''}".strip(),
}
for r in rentmeister_results
]
if category in ["all", "abrechnung"]:
# Suche nach Abrechnungen
abrechnung_query = Q()
if query and query != "all":
abrechnung_query = (
Q(land__gemarkung__icontains=query)
| Q(land__gemeinde__icontains=query)
| Q(land__flur__icontains=query)
| Q(land__flurstueck__icontains=query)
| Q(land__lfd_nr__icontains=query)
| Q(abrechnungsjahr__icontains=query)
| Q(bemerkungen__icontains=query)
)
abrechnung_results = LandAbrechnung.objects.filter(
abrechnung_query
).select_related("land")[:25]
results["abrechnung"] = [
{
"id": a.id,
"name": f"Abrechnung {a.abrechnungsjahr} - {a.land.gemarkung}, Flur {a.land.flur}",
"type": "Abrechnung",
"details": f"Flurstück: {a.land.flurstueck or 'N/A'} • Jahr: {a.abrechnungsjahr} • Grundsteuer: {a.grundsteuer_betrag or 0} € • Versicherung: {a.versicherungen_betrag or 0}",
}
for a in abrechnung_results
]
if category in ["all", "foerderung"]:
# Suche nach Förderungen
foerderung_query = Q()
if query and query != "all":
foerderung_query = (
Q(destinataer__nachname__icontains=query)
| Q(destinataer__vorname__icontains=query)
| Q(destinataer__institution__icontains=query)
| Q(destinataer__email__icontains=query)
| Q(jahr__icontains=query)
| Q(betrag__icontains=query)
| Q(kategorie__icontains=query)
| Q(status__icontains=query)
| Q(bemerkungen__icontains=query)
)
foerderung_results = Foerderung.objects.filter(foerderung_query).select_related(
"destinataer"
)[:25]
results["foerderung"] = [
{
"id": str(f.id), # Convert UUID to string for JSON serialization
"name": f"{f.destinataer.get_full_name() if f.destinataer else 'Unbekannt'} - {f.jahr}",
"type": "Förderung",
"details": f"Betrag: {f.betrag} € • Kategorie: {f.get_kategorie_display()} • Status: {f.get_status_display()} • Jahr: {f.jahr}",
}
for f in foerderung_results
]
return Response(results)
def create_paechter_link_for_verpachtung(paperless_id, paperless_title, verpachtung_id):
"""Hilfsfunktion: Erstellt automatisch eine Pächter-Verknüpfung für eine Verpachtung"""
try:
# Hole die LandVerpachtung und den zugehörigen Pächter
verpachtung = LandVerpachtung.objects.select_related("paechter").get(
id=verpachtung_id
)
if verpachtung.paechter:
# Prüfe, ob bereits eine Verknüpfung für dieses Dokument und diesen Pächter existiert
existing_link = DokumentLink.objects.filter(
paperless_document_id=paperless_id, paechter_id=verpachtung.paechter.id
).first()
if not existing_link:
# Erstelle automatische Pächter-Verknüpfung
DokumentLink.objects.create(
paperless_document_id=paperless_id,
titel=paperless_title,
kontext="paechter",
paechter_id=verpachtung.paechter.id,
)
return True
except (LandVerpachtung.DoesNotExist, Exception):
pass
return False
@csrf_exempt
@api_view(["POST"])
def link_document_create(request):
"""Erstellt eine Verknüpfung zwischen einem Paperless-Dokument und einem Datensatz"""
from django.db import transaction
try:
# Robust JSON parsing to tolerate Latin-1 encoded payloads (e.g., umlauts)
try:
payload = request.data
except Exception:
raw = request.body
try:
payload = json.loads(raw.decode("utf-8"))
except UnicodeDecodeError:
payload = json.loads(raw.decode("latin-1"))
paperless_id = payload.get("paperless_id")
paperless_title = payload.get("paperless_title")
paperless_url = payload.get("paperless_url")
link_type = payload.get(
"link_type"
) # 'destinataer', 'land', 'verpachtung', 'paechter', 'abrechnung'
link_id = payload.get("link_id")
if not all([paperless_id, paperless_title, paperless_url, link_type, link_id]):
return Response({"error": "Alle Felder sind erforderlich"}, status=400)
with transaction.atomic():
# Erstelle den DokumentLink
dokument_link = DokumentLink.objects.create(
paperless_document_id=paperless_id, # Korrigiert: 'paperless_document_id' statt 'paperless_id'
titel=paperless_title, # Korrigiert: 'titel' statt 'title'
kontext="anderes",
)
# Setze die entsprechenden UUID-Felder basierend auf dem Link-Typ
if link_type == "destinataer":
dokument_link.destinataer_id = link_id
elif link_type == "land":
dokument_link.land_id = link_id
elif link_type == "verpachtung":
# Use new LandVerpachtung field instead of legacy
dokument_link.land_verpachtung_id = link_id
elif link_type == "paechter":
dokument_link.paechter_id = link_id
elif link_type == "foerderung":
dokument_link.foerderung_id = link_id
elif link_type == "rentmeister":
dokument_link.rentmeister_id = link_id
elif link_type == "abrechnung":
dokument_link.abrechnung_id = link_id
dokument_link.save()
# Log the document linking action
from stiftung.audit import log_link
try:
# Get the linked entity name for logging
entity_name = paperless_title
if link_type == "destinataer":
from stiftung.models import Destinataer
entity = Destinataer.objects.get(id=link_id)
target_name = entity.get_full_name()
elif link_type == "land":
from stiftung.models import Land
entity = Land.objects.get(id=link_id)
target_name = str(entity)
elif link_type == "paechter":
from stiftung.models import Paechter
entity = Paechter.objects.get(id=link_id)
target_name = f"{entity.vorname} {entity.nachname}".strip()
elif link_type == "foerderung":
from stiftung.models import Foerderung
entity = Foerderung.objects.get(id=link_id)
target_name = f"{entity.destinataer.get_full_name() if entity.destinataer else 'Unbekannt'} - {entity.jahr}"
elif link_type == "verpachtung":
entity = LandVerpachtung.objects.get(id=link_id)
target_name = str(entity)
elif link_type == "rentmeister":
from stiftung.models import Rentmeister
entity = Rentmeister.objects.get(id=link_id)
target_name = entity.get_full_name()
else:
target_name = f"ID {link_id}"
log_link(
request=request,
entity_type="dokumentlink",
entity_id=str(dokument_link.id),
entity_name=entity_name,
target_type=link_type,
target_name=target_name,
)
except Exception as e:
# Don't fail the main operation if logging fails
print(f"Audit logging failed: {e}")
# Automatische Pächter-Verknüpfung NACH der Haupttransaktion
paechter_linked = False
if link_type == "verpachtung":
paechter_linked = create_paechter_link_for_verpachtung(
paperless_id, paperless_title, link_id
)
message = f"Dokument erfolgreich mit {link_type} verknüpft"
if paechter_linked:
message += " (automatisch auch mit Pächter verknüpft)"
return Response(
{"success": True, "message": message, "dokument_id": dokument_link.id}
)
except Exception as e:
return Response(
{"error": f"Fehler beim Erstellen der Verknüpfung: {str(e)}"}, status=500
)
# Legacy dokument_verknuepfung view removed - use dokument_management instead
@api_view(["GET"])
def link_document_list(request):
"""Listet alle bestehenden Dokument-Verknüpfungen auf, gruppiert nach Paperless-Document-ID"""
try:
dokument_links = DokumentLink.objects.all().order_by("-id")
# Group links by paperless_document_id to show multiple links per document
links_by_document = {}
for link in dokument_links:
paperless_id = link.paperless_document_id
if paperless_id not in links_by_document:
links_by_document[paperless_id] = {
"paperless_id": paperless_id,
"title": link.titel,
"paperless_url": f"/api/paperless/documents/{paperless_id}/",
"links": [],
}
# Create link info
link_info = {
"id": str(link.id), # Ensure UUID is stringified
"kontext": link.kontext,
"link_type": None,
"linked_object": None,
}
# Determine link type and get linked object details
if link.destinataer_id:
link_info["link_type"] = "destinataer"
try:
dest = Destinataer.objects.get(id=link.destinataer_id)
link_info["linked_object"] = {
"id": str(dest.id),
"type": "Destinatär",
"name": (
f"{dest.vorname} {dest.nachname}".strip()
if dest.vorname
else dest.institution
),
"details": (
f"Institution: {dest.institution}"
if dest.institution
else f"Person: {dest.vorname} {dest.nachname}".strip()
),
}
except Destinataer.DoesNotExist:
link_info["linked_object"] = {
"type": "Destinatär",
"name": "Gelöscht",
"details": "Datensatz nicht mehr verfügbar",
}
elif link.land_id:
link_info["link_type"] = "land"
try:
land = Land.objects.get(id=link.land_id)
link_info["linked_object"] = {
"id": str(land.id),
"type": "Land",
"name": f"{land.gemarkung} - {land.gemeinde}",
"details": f"Flur: {land.flur}, Größe: {land.groesse_qm or 0}",
}
except Land.DoesNotExist:
link_info["linked_object"] = {
"type": "Land",
"name": "Gelöscht",
"details": "Datensatz nicht mehr verfügbar",
}
elif link.paechter_id:
link_info["link_type"] = "paechter"
try:
p = Paechter.objects.get(id=link.paechter_id)
link_info["linked_object"] = {
"id": str(p.id),
"type": "Pächter",
"name": f"{(p.vorname + ' ') if p.vorname else ''}{p.nachname}",
"details": f"{p.ort or ''}",
}
except Paechter.DoesNotExist:
link_info["linked_object"] = {
"type": "Pächter",
"name": "Gelöscht",
"details": "Datensatz nicht mehr verfügbar",
}
elif link.land_verpachtung_id:
link_info["link_type"] = "verpachtung"
try:
from stiftung.models import LandVerpachtung
verp = LandVerpachtung.objects.select_related(
"paechter", "land"
).get(id=link.land_verpachtung_id)
link_info["linked_object"] = {
"id": str(verp.id),
"type": "Verpachtung",
"name": f"Verpachtung {(verp.paechter.vorname + ' ') if verp.paechter and verp.paechter.vorname else ''}{verp.paechter.nachname if verp.paechter else 'Unbekannt'}",
"details": f"Land: {verp.land.gemarkung} - {verp.land.gemeinde}, Pächter: {(verp.paechter.vorname + ' ') if verp.paechter and verp.paechter.vorname else ''}{verp.paechter.nachname if verp.paechter else 'Unbekannt'}, Vertrag: {verp.vertragsnummer}",
}
except LandVerpachtung.DoesNotExist:
link_info["linked_object"] = {
"type": "Verpachtung",
"name": "Gelöscht",
"details": "Datensatz nicht mehr verfügbar",
}
elif link.rentmeister_id:
link_info["link_type"] = "rentmeister"
try:
from stiftung.models import Rentmeister
rentmeister = Rentmeister.objects.get(id=link.rentmeister_id)
link_info["linked_object"] = {
"id": str(rentmeister.id),
"type": "Rentmeister",
"name": f"{(rentmeister.anrede + ' ') if rentmeister.anrede else ''}{(rentmeister.vorname + ' ') if rentmeister.vorname else ''}{rentmeister.nachname}"
+ (f" ({rentmeister.titel})" if rentmeister.titel else ""),
"details": f"Seit: {rentmeister.seit_datum.strftime('%d.%m.%Y') if rentmeister.seit_datum else 'Unbekannt'}"
+ (
f", Tel: {rentmeister.telefon}"
if rentmeister.telefon
else ""
)
+ (f", {rentmeister.email}" if rentmeister.email else ""),
"url": f"/geschaeftsfuehrung/rentmeister/{rentmeister.id}/",
}
except Rentmeister.DoesNotExist:
link_info["linked_object"] = {
"type": "Rentmeister",
"name": "Gelöscht",
"details": "Datensatz nicht mehr verfügbar",
}
elif link.abrechnung_id:
link_info["link_type"] = "abrechnung"
try:
abrechnung = LandAbrechnung.objects.select_related("land").get(
id=link.abrechnung_id
)
link_info["linked_object"] = {
"id": str(abrechnung.id),
"type": "Abrechnung",
"name": f"Abrechnung {abrechnung.abrechnungsjahr} - {abrechnung.land.gemarkung}",
"details": f"Land: {abrechnung.land.gemarkung} - {abrechnung.land.gemeinde}, Flur {abrechnung.land.flur}, Jahr {abrechnung.abrechnungsjahr}",
"url": f"/laendereien/abrechnungen/{abrechnung.id}/",
}
except LandAbrechnung.DoesNotExist:
link_info["linked_object"] = {
"type": "Abrechnung",
"name": "Gelöscht",
"details": "Datensatz nicht mehr verfügbar",
}
links_by_document[paperless_id]["links"].append(link_info)
# Convert to list format for frontend
results = list(links_by_document.values())
return Response(
{
"total_documents": len(results),
"total_links": sum(len(doc["links"]) for doc in results),
"links": results,
}
)
except Exception as e:
return Response(
{"error": f"Fehler beim Abrufen der Verknüpfungen: {str(e)}"}, status=500
)
@csrf_exempt
@api_view(["POST"])
def link_document_update(request):
"""Aktualisiert die Verknüpfung eines Dokuments (Re-Linking auf anderen Datensatz/Kontext)."""
from django.db import transaction
try:
# Robust JSON parsing to tolerate Latin-1 encoded payloads (e.g., umlauts)
try:
payload = request.data
except Exception:
raw = request.body
try:
payload = json.loads(raw.decode("utf-8"))
except UnicodeDecodeError:
payload = json.loads(raw.decode("latin-1"))
link_id = payload.get("link_id")
link_type = payload.get(
"link_type"
) # 'destinataer' | 'land' | 'verpachtung' | 'paechter' | 'rentmeister'
link_target_id = payload.get("link_id_target")
if not all([link_id, link_type, link_target_id]):
return Response(
{"error": "link_id, link_type und link_id_target sind erforderlich"},
status=400,
)
with transaction.atomic():
link = DokumentLink.objects.get(id=link_id)
old_verpachtung_id = (
link.verpachtung_id
) # Merke alte Verpachtung für Cleanup
paperless_id_for_cleanup = link.paperless_document_id
titel_for_new_link = link.titel
# Reset all associations first
link.destinataer_id = None
link.land_id = None
link.verpachtung_id = None
link.paechter_id = None
link.foerderung_id = None
link.rentmeister_id = None
link.kontext = link_type
if link_type == "destinataer":
link.destinataer_id = link_target_id
elif link_type == "land":
link.land_id = link_target_id
elif link_type == "verpachtung":
link.verpachtung_id = link_target_id
elif link_type == "paechter":
link.paechter_id = link_target_id
elif link_type == "foerderung":
link.foerderung_id = link_target_id
elif link_type == "rentmeister":
link.rentmeister_id = link_target_id
else:
return Response({"error": "Ungültiger link_type"}, status=400)
link.save()
# Automatische Pächter-Verknüpfung und Cleanup NACH der Haupttransaktion
paechter_linked = False
if link_type == "verpachtung":
paechter_linked = create_paechter_link_for_verpachtung(
paperless_id_for_cleanup, titel_for_new_link, link_target_id
)
# Cleanup: Entferne verwaiste Pächter-Verknüpfungen wenn von Verpachtung weg geändert
if old_verpachtung_id and link_type != "verpachtung":
try:
old_verpachtung = LandVerpachtung.objects.select_related(
"paechter"
).get(id=old_verpachtung_id)
if old_verpachtung.paechter:
# Prüfe ob noch andere Verpachtungs-Links für diesen Pächter existieren
other_verpachtung_links = DokumentLink.objects.filter(
paperless_document_id=paperless_id_for_cleanup,
verpachtung__paechter_id=old_verpachtung.paechter.id,
).exists()
if not other_verpachtung_links:
# Entferne automatisch erstellte Pächter-Verknüpfung
DokumentLink.objects.filter(
paperless_document_id=paperless_id_for_cleanup,
paechter_id=old_verpachtung.paechter.id,
kontext="paechter",
).delete()
except (LandVerpachtung.DoesNotExist, Exception):
pass
message = "Verknüpfung aktualisiert"
if paechter_linked:
message += " (automatisch auch mit Pächter verknüpft)"
return Response({"success": True, "message": message})
except DokumentLink.DoesNotExist:
return Response({"error": "Verknüpfung nicht gefunden"}, status=404)
except Exception as e:
return Response({"error": f"Unerwarteter Fehler: {e}"}, status=500)
@csrf_exempt
@api_view(["DELETE"])
def link_document_delete(request, link_id):
"""Löscht eine bestehende Verknüpfung."""
from django.db import transaction
try:
with transaction.atomic():
link = DokumentLink.objects.get(id=link_id)
verpachtung_id_for_cleanup = link.verpachtung_id
paperless_id_for_cleanup = link.paperless_document_id
# Log the unlinking action before deletion
from stiftung.audit import log_unlink
try:
# Determine what entity this was linked to
target_type = "unknown"
target_name = "Unknown"
if link.destinataer_id:
target_type = "destinataer"
try:
entity = Destinataer.objects.get(id=link.destinataer_id)
target_name = entity.get_full_name()
except Destinataer.DoesNotExist:
target_name = f"Destinatär ID {link.destinataer_id}"
elif link.land_id:
target_type = "land"
try:
entity = Land.objects.get(id=link.land_id)
target_name = str(entity)
except Land.DoesNotExist:
target_name = f"Land ID {link.land_id}"
elif link.paechter_id:
target_type = "paechter"
try:
entity = Paechter.objects.get(id=link.paechter_id)
target_name = f"{entity.vorname} {entity.nachname}".strip()
except Paechter.DoesNotExist:
target_name = f"Pächter ID {link.paechter_id}"
elif link.verpachtung_id:
target_type = "verpachtung"
try:
entity = LandVerpachtung.objects.get(id=link.verpachtung_id)
target_name = str(entity)
except LandVerpachtung.DoesNotExist:
target_name = f"Verpachtung ID {link.verpachtung_id}"
elif link.rentmeister_id:
target_type = "rentmeister"
try:
from stiftung.models import Rentmeister
entity = Rentmeister.objects.get(id=link.rentmeister_id)
target_name = entity.get_full_name()
except Rentmeister.DoesNotExist:
target_name = f"Rentmeister ID {link.rentmeister_id}"
log_unlink(
request=request,
entity_type="dokumentlink",
entity_id=str(link.id),
entity_name=link.titel,
target_type=target_type,
target_name=target_name,
)
except Exception as e:
# Don't fail the main operation if logging fails
print(f"Audit logging failed: {e}")
link.delete()
# Cleanup NACH der Haupttransaktion: Wenn eine Verpachtungs-Verknüpfung gelöscht wurde, prüfe Pächter-Links
if verpachtung_id_for_cleanup:
try:
verpachtung = LandVerpachtung.objects.select_related("paechter").get(
id=verpachtung_id_for_cleanup
)
if verpachtung.paechter:
# Prüfe ob noch andere Verpachtungs-Links für diesen Pächter existieren
other_verpachtung_links = DokumentLink.objects.filter(
paperless_document_id=paperless_id_for_cleanup,
verpachtung__paechter_id=verpachtung.paechter.id,
).exists()
if not other_verpachtung_links:
# Entferne automatisch erstellte Pächter-Verknüpfung
DokumentLink.objects.filter(
paperless_document_id=paperless_id_for_cleanup,
paechter_id=verpachtung.paechter.id,
kontext="paechter",
).delete()
except (LandVerpachtung.DoesNotExist, Exception):
pass
return Response({"success": True})
except DokumentLink.DoesNotExist:
return Response({"error": "Verknüpfung nicht gefunden"}, status=404)
except Exception as e:
return Response({"error": f"Unerwarteter Fehler: {e}"}, status=500)
@api_view(["GET"])
def gramps_search_api(request):
"""Probe-Endpoint: Suche Personen in Gramps Web nach q (Nachname, Vorname)."""
q = request.GET.get("q", "")
if not q:
return Response({"error": "Parameter q erforderlich"}, status=400)
client = get_gramps_client()
result = client.search_people(q)
return Response(result)
# Geschäftsführung Views
@login_required
def geschaeftsfuehrung(request):
"""Hauptansicht für die Geschäftsführung mit Übersicht"""
from datetime import datetime, timedelta
from django.db.models import Count, Sum
from stiftung.models import Rentmeister, StiftungsKonto, Verwaltungskosten
# Rentmeister-Übersicht
rentmeister = Rentmeister.objects.filter(aktiv=True).order_by("nachname", "vorname")
# Konten-Übersicht
konten = StiftungsKonto.objects.filter(aktiv=True).order_by(
"bank_name", "kontoname"
)
gesamtsaldo = konten.aggregate(total=Sum("saldo"))["total"] or 0
# Aktuelle Kosten (letzten 30 Tage)
heute = datetime.now().date()
vor_30_tagen = heute - timedelta(days=30)
aktuelle_kosten = Verwaltungskosten.objects.filter(
datum__gte=vor_30_tagen
).order_by("-datum")[:10]
# Statistiken
kosten_summe_monat = (
Verwaltungskosten.objects.filter(datum__gte=vor_30_tagen).aggregate(
total=Sum("betrag")
)["total"]
or 0
)
kosten_statistik = (
Verwaltungskosten.objects.filter(datum__gte=vor_30_tagen)
.values("kategorie")
.annotate(summe=Sum("betrag"), anzahl=Count("id"))
.order_by("-summe")
)
context = {
"rentmeister": rentmeister,
"konten": konten,
"gesamtsaldo": gesamtsaldo,
"aktuelle_kosten": aktuelle_kosten,
"kosten_summe_monat": kosten_summe_monat,
"kosten_statistik": kosten_statistik,
}
return render(request, "stiftung/geschaeftsfuehrung.html", context)
@login_required
def konto_list(request):
"""Liste aller Stiftungskonten"""
from django.db.models import Sum
from stiftung.models import StiftungsKonto
konten = StiftungsKonto.objects.all().order_by("bank_name", "kontoname")
gesamtsaldo = konten.aggregate(total=Sum("saldo"))["total"] or 0
context = {
"konten": konten,
"gesamtsaldo": gesamtsaldo,
}
return render(request, "stiftung/konto_list.html", context)
@login_required
def verwaltungskosten_list(request):
"""Liste aller Verwaltungskosten"""
from django.core.paginator import Paginator
from stiftung.models import Verwaltungskosten
kosten = Verwaltungskosten.objects.all().order_by("-datum", "-erstellt_am")
# Filter nach Kategorie
kategorie_filter = request.GET.get("kategorie")
if kategorie_filter:
kosten = kosten.filter(kategorie=kategorie_filter)
# Filter nach Status
status_filter = request.GET.get("status")
if status_filter:
kosten = kosten.filter(status=status_filter)
# Pagination
paginator = Paginator(kosten, 25)
page_number = request.GET.get("page")
page_obj = paginator.get_page(page_number)
# Für Filter-Dropdowns
kategorien = Verwaltungskosten.KATEGORIE_CHOICES
status_choices = Verwaltungskosten.STATUS_CHOICES
context = {
"page_obj": page_obj,
"kategorien": kategorien,
"status_choices": status_choices,
"kategorie_filter": kategorie_filter,
"status_filter": status_filter,
}
return render(request, "stiftung/verwaltungskosten_list.html", context)
@login_required
def rentmeister_list(request):
"""Liste aller Rentmeister"""
from stiftung.models import Rentmeister
rentmeister = Rentmeister.objects.all().order_by("nachname", "vorname")
# Aktive/Inaktive aufteilen
aktive_rentmeister = rentmeister.filter(aktiv=True)
ehemalige_rentmeister = rentmeister.filter(aktiv=False)
context = {
"aktive_rentmeister": aktive_rentmeister,
"ehemalige_rentmeister": ehemalige_rentmeister,
"total_count": rentmeister.count(),
}
return render(request, "stiftung/rentmeister_list.html", context)
@login_required
def rentmeister_detail(request, pk):
"""Detailansicht eines Rentmeisters mit seinen Ausgaben"""
from datetime import datetime, timedelta
from django.db.models import Count, Q, Sum
from stiftung.models import Rentmeister, Verwaltungskosten
rentmeister = get_object_or_404(Rentmeister, pk=pk)
# Ausgaben des Rentmeisters
ausgaben = Verwaltungskosten.objects.filter(rentmeister=rentmeister).order_by(
"-datum"
)
# Statistiken
heute = datetime.now().date()
aktueller_monat = heute.replace(day=1)
aktuelles_jahr = heute.replace(month=1, day=1)
stats = {
"gesamt_ausgaben": ausgaben.aggregate(total=Sum("betrag"))["total"] or 0,
"monat_ausgaben": ausgaben.filter(datum__gte=aktueller_monat).aggregate(
total=Sum("betrag")
)["total"]
or 0,
"jahr_ausgaben": ausgaben.filter(datum__gte=aktuelles_jahr).aggregate(
total=Sum("betrag")
)["total"]
or 0,
"anzahl_ausgaben": ausgaben.count(),
"offene_ausgaben": ausgaben.exclude(status="bezahlt").count(),
}
# Kategorie-Aufschlüsselung
kategorie_stats = (
ausgaben.values("kategorie")
.annotate(summe=Sum("betrag"), anzahl=Count("id"))
.order_by("-summe")
)
# Aktuelle Ausgaben (letzten 30 Tage)
vor_30_tagen = heute - timedelta(days=30)
aktuelle_ausgaben = ausgaben.filter(datum__gte=vor_30_tagen)[:10]
# Verknüpfte Dokumente laden
from stiftung.models import DokumentLink
verknuepfte_dokumente = DokumentLink.objects.filter(
rentmeister_id=rentmeister.id
).order_by("-id")[
:10
] # Neueste 10 Dokumente
context = {
"rentmeister": rentmeister,
"ausgaben": ausgaben[:20], # Nur erste 20 für Übersicht
"stats": stats,
"kategorie_stats": kategorie_stats,
"aktuelle_ausgaben": aktuelle_ausgaben,
"verknuepfte_dokumente": verknuepfte_dokumente,
}
return render(request, "stiftung/rentmeister_detail.html", context)
@login_required
def rentmeister_ausgaben(request, pk):
"""Vollständige Ausgabenliste eines Rentmeisters mit PDF Export"""
from django.core.paginator import Paginator
from django.db import models
from django.db.models import Count, Q, Sum
from stiftung.models import Rentmeister, Verwaltungskosten
rentmeister = get_object_or_404(Rentmeister, pk=pk)
# Handle PDF export request
if request.method == "POST" and "export_pdf" in request.POST:
selected_ids = request.POST.getlist("selected_expenses")
if selected_ids:
# Update status to 'in_bearbeitung' and log each change
from stiftung.audit import log_action
expenses_to_update = Verwaltungskosten.objects.filter(
id__in=selected_ids, rentmeister=rentmeister
)
updated_count = 0
for expense in expenses_to_update:
old_status = expense.status
expense.status = "in_bearbeitung"
expense.save()
updated_count += 1
# Log the status change
log_action(
request=request,
action="update",
entity_type="verwaltungskosten",
entity_id=str(expense.pk),
entity_name=expense.bezeichnung,
description=f'Ausgaben-Status für PDF-Export geändert von "{old_status}" zu "in_bearbeitung"',
changes={"status": {"old": old_status, "new": "in_bearbeitung"}},
)
messages.success(
request,
f"{updated_count} Ausgaben wurden zur Bearbeitung markiert und sind bereit für PDF Export.",
)
return redirect(
"stiftung:rentmeister_ausgaben_pdf",
pk=pk,
expense_ids=",".join(selected_ids),
)
# Get expenses grouped by status
ausgaben_by_status = {}
for status_code, status_name in Verwaltungskosten.STATUS_CHOICES:
ausgaben_by_status[status_code] = {
"name": status_name,
"ausgaben": Verwaltungskosten.objects.filter(
rentmeister=rentmeister, status=status_code
).order_by("-datum", "-erstellt_am"),
"total": Verwaltungskosten.objects.filter(
rentmeister=rentmeister, status=status_code
).aggregate(total=Sum("betrag"))["total"]
or 0,
}
# Get statistics
stats = Verwaltungskosten.objects.filter(rentmeister=rentmeister).aggregate(
total_count=Count("id"),
total_amount=Sum("betrag"),
geplant_count=Count("id", filter=Q(status="geplant")),
geplant_amount=Sum("betrag", filter=Q(status="geplant")),
in_bearbeitung_count=Count("id", filter=Q(status="in_bearbeitung")),
in_bearbeitung_amount=Sum("betrag", filter=Q(status="in_bearbeitung")),
bezahlt_count=Count("id", filter=Q(status="bezahlt")),
bezahlt_amount=Sum("betrag", filter=Q(status="bezahlt")),
)
context = {
"rentmeister": rentmeister,
"ausgaben_by_status": ausgaben_by_status,
"stats": stats,
"kategorien": Verwaltungskosten.KATEGORIE_CHOICES,
"status_choices": Verwaltungskosten.STATUS_CHOICES,
}
return render(request, "stiftung/rentmeister_ausgaben.html", context)
@login_required
def rentmeister_create(request):
"""Erstelle einen neuen Rentmeister"""
from stiftung.forms import RentmeisterForm
if request.method == "POST":
form = RentmeisterForm(request.POST)
if form.is_valid():
rentmeister = form.save()
messages.success(
request,
f"Rentmeister {rentmeister.get_full_name()} wurde erfolgreich angelegt.",
)
return redirect("stiftung:rentmeister_detail", pk=rentmeister.pk)
else:
form = RentmeisterForm()
context = {
"form": form,
"title": "Neuen Rentmeister anlegen",
"submit_text": "Rentmeister anlegen",
}
return render(request, "stiftung/rentmeister_form.html", context)
@login_required
def rentmeister_edit(request, pk):
"""Bearbeite einen bestehenden Rentmeister"""
from stiftung.forms import RentmeisterForm
from stiftung.models import Rentmeister
rentmeister = get_object_or_404(Rentmeister, pk=pk)
if request.method == "POST":
form = RentmeisterForm(request.POST, instance=rentmeister)
if form.is_valid():
rentmeister = form.save()
messages.success(
request,
f"Rentmeister {rentmeister.get_full_name()} wurde erfolgreich aktualisiert.",
)
return redirect("stiftung:rentmeister_detail", pk=rentmeister.pk)
else:
form = RentmeisterForm(instance=rentmeister)
context = {
"form": form,
"rentmeister": rentmeister,
"title": f"{rentmeister.get_full_name()} bearbeiten",
"submit_text": "Änderungen speichern",
}
return render(request, "stiftung/rentmeister_form.html", context)
@login_required
def konto_create(request):
"""Erstelle ein neues Stiftungskonto"""
from stiftung.forms import StiftungsKontoForm
if request.method == "POST":
form = StiftungsKontoForm(request.POST)
if form.is_valid():
konto = form.save()
messages.success(
request, f"Konto {konto.kontoname} wurde erfolgreich angelegt."
)
return redirect("stiftung:konto_list")
else:
form = StiftungsKontoForm()
context = {
"form": form,
"title": "Neues Konto anlegen",
"submit_text": "Konto anlegen",
}
return render(request, "stiftung/konto_form.html", context)
@login_required
def konto_edit(request, pk):
"""Bearbeite ein bestehendes Stiftungskonto"""
from stiftung.forms import StiftungsKontoForm
from stiftung.models import StiftungsKonto
konto = get_object_or_404(StiftungsKonto, pk=pk)
if request.method == "POST":
form = StiftungsKontoForm(request.POST, instance=konto)
if form.is_valid():
konto = form.save()
messages.success(
request, f"Konto {konto.kontoname} wurde erfolgreich aktualisiert."
)
return redirect("stiftung:konto_list")
else:
form = StiftungsKontoForm(instance=konto)
context = {
"form": form,
"konto": konto,
"title": f"Konto {konto.kontoname} bearbeiten",
"submit_text": "Änderungen speichern",
}
return render(request, "stiftung/konto_form.html", context)
@login_required
def konto_detail(request, pk):
"""Zeige Details eines Stiftungskontos"""
from django.db import models
from django.db.models import Count, Max, Q, Sum
from stiftung.models import BankTransaction, StiftungsKonto
konto = get_object_or_404(StiftungsKonto, pk=pk)
# Get transaction statistics
transactions = BankTransaction.objects.filter(konto=konto)
transaction_stats = transactions.aggregate(
total_count=Count("id"),
total_eingang=Sum("betrag", filter=Q(betrag__gt=0)),
total_ausgang=Sum("betrag", filter=Q(betrag__lt=0)),
last_transaction_date=Max("datum"),
)
# Recent transactions
recent_transactions = transactions.order_by("-datum", "-importiert_am")[:10]
context = {
"konto": konto,
"transaction_stats": transaction_stats,
"recent_transactions": recent_transactions,
}
return render(request, "stiftung/konto_detail.html", context)
@login_required
def verwaltungskosten_create(request):
"""Erstelle neue Verwaltungskosten"""
from stiftung.forms import VerwaltungskostenForm
from stiftung.models import Rentmeister
# Check if we're coming from a specific Rentmeister
rentmeister_id = request.GET.get("rentmeister")
initial_data = {}
redirect_url = "stiftung:verwaltungskosten_list"
if rentmeister_id:
try:
rentmeister = Rentmeister.objects.get(pk=rentmeister_id)
initial_data["rentmeister"] = rentmeister
redirect_url = "stiftung:rentmeister_detail"
redirect_args = [rentmeister_id]
except Rentmeister.DoesNotExist:
pass
if request.method == "POST":
form = VerwaltungskostenForm(request.POST)
if form.is_valid():
kosten = form.save()
messages.success(
request,
f'Verwaltungskosten "{kosten.bezeichnung}" wurden erfolgreich angelegt.',
)
if rentmeister_id:
return redirect(redirect_url, pk=rentmeister_id)
return redirect("stiftung:verwaltungskosten_list")
else:
form = VerwaltungskostenForm(initial=initial_data)
context = {
"form": form,
"title": "Neue Verwaltungskosten anlegen",
"submit_text": "Kosten anlegen",
}
return render(request, "stiftung/verwaltungskosten_form.html", context)
@login_required
def verwaltungskosten_edit(request, pk):
"""Bearbeite bestehende Verwaltungskosten"""
from stiftung.forms import VerwaltungskostenForm
from stiftung.models import Verwaltungskosten
verwaltungskosten = get_object_or_404(Verwaltungskosten, pk=pk)
if request.method == "POST":
form = VerwaltungskostenForm(request.POST, instance=verwaltungskosten)
if form.is_valid():
verwaltungskosten = form.save()
messages.success(
request,
f'Verwaltungskosten "{verwaltungskosten.bezeichnung}" wurden erfolgreich aktualisiert.',
)
return redirect("stiftung:verwaltungskosten_list")
else:
form = VerwaltungskostenForm(instance=verwaltungskosten)
context = {
"form": form,
"verwaltungskosten": verwaltungskosten,
"title": f"Verwaltungskosten bearbeiten: {verwaltungskosten.bezeichnung}",
"submit_text": "Änderungen speichern",
}
return render(request, "stiftung/verwaltungskosten_form.html", context)
@login_required
def mark_expense_paid(request):
"""Markiere eine Ausgabe als bezahlt"""
if request.method == "POST":
expense_id = request.POST.get("expense_id")
if expense_id:
try:
from stiftung.models import Verwaltungskosten
expense = Verwaltungskosten.objects.get(pk=expense_id)
old_status = expense.status
expense.status = "bezahlt"
expense.save()
# Log the status change
from stiftung.audit import log_action
log_action(
request=request,
action="update",
entity_type="verwaltungskosten",
entity_id=str(expense.pk),
entity_name=expense.bezeichnung,
description=f'Ausgaben-Status geändert von "{old_status}" zu "bezahlt"',
changes={"status": {"old": old_status, "new": "bezahlt"}},
)
messages.success(
request,
f'Ausgabe "{expense.bezeichnung}" wurde als bezahlt markiert.',
)
return redirect(
"stiftung:rentmeister_ausgaben", pk=expense.rentmeister.pk
)
except Verwaltungskosten.DoesNotExist:
messages.error(request, "Ausgabe nicht gefunden.")
return redirect("stiftung:verwaltungskosten_list")
# =============================================================================
# ADMINISTRATION VIEWS
# =============================================================================
@login_required
def administration(request):
"""Administration Dashboard"""
from datetime import datetime, timedelta
from django.db.models import Count
from stiftung.models import AuditLog, BackupJob
# Recent audit activity
recent_audit = AuditLog.objects.all()[:10]
# Audit statistics
heute = datetime.now().date()
stats = {
"total_logs": AuditLog.objects.count(),
"logs_today": AuditLog.objects.filter(timestamp__date=heute).count(),
"logs_week": AuditLog.objects.filter(
timestamp__gte=heute - timedelta(days=7)
).count(),
"recent_backups": BackupJob.objects.all()[:5],
"last_backup": BackupJob.objects.filter(status="completed").first(),
}
# User activity summary
user_activity = (
AuditLog.objects.values("username")
.annotate(count=Count("id"))
.order_by("-count")[:10]
)
context = {
"recent_audit": recent_audit,
"stats": stats,
"user_activity": user_activity,
}
return render(request, "stiftung/administration.html", context)
@login_required
def unterstuetzungen_list(request):
"""Liste der Destinatärunterstützungen (Administration)."""
status = request.GET.get("status", "")
export_format = (
request.POST.get("format")
if request.method == "POST"
else request.GET.get("format", "")
)
selected_ids_param = (
request.POST.get("selected_entries", "")
if request.method == "POST"
else request.GET.get("selected_entries", "")
)
selected_ids = (
[id for id in selected_ids_param.split(",") if id] if selected_ids_param else []
)
qs = DestinataerUnterstuetzung.objects.select_related(
"destinataer", "konto", "ausgezahlt_von", "wiederkehrend_von"
).order_by("-faellig_am", "destinataer__nachname")
if status:
qs = qs.filter(status=status)
# Enhanced CSV export with field selection
if export_format == "csv":
return export_unterstuetzungen_csv(request, qs, selected_ids)
# Enhanced PDF export with corporate identity
elif export_format == "pdf":
return export_unterstuetzungen_pdf(request, qs, selected_ids)
context = {
"unterstuetzungen": qs,
"status_filter": status,
}
return render(request, "stiftung/unterstuetzungen_list.html", context)
def export_unterstuetzungen_csv(request, queryset, selected_ids=None):
"""Enhanced CSV export with field selection"""
import csv
from datetime import datetime
from django.http import HttpResponse
# If specific entries are selected, filter to only those
if selected_ids:
queryset = queryset.filter(id__in=selected_ids)
# Get selected fields from request (default to all if none specified)
selected_fields_param = (
request.POST.get("selected_fields", "")
if request.method == "POST"
else request.GET.get("selected_fields", "")
)
selected_fields = selected_fields_param.split(",") if selected_fields_param else []
if not selected_fields:
# Default field set
selected_fields = [
"destinataer_name",
"betrag",
"faellig_am",
"status",
"empfaenger_iban",
"empfaenger_name",
"beschreibung",
]
# Field definitions with headers and data extraction
field_definitions = {
# Core payment fields
"id": ("ID", lambda u: str(u.id)),
"betrag": ("Betrag (€)", lambda u: f"{u.betrag:.2f}"),
"faellig_am": (
"Fällig am",
lambda u: u.faellig_am.strftime("%d.%m.%Y") if u.faellig_am else "",
),
"status": ("Status", lambda u: u.get_status_display()),
"beschreibung": ("Beschreibung", lambda u: u.beschreibung or ""),
"ausgezahlt_am": (
"Ausgezahlt am",
lambda u: u.ausgezahlt_am.strftime("%d.%m.%Y") if u.ausgezahlt_am else "",
),
"erstellt_am": (
"Erstellt am",
lambda u: u.erstellt_am.strftime("%d.%m.%Y %H:%M") if u.erstellt_am else "",
),
"aktualisiert_am": (
"Aktualisiert am",
lambda u: (
u.aktualisiert_am.strftime("%d.%m.%Y %H:%M")
if u.aktualisiert_am
else ""
),
),
# Destinataer fields
"destinataer_name": (
"Destinatär Name",
lambda u: u.destinataer.get_full_name() if u.destinataer else "",
),
"destinataer_vorname": (
"Vorname",
lambda u: u.destinataer.vorname if u.destinataer else "",
),
"destinataer_nachname": (
"Nachname",
lambda u: u.destinataer.nachname if u.destinataer else "",
),
"familienzweig": (
"Familienzweig",
lambda u: u.destinataer.familienzweig if u.destinataer else "",
),
"geburtsdatum": (
"Geburtsdatum",
lambda u: (
u.destinataer.geburtsdatum.strftime("%d.%m.%Y")
if u.destinataer and u.destinataer.geburtsdatum
else ""
),
),
"email": ("E-Mail", lambda u: u.destinataer.email if u.destinataer else ""),
"telefon": (
"Telefon",
lambda u: u.destinataer.telefon if u.destinataer else "",
),
"destinataer_iban": (
"Destinatär IBAN",
lambda u: u.destinataer.iban if u.destinataer else "",
),
"strasse": ("Straße", lambda u: u.destinataer.strasse if u.destinataer else ""),
"plz": ("PLZ", lambda u: u.destinataer.plz if u.destinataer else ""),
"ort": ("Ort", lambda u: u.destinataer.ort if u.destinataer else ""),
"adresse": (
"Adresse",
lambda u: (
f"{u.destinataer.strasse}, {u.destinataer.plz} {u.destinataer.ort}".strip(
", "
)
if u.destinataer
else ""
),
),
"berufsgruppe": (
"Berufsgruppe",
lambda u: u.destinataer.berufsgruppe if u.destinataer else "",
),
"ausbildungsstand": (
"Ausbildungsstand",
lambda u: u.destinataer.ausbildungsstand if u.destinataer else "",
),
"institution": (
"Institution",
lambda u: u.destinataer.institution if u.destinataer else "",
),
"jaehrliches_einkommen": (
"Jährliches Einkommen (€)",
lambda u: (
f"{u.destinataer.jaehrliches_einkommen:.2f}"
if u.destinataer and u.destinataer.jaehrliches_einkommen
else ""
),
),
"haushaltsgroesse": (
"Haushaltsgröße",
lambda u: (
str(u.destinataer.haushaltsgroesse)
if u.destinataer and u.destinataer.haushaltsgroesse
else ""
),
),
"monatliche_bezuege": (
"Monatliche Bezüge (€)",
lambda u: (
f"{u.destinataer.monatliche_bezuege:.2f}"
if u.destinataer and u.destinataer.monatliche_bezuege
else ""
),
),
"vermoegen": (
"Vermögen (€)",
lambda u: (
f"{u.destinataer.vermoegen:.2f}"
if u.destinataer and u.destinataer.vermoegen
else ""
),
),
# Payment details
"empfaenger_iban": ("Empfänger IBAN", lambda u: u.empfaenger_iban or ""),
"empfaenger_name": ("Empfänger Name", lambda u: u.empfaenger_name or ""),
"verwendungszweck": ("Verwendungszweck", lambda u: u.verwendungszweck or ""),
# Account fields
"konto_name": ("Konto", lambda u: str(u.konto) if u.konto else ""),
"konto_bank": ("Bank", lambda u: u.konto.bank_name if u.konto else ""),
"konto_iban": ("Konto IBAN", lambda u: u.konto.iban if u.konto else ""),
# System fields
"ausgezahlt_von": (
"Ausgezahlt von",
lambda u: u.ausgezahlt_von.get_full_name() if u.ausgezahlt_von else "",
),
"ist_wiederkehrend": (
"Wiederkehrend",
lambda u: "Ja" if u.wiederkehrend_von else "Nein",
),
}
# Create CSV response
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
filename = f"unterstuetzungen_{timestamp}.csv"
response = HttpResponse(content_type="text/csv; charset=utf-8")
response["Content-Disposition"] = f'attachment; filename="{filename}"'
writer = csv.writer(response, delimiter=";", quoting=csv.QUOTE_ALL)
# Write headers
headers = [
field_definitions[field][0]
for field in selected_fields
if field in field_definitions
]
writer.writerow(headers)
# Write data rows
for u in queryset:
row = []
for field in selected_fields:
if field in field_definitions:
try:
value = field_definitions[field][1](u)
row.append(value)
except Exception:
row.append("") # Fallback for any errors
else:
row.append("") # Unknown field
writer.writerow(row)
return response
def export_unterstuetzungen_pdf(request, queryset, selected_ids=None):
"""Enhanced PDF export with corporate identity and field selection"""
# If specific entries are selected, filter to only those
if selected_ids:
queryset = queryset.filter(id__in=selected_ids)
# Get selected fields from request (default to key fields if none specified)
selected_fields_param = (
request.POST.get("selected_fields", "")
if request.method == "POST"
else request.GET.get("selected_fields", "")
)
selected_fields = selected_fields_param.split(",") if selected_fields_param else []
if not selected_fields:
# Default field set for PDF (fewer fields than CSV for better readability)
selected_fields = [
"destinataer_name",
"betrag",
"faellig_am",
"status",
"beschreibung",
"ausgezahlt_am",
]
# Field definitions with display names (reuse from CSV but select PDF-appropriate subset)
field_definitions = {
# Core payment fields
"destinataer_name": "Destinatär",
"betrag": "Betrag (€)",
"faellig_am": "Fällig am",
"status": "Status",
"beschreibung": "Beschreibung",
"ausgezahlt_am": "Ausgezahlt am",
"erstellt_am": "Erstellt am",
"empfaenger_iban": "Empfänger IBAN",
"empfaenger_name": "Empfänger",
"verwendungszweck": "Verwendungszweck",
"konto_name": "Konto",
"ist_wiederkehrend": "Wiederkehrend",
}
# Filter to only include fields that are both selected and defined
filtered_fields = {
k: v for k, v in field_definitions.items() if k in selected_fields
}
# Prepare data with field extraction logic
data_for_pdf = []
for item in queryset:
row_data = {}
for field_key in filtered_fields.keys():
try:
if field_key == "destinataer_name":
row_data[field_key] = (
item.destinataer.get_full_name() if item.destinataer else ""
)
elif field_key == "betrag":
row_data[field_key] = f"{item.betrag:.2f}" if item.betrag else ""
elif field_key == "faellig_am":
row_data[field_key] = (
item.faellig_am.strftime("%d.%m.%Y") if item.faellig_am else ""
)
elif field_key == "status":
row_data[field_key] = item.get_status_display()
elif field_key == "beschreibung":
row_data[field_key] = item.beschreibung or ""
elif field_key == "ausgezahlt_am":
row_data[field_key] = (
item.ausgezahlt_am.strftime("%d.%m.%Y")
if item.ausgezahlt_am
else ""
)
elif field_key == "erstellt_am":
row_data[field_key] = (
item.erstellt_am.strftime("%d.%m.%Y")
if item.erstellt_am
else ""
)
elif field_key == "empfaenger_iban":
row_data[field_key] = item.empfaenger_iban or ""
elif field_key == "empfaenger_name":
row_data[field_key] = item.empfaenger_name or ""
elif field_key == "verwendungszweck":
row_data[field_key] = item.verwendungszweck or ""
elif field_key == "konto_name":
row_data[field_key] = str(item.konto) if item.konto else ""
elif field_key == "ist_wiederkehrend":
row_data[field_key] = "Ja" if item.wiederkehrend_von else "Nein"
else:
# Generic field access
row_data[field_key] = getattr(item, field_key, "") or ""
except Exception:
row_data[field_key] = "" # Fallback for any errors
data_for_pdf.append(row_data)
# Use PDF generator
pdf_gen = get_pdf_generator()
return pdf_gen.export_data_list_pdf(
data=data_for_pdf,
fields_config=filtered_fields,
title="Unterstützungen Export",
filename_prefix="unterstuetzungen",
request_user=request.user,
)
def export_foerderungen_csv(request, queryset, selected_ids=None):
"""Enhanced CSV export for Förderungen with field selection"""
import csv
from datetime import datetime
from django.http import HttpResponse
# If specific entries are selected, filter to only those
if selected_ids:
queryset = queryset.filter(id__in=selected_ids)
# Get selected fields from request (default to all if none specified)
selected_fields_param = (
request.POST.get("selected_fields", "")
if request.method == "POST"
else request.GET.get("selected_fields", "")
)
selected_fields = selected_fields_param.split(",") if selected_fields_param else []
if not selected_fields:
# Default field set
selected_fields = [
"destinataer_name",
"jahr",
"betrag",
"kategorie",
"status",
"antragsdatum",
"beschreibung",
]
# Field definitions with headers and data extraction
field_definitions = {
# Core fields
"id": ("ID", lambda f: str(f.id)),
"destinataer_name": (
"Destinatär Name",
lambda f: f.destinataer.get_full_name() if f.destinataer else "",
),
"jahr": ("Jahr", lambda f: str(f.jahr)),
"betrag": ("Betrag (€)", lambda f: f"{f.betrag:.2f}"),
"kategorie": ("Kategorie", lambda f: f.get_kategorie_display()),
"status": ("Status", lambda f: f.get_status_display()),
"antragsdatum": (
"Antragsdatum",
lambda f: f.antragsdatum.strftime("%d.%m.%Y") if f.antragsdatum else "",
),
"bewilligungsdatum": (
"Bewilligungsdatum",
lambda f: (
f.bewilligungsdatum.strftime("%d.%m.%Y") if f.bewilligungsdatum else ""
),
),
"auszahlungsdatum": (
"Auszahlungsdatum",
lambda f: (
f.auszahlungsdatum.strftime("%d.%m.%Y") if f.auszahlungsdatum else ""
),
),
"beschreibung": ("Beschreibung", lambda f: f.beschreibung or ""),
"begruendung": ("Begründung", lambda f: f.begruendung or ""),
"verwendungsnachweis_datum": (
"Verwendungsnachweis Datum",
lambda f: (
f.verwendungsnachweis_datum.strftime("%d.%m.%Y")
if f.verwendungsnachweis_datum
else ""
),
),
"verwendungsnachweis_status": (
"Verwendungsnachweis Status",
lambda f: (
f.get_verwendungsnachweis_status_display()
if f.verwendungsnachweis_status
else ""
),
),
# Destinataer fields
"destinataer_vorname": (
"Vorname",
lambda f: f.destinataer.vorname if f.destinataer else "",
),
"destinataer_nachname": (
"Nachname",
lambda f: f.destinataer.nachname if f.destinataer else "",
),
"familienzweig": (
"Familienzweig",
lambda f: f.destinataer.familienzweig if f.destinataer else "",
),
"email": ("E-Mail", lambda f: f.destinataer.email if f.destinataer else ""),
"telefon": (
"Telefon",
lambda f: f.destinataer.telefon if f.destinataer else "",
),
"adresse": (
"Adresse",
lambda f: (
f"{f.destinataer.strasse}, {f.destinataer.plz} {f.destinataer.ort}".strip(
", "
)
if f.destinataer
else ""
),
),
"berufsgruppe": (
"Berufsgruppe",
lambda f: f.destinataer.berufsgruppe if f.destinataer else "",
),
"ausbildungsstand": (
"Ausbildungsstand",
lambda f: f.destinataer.ausbildungsstand if f.destinataer else "",
),
"institution": (
"Institution",
lambda f: f.destinataer.institution if f.destinataer else "",
),
# System fields
"erstellt_am": (
"Erstellt am",
lambda f: f.erstellt_am.strftime("%d.%m.%Y %H:%M") if f.erstellt_am else "",
),
"aktualisiert_am": (
"Aktualisiert am",
lambda f: (
f.aktualisiert_am.strftime("%d.%m.%Y %H:%M")
if f.aktualisiert_am
else ""
),
),
}
# Create CSV response
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
filename = f"foerderungen_{timestamp}.csv"
response = HttpResponse(content_type="text/csv; charset=utf-8")
response["Content-Disposition"] = f'attachment; filename="{filename}"'
writer = csv.writer(response, delimiter=";", quoting=csv.QUOTE_ALL)
# Write headers
headers = [
field_definitions[field][0]
for field in selected_fields
if field in field_definitions
]
writer.writerow(headers)
# Write data rows
for f in queryset:
row = []
for field in selected_fields:
if field in field_definitions:
try:
value = field_definitions[field][1](f)
row.append(value)
except Exception:
row.append("") # Fallback for any errors
else:
row.append("") # Unknown field
writer.writerow(row)
return response
def export_foerderungen_pdf(request, queryset, selected_ids=None):
"""Enhanced PDF export for Förderungen with corporate identity and field selection"""
# If specific entries are selected, filter to only those
if selected_ids:
queryset = queryset.filter(id__in=selected_ids)
# Get selected fields from request (default to key fields if none specified)
selected_fields_param = (
request.POST.get("selected_fields", "")
if request.method == "POST"
else request.GET.get("selected_fields", "")
)
selected_fields = selected_fields_param.split(",") if selected_fields_param else []
if not selected_fields:
# Default field set for PDF (fewer fields than CSV for better readability)
selected_fields = [
"destinataer_name",
"jahr",
"betrag",
"kategorie",
"status",
"antragsdatum",
]
# Field definitions with display names
field_definitions = {
"destinataer_name": "Destinatär",
"jahr": "Jahr",
"betrag": "Betrag (€)",
"kategorie": "Kategorie",
"status": "Status",
"antragsdatum": "Antragsdatum",
"bewilligungsdatum": "Bewilligungsdatum",
"auszahlungsdatum": "Auszahlungsdatum",
"beschreibung": "Beschreibung",
"begruendung": "Begründung",
"verwendungsnachweis_status": "Verwendungsnachweis",
}
# Filter to only include fields that are both selected and defined
filtered_fields = {
k: v for k, v in field_definitions.items() if k in selected_fields
}
# Prepare data with field extraction logic
data_for_pdf = []
for item in queryset:
row_data = {}
for field_key in filtered_fields.keys():
try:
if field_key == "destinataer_name":
row_data[field_key] = (
item.destinataer.get_full_name() if item.destinataer else ""
)
elif field_key == "jahr":
row_data[field_key] = str(item.jahr)
elif field_key == "betrag":
row_data[field_key] = f"{item.betrag:.2f}" if item.betrag else ""
elif field_key == "kategorie":
row_data[field_key] = item.get_kategorie_display()
elif field_key == "status":
row_data[field_key] = item.get_status_display()
elif field_key == "antragsdatum":
row_data[field_key] = (
item.antragsdatum.strftime("%d.%m.%Y")
if item.antragsdatum
else ""
)
elif field_key == "bewilligungsdatum":
row_data[field_key] = (
item.bewilligungsdatum.strftime("%d.%m.%Y")
if item.bewilligungsdatum
else ""
)
elif field_key == "auszahlungsdatum":
row_data[field_key] = (
item.auszahlungsdatum.strftime("%d.%m.%Y")
if item.auszahlungsdatum
else ""
)
elif field_key == "beschreibung":
row_data[field_key] = (item.beschreibung or "")[:100] + (
"..." if len(item.beschreibung or "") > 100 else ""
)
elif field_key == "begruendung":
row_data[field_key] = (item.begruendung or "")[:100] + (
"..." if len(item.begruendung or "") > 100 else ""
)
elif field_key == "verwendungsnachweis_status":
row_data[field_key] = (
item.get_verwendungsnachweis_status_display()
if item.verwendungsnachweis_status
else ""
)
else:
# Generic field access
row_data[field_key] = getattr(item, field_key, "") or ""
except Exception:
row_data[field_key] = "" # Fallback for any errors
data_for_pdf.append(row_data)
# Use PDF generator
pdf_gen = get_pdf_generator()
return pdf_gen.export_data_list_pdf(
data=data_for_pdf,
fields_config=filtered_fields,
title="Förderungen Export",
filename_prefix="foerderungen",
request_user=request.user,
)
@login_required
def unterstuetzung_edit(request, pk):
obj = get_object_or_404(DestinataerUnterstuetzung, pk=pk)
if request.method == "POST":
form = DestinataerUnterstuetzungForm(request.POST, instance=obj)
if form.is_valid():
form.save()
messages.success(request, "Unterstützung aktualisiert.")
return redirect("stiftung:unterstuetzungen_list")
else:
form = DestinataerUnterstuetzungForm(instance=obj)
return render(
request,
"stiftung/unterstuetzung_form.html",
{"form": form, "title": "Unterstützung bearbeiten"},
)
@login_required
def unterstuetzung_delete(request, pk):
obj = get_object_or_404(DestinataerUnterstuetzung, pk=pk)
# Check if this will also delete the recurring template
will_delete_template = False
if obj.wiederkehrend_von:
andere_zahlungen = (
DestinataerUnterstuetzung.objects.filter(
wiederkehrend_von=obj.wiederkehrend_von
)
.exclude(pk=pk)
.exists()
)
will_delete_template = not andere_zahlungen
if request.method == "POST":
# Check if this support payment is linked to a recurring payment template
wiederkehrend_template = obj.wiederkehrend_von
# Delete the support payment
obj.delete()
# If this was generated from a recurring template and there are no other
# payments from this template, delete the template too
if wiederkehrend_template:
# Check if there are other payments from this recurring template
andere_zahlungen = DestinataerUnterstuetzung.objects.filter(
wiederkehrend_von=wiederkehrend_template
).exists()
# If no other payments exist from this template, delete the template too
if not andere_zahlungen:
wiederkehrend_template.delete()
messages.success(
request,
"Unterstützung und wiederkehrende Zahlungsvorlage gelöscht.",
)
else:
messages.success(request, "Unterstützung gelöscht.")
else:
messages.success(request, "Unterstützung gelöscht.")
return redirect("stiftung:unterstuetzungen_list")
context = {
"obj": obj,
"will_delete_template": will_delete_template,
}
return render(request, "stiftung/unterstuetzung_confirm_delete.html", context)
@login_required
def destinataer_notiz_create(request, pk):
destinataer = get_object_or_404(Destinataer, pk=pk)
if request.method == "POST":
form = DestinataerNotizForm(request.POST, request.FILES)
if form.is_valid():
note = form.save(commit=False)
note.destinataer = destinataer
note.erstellt_von = request.user
note.save()
messages.success(request, "Notiz wurde gespeichert.")
return redirect("stiftung:destinataer_detail", pk=destinataer.pk)
else:
# Debug: show what validation failed
for field, errors in form.errors.items():
messages.error(request, f'Fehler in {field}: {", ".join(errors)}')
else:
form = DestinataerNotizForm()
return render(
request,
"stiftung/destinataer_notiz_form.html",
{"form": form, "destinataer": destinataer, "title": "Notiz hinzufügen"},
)
@login_required
def destinataer_export(request, pk):
"""Export complete Destinatär data as ZIP with documents"""
import json
import os
import tempfile
import zipfile
from django.http import HttpResponse
destinataer = get_object_or_404(Destinataer, pk=pk)
# Create a temporary file for the ZIP
temp_file = tempfile.NamedTemporaryFile(delete=False, suffix=".zip")
try:
with zipfile.ZipFile(temp_file.name, "w", zipfile.ZIP_DEFLATED) as zipf:
# 1. Entity data as JSON
entity_data = {
"id": str(destinataer.id),
"vorname": destinataer.vorname,
"nachname": destinataer.nachname,
"geburtsdatum": (
destinataer.geburtsdatum.isoformat()
if destinataer.geburtsdatum
else None
),
"email": destinataer.email,
"telefon": destinataer.telefon,
"iban": destinataer.iban,
"strasse": destinataer.strasse,
"plz": destinataer.plz,
"ort": destinataer.ort,
"familienzweig": destinataer.get_familienzweig_display(),
"berufsgruppe": destinataer.get_berufsgruppe_display(),
"ausbildungsstand": destinataer.ausbildungsstand,
"institution": destinataer.institution,
"projekt_beschreibung": destinataer.projekt_beschreibung,
"jaehrliches_einkommen": (
str(destinataer.jaehrliches_einkommen)
if destinataer.jaehrliches_einkommen
else None
),
"finanzielle_notlage": destinataer.finanzielle_notlage,
"ist_abkoemmling": destinataer.ist_abkoemmling,
"haushaltsgroesse": destinataer.haushaltsgroesse,
"monatliche_bezuege": (
str(destinataer.monatliche_bezuege)
if destinataer.monatliche_bezuege
else None
),
"vermoegen": (
str(destinataer.vermoegen) if destinataer.vermoegen else None
),
"unterstuetzung_bestaetigt": destinataer.unterstuetzung_bestaetigt,
"vierteljaehrlicher_betrag": (
str(destinataer.vierteljaehrlicher_betrag)
if destinataer.vierteljaehrlicher_betrag
else None
),
"standard_konto": (
str(destinataer.standard_konto)
if destinataer.standard_konto
else None
),
"studiennachweis_erforderlich": destinataer.studiennachweis_erforderlich,
"letzter_studiennachweis": (
destinataer.letzter_studiennachweis.isoformat()
if destinataer.letzter_studiennachweis
else None
),
"notizen": destinataer.notizen,
"aktiv": destinataer.aktiv,
"export_datum": timezone.now().isoformat(),
"export_user": request.user.username,
}
zipf.writestr(
"destinataer_data.json",
json.dumps(entity_data, indent=2, ensure_ascii=False),
)
# 2. Notes with attachments
notizen = DestinataerNotiz.objects.filter(destinataer=destinataer).order_by(
"-erstellt_am"
)
notes_data = []
for note in notizen:
note_data = {
"titel": note.titel,
"text": note.text,
"erstellt_am": note.erstellt_am.isoformat(),
"erstellt_von": (
note.erstellt_von.username if note.erstellt_von else None
),
"datei_name": note.datei.name if note.datei else None,
}
notes_data.append(note_data)
# Add attachment file if exists
if note.datei and os.path.exists(note.datei.path):
zipf.write(
note.datei.path,
f"notizen_anhaenge/{os.path.basename(note.datei.name)}",
)
if notes_data:
zipf.writestr(
"notizen.json", json.dumps(notes_data, indent=2, ensure_ascii=False)
)
# 3. Linked documents from Paperless
dokumente = DokumentLink.objects.filter(destinataer_id=destinataer.pk)
docs_data = []
for doc in dokumente:
doc_data = {
"paperless_id": doc.paperless_document_id,
"titel": doc.titel,
"kontext": doc.get_kontext_display(),
"beschreibung": doc.beschreibung,
}
docs_data.append(doc_data)
# Try to download document from Paperless
try:
if (
hasattr(settings, "PAPERLESS_API_URL")
and settings.PAPERLESS_API_URL
):
doc_url = f"{settings.PAPERLESS_API_URL}/api/documents/{doc.paperless_document_id}/download/"
headers = {}
if (
hasattr(settings, "PAPERLESS_API_TOKEN")
and settings.PAPERLESS_API_TOKEN
):
headers["Authorization"] = (
f"Token {settings.PAPERLESS_API_TOKEN}"
)
response = requests.get(doc_url, headers=headers, timeout=30)
if response.status_code == 200:
# Determine file extension from Content-Type or use .pdf as fallback
content_type = response.headers.get("content-type", "")
if "pdf" in content_type:
ext = ".pdf"
elif "jpeg" in content_type or "jpg" in content_type:
ext = ".jpg"
elif "png" in content_type:
ext = ".png"
else:
ext = ".pdf" # fallback
safe_filename = f"dokument_{doc.paperless_document_id}_{doc.titel.replace('/', '_')[:50]}{ext}"
zipf.writestr(
f"dokumente/{safe_filename}", response.content
)
doc_data["downloaded"] = True
else:
doc_data["download_error"] = f"HTTP {response.status_code}"
except Exception as e:
doc_data["download_error"] = str(e)
if docs_data:
zipf.writestr(
"dokumente.json",
json.dumps(docs_data, indent=2, ensure_ascii=False),
)
# Prepare response
with open(temp_file.name, "rb") as f:
response = HttpResponse(f.read(), content_type="application/zip")
filename = f"destinataer_{destinataer.nachname}_{destinataer.vorname}_{timezone.now().strftime('%Y%m%d_%H%M%S')}.zip"
response["Content-Disposition"] = f'attachment; filename="{filename}"'
return response
finally:
# Clean up temp file
try:
os.unlink(temp_file.name)
except:
pass
@login_required
def paechter_export(request, pk):
"""Export complete Pächter data as ZIP with documents"""
import json
import os
import tempfile
import zipfile
from django.http import HttpResponse
paechter = get_object_or_404(Paechter, pk=pk)
# Create a temporary file for the ZIP
temp_file = tempfile.NamedTemporaryFile(delete=False, suffix=".zip")
try:
with zipfile.ZipFile(temp_file.name, "w", zipfile.ZIP_DEFLATED) as zipf:
# 1. Entity data as JSON
entity_data = {
"id": str(paechter.id),
"vorname": paechter.vorname,
"nachname": paechter.nachname,
"geburtsdatum": (
paechter.geburtsdatum.isoformat() if paechter.geburtsdatum else None
),
"email": paechter.email,
"telefon": paechter.telefon,
"iban": paechter.iban,
"strasse": paechter.strasse,
"plz": paechter.plz,
"ort": paechter.ort,
"personentyp": paechter.get_personentyp_display(),
"pachtnummer": paechter.pachtnummer,
"pachtbeginn_erste": (
paechter.pachtbeginn_erste.isoformat()
if paechter.pachtbeginn_erste
else None
),
"pachtende_letzte": (
paechter.pachtende_letzte.isoformat()
if paechter.pachtende_letzte
else None
),
"pachtzins_aktuell": (
str(paechter.pachtzins_aktuell)
if paechter.pachtzins_aktuell
else None
),
"landwirtschaftliche_ausbildung": paechter.landwirtschaftliche_ausbildung,
"berufserfahrung_jahre": paechter.berufserfahrung_jahre,
"spezialisierung": paechter.spezialisierung,
"notizen": paechter.notizen,
"aktiv": paechter.aktiv,
"gesamt_pachtflaeche": float(paechter.get_gesamt_pachtflaeche()),
"gesamt_pachtzins": float(paechter.get_gesamt_pachtzins()),
"export_datum": timezone.now().isoformat(),
"export_user": request.user.username,
}
zipf.writestr(
"paechter_data.json",
json.dumps(entity_data, indent=2, ensure_ascii=False),
)
# 2. Linked documents from Paperless
dokumente = DokumentLink.objects.filter(paechter_id=paechter.pk)
docs_data = []
for doc in dokumente:
doc_data = {
"paperless_id": doc.paperless_document_id,
"titel": doc.titel,
"kontext": doc.get_kontext_display(),
"beschreibung": doc.beschreibung,
}
docs_data.append(doc_data)
# Try to download document from Paperless
try:
if (
hasattr(settings, "PAPERLESS_API_URL")
and settings.PAPERLESS_API_URL
):
doc_url = f"{settings.PAPERLESS_API_URL}/api/documents/{doc.paperless_document_id}/download/"
headers = {}
if (
hasattr(settings, "PAPERLESS_API_TOKEN")
and settings.PAPERLESS_API_TOKEN
):
headers["Authorization"] = (
f"Token {settings.PAPERLESS_API_TOKEN}"
)
response = requests.get(doc_url, headers=headers, timeout=30)
if response.status_code == 200:
content_type = response.headers.get("content-type", "")
if "pdf" in content_type:
ext = ".pdf"
elif "jpeg" in content_type or "jpg" in content_type:
ext = ".jpg"
elif "png" in content_type:
ext = ".png"
else:
ext = ".pdf"
safe_filename = f"dokument_{doc.paperless_document_id}_{doc.titel.replace('/', '_')[:50]}{ext}"
zipf.writestr(
f"dokumente/{safe_filename}", response.content
)
doc_data["downloaded"] = True
else:
doc_data["download_error"] = f"HTTP {response.status_code}"
except Exception as e:
doc_data["download_error"] = str(e)
if docs_data:
zipf.writestr(
"dokumente.json",
json.dumps(docs_data, indent=2, ensure_ascii=False),
)
# Prepare response
with open(temp_file.name, "rb") as f:
response = HttpResponse(f.read(), content_type="application/zip")
filename = f"paechter_{paechter.nachname}_{paechter.vorname}_{timezone.now().strftime('%Y%m%d_%H%M%S')}.zip"
response["Content-Disposition"] = f'attachment; filename="{filename}"'
return response
finally:
try:
os.unlink(temp_file.name)
except:
pass
@login_required
def land_export(request, pk):
"""Export complete Land data as ZIP with documents"""
import json
import os
import tempfile
import zipfile
from django.http import HttpResponse
land = get_object_or_404(Land, pk=pk)
# Create a temporary file for the ZIP
temp_file = tempfile.NamedTemporaryFile(delete=False, suffix=".zip")
try:
with zipfile.ZipFile(temp_file.name, "w", zipfile.ZIP_DEFLATED) as zipf:
# 1. Entity data as JSON
entity_data = {
"id": str(land.id),
"lfd_nr": land.lfd_nr,
"ew_nummer": land.ew_nummer,
"amtsgericht": land.amtsgericht,
"gemeinde": land.gemeinde,
"gemarkung": land.gemarkung,
"flur": land.flur,
"flurstueck": land.flurstueck,
"groesse_qm": str(land.groesse_qm),
"gruenland_qm": str(land.gruenland_qm),
"acker_qm": str(land.acker_qm),
"wald_qm": str(land.wald_qm),
"sonstiges_qm": str(land.sonstiges_qm),
"verpachtete_gesamtflaeche": str(land.verpachtete_gesamtflaeche),
"flaeche_alte_liste": (
str(land.flaeche_alte_liste) if land.flaeche_alte_liste else None
),
"verp_flaeche_aktuell": str(land.verp_flaeche_aktuell),
"anteil_grundsteuer": (
str(land.anteil_grundsteuer) if land.anteil_grundsteuer else None
),
"anteil_lwk": str(land.anteil_lwk) if land.anteil_lwk else None,
"aktiv": land.aktiv,
"notizen": land.notizen,
"erstellt_am": land.erstellt_am.isoformat(),
"aktualisiert_am": land.aktualisiert_am.isoformat(),
"gesamtflaeche_berechnet": float(land.get_gesamtflaeche()),
"verpachtungsgrad": float(land.get_verpachtungsgrad()),
"export_datum": timezone.now().isoformat(),
"export_user": request.user.username,
}
zipf.writestr(
"land_data.json", json.dumps(entity_data, indent=2, ensure_ascii=False)
)
# 2. Linked documents from Paperless
dokumente = DokumentLink.objects.filter(land_id=land.pk)
docs_data = []
for doc in dokumente:
doc_data = {
"paperless_id": doc.paperless_document_id,
"titel": doc.titel,
"kontext": doc.get_kontext_display(),
"beschreibung": doc.beschreibung,
}
docs_data.append(doc_data)
# Try to download document from Paperless
try:
if (
hasattr(settings, "PAPERLESS_API_URL")
and settings.PAPERLESS_API_URL
):
doc_url = f"{settings.PAPERLESS_API_URL}/api/documents/{doc.paperless_document_id}/download/"
headers = {}
if (
hasattr(settings, "PAPERLESS_API_TOKEN")
and settings.PAPERLESS_API_TOKEN
):
headers["Authorization"] = (
f"Token {settings.PAPERLESS_API_TOKEN}"
)
response = requests.get(doc_url, headers=headers, timeout=30)
if response.status_code == 200:
content_type = response.headers.get("content-type", "")
if "pdf" in content_type:
ext = ".pdf"
elif "jpeg" in content_type or "jpg" in content_type:
ext = ".jpg"
elif "png" in content_type:
ext = ".png"
else:
ext = ".pdf"
safe_filename = f"dokument_{doc.paperless_document_id}_{doc.titel.replace('/', '_')[:50]}{ext}"
zipf.writestr(
f"dokumente/{safe_filename}", response.content
)
doc_data["downloaded"] = True
else:
doc_data["download_error"] = f"HTTP {response.status_code}"
except Exception as e:
doc_data["download_error"] = str(e)
if docs_data:
zipf.writestr(
"dokumente.json",
json.dumps(docs_data, indent=2, ensure_ascii=False),
)
# Prepare response
with open(temp_file.name, "rb") as f:
response = HttpResponse(f.read(), content_type="application/zip")
filename = f"land_{land.gemeinde}_{land.gemarkung}_flur{land.flur}_{timezone.now().strftime('%Y%m%d_%H%M%S')}.zip"
response["Content-Disposition"] = f'attachment; filename="{filename}"'
return response
finally:
try:
os.unlink(temp_file.name)
except:
pass
@login_required
def verpachtung_export(request, pk):
"""Export complete Verpachtung data as ZIP with documents"""
import json
import os
import tempfile
import zipfile
from django.http import HttpResponse
verpachtung = get_object_or_404(LandVerpachtung, pk=pk)
# Create a temporary file for the ZIP
temp_file = tempfile.NamedTemporaryFile(delete=False, suffix=".zip")
try:
with zipfile.ZipFile(temp_file.name, "w", zipfile.ZIP_DEFLATED) as zipf:
# 1. Entity data as JSON
entity_data = {
"id": str(verpachtung.id),
"vertragsnummer": verpachtung.vertragsnummer,
"land": str(verpachtung.land),
"land_id": str(verpachtung.land.id),
"paechter": str(verpachtung.paechter),
"paechter_id": str(verpachtung.paechter.id),
"pachtbeginn": verpachtung.pachtbeginn.isoformat(),
"pachtende": verpachtung.pachtende.isoformat(),
"verlaengerung": (
verpachtung.verlaengerung.isoformat()
if verpachtung.verlaengerung
else None
),
"pachtzins_pro_qm": str(verpachtung.pachtzins_pro_qm),
"pachtzins_jaehrlich": str(verpachtung.pachtzins_pauschal),
"verpachtete_flaeche": str(verpachtung.verpachtete_flaeche),
"status": verpachtung.get_status_display(),
"verwendungsnachweis": (
str(verpachtung.verwendungsnachweis)
if verpachtung.verwendungsnachweis
else None
),
"bemerkungen": verpachtung.bemerkungen,
"erstellt_am": verpachtung.erstellt_am.isoformat(),
"aktualisiert_am": verpachtung.aktualisiert_am.isoformat(),
"vertragsdauer_tage": verpachtung.get_vertragsdauer_tage(),
"restlaufzeit_tage": verpachtung.get_restlaufzeit_tage(),
"ist_aktiv": verpachtung.is_aktiv(),
"export_datum": timezone.now().isoformat(),
"export_user": request.user.username,
}
zipf.writestr(
"verpachtung_data.json",
json.dumps(entity_data, indent=2, ensure_ascii=False),
)
# 2. Linked documents from Paperless
dokumente = DokumentLink.objects.filter(verpachtung_id=verpachtung.pk)
docs_data = []
for doc in dokumente:
doc_data = {
"paperless_id": doc.paperless_document_id,
"titel": doc.titel,
"kontext": doc.get_kontext_display(),
"beschreibung": doc.beschreibung,
}
docs_data.append(doc_data)
# Try to download document from Paperless
try:
if (
hasattr(settings, "PAPERLESS_API_URL")
and settings.PAPERLESS_API_URL
):
doc_url = f"{settings.PAPERLESS_API_URL}/api/documents/{doc.paperless_document_id}/download/"
headers = {}
if (
hasattr(settings, "PAPERLESS_API_TOKEN")
and settings.PAPERLESS_API_TOKEN
):
headers["Authorization"] = (
f"Token {settings.PAPERLESS_API_TOKEN}"
)
response = requests.get(doc_url, headers=headers, timeout=30)
if response.status_code == 200:
content_type = response.headers.get("content-type", "")
if "pdf" in content_type:
ext = ".pdf"
elif "jpeg" in content_type or "jpg" in content_type:
ext = ".jpg"
elif "png" in content_type:
ext = ".png"
else:
ext = ".pdf"
safe_filename = f"dokument_{doc.paperless_document_id}_{doc.titel.replace('/', '_')[:50]}{ext}"
zipf.writestr(
f"dokumente/{safe_filename}", response.content
)
doc_data["downloaded"] = True
else:
doc_data["download_error"] = f"HTTP {response.status_code}"
except Exception as e:
doc_data["download_error"] = str(e)
if docs_data:
zipf.writestr(
"dokumente.json",
json.dumps(docs_data, indent=2, ensure_ascii=False),
)
# Prepare response
with open(temp_file.name, "rb") as f:
response = HttpResponse(f.read(), content_type="application/zip")
filename = f"verpachtung_{verpachtung.vertragsnummer}_{timezone.now().strftime('%Y%m%d_%H%M%S')}.zip"
response["Content-Disposition"] = f'attachment; filename="{filename}"'
return response
finally:
try:
os.unlink(temp_file.name)
except:
pass
@login_required
def audit_log_list(request):
"""Liste aller Audit Log Einträge"""
from django.core.paginator import Paginator
from stiftung.models import AuditLog
logs = AuditLog.objects.all()
# Filter
user_filter = request.GET.get("user")
if user_filter:
logs = logs.filter(username__icontains=user_filter)
action_filter = request.GET.get("action")
if action_filter:
logs = logs.filter(action=action_filter)
entity_filter = request.GET.get("entity_type")
if entity_filter:
logs = logs.filter(entity_type=entity_filter)
date_from = request.GET.get("date_from")
if date_from:
logs = logs.filter(timestamp__date__gte=date_from)
date_to = request.GET.get("date_to")
if date_to:
logs = logs.filter(timestamp__date__lte=date_to)
# Pagination
paginator = Paginator(logs, 50)
page_number = request.GET.get("page")
page_obj = paginator.get_page(page_number)
context = {
"page_obj": page_obj,
"action_choices": AuditLog.ACTION_TYPES,
"entity_choices": AuditLog.ENTITY_TYPES,
"user_filter": user_filter,
"action_filter": action_filter,
"entity_filter": entity_filter,
"date_from": date_from,
"date_to": date_to,
}
return render(request, "stiftung/audit_log_list.html", context)
@login_required
def backup_management(request):
"""Backup Management Interface"""
from django.core.paginator import Paginator
from stiftung.models import BackupJob
# Handle backup creation
if request.method == "POST":
backup_type = request.POST.get("backup_type", "full")
# Create backup job
backup_job = BackupJob.objects.create(
backup_type=backup_type, created_by=request.user
)
# Log the backup initiation
from stiftung.audit import log_system_action
log_system_action(
request=request,
action="backup",
description=f"Backup-Job erstellt: {backup_job.get_backup_type_display()}",
details={"backup_job_id": str(backup_job.id), "backup_type": backup_type},
)
# Start backup process asynchronously (we'll create a simple version for now)
import threading
from stiftung.backup_utils import run_backup
backup_thread = threading.Thread(target=run_backup, args=(str(backup_job.id),))
backup_thread.start()
messages.success(
request,
f'Backup-Job "{backup_job.get_backup_type_display()}" wurde gestartet.',
)
return redirect("stiftung:backup_management")
# List backup jobs
backup_jobs = BackupJob.objects.all()
# Pagination
paginator = Paginator(backup_jobs, 20)
page_number = request.GET.get("page")
page_obj = paginator.get_page(page_number)
context = {
"page_obj": page_obj,
"backup_types": BackupJob.TYPE_CHOICES,
}
return render(request, "stiftung/backup_management.html", context)
@login_required
def backup_download(request, backup_id):
"""Download a backup file"""
import os
from django.http import FileResponse, Http404
from stiftung.models import BackupJob
try:
backup_job = BackupJob.objects.get(id=backup_id, status="completed")
except BackupJob.DoesNotExist:
raise Http404("Backup nicht gefunden oder nicht vollständig")
backup_path = os.path.join("/app/backups", backup_job.backup_filename)
if not os.path.exists(backup_path):
raise Http404("Backup-Datei nicht gefunden")
# Log download
from stiftung.audit import log_system_action
log_system_action(
request=request,
action="export",
description=f"Backup heruntergeladen: {backup_job.backup_filename}",
details={"backup_job_id": str(backup_job.id)},
)
response = FileResponse(
open(backup_path, "rb"), as_attachment=True, filename=backup_job.backup_filename
)
return response
@login_required
def backup_restore(request):
"""Restore from backup"""
if request.method == "POST":
from stiftung.models import BackupJob
backup_file = request.FILES.get("backup_file")
if not backup_file:
messages.error(request, "Bitte wählen Sie eine Backup-Datei aus.")
return redirect("stiftung:backup_management")
# Validate file
if not backup_file.name.endswith(".tar.gz"):
messages.error(
request, "Ungültiges Backup-Format. Nur .tar.gz Dateien sind erlaubt."
)
return redirect("stiftung:backup_management")
# Save uploaded file
import os
import tempfile
temp_dir = tempfile.mkdtemp()
backup_path = os.path.join(temp_dir, backup_file.name)
with open(backup_path, "wb+") as destination:
for chunk in backup_file.chunks():
destination.write(chunk)
# Create restore job
restore_job = BackupJob.objects.create(
backup_type="full",
created_by=request.user,
backup_filename=backup_file.name,
)
# Log restore initiation
from stiftung.audit import log_system_action
log_system_action(
request=request,
action="restore",
description=f"Wiederherstellung gestartet von: {backup_file.name}",
details={
"restore_job_id": str(restore_job.id),
"filename": backup_file.name,
},
)
# Start restore process
import threading
from stiftung.backup_utils import run_restore
restore_thread = threading.Thread(
target=run_restore, args=(str(restore_job.id), backup_path)
)
restore_thread.start()
messages.success(
request, f'Wiederherstellung von "{backup_file.name}" wurde gestartet.'
)
return redirect("stiftung:backup_management")
return redirect("stiftung:backup_management")
# =============================================================================
# USER MANAGEMENT VIEWS
# =============================================================================
@login_required
def user_management(request):
"""User Management Dashboard"""
from django.contrib.auth.models import User
from django.core.paginator import Paginator
from django.db.models import Q
# Check permission
if not request.user.has_perm("stiftung.manage_users"):
messages.error(
request, "Sie haben keine Berechtigung für die Benutzerverwaltung."
)
return redirect("stiftung:administration")
users = User.objects.all().order_by("username")
# Search functionality
search = request.GET.get("search")
if search:
users = users.filter(
Q(username__icontains=search)
| Q(email__icontains=search)
| Q(first_name__icontains=search)
| Q(last_name__icontains=search)
)
# Filter by status
status_filter = request.GET.get("status")
if status_filter == "active":
users = users.filter(is_active=True)
elif status_filter == "inactive":
users = users.filter(is_active=False)
elif status_filter == "staff":
users = users.filter(is_staff=True)
# Pagination
paginator = Paginator(users, 20)
page_number = request.GET.get("page")
page_obj = paginator.get_page(page_number)
# Statistics
stats = {
"total_users": User.objects.count(),
"active_users": User.objects.filter(is_active=True).count(),
"staff_users": User.objects.filter(is_staff=True).count(),
"inactive_users": User.objects.filter(is_active=False).count(),
}
context = {
"page_obj": page_obj,
"stats": stats,
"search": search,
"status_filter": status_filter,
}
return render(request, "stiftung/user_management.html", context)
@login_required
def user_create(request):
"""Create a new user"""
from django.contrib.auth.models import User
from stiftung.forms import UserCreationForm
# Check permission
if not request.user.has_perm("stiftung.manage_users"):
messages.error(
request, "Sie haben keine Berechtigung für die Benutzerverwaltung."
)
return redirect("stiftung:administration")
if request.method == "POST":
form = UserCreationForm(request.POST)
if form.is_valid():
# Create user
user = User.objects.create_user(
username=form.cleaned_data["username"],
email=form.cleaned_data["email"],
password=form.cleaned_data["password1"],
first_name=form.cleaned_data["first_name"],
last_name=form.cleaned_data["last_name"],
is_active=form.cleaned_data["is_active"],
is_staff=form.cleaned_data["is_staff"],
)
# Log user creation
from stiftung.audit import log_action
log_action(
request=request,
action="create",
entity_type="user",
entity_id=str(user.pk),
entity_name=user.username,
description=f'Neuer Benutzer "{user.username}" wurde erstellt',
)
messages.success(
request, f'Benutzer "{user.username}" wurde erfolgreich erstellt.'
)
return redirect("stiftung:user_detail", pk=user.pk)
else:
form = UserCreationForm()
context = {"form": form, "title": "Neuen Benutzer erstellen"}
return render(request, "stiftung/user_form.html", context)
@login_required
def user_detail(request, pk):
"""User detail view"""
from django.contrib.auth.models import User
# Check permission
if not request.user.has_perm("stiftung.manage_users"):
messages.error(
request, "Sie haben keine Berechtigung für die Benutzerverwaltung."
)
return redirect("stiftung:administration")
user = get_object_or_404(User, pk=pk)
# Get user's permissions
user_permissions = user.get_all_permissions()
stiftung_permissions = [
perm for perm in user_permissions if perm.startswith("stiftung.")
]
# Get recent audit activity
from stiftung.models import AuditLog
recent_activity = AuditLog.objects.filter(user=user).order_by("-timestamp")[:10]
context = {
"user_obj": user, # Use user_obj to avoid conflict with request.user
"stiftung_permissions": stiftung_permissions,
"recent_activity": recent_activity,
}
return render(request, "stiftung/user_detail.html", context)
@login_required
def user_edit(request, pk):
"""Edit user"""
from django.contrib.auth.models import User
from stiftung.forms import UserUpdateForm
# Check permission
if not request.user.has_perm("stiftung.manage_users"):
messages.error(
request, "Sie haben keine Berechtigung für die Benutzerverwaltung."
)
return redirect("stiftung:administration")
user = get_object_or_404(User, pk=pk)
if request.method == "POST":
form = UserUpdateForm(request.POST, instance=user)
if form.is_valid():
# Track changes
from stiftung.audit import log_action, track_model_changes
old_user = User.objects.get(pk=user.pk)
updated_user = form.save()
# Log changes
changes = track_model_changes(old_user, updated_user)
if changes:
log_action(
request=request,
action="update",
entity_type="user",
entity_id=str(updated_user.pk),
entity_name=updated_user.username,
description=f'Benutzer "{updated_user.username}" wurde aktualisiert',
changes=changes,
)
messages.success(
request,
f'Benutzer "{updated_user.username}" wurde erfolgreich aktualisiert.',
)
return redirect("stiftung:user_detail", pk=updated_user.pk)
else:
form = UserUpdateForm(instance=user)
context = {
"form": form,
"user_obj": user,
"title": f'Benutzer "{user.username}" bearbeiten',
}
return render(request, "stiftung/user_form.html", context)
@login_required
def user_change_password(request, pk):
"""Change user password"""
from django.contrib.auth.models import User
from stiftung.forms import PasswordChangeForm
# Check permission
if not request.user.has_perm("stiftung.manage_users"):
messages.error(
request, "Sie haben keine Berechtigung für die Benutzerverwaltung."
)
return redirect("stiftung:administration")
user = get_object_or_404(User, pk=pk)
if request.method == "POST":
form = PasswordChangeForm(request.POST)
if form.is_valid():
user.set_password(form.cleaned_data["new_password1"])
user.save()
# Log password change
from stiftung.audit import log_action
log_action(
request=request,
action="update",
entity_type="user",
entity_id=str(user.pk),
entity_name=user.username,
description=f'Passwort für Benutzer "{user.username}" wurde geändert',
)
messages.success(
request,
f'Passwort für Benutzer "{user.username}" wurde erfolgreich geändert.',
)
return redirect("stiftung:user_detail", pk=user.pk)
else:
form = PasswordChangeForm()
context = {
"form": form,
"user_obj": user,
"title": f'Passwort für "{user.username}" ändern',
}
return render(request, "stiftung/user_change_password.html", context)
@login_required
def user_permissions(request, pk):
"""Manage user permissions"""
from django.contrib.auth.models import Permission, User
from stiftung.forms import UserPermissionForm
# Check permission
if not request.user.has_perm("stiftung.manage_permissions"):
messages.error(
request, "Sie haben keine Berechtigung für die Berechtigungsverwaltung."
)
return redirect("stiftung:administration")
user = get_object_or_404(User, pk=pk)
if request.method == "POST":
form = UserPermissionForm(request.POST, user=user)
if form.is_valid():
# Get selected permissions
selected_perms = []
for field_name, value in form.cleaned_data.items():
if field_name.startswith("perm_") and value:
perm_id = field_name.replace("perm_", "")
selected_perms.append(int(perm_id))
# Get current stiftung permissions
current_perms = user.user_permissions.filter(
content_type__app_label="stiftung"
)
current_perm_ids = set(current_perms.values_list("id", flat=True))
selected_perm_ids = set(selected_perms)
# Remove permissions that are no longer selected
to_remove = current_perm_ids - selected_perm_ids
if to_remove:
user.user_permissions.remove(
*Permission.objects.filter(id__in=to_remove)
)
# Add new permissions
to_add = selected_perm_ids - current_perm_ids
if to_add:
user.user_permissions.add(*Permission.objects.filter(id__in=to_add))
# Log permission changes
from stiftung.audit import log_action
if to_remove or to_add:
changes = {
"removed_permissions": list(
Permission.objects.filter(id__in=to_remove).values_list(
"name", flat=True
)
),
"added_permissions": list(
Permission.objects.filter(id__in=to_add).values_list(
"name", flat=True
)
),
}
log_action(
request=request,
action="update",
entity_type="user",
entity_id=str(user.pk),
entity_name=user.username,
description=f'Berechtigungen für Benutzer "{user.username}" wurden aktualisiert',
changes=changes,
)
messages.success(
request,
f'Berechtigungen für Benutzer "{user.username}" wurden erfolgreich aktualisiert.',
)
return redirect("stiftung:user_detail", pk=user.pk)
else:
form = UserPermissionForm(user=user)
context = {
"form": form,
"user_obj": user,
"permission_groups": form.get_permission_groups(),
"title": f'Berechtigungen für "{user.username}"',
}
return render(request, "stiftung/user_permissions.html", context)
@login_required
def user_delete(request, pk):
"""Delete user"""
from django.contrib.auth.models import User
# Check permission
if not request.user.has_perm("stiftung.manage_users"):
messages.error(
request, "Sie haben keine Berechtigung für die Benutzerverwaltung."
)
return redirect("stiftung:administration")
user = get_object_or_404(User, pk=pk)
# Prevent deletion of current user
if user == request.user:
messages.error(request, "Sie können sich nicht selbst löschen.")
return redirect("stiftung:user_detail", pk=pk)
if request.method == "POST":
username = user.username
# Log deletion before deleting
from stiftung.audit import log_action
log_action(
request=request,
action="delete",
entity_type="user",
entity_id=str(user.pk),
entity_name=username,
description=f'Benutzer "{username}" wurde gelöscht',
)
user.delete()
messages.success(request, f'Benutzer "{username}" wurde erfolgreich gelöscht.')
return redirect("stiftung:user_management")
context = {"user_obj": user, "title": f'Benutzer "{user.username}" löschen'}
return render(request, "stiftung/user_delete.html", context)
# =============================================================================
# AUTHENTICATION VIEWS
# =============================================================================
def user_login(request):
"""User login view"""
from django.contrib.auth import authenticate, login
from django.contrib.auth.forms import AuthenticationForm
if request.user.is_authenticated:
return redirect("stiftung:dashboard")
if request.method == "POST":
form = AuthenticationForm(request, data=request.POST)
if form.is_valid():
username = form.cleaned_data.get("username")
password = form.cleaned_data.get("password")
user = authenticate(username=username, password=password)
if user is not None:
login(request, user)
# Log the login
from stiftung.audit import log_login
log_login(request, user)
messages.success(request, f"Willkommen zurück, {user.username}!")
# Redirect to safe next URL path or dashboard
next_param = request.GET.get("next") or request.POST.get("next")
if next_param and next_param.startswith("/"):
return redirect(next_param)
return redirect("stiftung:dashboard")
else:
messages.error(request, "Ungültige Anmeldedaten.")
else:
messages.error(request, "Bitte korrigieren Sie die Fehler im Formular.")
else:
form = AuthenticationForm()
context = {"form": form, "next": request.GET.get("next", "")}
return render(request, "stiftung/login.html", context)
@login_required
def user_logout(request):
"""User logout view"""
from django.contrib.auth import logout
# Log the logout before actually logging out
from stiftung.audit import log_logout
log_logout(request, request.user)
username = request.user.username
logout(request)
messages.success(request, f"Sie wurden erfolgreich abgemeldet, {username}.")
return redirect("stiftung:login")
# ============================================================================
# LANDABRECHNUNGS VIEWS
# ============================================================================
@login_required
def land_abrechnung_list(request):
"""Liste aller Landabrechnungen"""
abrechnungen = LandAbrechnung.objects.select_related("land").all()
# Filter
jahr_filter = request.GET.get("jahr")
land_filter = request.GET.get("land")
if jahr_filter:
abrechnungen = abrechnungen.filter(abrechnungsjahr=jahr_filter)
if land_filter:
abrechnungen = abrechnungen.filter(land__pk=land_filter)
# Pagination
paginator = Paginator(abrechnungen, 20)
page_number = request.GET.get("page")
abrechnungen = paginator.get_page(page_number)
# Statistiken
stats = LandAbrechnung.objects.aggregate(
total_einnahmen=Sum("pacht_vereinnahmt"),
total_ausgaben=Sum("grundsteuer_betrag"),
anzahl_abrechnungen=Count("id"),
)
context = {
"abrechnungen": abrechnungen,
"stats": stats,
"jahre": LandAbrechnung.objects.values_list("abrechnungsjahr", flat=True)
.distinct()
.order_by("-abrechnungsjahr"),
"laendereien": Land.objects.filter(aktiv=True).order_by(
"gemeinde", "gemarkung"
),
"jahr_filter": jahr_filter,
"land_filter": land_filter,
}
return render(request, "stiftung/land_abrechnung_list.html", context)
@login_required
def land_abrechnung_detail(request, pk):
"""Detail-Ansicht einer Landabrechnung"""
abrechnung = get_object_or_404(LandAbrechnung, pk=pk)
context = {
"abrechnung": abrechnung,
"land": abrechnung.land,
}
return render(request, "stiftung/land_abrechnung_detail.html", context)
@login_required
def land_abrechnung_create(request):
"""Neue Landabrechnung erstellen"""
from .forms import LandAbrechnungForm
land_pk = request.GET.get("land")
initial = {}
land = None
if land_pk:
land = get_object_or_404(Land, pk=land_pk)
initial["land"] = land
initial["abrechnungsjahr"] = datetime.now().year
# Automatische Vorausfüllung aus Verpachtungsdaten
if land.pachtzins_pauschal:
initial["pacht_vereinnahmt"] = land.pachtzins_pauschal
if request.method == "POST":
form = LandAbrechnungForm(request.POST, request.FILES)
if form.is_valid():
abrechnung = form.save()
messages.success(
request,
f"Landabrechnung für {abrechnung.land} ({abrechnung.abrechnungsjahr}) wurde erfolgreich erstellt.",
)
return redirect("stiftung:land_abrechnung_detail", pk=abrechnung.pk)
else:
form = LandAbrechnungForm(initial=initial)
context = {
"form": form,
"title": "Neue Landabrechnung",
"land": land,
}
return render(request, "stiftung/land_abrechnung_form.html", context)
@login_required
def land_abrechnung_update(request, pk):
"""Landabrechnung bearbeiten"""
from .forms import LandAbrechnungForm
abrechnung = get_object_or_404(LandAbrechnung, pk=pk)
if request.method == "POST":
form = LandAbrechnungForm(request.POST, request.FILES, instance=abrechnung)
if form.is_valid():
abrechnung = form.save()
messages.success(
request,
f"Landabrechnung für {abrechnung.land} ({abrechnung.abrechnungsjahr}) wurde erfolgreich aktualisiert.",
)
return redirect("stiftung:land_abrechnung_detail", pk=abrechnung.pk)
else:
form = LandAbrechnungForm(instance=abrechnung)
context = {
"form": form,
"abrechnung": abrechnung,
"title": f"Abrechnung bearbeiten - {abrechnung.land} ({abrechnung.abrechnungsjahr})",
}
return render(request, "stiftung/land_abrechnung_form.html", context)
@login_required
def land_abrechnung_delete(request, pk):
"""Landabrechnung löschen"""
abrechnung = get_object_or_404(LandAbrechnung, pk=pk)
land = abrechnung.land
if request.method == "POST":
abrechnung.delete()
messages.success(
request,
f"Landabrechnung für {land} ({abrechnung.abrechnungsjahr}) wurde erfolgreich gelöscht.",
)
return redirect("stiftung:land_detail", pk=land.pk)
context = {
"abrechnung": abrechnung,
"land": land,
}
return render(request, "stiftung/land_abrechnung_confirm_delete.html", context)
# ============================================================================
# VEREINHEITLICHTE VERPACHTUNGS VIEWS
# ============================================================================
@login_required
def land_verpachtung_create(request, land_pk):
"""Erstelle eine neue Verpachtung direkt im Land-Model"""
from datetime import datetime as dt
land = get_object_or_404(Land, pk=land_pk)
if request.method == "POST":
# Einfaches Formular für die wichtigsten Verpachtungsfelder
aktueller_paechter_id = request.POST.get("aktueller_paechter")
pachtbeginn = request.POST.get("pachtbeginn")
pachtende = request.POST.get("pachtende")
pachtzins_pauschal = request.POST.get("pachtzins_pauschal")
zahlungsweise = request.POST.get("zahlungsweise")
ust_option = request.POST.get("ust_option") == "on"
if aktueller_paechter_id and pachtbeginn:
paechter = get_object_or_404(Paechter, pk=aktueller_paechter_id)
verpachtete_flaeche = request.POST.get("verpachtete_flaeche")
# Validiere verpachtete Fläche
if not verpachtete_flaeche:
verpachtete_flaeche = land.groesse_qm # Standard: gesamte Fläche
else:
verpachtete_flaeche = float(verpachtete_flaeche)
if verpachtete_flaeche > land.groesse_qm:
messages.error(
request,
f"Die verpachtete Fläche ({verpachtete_flaeche} qm) kann nicht größer als die Gesamtfläche ({land.groesse_qm} qm) sein.",
)
# Erstelle context für Fehlerfall
paechter_list = Paechter.objects.filter(aktiv=True).order_by(
"nachname", "vorname"
)
verfuegbare_flaeche = land.groesse_qm
if land.verp_flaeche_aktuell and land.verp_flaeche_aktuell > 0:
verfuegbare_flaeche = (
land.groesse_qm - land.verp_flaeche_aktuell
)
context = {
"land": land,
"paechter_list": paechter_list,
"current_year": dt.now().year,
"is_edit": False,
"verfuegbare_flaeche": verfuegbare_flaeche,
}
return render(
request, "stiftung/land_verpachtung_form.html", context
)
# Land aktualisieren
land.aktueller_paechter = paechter
land.paechter_name = paechter.get_full_name()
land.paechter_anschrift = f"{paechter.strasse or ''}\n{paechter.plz or ''} {paechter.ort or ''}".strip()
land.pachtbeginn = pachtbeginn
land.pachtende = pachtende if pachtende else None
land.pachtzins_pauschal = pachtzins_pauschal if pachtzins_pauschal else None
land.zahlungsweise = zahlungsweise
land.ust_option = ust_option
land.verp_flaeche_aktuell = verpachtete_flaeche
land.verpachtete_gesamtflaeche = verpachtete_flaeche
land.save()
# Erstelle LandVerpachtung-Objekt für bessere Nachverfolgung
land_verpachtung = LandVerpachtung.objects.create(
land=land,
paechter=paechter,
vertragsnummer=f"V-{land.lfd_nr}-{dt.now().year}",
pachtbeginn=pachtbeginn,
pachtende=pachtende if pachtende else None,
verpachtete_flaeche=verpachtete_flaeche,
pachtzins_pauschal=pachtzins_pauschal if pachtzins_pauschal else 0,
zahlungsweise=zahlungsweise,
ust_option=ust_option,
status="aktiv",
)
# Erstelle automatisch eine Abrechnung für das aktuelle Jahr
current_year = dt.now().year
# Berechne erwartete jährliche Pacht basierend auf Zahlungsweise
expected_annual_rent = pachtzins_pauschal if pachtzins_pauschal else 0
abrechnung, created = LandAbrechnung.objects.get_or_create(
land=land,
abrechnungsjahr=current_year,
defaults={
"pacht_vereinnahmt": expected_annual_rent, # Setze erwartete Jahrespacht
"umlagen_vereinnahmt": 0,
"grundsteuer_betrag": 0,
"versicherungen_betrag": 0,
},
)
# Falls Abrechnung bereits existiert, aktualisiere die Pacht wenn höher
if not created and expected_annual_rent > abrechnung.pacht_vereinnahmt:
abrechnung.pacht_vereinnahmt = expected_annual_rent
abrechnung.save()
success_msg = f"Verpachtung von {land} an {paechter.get_full_name()} wurde erfolgreich erstellt."
if created:
success_msg += (
f" Abrechnung für {current_year} wurde automatisch angelegt"
)
if expected_annual_rent > 0:
success_msg += f" (Erwartete Jahrespacht: {expected_annual_rent}€)"
success_msg += "."
elif expected_annual_rent > 0:
success_msg += f" Erwartete Jahrespacht in Abrechnung {current_year} wurde aktualisiert ({expected_annual_rent}€)."
messages.success(request, success_msg)
return redirect("stiftung:land_detail", pk=land.pk)
else:
messages.error(request, "Bitte füllen Sie alle Pflichtfelder aus.")
# Verfügbare Pächter
paechter_list = Paechter.objects.filter(aktiv=True).order_by("nachname", "vorname")
# Berechne verfügbare Fläche (Gesamtfläche minus bereits verpachtete Fläche)
verfuegbare_flaeche = land.groesse_qm
if land.verp_flaeche_aktuell and land.verp_flaeche_aktuell > 0:
verfuegbare_flaeche = land.groesse_qm - land.verp_flaeche_aktuell
context = {
"land": land,
"paechter_list": paechter_list,
"current_year": dt.now().year,
"is_edit": False,
"verfuegbare_flaeche": verfuegbare_flaeche,
}
return render(request, "stiftung/land_verpachtung_form.html", context)
@login_required
def land_verpachtung_end(request, land_pk):
"""Beende die aktuelle Verpachtung eines Landes"""
land = get_object_or_404(Land, pk=land_pk)
if request.method == "POST":
# Verpachtung beenden
land.aktueller_paechter = None
land.paechter_name = None
land.paechter_anschrift = None
land.pachtende = datetime.now().date()
land.save()
messages.success(request, f"Verpachtung von {land} wurde beendet.")
return redirect("stiftung:land_detail", pk=land.pk)
context = {
"land": land,
}
return render(request, "stiftung/land_verpachtung_end.html", context)
@login_required
def land_verpachtung_edit(request, land_pk):
"""Bearbeite eine bestehende Verpachtung direkt im Land-Model"""
land = get_object_or_404(Land, pk=land_pk)
if request.method == "POST":
# Einfaches Formular für die wichtigsten Verpachtungsfelder
aktueller_paechter_id = request.POST.get("aktueller_paechter")
pachtbeginn = request.POST.get("pachtbeginn")
pachtende = request.POST.get("pachtende")
pachtzins_pauschal = request.POST.get("pachtzins_pauschal")
zahlungsweise = request.POST.get("zahlungsweise")
ust_option = request.POST.get("ust_option") == "on"
verpachtete_flaeche = request.POST.get("verpachtete_flaeche")
if aktueller_paechter_id and pachtbeginn:
paechter = get_object_or_404(Paechter, pk=aktueller_paechter_id)
# Land aktualisieren
land.aktueller_paechter = paechter
land.paechter_name = paechter.get_full_name()
land.paechter_anschrift = f"{paechter.strasse or ''}\n{paechter.plz or ''} {paechter.ort or ''}".strip()
land.pachtbeginn = pachtbeginn
land.pachtende = pachtende if pachtende else None
land.pachtzins_pauschal = pachtzins_pauschal if pachtzins_pauschal else None
land.zahlungsweise = zahlungsweise
land.ust_option = ust_option
if verpachtete_flaeche:
land.verp_flaeche_aktuell = verpachtete_flaeche
land.save()
messages.success(
request,
f"Verpachtung von {land} an {paechter.get_full_name()} wurde erfolgreich aktualisiert.",
)
return redirect("stiftung:land_detail", pk=land.pk)
else:
messages.error(request, "Bitte füllen Sie alle Pflichtfelder aus.")
# Verfügbare Pächter
paechter_list = Paechter.objects.filter(aktiv=True).order_by("nachname", "vorname")
# Berechne verfügbare Fläche (Gesamtfläche minus bereits verpachtete Fläche)
verfuegbare_flaeche = land.groesse_qm
if land.verp_flaeche_aktuell and land.verp_flaeche_aktuell > 0:
verfuegbare_flaeche = land.groesse_qm - land.verp_flaeche_aktuell
context = {
"land": land,
"paechter_list": paechter_list,
"current_year": datetime.now().year,
"is_edit": True,
"verfuegbare_flaeche": verfuegbare_flaeche,
}
return render(request, "stiftung/land_verpachtung_form.html", context)
# Settings Management Views
@login_required
def app_settings(request):
"""Application settings management interface"""
# Group settings by category
categories = {}
for setting in AppConfiguration.objects.filter(is_active=True).order_by(
"category", "order", "display_name"
):
if setting.category not in categories:
categories[setting.category] = []
categories[setting.category].append(setting)
if request.method == "POST":
# Handle form submission
updated_count = 0
for key, value in request.POST.items():
if key.startswith("setting_"):
setting_key = key.replace("setting_", "")
try:
setting = AppConfiguration.objects.get(
key=setting_key, is_active=True
)
if not setting.is_system and setting.value != value:
setting.value = value
setting.save()
updated_count += 1
except AppConfiguration.DoesNotExist:
continue
if updated_count > 0:
messages.success(request, f"Successfully updated {updated_count} settings!")
else:
messages.info(request, "No changes were made.")
return redirect("stiftung:app_settings")
context = {
"categories": categories,
"title": "Application Settings",
}
return render(request, "stiftung/app_settings.html", context)
# Unterstützungen Views (Destinataer-focused)
@login_required
def unterstuetzungen_all(request):
"""List all support payments - destinataer-focused view"""
status = request.GET.get("status")
destinataer_id = request.GET.get("destinataer")
export = request.GET.get("format", "")
selected_ids = (
request.POST.getlist("selected_entries") if request.method == "POST" else []
)
unterstuetzungen = DestinataerUnterstuetzung.objects.select_related(
"destinataer", "konto", "ausgezahlt_von", "wiederkehrend_von"
).order_by("-faellig_am")
# Filtering
if status:
unterstuetzungen = unterstuetzungen.filter(status=status)
if destinataer_id:
unterstuetzungen = unterstuetzungen.filter(destinataer_id=destinataer_id)
# Enhanced CSV export with field selection
if export == "csv":
return export_unterstuetzungen_csv(request, unterstuetzungen, selected_ids)
# PDF export (simple table via WeasyPrint; graceful fallback if missing)
if export == "pdf":
try:
from django.template.loader import render_to_string
from weasyprint import HTML
html = render_to_string(
"stiftung/unterstuetzungen_pdf.html",
{"unterstuetzungen": unterstuetzungen},
)
from django.http import HttpResponse
pdf = HTML(string=html).write_pdf()
resp = HttpResponse(pdf, content_type="application/pdf")
resp["Content-Disposition"] = "inline; filename=unterstuetzungen.pdf"
return resp
except Exception:
pass
# Statistics
total_betrag = unterstuetzungen.aggregate(total=Sum("betrag"))["total"] or 0
avg_betrag = unterstuetzungen.aggregate(avg=Avg("betrag"))["avg"] or 0
# Available destinataer for filter
destinataer = Destinataer.objects.all().order_by("nachname", "vorname")
context = {
"page_obj": unterstuetzungen, # Use directly for now (pagination can be added later)
"unterstuetzungen": unterstuetzungen,
"title": "Alle Unterstützungen",
"status_filter": status,
"total_betrag": total_betrag,
"avg_betrag": avg_betrag,
"status_choices": DestinataerUnterstuetzung.STATUS_CHOICES,
"destinataer": destinataer,
}
return render(request, "stiftung/unterstuetzungen_all.html", context)
@login_required
def unterstuetzung_create(request):
"""Create a new support payment"""
# Get destinataer from URL parameter if provided
destinataer_id = request.GET.get("destinataer")
initial = {}
if destinataer_id:
initial["destinataer"] = destinataer_id
# Pre-populate IBAN and name if destinataer is specified
try:
destinataer = Destinataer.objects.get(pk=destinataer_id)
if hasattr(destinataer, "iban") and destinataer.iban:
initial["empfaenger_iban"] = destinataer.iban
initial["empfaenger_name"] = destinataer.get_full_name()
except Destinataer.DoesNotExist:
pass
if request.method == "POST":
form = UnterstuetzungForm(request.POST)
if form.is_valid():
ist_wiederkehrend = form.cleaned_data.get("ist_wiederkehrend", False)
if ist_wiederkehrend:
# Create recurring payment template
wiederkehrend = UnterstuetzungWiederkehrend.objects.create(
destinataer=form.cleaned_data["destinataer"],
konto=form.cleaned_data["konto"],
betrag=form.cleaned_data["betrag"],
intervall=form.cleaned_data["intervall"],
beschreibung=form.cleaned_data["beschreibung"],
empfaenger_iban=form.cleaned_data["empfaenger_iban"],
empfaenger_name=form.cleaned_data["empfaenger_name"],
verwendungszweck=form.cleaned_data["verwendungszweck"],
erste_zahlung_am=form.cleaned_data["faellig_am"],
letzte_zahlung_am=form.cleaned_data.get("letzte_zahlung_am"),
naechste_generierung=form.cleaned_data["faellig_am"],
erstellt_von=request.user,
)
# Create the first payment
unterstuetzung = form.save(commit=False)
unterstuetzung.wiederkehrend_von = wiederkehrend
unterstuetzung.save()
messages.success(
request,
f"Wiederkehrende Unterstützung für {unterstuetzung.destinataer} wurde erfolgreich erstellt. Die erste Zahlung ist am {unterstuetzung.faellig_am} fällig.",
)
else:
# Create single payment
unterstuetzung = form.save()
messages.success(
request,
f"Unterstützung für {unterstuetzung.destinataer} wurde erfolgreich erstellt.",
)
return redirect("stiftung:unterstuetzung_detail", pk=unterstuetzung.pk)
else:
form = UnterstuetzungForm(initial=initial)
context = {
"form": form,
"title": "Neue Unterstützung erstellen",
}
return render(request, "stiftung/unterstuetzung_form.html", context)
@login_required
def get_destinataer_info(request, destinataer_id):
"""AJAX endpoint to get Destinataer IBAN and name information"""
try:
destinataer = Destinataer.objects.get(pk=destinataer_id)
data = {
"success": True,
"name": destinataer.get_full_name(),
"iban": getattr(destinataer, "iban", "") or "",
}
except Destinataer.DoesNotExist:
data = {"success": False, "error": "Destinataer not found"}
return JsonResponse(data)
@login_required
def unterstuetzung_detail(request, pk):
"""View support payment details"""
unterstuetzung = get_object_or_404(DestinataerUnterstuetzung, pk=pk)
# Check if this payment can be marked as paid
can_mark_paid = unterstuetzung.can_be_marked_paid()
context = {
"unterstuetzung": unterstuetzung,
"title": f"Unterstützung für {unterstuetzung.destinataer.get_full_name()}",
"can_mark_paid": can_mark_paid,
}
return render(request, "stiftung/unterstuetzung_detail.html", context)
@login_required
def unterstuetzung_mark_paid(request, pk):
"""Mark a support payment as paid"""
unterstuetzung = get_object_or_404(DestinataerUnterstuetzung, pk=pk)
if not unterstuetzung.can_be_marked_paid():
messages.error(
request, "Diese Unterstützung kann nicht als bezahlt markiert werden."
)
return redirect("stiftung:unterstuetzung_detail", pk=pk)
if request.method == "POST":
form = UnterstuetzungMarkAsPaidForm(request.POST)
if form.is_valid():
unterstuetzung.status = "ausgezahlt"
unterstuetzung.ausgezahlt_am = form.cleaned_data["ausgezahlt_am"]
unterstuetzung.ausgezahlt_von = request.user
# Add optional note to description
bemerkung = form.cleaned_data.get("bemerkung")
if bemerkung:
if unterstuetzung.beschreibung:
unterstuetzung.beschreibung += f" | Zahlung: {bemerkung}"
else:
unterstuetzung.beschreibung = f"Zahlung: {bemerkung}"
unterstuetzung.save()
messages.success(request, f"Unterstützung wurde als bezahlt markiert.")
return redirect("stiftung:unterstuetzung_detail", pk=pk)
else:
form = UnterstuetzungMarkAsPaidForm()
context = {
"form": form,
"unterstuetzung": unterstuetzung,
"title": f"Zahlung markieren - {unterstuetzung.destinataer.get_full_name()}",
}
return render(request, "stiftung/unterstuetzung_mark_paid.html", context)
@login_required
def wiederkehrende_unterstuetzungen(request):
"""List all recurring support payment templates"""
from django.db.models import Count
# Check for cleanup request
if request.GET.get("cleanup") == "1":
# Find templates with no associated payments
verwaiste_templates = UnterstuetzungWiederkehrend.objects.annotate(
zahlung_count=Count("destinataerunterstuetzung")
).filter(zahlung_count=0)
if verwaiste_templates.exists():
anzahl_geloescht = verwaiste_templates.count()
template_namen = list(
verwaiste_templates.values_list("destinataer__nachname", flat=True)
)
verwaiste_templates.delete()
messages.success(
request,
f'{anzahl_geloescht} verwaiste Zahlungsvorlagen bereinigt: {", ".join(template_namen[:5])}{"..." if len(template_namen) > 5 else ""}',
)
else:
messages.info(request, "Keine verwaisten Zahlungsvorlagen gefunden.")
return redirect("stiftung:wiederkehrende_unterstuetzungen")
# Get all templates with payment counts
templates = (
UnterstuetzungWiederkehrend.objects.select_related("destinataer", "konto")
.annotate(aktive_zahlungen=Count("destinataerunterstuetzung"))
.all()
)
context = {
"templates": templates,
"title": "Wiederkehrende Unterstützungen",
}
return render(request, "stiftung/wiederkehrende_unterstuetzungen.html", context)
@login_required
def edit_help_box(request):
"""Bearbeite oder erstelle eine Hilfs-Infobox"""
from .models import HelpBox
# Nur root oder Superuser dürfen bearbeiten
if request.user.username != "root" and not request.user.is_superuser:
messages.error(
request, "Sie haben keine Berechtigung, Hilfsboxen zu bearbeiten."
)
return redirect("stiftung:dashboard")
if request.method == "POST":
page_key = request.POST.get("page_key")
title = request.POST.get("title")
content = request.POST.get("content")
is_active = request.POST.get("is_active") == "on"
if not page_key or not title or not content:
messages.error(request, "Alle Felder sind erforderlich.")
return redirect(request.META.get("HTTP_REFERER", "stiftung:dashboard"))
# Hilfsbox erstellen oder aktualisieren
help_box, created = HelpBox.objects.get_or_create(
page_key=page_key,
defaults={
"title": title,
"content": content,
"is_active": is_active,
"created_by": request.user.username,
"updated_by": request.user.username,
},
)
if not created:
# Existierende Hilfsbox aktualisieren
help_box.title = title
help_box.content = content
help_box.is_active = is_active
help_box.updated_by = request.user.username
help_box.save()
messages.success(request, f'Hilfsbox "{title}" wurde aktualisiert.')
else:
messages.success(request, f'Hilfsbox "{title}" wurde erstellt.')
# Zurück zur vorherigen Seite
return redirect(request.META.get("HTTP_REFERER", "stiftung:dashboard"))
# GET Request - Zeige Admin-Übersicht der Hilfsboxen
help_boxes = HelpBox.objects.all().order_by("page_key", "-updated_at")
# Statistiken berechnen
active_count = help_boxes.filter(is_active=True).count()
inactive_count = help_boxes.filter(is_active=False).count()
existing_pages = set(help_boxes.values_list("page_key", flat=True))
# Verfügbare Seiten aus dem Model holen
available_pages = HelpBox.PAGE_CHOICES
context = {
"help_boxes": help_boxes,
"active_count": active_count,
"inactive_count": inactive_count,
"existing_pages": existing_pages,
"available_pages": available_pages,
"title": "Hilfs-Infoboxen verwalten",
}
return render(request, "stiftung/help_boxes_admin.html", context)
# =============================================================================
# Verpachtung Management Views (Standalone CRUD)
# =============================================================================
@login_required
def verpachtung_detail(request, pk):
"""Standalone detail view for verpachtung"""
verpachtung = get_object_or_404(LandVerpachtung, pk=pk)
# Alle mit dieser Verpachtung verknüpften Dokumente laden
verknuepfte_dokumente = DokumentLink.objects.filter(
land_verpachtung_id=verpachtung.pk
).order_by("kontext", "titel")
context = {
"verpachtung": verpachtung,
"landverpachtung": verpachtung, # Template compatibility
"verknuepfte_dokumente": verknuepfte_dokumente,
"title": f"Verpachtung {verpachtung.vertragsnummer}",
}
return render(request, "stiftung/verpachtung_detail.html", context)
@login_required
def verpachtung_create(request):
"""Standalone create view for verpachtung"""
from .forms import LandVerpachtungForm
from datetime import datetime as dt
if request.method == 'POST':
form = LandVerpachtungForm(request.POST, request.FILES)
if form.is_valid():
verpachtung = form.save()
# Update the Land model to reflect this verpachtung
land = verpachtung.land
land.aktueller_paechter = verpachtung.paechter
land.paechter_name = verpachtung.paechter.get_full_name()
land.paechter_anschrift = f"{verpachtung.paechter.strasse or ''}\n{verpachtung.paechter.plz or ''} {verpachtung.paechter.ort or ''}".strip()
land.pachtbeginn = verpachtung.pachtbeginn
land.pachtende = verpachtung.pachtende
land.pachtzins_pauschal = verpachtung.pachtzins_pauschal
land.zahlungsweise = verpachtung.zahlungsweise
land.ust_option = verpachtung.ust_option
land.verpachtete_gesamtflaeche = verpachtung.verpachtete_flaeche
land.verp_flaeche_aktuell = verpachtung.verpachtete_flaeche
land.save()
# Create automatic abrechnung
current_year = dt.now().year
expected_annual_rent = verpachtung.pachtzins_pauschal if verpachtung.pachtzins_pauschal else 0
abrechnung, created = LandAbrechnung.objects.get_or_create(
land=land,
abrechnungsjahr=current_year,
defaults={
"pacht_vereinnahmt": expected_annual_rent,
"umlagen_vereinnahmt": 0,
"grundsteuer_betrag": 0,
"versicherungen_betrag": 0,
},
)
if not created and expected_annual_rent > abrechnung.pacht_vereinnahmt:
abrechnung.pacht_vereinnahmt = expected_annual_rent
abrechnung.save()
success_msg = f'Verpachtung "{verpachtung.vertragsnummer}" wurde erfolgreich erstellt.'
if created:
success_msg += f" Abrechnung für {current_year} wurde automatisch angelegt"
if expected_annual_rent > 0:
success_msg += f" (Erwartete Jahrespacht: {expected_annual_rent}€)"
success_msg += "."
elif expected_annual_rent > 0:
success_msg += f" Erwartete Jahrespacht in Abrechnung {current_year} wurde aktualisiert ({expected_annual_rent}€)."
messages.success(request, success_msg)
return redirect('stiftung:verpachtung_detail', pk=verpachtung.pk)
else:
form = LandVerpachtungForm()
# Get available Länder and Pächter for the template
laender_list = Land.objects.all().order_by('lfd_nr')
paechter_list = Paechter.objects.filter(aktiv=True).order_by('nachname', 'vorname')
context = {
'form': form,
'title': 'Neue Verpachtung erstellen',
'laender_list': laender_list,
'paechter_list': paechter_list,
'current_year': dt.now().year,
'is_edit': False,
}
return render(request, 'stiftung/verpachtung_form.html', context)
@login_required
def verpachtung_update(request, pk):
"""Standalone update view for verpachtung"""
return land_verpachtung_update(request, pk)
@login_required
def verpachtung_delete(request, pk):
"""Standalone delete view for verpachtung"""
verpachtung = get_object_or_404(LandVerpachtung, pk=pk)
if request.method == 'POST':
vertragsnummer = verpachtung.vertragsnummer
verpachtung.delete()
messages.success(
request,
f'Verpachtung "{vertragsnummer}" wurde erfolgreich gelöscht.'
)
return redirect('stiftung:verpachtung_list')
context = {
'verpachtung': verpachtung,
'title': f'Verpachtung {verpachtung.vertragsnummer} löschen',
}
return render(request, 'stiftung/verpachtung_confirm_delete.html', context)