🐛 Error Fixed: - AttributeError: 'VierteljahresNachweis' object has no attribute 'completion_percentage' - Line 5347 in destinataer_export view was calling completion_percentage() - Corrected to call get_completion_percentage() which is the actual method name ✅ Export functionality now works correctly for destinataers with quarterly confirmations The error occurred when exporting a destinataer that had quarterly tracking data. All quarterly confirmation completion percentages are now properly exported.
7769 lines
288 KiB
Python
7769 lines
288 KiB
Python
import csv
|
|
import io
|
|
import json
|
|
import os
|
|
import time
|
|
from datetime import datetime, timedelta
|
|
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, VierteljahresNachweis)
|
|
|
|
|
|
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, VierteljahresNachweisForm)
|
|
|
|
|
|
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")
|
|
|
|
# Quarterly confirmations - load for current and next year
|
|
from datetime import date
|
|
current_year = date.today().year
|
|
quarterly_confirmations = VierteljahresNachweis.objects.filter(
|
|
destinataer=destinataer,
|
|
jahr__in=[current_year, current_year + 1]
|
|
).order_by('-jahr', '-quartal')
|
|
|
|
# Create missing quarterly confirmations for current year
|
|
# Quarterly tracking is now always available regardless of study proof requirements
|
|
for quartal in range(1, 5): # Q1-Q4
|
|
nachweis, created = VierteljahresNachweis.get_or_create_for_period(
|
|
destinataer, current_year, quartal
|
|
)
|
|
|
|
# Reload to get any newly created confirmations
|
|
quarterly_confirmations = VierteljahresNachweis.objects.filter(
|
|
destinataer=destinataer,
|
|
jahr__in=[current_year, current_year + 1]
|
|
).order_by('-jahr', '-quartal')
|
|
|
|
# Generate available years for the add quarter dropdown (current year + next 5 years)
|
|
available_years = list(range(current_year, current_year + 6))
|
|
|
|
# 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,
|
|
"quarterly_confirmations": quarterly_confirmations,
|
|
"available_years": available_years,
|
|
"current_year": current_year,
|
|
}
|
|
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()
|
|
|
|
# Note: Support payments are now only created through quarterly confirmations
|
|
# No automatic creation when unterstuetzung_bestaetigt is checked
|
|
|
|
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()
|
|
# Note: Support payments are now only created through quarterly confirmations
|
|
# No automatic creation when unterstuetzung_bestaetigt is checked
|
|
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_groesse_qm = float(aggregates.get("sum_groesse_qm") or 0)
|
|
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
|
|
|
|
# Calculate verpachtung statistics
|
|
total_plots = lands.count()
|
|
verpachtete_plots = lands.filter(verp_flaeche_aktuell__gt=0).count()
|
|
unveerpachtete_plots = total_plots - verpachtete_plots
|
|
|
|
def pct(part, total):
|
|
return round((part / total) * 100, 1) if total and part is not None else 0.0
|
|
|
|
stats = {
|
|
"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_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),
|
|
"total_plots": total_plots,
|
|
"verpachtete_plots": verpachtete_plots,
|
|
"unveerpachtete_plots": unveerpachtete_plots,
|
|
"pct_verpachtet": pct(verpachtete_plots, total_plots),
|
|
"pct_unveerpachtet": pct(unveerpachtete_plots, total_plots),
|
|
}
|
|
|
|
# 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)
|
|
|
|
# Debug: Print form data
|
|
print("=== LAND CREATE DEBUG ===")
|
|
print(f"POST data: {dict(request.POST)}")
|
|
print(f"Form is valid: {form.is_valid()}")
|
|
|
|
if not form.is_valid():
|
|
print(f"Form errors: {form.errors}")
|
|
print(f"Form non-field errors: {form.non_field_errors()}")
|
|
# Add error messages for debugging
|
|
for field, errors in form.errors.items():
|
|
for error in errors:
|
|
messages.error(request, f"{field}: {error}")
|
|
|
|
if form.is_valid():
|
|
try:
|
|
land = form.save()
|
|
messages.success(request, f'Länderei "{land}" wurde erfolgreich erstellt.')
|
|
print(f"Successfully created land: {land}")
|
|
return redirect("stiftung:land_detail", pk=land.pk)
|
|
except Exception as e:
|
|
print(f"Error saving land: {e}")
|
|
messages.error(request, f"Fehler beim Speichern: {str(e)}")
|
|
else:
|
|
messages.error(request, "Bitte korrigieren Sie die Fehler im Formular.")
|
|
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} m²",
|
|
}
|
|
for l in land_results
|
|
]
|
|
|
|
if category in ["all", "verpachtung"]:
|
|
# Suche nach Verpachtungen (using new LandVerpachtung model)
|
|
verpachtung_query = Q()
|
|
if query and query != "all":
|
|
verpachtung_query = (
|
|
Q(paechter__nachname__icontains=query)
|
|
| Q(paechter__vorname__icontains=query)
|
|
| Q(paechter__ort__icontains=query)
|
|
| Q(paechter__email__icontains=query)
|
|
| Q(paechter__pachtnummer__icontains=query)
|
|
| Q(land__gemarkung__icontains=query)
|
|
| Q(land__gemeinde__icontains=query)
|
|
| Q(land__flur__icontains=query)
|
|
| Q(land__flurstueck__icontains=query)
|
|
| Q(land__lfd_nr__icontains=query)
|
|
| Q(vertragsnummer__icontains=query)
|
|
| Q(pachtzins_pauschal__icontains=query)
|
|
| Q(bemerkungen__icontains=query)
|
|
)
|
|
|
|
verpachtung_results = LandVerpachtung.objects.filter(
|
|
verpachtung_query
|
|
).select_related("paechter", "land")[:25]
|
|
results["verpachtung"] = [
|
|
{
|
|
"id": v.id,
|
|
"name": f"{(v.paechter.vorname + ' ') if v.paechter and v.paechter.vorname else ''}{v.paechter.nachname if v.paechter else 'Unbekannt'} → {v.land.gemarkung}, Flur {v.land.flur}",
|
|
"type": "Verpachtung",
|
|
"details": f"Vertrag: {v.vertragsnummer} • Flurstück: {v.land.flurstueck or 'N/A'} • Pachtzins: {v.pachtzins_pauschal or 'N/A'} €/Jahr • Zeitraum: {v.pachtbeginn.strftime('%Y') if v.pachtbeginn else '?'}-{v.pachtende.strftime('%Y') if v.pachtende else 'laufend'}",
|
|
}
|
|
for v in verpachtung_results
|
|
]
|
|
|
|
if category in ["all", "paechter"]:
|
|
# Suche nach Pächtern
|
|
paechter_query = Q()
|
|
if query and query != "all":
|
|
paechter_query = (
|
|
Q(nachname__icontains=query)
|
|
| Q(vorname__icontains=query)
|
|
| Q(ort__icontains=query)
|
|
| Q(email__icontains=query)
|
|
| Q(telefon__icontains=query)
|
|
| Q(strasse__icontains=query)
|
|
| Q(pachtnummer__icontains=query)
|
|
| Q(plz__icontains=query)
|
|
| Q(notizen__icontains=query)
|
|
)
|
|
paechter_results = Paechter.objects.filter(paechter_query)[:25]
|
|
results["paechter"] = [
|
|
{
|
|
"id": p.id,
|
|
"name": f"{(p.vorname + ' ') if p.vorname else ''}{p.nachname}"
|
|
+ (f" (#{p.pachtnummer})" if p.pachtnummer else ""),
|
|
"type": "Pächter",
|
|
"details": f"{p.strasse or ''}{(', ' + p.plz + ' ' + p.ort) if p.plz and p.ort else (' ' + p.ort) if p.ort else ''} {('• ' + p.email) if p.email else ''} {('• Tel: ' + p.telefon) if p.telefon else ''}".strip(),
|
|
}
|
|
for p in paechter_results
|
|
]
|
|
|
|
if category in ["all", "rentmeister"]:
|
|
# Suche nach Rentmeistern
|
|
from stiftung.models import Rentmeister
|
|
|
|
rentmeister_query = Q()
|
|
if query and query != "all":
|
|
rentmeister_query = (
|
|
Q(nachname__icontains=query)
|
|
| Q(vorname__icontains=query)
|
|
| Q(ort__icontains=query)
|
|
| Q(email__icontains=query)
|
|
| Q(telefon__icontains=query)
|
|
| Q(strasse__icontains=query)
|
|
| Q(plz__icontains=query)
|
|
| Q(notizen__icontains=query)
|
|
| Q(titel__icontains=query)
|
|
| Q(mobil__icontains=query)
|
|
)
|
|
rentmeister_results = Rentmeister.objects.filter(rentmeister_query)[:25]
|
|
results["rentmeister"] = [
|
|
{
|
|
"id": r.id,
|
|
"name": f"{(r.anrede + ' ') if r.anrede else ''}{(r.vorname + ' ') if r.vorname else ''}{r.nachname}"
|
|
+ (f" ({r.titel})" if r.titel else ""),
|
|
"type": "Rentmeister",
|
|
"details": f"{r.strasse or ''}{(', ' + r.plz + ' ' + r.ort) if r.plz and r.ort else (' ' + r.ort) if r.ort else ''} {('• ' + r.email) if r.email else ''} {('• Tel: ' + r.telefon) if r.telefon else ''} {('• Mobil: ' + r.mobil) if r.mobil else ''}".strip(),
|
|
}
|
|
for r in rentmeister_results
|
|
]
|
|
|
|
if category in ["all", "abrechnung"]:
|
|
# Suche nach Abrechnungen
|
|
abrechnung_query = Q()
|
|
if query and query != "all":
|
|
abrechnung_query = (
|
|
Q(land__gemarkung__icontains=query)
|
|
| Q(land__gemeinde__icontains=query)
|
|
| Q(land__flur__icontains=query)
|
|
| Q(land__flurstueck__icontains=query)
|
|
| Q(land__lfd_nr__icontains=query)
|
|
| Q(abrechnungsjahr__icontains=query)
|
|
| Q(bemerkungen__icontains=query)
|
|
)
|
|
|
|
abrechnung_results = LandAbrechnung.objects.filter(
|
|
abrechnung_query
|
|
).select_related("land")[:25]
|
|
results["abrechnung"] = [
|
|
{
|
|
"id": a.id,
|
|
"name": f"Abrechnung {a.abrechnungsjahr} - {a.land.gemarkung}, Flur {a.land.flur}",
|
|
"type": "Abrechnung",
|
|
"details": f"Flurstück: {a.land.flurstueck or 'N/A'} • Jahr: {a.abrechnungsjahr} • Grundsteuer: {a.grundsteuer_betrag or 0} € • Versicherung: {a.versicherungen_betrag or 0} €",
|
|
}
|
|
for a in abrechnung_results
|
|
]
|
|
|
|
if category in ["all", "foerderung"]:
|
|
# Suche nach Förderungen
|
|
foerderung_query = Q()
|
|
if query and query != "all":
|
|
foerderung_query = (
|
|
Q(destinataer__nachname__icontains=query)
|
|
| Q(destinataer__vorname__icontains=query)
|
|
| Q(destinataer__institution__icontains=query)
|
|
| Q(destinataer__email__icontains=query)
|
|
| Q(jahr__icontains=query)
|
|
| Q(betrag__icontains=query)
|
|
| Q(kategorie__icontains=query)
|
|
| Q(status__icontains=query)
|
|
| Q(bemerkungen__icontains=query)
|
|
)
|
|
|
|
foerderung_results = Foerderung.objects.filter(foerderung_query).select_related(
|
|
"destinataer"
|
|
)[:25]
|
|
results["foerderung"] = [
|
|
{
|
|
"id": str(f.id), # Convert UUID to string for JSON serialization
|
|
"name": f"{f.destinataer.get_full_name() if f.destinataer else 'Unbekannt'} - {f.jahr}",
|
|
"type": "Förderung",
|
|
"details": f"Betrag: {f.betrag} € • Kategorie: {f.get_kategorie_display()} • Status: {f.get_status_display()} • Jahr: {f.jahr}",
|
|
}
|
|
for f in foerderung_results
|
|
]
|
|
|
|
return Response(results)
|
|
|
|
|
|
def create_paechter_link_for_verpachtung(paperless_id, paperless_title, verpachtung_id):
|
|
"""Hilfsfunktion: Erstellt automatisch eine Pächter-Verknüpfung für eine Verpachtung"""
|
|
try:
|
|
# Hole die LandVerpachtung und den zugehörigen Pächter
|
|
verpachtung = LandVerpachtung.objects.select_related("paechter").get(
|
|
id=verpachtung_id
|
|
)
|
|
if verpachtung.paechter:
|
|
# Prüfe, ob bereits eine Verknüpfung für dieses Dokument und diesen Pächter existiert
|
|
existing_link = DokumentLink.objects.filter(
|
|
paperless_document_id=paperless_id, paechter_id=verpachtung.paechter.id
|
|
).first()
|
|
|
|
if not existing_link:
|
|
# Erstelle automatische Pächter-Verknüpfung
|
|
DokumentLink.objects.create(
|
|
paperless_document_id=paperless_id,
|
|
titel=paperless_title,
|
|
kontext="paechter",
|
|
paechter_id=verpachtung.paechter.id,
|
|
)
|
|
return True
|
|
except (LandVerpachtung.DoesNotExist, Exception):
|
|
pass
|
|
return False
|
|
|
|
|
|
@csrf_exempt
|
|
@api_view(["POST"])
|
|
def link_document_create(request):
|
|
"""Erstellt eine Verknüpfung zwischen einem Paperless-Dokument und einem Datensatz"""
|
|
from django.db import transaction
|
|
|
|
try:
|
|
# Robust JSON parsing to tolerate Latin-1 encoded payloads (e.g., umlauts)
|
|
try:
|
|
payload = request.data
|
|
except Exception:
|
|
raw = request.body
|
|
try:
|
|
payload = json.loads(raw.decode("utf-8"))
|
|
except UnicodeDecodeError:
|
|
payload = json.loads(raw.decode("latin-1"))
|
|
|
|
paperless_id = payload.get("paperless_id")
|
|
paperless_title = payload.get("paperless_title")
|
|
paperless_url = payload.get("paperless_url")
|
|
link_type = payload.get(
|
|
"link_type"
|
|
) # 'destinataer', 'land', 'verpachtung', 'paechter', 'abrechnung'
|
|
link_id = payload.get("link_id")
|
|
|
|
if not all([paperless_id, paperless_title, paperless_url, link_type, link_id]):
|
|
return Response({"error": "Alle Felder sind erforderlich"}, status=400)
|
|
|
|
with transaction.atomic():
|
|
# Erstelle den DokumentLink
|
|
dokument_link = DokumentLink.objects.create(
|
|
paperless_document_id=paperless_id, # Korrigiert: 'paperless_document_id' statt 'paperless_id'
|
|
titel=paperless_title, # Korrigiert: 'titel' statt 'title'
|
|
kontext="anderes",
|
|
)
|
|
|
|
# Setze die entsprechenden UUID-Felder basierend auf dem Link-Typ
|
|
if link_type == "destinataer":
|
|
dokument_link.destinataer_id = link_id
|
|
elif link_type == "land":
|
|
dokument_link.land_id = link_id
|
|
elif link_type == "verpachtung":
|
|
# Use new LandVerpachtung field instead of legacy
|
|
dokument_link.land_verpachtung_id = link_id
|
|
elif link_type == "paechter":
|
|
dokument_link.paechter_id = link_id
|
|
elif link_type == "foerderung":
|
|
dokument_link.foerderung_id = link_id
|
|
elif link_type == "rentmeister":
|
|
dokument_link.rentmeister_id = link_id
|
|
elif link_type == "abrechnung":
|
|
dokument_link.abrechnung_id = link_id
|
|
|
|
dokument_link.save()
|
|
|
|
# Log the document linking action
|
|
from stiftung.audit import log_link
|
|
|
|
try:
|
|
# Get the linked entity name for logging
|
|
entity_name = paperless_title
|
|
if link_type == "destinataer":
|
|
from stiftung.models import Destinataer
|
|
|
|
entity = Destinataer.objects.get(id=link_id)
|
|
target_name = entity.get_full_name()
|
|
elif link_type == "land":
|
|
from stiftung.models import Land
|
|
|
|
entity = Land.objects.get(id=link_id)
|
|
target_name = str(entity)
|
|
elif link_type == "paechter":
|
|
from stiftung.models import Paechter
|
|
|
|
entity = Paechter.objects.get(id=link_id)
|
|
target_name = f"{entity.vorname} {entity.nachname}".strip()
|
|
elif link_type == "foerderung":
|
|
from stiftung.models import Foerderung
|
|
|
|
entity = Foerderung.objects.get(id=link_id)
|
|
target_name = f"{entity.destinataer.get_full_name() if entity.destinataer else 'Unbekannt'} - {entity.jahr}"
|
|
elif link_type == "verpachtung":
|
|
entity = LandVerpachtung.objects.get(id=link_id)
|
|
target_name = str(entity)
|
|
elif link_type == "rentmeister":
|
|
from stiftung.models import Rentmeister
|
|
|
|
entity = Rentmeister.objects.get(id=link_id)
|
|
target_name = entity.get_full_name()
|
|
else:
|
|
target_name = f"ID {link_id}"
|
|
|
|
log_link(
|
|
request=request,
|
|
entity_type="dokumentlink",
|
|
entity_id=str(dokument_link.id),
|
|
entity_name=entity_name,
|
|
target_type=link_type,
|
|
target_name=target_name,
|
|
)
|
|
except Exception as e:
|
|
# Don't fail the main operation if logging fails
|
|
print(f"Audit logging failed: {e}")
|
|
|
|
# Automatische Pächter-Verknüpfung NACH der Haupttransaktion
|
|
paechter_linked = False
|
|
if link_type == "verpachtung":
|
|
paechter_linked = create_paechter_link_for_verpachtung(
|
|
paperless_id, paperless_title, link_id
|
|
)
|
|
|
|
message = f"Dokument erfolgreich mit {link_type} verknüpft"
|
|
if paechter_linked:
|
|
message += " (automatisch auch mit Pächter verknüpft)"
|
|
|
|
return Response(
|
|
{"success": True, "message": message, "dokument_id": dokument_link.id}
|
|
)
|
|
|
|
except Exception as e:
|
|
return Response(
|
|
{"error": f"Fehler beim Erstellen der Verknüpfung: {str(e)}"}, status=500
|
|
)
|
|
|
|
|
|
# Legacy dokument_verknuepfung view removed - use dokument_management instead
|
|
|
|
|
|
@api_view(["GET"])
|
|
def link_document_list(request):
|
|
"""Listet alle bestehenden Dokument-Verknüpfungen auf, gruppiert nach Paperless-Document-ID"""
|
|
try:
|
|
dokument_links = DokumentLink.objects.all().order_by("-id")
|
|
|
|
# Group links by paperless_document_id to show multiple links per document
|
|
links_by_document = {}
|
|
|
|
for link in dokument_links:
|
|
paperless_id = link.paperless_document_id
|
|
|
|
if paperless_id not in links_by_document:
|
|
links_by_document[paperless_id] = {
|
|
"paperless_id": paperless_id,
|
|
"title": link.titel,
|
|
"paperless_url": f"/api/paperless/documents/{paperless_id}/",
|
|
"links": [],
|
|
}
|
|
|
|
# Create link info
|
|
link_info = {
|
|
"id": str(link.id), # Ensure UUID is stringified
|
|
"kontext": link.kontext,
|
|
"link_type": None,
|
|
"linked_object": None,
|
|
}
|
|
|
|
# Determine link type and get linked object details
|
|
if link.destinataer_id:
|
|
link_info["link_type"] = "destinataer"
|
|
try:
|
|
dest = Destinataer.objects.get(id=link.destinataer_id)
|
|
link_info["linked_object"] = {
|
|
"id": str(dest.id),
|
|
"type": "Destinatär",
|
|
"name": (
|
|
f"{dest.vorname} {dest.nachname}".strip()
|
|
if dest.vorname
|
|
else dest.institution
|
|
),
|
|
"details": (
|
|
f"Institution: {dest.institution}"
|
|
if dest.institution
|
|
else f"Person: {dest.vorname} {dest.nachname}".strip()
|
|
),
|
|
}
|
|
except Destinataer.DoesNotExist:
|
|
link_info["linked_object"] = {
|
|
"type": "Destinatär",
|
|
"name": "Gelöscht",
|
|
"details": "Datensatz nicht mehr verfügbar",
|
|
}
|
|
|
|
elif link.land_id:
|
|
link_info["link_type"] = "land"
|
|
try:
|
|
land = Land.objects.get(id=link.land_id)
|
|
link_info["linked_object"] = {
|
|
"id": str(land.id),
|
|
"type": "Land",
|
|
"name": f"{land.gemarkung} - {land.gemeinde}",
|
|
"details": f"Flur: {land.flur}, Größe: {land.groesse_qm or 0} m²",
|
|
}
|
|
except Land.DoesNotExist:
|
|
link_info["linked_object"] = {
|
|
"type": "Land",
|
|
"name": "Gelöscht",
|
|
"details": "Datensatz nicht mehr verfügbar",
|
|
}
|
|
|
|
elif link.paechter_id:
|
|
link_info["link_type"] = "paechter"
|
|
try:
|
|
p = Paechter.objects.get(id=link.paechter_id)
|
|
link_info["linked_object"] = {
|
|
"id": str(p.id),
|
|
"type": "Pächter",
|
|
"name": f"{(p.vorname + ' ') if p.vorname else ''}{p.nachname}",
|
|
"details": f"{p.ort or ''}",
|
|
}
|
|
except Paechter.DoesNotExist:
|
|
link_info["linked_object"] = {
|
|
"type": "Pächter",
|
|
"name": "Gelöscht",
|
|
"details": "Datensatz nicht mehr verfügbar",
|
|
}
|
|
|
|
elif link.land_verpachtung_id:
|
|
link_info["link_type"] = "verpachtung"
|
|
try:
|
|
from stiftung.models import LandVerpachtung
|
|
|
|
verp = LandVerpachtung.objects.select_related(
|
|
"paechter", "land"
|
|
).get(id=link.land_verpachtung_id)
|
|
link_info["linked_object"] = {
|
|
"id": str(verp.id),
|
|
"type": "Verpachtung",
|
|
"name": f"Verpachtung {(verp.paechter.vorname + ' ') if verp.paechter and verp.paechter.vorname else ''}{verp.paechter.nachname if verp.paechter else 'Unbekannt'}",
|
|
"details": f"Land: {verp.land.gemarkung} - {verp.land.gemeinde}, Pächter: {(verp.paechter.vorname + ' ') if verp.paechter and verp.paechter.vorname else ''}{verp.paechter.nachname if verp.paechter else 'Unbekannt'}, Vertrag: {verp.vertragsnummer}",
|
|
}
|
|
except LandVerpachtung.DoesNotExist:
|
|
link_info["linked_object"] = {
|
|
"type": "Verpachtung",
|
|
"name": "Gelöscht",
|
|
"details": "Datensatz nicht mehr verfügbar",
|
|
}
|
|
|
|
elif link.rentmeister_id:
|
|
link_info["link_type"] = "rentmeister"
|
|
try:
|
|
from stiftung.models import Rentmeister
|
|
|
|
rentmeister = Rentmeister.objects.get(id=link.rentmeister_id)
|
|
link_info["linked_object"] = {
|
|
"id": str(rentmeister.id),
|
|
"type": "Rentmeister",
|
|
"name": f"{(rentmeister.anrede + ' ') if rentmeister.anrede else ''}{(rentmeister.vorname + ' ') if rentmeister.vorname else ''}{rentmeister.nachname}"
|
|
+ (f" ({rentmeister.titel})" if rentmeister.titel else ""),
|
|
"details": f"Seit: {rentmeister.seit_datum.strftime('%d.%m.%Y') if rentmeister.seit_datum else 'Unbekannt'}"
|
|
+ (
|
|
f", Tel: {rentmeister.telefon}"
|
|
if rentmeister.telefon
|
|
else ""
|
|
)
|
|
+ (f", {rentmeister.email}" if rentmeister.email else ""),
|
|
"url": f"/geschaeftsfuehrung/rentmeister/{rentmeister.id}/",
|
|
}
|
|
except Rentmeister.DoesNotExist:
|
|
link_info["linked_object"] = {
|
|
"type": "Rentmeister",
|
|
"name": "Gelöscht",
|
|
"details": "Datensatz nicht mehr verfügbar",
|
|
}
|
|
|
|
elif link.abrechnung_id:
|
|
link_info["link_type"] = "abrechnung"
|
|
try:
|
|
abrechnung = LandAbrechnung.objects.select_related("land").get(
|
|
id=link.abrechnung_id
|
|
)
|
|
link_info["linked_object"] = {
|
|
"id": str(abrechnung.id),
|
|
"type": "Abrechnung",
|
|
"name": f"Abrechnung {abrechnung.abrechnungsjahr} - {abrechnung.land.gemarkung}",
|
|
"details": f"Land: {abrechnung.land.gemarkung} - {abrechnung.land.gemeinde}, Flur {abrechnung.land.flur}, Jahr {abrechnung.abrechnungsjahr}",
|
|
"url": f"/laendereien/abrechnungen/{abrechnung.id}/",
|
|
}
|
|
except LandAbrechnung.DoesNotExist:
|
|
link_info["linked_object"] = {
|
|
"type": "Abrechnung",
|
|
"name": "Gelöscht",
|
|
"details": "Datensatz nicht mehr verfügbar",
|
|
}
|
|
|
|
links_by_document[paperless_id]["links"].append(link_info)
|
|
|
|
# Convert to list format for frontend
|
|
results = list(links_by_document.values())
|
|
|
|
return Response(
|
|
{
|
|
"total_documents": len(results),
|
|
"total_links": sum(len(doc["links"]) for doc in results),
|
|
"links": results,
|
|
}
|
|
)
|
|
|
|
except Exception as e:
|
|
return Response(
|
|
{"error": f"Fehler beim Abrufen der Verknüpfungen: {str(e)}"}, status=500
|
|
)
|
|
|
|
|
|
@csrf_exempt
|
|
@api_view(["POST"])
|
|
def link_document_update(request):
|
|
"""Aktualisiert die Verknüpfung eines Dokuments (Re-Linking auf anderen Datensatz/Kontext)."""
|
|
from django.db import transaction
|
|
|
|
try:
|
|
# Robust JSON parsing to tolerate Latin-1 encoded payloads (e.g., umlauts)
|
|
try:
|
|
payload = request.data
|
|
except Exception:
|
|
raw = request.body
|
|
try:
|
|
payload = json.loads(raw.decode("utf-8"))
|
|
except UnicodeDecodeError:
|
|
payload = json.loads(raw.decode("latin-1"))
|
|
|
|
link_id = payload.get("link_id")
|
|
link_type = payload.get(
|
|
"link_type"
|
|
) # 'destinataer' | 'land' | 'verpachtung' | 'paechter' | 'rentmeister'
|
|
link_target_id = payload.get("link_id_target")
|
|
if not all([link_id, link_type, link_target_id]):
|
|
return Response(
|
|
{"error": "link_id, link_type und link_id_target sind erforderlich"},
|
|
status=400,
|
|
)
|
|
|
|
with transaction.atomic():
|
|
link = DokumentLink.objects.get(id=link_id)
|
|
old_verpachtung_id = (
|
|
link.verpachtung_id
|
|
) # Merke alte Verpachtung für Cleanup
|
|
paperless_id_for_cleanup = link.paperless_document_id
|
|
titel_for_new_link = link.titel
|
|
|
|
# Reset all associations first
|
|
link.destinataer_id = None
|
|
link.land_id = None
|
|
link.verpachtung_id = None
|
|
link.paechter_id = None
|
|
link.foerderung_id = None
|
|
link.rentmeister_id = None
|
|
link.kontext = link_type
|
|
|
|
if link_type == "destinataer":
|
|
link.destinataer_id = link_target_id
|
|
elif link_type == "land":
|
|
link.land_id = link_target_id
|
|
elif link_type == "verpachtung":
|
|
link.verpachtung_id = link_target_id
|
|
elif link_type == "paechter":
|
|
link.paechter_id = link_target_id
|
|
elif link_type == "foerderung":
|
|
link.foerderung_id = link_target_id
|
|
elif link_type == "rentmeister":
|
|
link.rentmeister_id = link_target_id
|
|
else:
|
|
return Response({"error": "Ungültiger link_type"}, status=400)
|
|
|
|
link.save()
|
|
|
|
# Automatische Pächter-Verknüpfung und Cleanup NACH der Haupttransaktion
|
|
paechter_linked = False
|
|
if link_type == "verpachtung":
|
|
paechter_linked = create_paechter_link_for_verpachtung(
|
|
paperless_id_for_cleanup, titel_for_new_link, link_target_id
|
|
)
|
|
|
|
# Cleanup: Entferne verwaiste Pächter-Verknüpfungen wenn von Verpachtung weg geändert
|
|
if old_verpachtung_id and link_type != "verpachtung":
|
|
try:
|
|
old_verpachtung = LandVerpachtung.objects.select_related(
|
|
"paechter"
|
|
).get(id=old_verpachtung_id)
|
|
if old_verpachtung.paechter:
|
|
# Prüfe ob noch andere Verpachtungs-Links für diesen Pächter existieren
|
|
other_verpachtung_links = DokumentLink.objects.filter(
|
|
paperless_document_id=paperless_id_for_cleanup,
|
|
verpachtung__paechter_id=old_verpachtung.paechter.id,
|
|
).exists()
|
|
|
|
if not other_verpachtung_links:
|
|
# Entferne automatisch erstellte Pächter-Verknüpfung
|
|
DokumentLink.objects.filter(
|
|
paperless_document_id=paperless_id_for_cleanup,
|
|
paechter_id=old_verpachtung.paechter.id,
|
|
kontext="paechter",
|
|
).delete()
|
|
except (LandVerpachtung.DoesNotExist, Exception):
|
|
pass
|
|
|
|
message = "Verknüpfung aktualisiert"
|
|
if paechter_linked:
|
|
message += " (automatisch auch mit Pächter verknüpft)"
|
|
|
|
return Response({"success": True, "message": message})
|
|
except DokumentLink.DoesNotExist:
|
|
return Response({"error": "Verknüpfung nicht gefunden"}, status=404)
|
|
except Exception as e:
|
|
return Response({"error": f"Unerwarteter Fehler: {e}"}, status=500)
|
|
|
|
|
|
@csrf_exempt
|
|
@api_view(["DELETE"])
|
|
def link_document_delete(request, link_id):
|
|
"""Löscht eine bestehende Verknüpfung."""
|
|
from django.db import transaction
|
|
|
|
try:
|
|
with transaction.atomic():
|
|
link = DokumentLink.objects.get(id=link_id)
|
|
verpachtung_id_for_cleanup = link.verpachtung_id
|
|
paperless_id_for_cleanup = link.paperless_document_id
|
|
|
|
# Log the unlinking action before deletion
|
|
from stiftung.audit import log_unlink
|
|
|
|
try:
|
|
# Determine what entity this was linked to
|
|
target_type = "unknown"
|
|
target_name = "Unknown"
|
|
|
|
if link.destinataer_id:
|
|
target_type = "destinataer"
|
|
try:
|
|
entity = Destinataer.objects.get(id=link.destinataer_id)
|
|
target_name = entity.get_full_name()
|
|
except Destinataer.DoesNotExist:
|
|
target_name = f"Destinatär ID {link.destinataer_id}"
|
|
elif link.land_id:
|
|
target_type = "land"
|
|
try:
|
|
entity = Land.objects.get(id=link.land_id)
|
|
target_name = str(entity)
|
|
except Land.DoesNotExist:
|
|
target_name = f"Land ID {link.land_id}"
|
|
elif link.paechter_id:
|
|
target_type = "paechter"
|
|
try:
|
|
entity = Paechter.objects.get(id=link.paechter_id)
|
|
target_name = f"{entity.vorname} {entity.nachname}".strip()
|
|
except Paechter.DoesNotExist:
|
|
target_name = f"Pächter ID {link.paechter_id}"
|
|
elif link.verpachtung_id:
|
|
target_type = "verpachtung"
|
|
try:
|
|
entity = LandVerpachtung.objects.get(id=link.verpachtung_id)
|
|
target_name = str(entity)
|
|
except LandVerpachtung.DoesNotExist:
|
|
target_name = f"Verpachtung ID {link.verpachtung_id}"
|
|
elif link.rentmeister_id:
|
|
target_type = "rentmeister"
|
|
try:
|
|
from stiftung.models import Rentmeister
|
|
|
|
entity = Rentmeister.objects.get(id=link.rentmeister_id)
|
|
target_name = entity.get_full_name()
|
|
except Rentmeister.DoesNotExist:
|
|
target_name = f"Rentmeister ID {link.rentmeister_id}"
|
|
|
|
log_unlink(
|
|
request=request,
|
|
entity_type="dokumentlink",
|
|
entity_id=str(link.id),
|
|
entity_name=link.titel,
|
|
target_type=target_type,
|
|
target_name=target_name,
|
|
)
|
|
except Exception as e:
|
|
# Don't fail the main operation if logging fails
|
|
print(f"Audit logging failed: {e}")
|
|
|
|
link.delete()
|
|
|
|
# Cleanup NACH der Haupttransaktion: Wenn eine Verpachtungs-Verknüpfung gelöscht wurde, prüfe Pächter-Links
|
|
if verpachtung_id_for_cleanup:
|
|
try:
|
|
verpachtung = LandVerpachtung.objects.select_related("paechter").get(
|
|
id=verpachtung_id_for_cleanup
|
|
)
|
|
if verpachtung.paechter:
|
|
# Prüfe ob noch andere Verpachtungs-Links für diesen Pächter existieren
|
|
other_verpachtung_links = DokumentLink.objects.filter(
|
|
paperless_document_id=paperless_id_for_cleanup,
|
|
verpachtung__paechter_id=verpachtung.paechter.id,
|
|
).exists()
|
|
|
|
if not other_verpachtung_links:
|
|
# Entferne automatisch erstellte Pächter-Verknüpfung
|
|
DokumentLink.objects.filter(
|
|
paperless_document_id=paperless_id_for_cleanup,
|
|
paechter_id=verpachtung.paechter.id,
|
|
kontext="paechter",
|
|
).delete()
|
|
except (LandVerpachtung.DoesNotExist, Exception):
|
|
pass
|
|
|
|
return Response({"success": True})
|
|
except DokumentLink.DoesNotExist:
|
|
return Response({"error": "Verknüpfung nicht gefunden"}, status=404)
|
|
except Exception as e:
|
|
return Response({"error": f"Unerwarteter Fehler: {e}"}, status=500)
|
|
|
|
|
|
@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)
|
|
|
|
# Get quarterly confirmation statistics
|
|
quarterly_stats = {}
|
|
total_quarterly = VierteljahresNachweis.objects.count()
|
|
for status_code, status_name in VierteljahresNachweis.STATUS_CHOICES:
|
|
count = VierteljahresNachweis.objects.filter(status=status_code).count()
|
|
quarterly_stats[status_code] = {
|
|
'name': status_name,
|
|
'count': count
|
|
}
|
|
|
|
context = {
|
|
"unterstuetzungen": qs,
|
|
"status_filter": status,
|
|
"quarterly_stats": quarterly_stats,
|
|
"total_quarterly": total_quarterly,
|
|
}
|
|
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 (handle both 'fields' and 'selected_fields' parameter names)
|
|
selected_fields_param = ""
|
|
if request.method == "POST":
|
|
# Try 'fields' first (new format), then 'selected_fields' (legacy)
|
|
fields_list = request.POST.getlist("fields")
|
|
if fields_list:
|
|
selected_fields_param = ",".join(fields_list)
|
|
else:
|
|
selected_fields_param = request.POST.get("selected_fields", "")
|
|
else:
|
|
# Try 'fields' first (new format), then 'selected_fields' (legacy)
|
|
fields_list = request.GET.getlist("fields")
|
|
if fields_list:
|
|
selected_fields_param = ",".join(fields_list)
|
|
else:
|
|
selected_fields_param = 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",
|
|
"empfaenger_iban",
|
|
"verwendungszweck",
|
|
"status",
|
|
"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 (handle both 'fields' and 'selected_fields' parameter names)
|
|
selected_fields_param = ""
|
|
if request.method == "POST":
|
|
# Try 'fields' first (new format), then 'selected_fields' (legacy)
|
|
fields_list = request.POST.getlist("fields")
|
|
if fields_list:
|
|
selected_fields_param = ",".join(fields_list)
|
|
else:
|
|
selected_fields_param = request.POST.get("selected_fields", "")
|
|
else:
|
|
# Try 'fields' first (new format), then 'selected_fields' (legacy)
|
|
fields_list = request.GET.getlist("fields")
|
|
if fields_list:
|
|
selected_fields_param = ",".join(fields_list)
|
|
else:
|
|
selected_fields_param = 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",
|
|
"empfaenger_iban",
|
|
"verwendungszweck",
|
|
"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),
|
|
"anrede": destinataer.get_anrede_display() if hasattr(destinataer, 'anrede') and destinataer.anrede else None,
|
|
"titel": destinataer.titel if hasattr(destinataer, 'titel') else None,
|
|
"vorname": destinataer.vorname,
|
|
"nachname": destinataer.nachname,
|
|
"geburtsdatum": (
|
|
destinataer.geburtsdatum.isoformat()
|
|
if destinataer.geburtsdatum
|
|
else None
|
|
),
|
|
"email": destinataer.email,
|
|
"telefon": destinataer.telefon,
|
|
"mobil": destinataer.mobil if hasattr(destinataer, 'mobil') else None,
|
|
"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),
|
|
)
|
|
|
|
# 4. Quarterly Confirmations with documents
|
|
quarterly_confirmations = destinataer.quartalseinreichungen.all().order_by("-jahr", "-quartal")
|
|
quarterly_data = []
|
|
|
|
for confirmation in quarterly_confirmations:
|
|
confirmation_data = {
|
|
"id": str(confirmation.id),
|
|
"jahr": confirmation.jahr,
|
|
"quartal": confirmation.quartal,
|
|
"quartal_display": confirmation.get_quartal_display(),
|
|
"status": confirmation.status,
|
|
"status_display": confirmation.get_status_display(),
|
|
"studiennachweis_erforderlich": confirmation.studiennachweis_erforderlich,
|
|
"studiennachweis_eingereicht": confirmation.studiennachweis_eingereicht,
|
|
"studiennachweis_bemerkung": confirmation.studiennachweis_bemerkung,
|
|
"einkommenssituation_bestaetigt": confirmation.einkommenssituation_bestaetigt,
|
|
"einkommenssituation_text": confirmation.einkommenssituation_text,
|
|
"vermogenssituation_bestaetigt": confirmation.vermogenssituation_bestaetigt,
|
|
"vermogenssituation_text": confirmation.vermogenssituation_text,
|
|
"weitere_dokumente_beschreibung": confirmation.weitere_dokumente_beschreibung,
|
|
"interne_notizen": confirmation.interne_notizen,
|
|
"erstellt_am": confirmation.erstellt_am.isoformat(),
|
|
"aktualisiert_am": confirmation.aktualisiert_am.isoformat(),
|
|
"eingereicht_am": confirmation.eingereicht_am.isoformat() if confirmation.eingereicht_am else None,
|
|
"geprueft_am": confirmation.geprueft_am.isoformat() if confirmation.geprueft_am else None,
|
|
"geprueft_von": confirmation.geprueft_von.username if confirmation.geprueft_von else None,
|
|
"faelligkeitsdatum": confirmation.faelligkeitsdatum.isoformat() if confirmation.faelligkeitsdatum else None,
|
|
"completion_percentage": confirmation.get_completion_percentage(),
|
|
"uploaded_files": []
|
|
}
|
|
|
|
# Add uploaded files from quarterly confirmation
|
|
quarterly_files = [
|
|
("studiennachweis", confirmation.studiennachweis_datei),
|
|
("einkommenssituation", confirmation.einkommenssituation_datei),
|
|
("vermogenssituation", confirmation.vermogenssituation_datei),
|
|
("weitere_dokumente", confirmation.weitere_dokumente),
|
|
]
|
|
|
|
for file_type, file_field in quarterly_files:
|
|
if file_field and os.path.exists(file_field.path):
|
|
file_info = {
|
|
"type": file_type,
|
|
"name": os.path.basename(file_field.name),
|
|
"path": file_field.name
|
|
}
|
|
confirmation_data["uploaded_files"].append(file_info)
|
|
|
|
# Add file to ZIP
|
|
safe_filename = f"quarterly_{confirmation.jahr}_Q{confirmation.quartal}_{file_type}_{os.path.basename(file_field.name)}"
|
|
zipf.write(
|
|
file_field.path,
|
|
f"vierteljahresnachweis/{safe_filename}"
|
|
)
|
|
|
|
quarterly_data.append(confirmation_data)
|
|
|
|
if quarterly_data:
|
|
zipf.writestr(
|
|
"vierteljahresnachweis.json",
|
|
json.dumps(quarterly_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 format
|
|
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 to temporary location
|
|
import os
|
|
import tempfile
|
|
|
|
temp_dir = tempfile.mkdtemp()
|
|
backup_path = os.path.join(temp_dir, backup_file.name)
|
|
|
|
try:
|
|
with open(backup_path, "wb+") as destination:
|
|
for chunk in backup_file.chunks():
|
|
destination.write(chunk)
|
|
|
|
# Validate the backup file
|
|
from stiftung.backup_utils import validate_backup_file
|
|
|
|
is_valid, message = validate_backup_file(backup_path)
|
|
if not is_valid:
|
|
messages.error(request, f"Ungültiges Backup: {message}")
|
|
return redirect("stiftung:backup_management")
|
|
|
|
# Show validation success
|
|
messages.info(request, f"Backup validiert: {message}")
|
|
|
|
# Create restore job
|
|
restore_job = BackupJob.objects.create(
|
|
operation="restore",
|
|
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. '
|
|
f'Überwachen Sie den Fortschritt in der Backup-Historie.'
|
|
)
|
|
return redirect("stiftung:backup_management")
|
|
|
|
except Exception as e:
|
|
messages.error(request, f"Fehler beim Verarbeiten der Backup-Datei: {e}")
|
|
return redirect("stiftung:backup_management")
|
|
|
|
return redirect("stiftung:backup_management")
|
|
|
|
|
|
@login_required
|
|
def backup_cancel(request, backup_id):
|
|
"""Cancel a running backup job"""
|
|
try:
|
|
backup_job = BackupJob.objects.get(id=backup_id)
|
|
|
|
# Only allow cancelling running or pending jobs
|
|
if backup_job.status not in ['running', 'pending']:
|
|
messages.error(request, "Nur laufende oder wartende Backups können abgebrochen werden.")
|
|
return redirect("stiftung:backup_management")
|
|
|
|
# Check if user has permission to cancel (either own job or admin)
|
|
if backup_job.created_by != request.user and not request.user.is_staff:
|
|
messages.error(request, "Sie können nur Ihre eigenen Backup-Jobs abbrechen.")
|
|
return redirect("stiftung:backup_management")
|
|
|
|
# Mark as cancelled
|
|
from django.utils import timezone
|
|
backup_job.status = "cancelled"
|
|
backup_job.completed_at = timezone.now()
|
|
backup_job.error_message = f"Abgebrochen von {request.user.username}"
|
|
backup_job.save()
|
|
|
|
# Log the cancellation
|
|
from stiftung.audit import log_system_action
|
|
log_system_action(
|
|
request=request,
|
|
action="backup_cancel",
|
|
description=f"Backup-Job abgebrochen: {backup_job.get_backup_type_display()}",
|
|
details={"backup_job_id": str(backup_job.id)},
|
|
)
|
|
|
|
messages.success(request, f"Backup-Job wurde abgebrochen.")
|
|
|
|
except BackupJob.DoesNotExist:
|
|
messages.error(request, "Backup-Job nicht gefunden.")
|
|
except Exception as e:
|
|
messages.error(request, f"Fehler beim Abbrechen des Backup-Jobs: {e}")
|
|
|
|
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
|
|
|
|
# Get quarterly confirmation statistics
|
|
quarterly_stats = {}
|
|
total_quarterly = VierteljahresNachweis.objects.count()
|
|
for status_code, status_name in VierteljahresNachweis.STATUS_CHOICES:
|
|
count = VierteljahresNachweis.objects.filter(status=status_code).count()
|
|
quarterly_stats[status_code] = {
|
|
'name': status_name,
|
|
'count': count
|
|
}
|
|
|
|
# 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,
|
|
"quarterly_stats": quarterly_stats,
|
|
"total_quarterly": total_quarterly,
|
|
"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)
|
|
|
|
|
|
@login_required
|
|
def quarterly_confirmation_update(request, pk):
|
|
"""Update quarterly confirmation for destinataer"""
|
|
nachweis = get_object_or_404(VierteljahresNachweis, pk=pk)
|
|
|
|
if request.method == "POST":
|
|
form = VierteljahresNachweisForm(request.POST, request.FILES, instance=nachweis)
|
|
if form.is_valid():
|
|
quarterly_proof = form.save(commit=False)
|
|
|
|
# Calculate current status before saving
|
|
old_status = nachweis.status
|
|
|
|
# Auto-update status based on completion
|
|
if quarterly_proof.is_complete():
|
|
if quarterly_proof.status in ['offen', 'teilweise']:
|
|
quarterly_proof.status = 'eingereicht'
|
|
quarterly_proof.eingereicht_am = timezone.now()
|
|
else:
|
|
# If not complete, set to teilweise if some fields are filled
|
|
has_partial_data = (
|
|
quarterly_proof.einkommenssituation_bestaetigt or
|
|
quarterly_proof.vermogenssituation_bestaetigt or
|
|
quarterly_proof.studiennachweis_eingereicht
|
|
)
|
|
if has_partial_data and quarterly_proof.status == 'offen':
|
|
quarterly_proof.status = 'teilweise'
|
|
|
|
quarterly_proof.save()
|
|
|
|
# Try to create automatic support payment if complete
|
|
if quarterly_proof.is_complete() and quarterly_proof.status == 'eingereicht':
|
|
support_payment = create_quarterly_support_payment(quarterly_proof)
|
|
if support_payment:
|
|
messages.success(
|
|
request,
|
|
f"Automatische Unterstützung über {support_payment.betrag}€ für {support_payment.destinataer.get_full_name()} wurde erstellt (fällig am {support_payment.faellig_am.strftime('%d.%m.%Y')})."
|
|
)
|
|
else:
|
|
# Log why payment wasn't created
|
|
reasons = []
|
|
if not quarterly_proof.destinataer.vierteljaehrlicher_betrag:
|
|
reasons.append("kein vierteljährlicher Betrag hinterlegt")
|
|
if not quarterly_proof.destinataer.iban:
|
|
reasons.append("keine IBAN hinterlegt")
|
|
if not quarterly_proof.destinataer.standard_konto and not StiftungsKonto.objects.exists():
|
|
reasons.append("kein Auszahlungskonto verfügbar")
|
|
|
|
if reasons:
|
|
messages.warning(
|
|
request,
|
|
f"Automatische Unterstützung konnte nicht erstellt werden: {', '.join(reasons)}"
|
|
)
|
|
|
|
# Debug message to see what happened
|
|
status_changed = old_status != quarterly_proof.status
|
|
status_msg = f" (Status: {old_status} → {quarterly_proof.status})" if status_changed else f" (Status: {quarterly_proof.status})"
|
|
|
|
messages.success(
|
|
request,
|
|
f"Vierteljahresnachweis für {nachweis.destinataer.get_full_name()} "
|
|
f"({nachweis.jahr} Q{nachweis.quartal}) wurde erfolgreich aktualisiert{status_msg}."
|
|
)
|
|
return redirect("stiftung:destinataer_detail", pk=nachweis.destinataer.pk)
|
|
else:
|
|
# Add form errors to messages
|
|
for field, errors in form.errors.items():
|
|
for error in errors:
|
|
messages.error(request, f"Fehler in {field}: {error}")
|
|
|
|
# If GET request or form errors, redirect back to destinataer detail
|
|
return redirect("stiftung:destinataer_detail", pk=nachweis.destinataer.pk)
|
|
|
|
|
|
def create_quarterly_support_payment(nachweis):
|
|
"""
|
|
Get or create a single support payment for this quarterly confirmation
|
|
Ensures only one payment exists per destinataer per quarter
|
|
"""
|
|
destinataer = nachweis.destinataer
|
|
|
|
# Check if all requirements are met
|
|
if not nachweis.is_complete():
|
|
return None
|
|
|
|
# Check if destinataer has required payment info
|
|
if not destinataer.vierteljaehrlicher_betrag or destinataer.vierteljaehrlicher_betrag <= 0:
|
|
return None
|
|
|
|
if not destinataer.iban:
|
|
return None
|
|
|
|
# Calculate quarter date range for more robust search
|
|
quarter_start = datetime(nachweis.jahr, (nachweis.quartal - 1) * 3 + 1, 1).date()
|
|
if nachweis.quartal == 4: # Q4 special case
|
|
quarter_end = datetime(nachweis.jahr + 1, 1, 1).date()
|
|
else:
|
|
quarter_end = datetime(nachweis.jahr, nachweis.quartal * 3 + 1, 1).date()
|
|
|
|
# Search for existing payment - use broader criteria to catch all possibilities
|
|
existing_payment = DestinataerUnterstuetzung.objects.filter(
|
|
destinataer=destinataer,
|
|
faellig_am__gte=quarter_start,
|
|
faellig_am__lt=quarter_end
|
|
).filter(
|
|
Q(beschreibung__contains=f"Q{nachweis.quartal}/{nachweis.jahr}") |
|
|
Q(beschreibung__contains=f"Vierteljährliche Unterstützung")
|
|
).first()
|
|
|
|
if existing_payment:
|
|
# Update existing payment to ensure it matches current requirements
|
|
existing_payment.betrag = destinataer.vierteljaehrlicher_betrag
|
|
existing_payment.empfaenger_iban = destinataer.iban
|
|
existing_payment.empfaenger_name = destinataer.get_full_name()
|
|
existing_payment.verwendungszweck = f"Vierteljährliche Unterstützung Q{nachweis.quartal}/{nachweis.jahr} - {destinataer.get_full_name()}"
|
|
existing_payment.beschreibung = f"Vierteljährliche Unterstützung Q{nachweis.quartal}/{nachweis.jahr} (automatisch erstellt)"
|
|
existing_payment.save()
|
|
return existing_payment
|
|
|
|
# Get default payment account
|
|
default_konto = destinataer.standard_konto
|
|
if not default_konto:
|
|
# Try to get any StiftungsKonto
|
|
default_konto = StiftungsKonto.objects.first()
|
|
if not default_konto:
|
|
return None
|
|
|
|
# Calculate payment due date (last day of quarter)
|
|
quarter_end_month = nachweis.quartal * 3
|
|
|
|
if nachweis.quartal == 1: # Q1: January-March (ends March 31)
|
|
quarter_end_day = 31
|
|
elif nachweis.quartal == 2: # Q2: April-June (ends June 30)
|
|
quarter_end_day = 30
|
|
elif nachweis.quartal == 3: # Q3: July-September (ends September 30)
|
|
quarter_end_day = 30
|
|
else: # Q4: October-December (ends December 31)
|
|
quarter_end_day = 31
|
|
|
|
payment_due_date = datetime(nachweis.jahr, quarter_end_month, quarter_end_day).date()
|
|
|
|
# Create the support payment
|
|
payment = DestinataerUnterstuetzung.objects.create(
|
|
destinataer=destinataer,
|
|
konto=default_konto,
|
|
betrag=destinataer.vierteljaehrlicher_betrag,
|
|
faellig_am=payment_due_date,
|
|
status='geplant',
|
|
beschreibung=f"Vierteljährliche Unterstützung Q{nachweis.quartal}/{nachweis.jahr} (automatisch erstellt)",
|
|
empfaenger_iban=destinataer.iban,
|
|
empfaenger_name=destinataer.get_full_name(),
|
|
verwendungszweck=f"Vierteljährliche Unterstützung Q{nachweis.quartal}/{nachweis.jahr} - {destinataer.get_full_name()}",
|
|
erstellt_am=timezone.now(),
|
|
aktualisiert_am=timezone.now()
|
|
)
|
|
|
|
return payment
|
|
|
|
|
|
@login_required
|
|
def quarterly_confirmation_create(request, destinataer_id):
|
|
"""Create a new quarterly confirmation for a destinataer"""
|
|
destinataer = get_object_or_404(Destinataer, pk=destinataer_id)
|
|
|
|
if request.method == "POST":
|
|
jahr = request.POST.get('jahr')
|
|
quartal = request.POST.get('quartal')
|
|
|
|
if jahr and quartal:
|
|
try:
|
|
jahr = int(jahr)
|
|
quartal = int(quartal)
|
|
|
|
# Check if this quarter already exists
|
|
existing = VierteljahresNachweis.objects.filter(
|
|
destinataer=destinataer,
|
|
jahr=jahr,
|
|
quartal=quartal
|
|
).exists()
|
|
|
|
if existing:
|
|
messages.warning(
|
|
request,
|
|
f"Quartal {jahr} Q{quartal} existiert bereits für {destinataer.get_full_name()}."
|
|
)
|
|
else:
|
|
# Create new quarterly confirmation
|
|
nachweis = VierteljahresNachweis.objects.create(
|
|
destinataer=destinataer,
|
|
jahr=jahr,
|
|
quartal=quartal,
|
|
studiennachweis_erforderlich=True, # Always required now
|
|
)
|
|
|
|
# Set deadline (15th of second month of quarter)
|
|
deadline_months = {1: 5, 2: 8, 3: 11, 4: 2} # Q1->May, Q2->Aug, Q3->Nov, Q4->Feb(next year)
|
|
deadline_month = deadline_months[quartal]
|
|
deadline_year = jahr if quartal != 4 else jahr + 1
|
|
|
|
from datetime import date
|
|
nachweis.faelligkeitsdatum = date(deadline_year, deadline_month, 15)
|
|
nachweis.save()
|
|
|
|
messages.success(
|
|
request,
|
|
f"Quartal {jahr} Q{quartal} wurde erfolgreich für {destinataer.get_full_name()} erstellt."
|
|
)
|
|
|
|
except (ValueError, TypeError):
|
|
messages.error(request, "Ungültige Jahr- oder Quartalswerte.")
|
|
else:
|
|
messages.error(request, "Jahr und Quartal müssen angegeben werden.")
|
|
|
|
return redirect("stiftung:destinataer_detail", pk=destinataer.pk)
|
|
|
|
|
|
@login_required
|
|
def quarterly_confirmation_edit(request, pk):
|
|
"""Standalone edit view for quarterly confirmation"""
|
|
nachweis = get_object_or_404(VierteljahresNachweis, pk=pk)
|
|
|
|
if request.method == "POST":
|
|
form = VierteljahresNachweisForm(request.POST, request.FILES, instance=nachweis)
|
|
if form.is_valid():
|
|
quarterly_proof = form.save(commit=False)
|
|
|
|
# Calculate current status before saving
|
|
old_status = nachweis.status
|
|
|
|
# Auto-update status based on completion
|
|
if quarterly_proof.is_complete():
|
|
if quarterly_proof.status in ['offen', 'teilweise']:
|
|
quarterly_proof.status = 'eingereicht'
|
|
quarterly_proof.eingereicht_am = timezone.now()
|
|
else:
|
|
# If not complete, set to teilweise if some fields are filled
|
|
has_partial_data = (
|
|
quarterly_proof.einkommenssituation_bestaetigt or
|
|
quarterly_proof.vermogenssituation_bestaetigt or
|
|
quarterly_proof.studiennachweis_eingereicht
|
|
)
|
|
if has_partial_data and quarterly_proof.status == 'offen':
|
|
quarterly_proof.status = 'teilweise'
|
|
|
|
quarterly_proof.save()
|
|
|
|
# Try to create automatic support payment if complete
|
|
if quarterly_proof.is_complete() and quarterly_proof.status == 'eingereicht':
|
|
support_payment = create_quarterly_support_payment(quarterly_proof)
|
|
if support_payment:
|
|
messages.success(
|
|
request,
|
|
f"Automatische Unterstützung über {support_payment.betrag}€ für {support_payment.destinataer.get_full_name()} wurde erstellt (fällig am {support_payment.faellig_am.strftime('%d.%m.%Y')})."
|
|
)
|
|
else:
|
|
# Log why payment wasn't created
|
|
reasons = []
|
|
if not quarterly_proof.destinataer.vierteljaehrlicher_betrag:
|
|
reasons.append("kein vierteljährlicher Betrag hinterlegt")
|
|
if not quarterly_proof.destinataer.iban:
|
|
reasons.append("keine IBAN hinterlegt")
|
|
if not quarterly_proof.destinataer.standard_konto and not StiftungsKonto.objects.exists():
|
|
reasons.append("kein Auszahlungskonto verfügbar")
|
|
|
|
if reasons:
|
|
messages.warning(
|
|
request,
|
|
f"Automatische Unterstützung konnte nicht erstellt werden: {', '.join(reasons)}"
|
|
)
|
|
|
|
# Debug message to see what happened
|
|
status_changed = old_status != quarterly_proof.status
|
|
status_msg = f" (Status: {old_status} → {quarterly_proof.status})" if status_changed else f" (Status: {quarterly_proof.status})"
|
|
|
|
messages.success(
|
|
request,
|
|
f"Vierteljahresnachweis für {nachweis.destinataer.get_full_name()} "
|
|
f"({nachweis.jahr} Q{nachweis.quartal}) wurde erfolgreich aktualisiert{status_msg}."
|
|
)
|
|
return redirect("stiftung:destinataer_detail", pk=nachweis.destinataer.pk)
|
|
else:
|
|
# Add form errors to messages
|
|
for field, errors in form.errors.items():
|
|
for error in errors:
|
|
messages.error(request, f"Fehler in {field}: {error}")
|
|
else:
|
|
form = VierteljahresNachweisForm(instance=nachweis)
|
|
|
|
context = {
|
|
'form': form,
|
|
'nachweis': nachweis,
|
|
'destinataer': nachweis.destinataer,
|
|
'title': f'Vierteljahresnachweis bearbeiten - {nachweis.destinataer.get_full_name()} {nachweis.jahr} Q{nachweis.quartal}',
|
|
}
|
|
return render(request, 'stiftung/quarterly_confirmation_edit.html', context)
|
|
|
|
|
|
@login_required
|
|
def quarterly_confirmation_approve(request, pk):
|
|
"""Approve quarterly confirmation (staff only)"""
|
|
if not request.user.is_staff:
|
|
messages.error(request, "Sie haben keine Berechtigung für diese Aktion.")
|
|
return redirect("stiftung:destinataer_list")
|
|
|
|
nachweis = get_object_or_404(VierteljahresNachweis, pk=pk)
|
|
|
|
if request.method == "POST":
|
|
if nachweis.status in ['eingereicht', 'geprueft']:
|
|
# Check if we need to create or update support payment
|
|
related_payment = nachweis.get_related_support_payment()
|
|
|
|
if nachweis.status == 'eingereicht' or (nachweis.status == 'geprueft' and not related_payment):
|
|
# Approve the quarterly confirmation
|
|
nachweis.status = 'geprueft'
|
|
nachweis.geprueft_am = timezone.now()
|
|
nachweis.geprueft_von = request.user
|
|
nachweis.save()
|
|
|
|
# Handle support payment - create if missing, update if exists
|
|
if not related_payment:
|
|
# Create new support payment
|
|
related_payment = create_quarterly_support_payment(nachweis)
|
|
if related_payment:
|
|
related_payment.status = 'in_bearbeitung'
|
|
related_payment.aktualisiert_am = timezone.now()
|
|
related_payment.save()
|
|
|
|
messages.success(
|
|
request,
|
|
f"Vierteljahresnachweis freigegeben und neue Unterstützung über {related_payment.betrag}€ für {nachweis.destinataer.get_full_name()} "
|
|
f"({nachweis.jahr} Q{nachweis.quartal}) wurde erstellt."
|
|
)
|
|
else:
|
|
messages.warning(
|
|
request,
|
|
f"Vierteljahresnachweis freigegeben, aber Unterstützung konnte nicht erstellt werden. "
|
|
f"Bitte prüfen Sie die Einstellungen für {nachweis.destinataer.get_full_name()}."
|
|
)
|
|
elif related_payment.status == 'geplant':
|
|
# Update existing payment
|
|
related_payment.status = 'in_bearbeitung'
|
|
related_payment.aktualisiert_am = timezone.now()
|
|
related_payment.save()
|
|
|
|
messages.success(
|
|
request,
|
|
f"Vierteljahresnachweis und zugehörige Unterstützung für {nachweis.destinataer.get_full_name()} "
|
|
f"({nachweis.jahr} Q{nachweis.quartal}) wurden freigegeben."
|
|
)
|
|
else:
|
|
messages.success(
|
|
request,
|
|
f"Vierteljahresnachweis für {nachweis.destinataer.get_full_name()} "
|
|
f"({nachweis.jahr} Q{nachweis.quartal}) wurde freigegeben."
|
|
)
|
|
else:
|
|
messages.error(
|
|
request,
|
|
"Nur eingereichte oder bereits genehmigte Nachweise können verarbeitet werden."
|
|
)
|
|
|
|
return redirect("stiftung:destinataer_detail", pk=nachweis.destinataer.pk)
|
|
|
|
|
|
@login_required
|
|
def quarterly_confirmation_reset(request, pk):
|
|
"""Reset quarterly confirmation status (staff only)"""
|
|
if not request.user.is_staff:
|
|
messages.error(request, "Sie haben keine Berechtigung für diese Aktion.")
|
|
return redirect("stiftung:destinataer_list")
|
|
|
|
nachweis = get_object_or_404(VierteljahresNachweis, pk=pk)
|
|
|
|
if request.method == "POST":
|
|
if nachweis.status in ['geprueft', 'eingereicht']:
|
|
# Reset the quarterly confirmation status
|
|
nachweis.status = 'eingereicht' if nachweis.is_complete() else 'teilweise'
|
|
nachweis.geprueft_am = None
|
|
nachweis.geprueft_von = None
|
|
nachweis.aktualisiert_am = timezone.now()
|
|
nachweis.save()
|
|
|
|
# Reset related support payment status if it exists
|
|
related_payment = nachweis.get_related_support_payment()
|
|
if related_payment and related_payment.status == 'in_bearbeitung':
|
|
related_payment.status = 'geplant'
|
|
related_payment.aktualisiert_am = timezone.now()
|
|
related_payment.save()
|
|
|
|
messages.success(
|
|
request,
|
|
f"Vierteljahresnachweis und zugehörige Unterstützung für {nachweis.destinataer.get_full_name()} "
|
|
f"({nachweis.jahr} Q{nachweis.quartal}) wurden zurückgesetzt."
|
|
)
|
|
else:
|
|
messages.success(
|
|
request,
|
|
f"Vierteljahresnachweis für {nachweis.destinataer.get_full_name()} "
|
|
f"({nachweis.jahr} Q{nachweis.quartal}) wurde zurückgesetzt."
|
|
)
|
|
else:
|
|
messages.error(
|
|
request,
|
|
"Nur genehmigte oder eingereichte Nachweise können zurückgesetzt werden."
|
|
)
|
|
|
|
return redirect("stiftung:destinataer_detail", pk=nachweis.destinataer.pk)
|