Files
stiftung-management-system/app/stiftung/views.py
2025-09-06 18:31:54 +02:00

4898 lines
198 KiB
Python

import os
import requests
import csv
import io
from datetime import datetime
from django.shortcuts import render, get_object_or_404, redirect
from django.contrib import messages
from django.contrib.auth.decorators import login_required
from django.core.paginator import Paginator
from django.db.models import Q, Sum, Count, Avg, Value, IntegerField, DecimalField, F
from django.db.models.functions import Coalesce, Cast, Replace, NullIf
from decimal import Decimal
import time
from django.http import JsonResponse
from django.views.decorators.csrf import csrf_exempt
from django.utils import timezone
from rest_framework.response import Response
from rest_framework.decorators import api_view
from django.conf import settings
from .models import Person, Paechter, Destinataer, DokumentLink, Foerderung, Land, Verpachtung, CSVImport, LandAbrechnung, LandVerpachtung
import json
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 .forms import PersonForm, PaechterForm, DestinataerForm, DokumentLinkForm, FoerderungForm, LandForm, VerpachtungForm, DestinataerUnterstuetzungForm, DestinataerNotizForm
from stiftung.models import DestinataerUnterstuetzung, DestinataerNotiz
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."""
url = getattr(settings, "PAPERLESS_API_URL", None)
if not url:
return Response({'error': 'Paperless API not configured'}, status=400)
base_url = url.rstrip('/api') if url.endswith('/api') else url
return redirect(f"{base_url}/documents/{doc_id}/")
@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:
# 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,
'finanzielle_notlage': row.get('Finanzielle_Notlage', 'false').lower() == 'true',
'notizen': row.get('Notizen', '').strip() or None,
'aktiv': row.get('Aktiv', 'true').lower() == 'true',
}
# 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
# 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')
verpachtungen = person.verpachtung_set.all().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 = {
'name': ['nachname', 'vorname'],
'familienzweig': ['familienzweig'],
'berufsgruppe': ['berufsgruppe'],
'institution': ['institution'],
'email': ['email'],
'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')
# No inline form anymore
# Notizen laden
notizen_eintraege = DestinataerNotiz.objects.filter(destinataer=destinataer).order_by('-erstellt_am')
context = {
'destinataer': destinataer,
'verknuepfte_dokumente': verknuepfte_dokumente,
'foerderungen': foerderungen,
'notizen_eintraege': notizen_eintraege,
}
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)
if form.is_valid():
destinataer = form.save()
try:
# Auto-create a Destinatärunterstützung if conditions are met
if (
destinataer.aktiv
and destinataer.unterstuetzung_bestaetigt
and destinataer.standard_konto
and destinataer.vierteljaehrlicher_betrag
and destinataer.vierteljaehrlicher_betrag > 0
):
from decimal import Decimal
from stiftung.models import DestinataerUnterstuetzung
heute = timezone.now().date()
beschreibung = f"Vierteljährliche Vorauszahlung für {destinataer.get_full_name()}"
# ensure only one upcoming planned entry; update if one exists
existing = DestinataerUnterstuetzung.objects.filter(
destinataer=destinataer,
status='geplant'
).order_by('faellig_am').first()
if existing:
existing.konto = destinataer.standard_konto
existing.betrag = Decimal(destinataer.vierteljaehrlicher_betrag)
existing.faellig_am = heute
existing.beschreibung = beschreibung
existing.save()
else:
DestinataerUnterstuetzung.objects.create(
destinataer=destinataer,
konto=destinataer.standard_konto,
betrag=Decimal(destinataer.vierteljaehrlicher_betrag),
faellig_am=heute,
status='geplant',
beschreibung=beschreibung,
)
except Exception:
pass
messages.success(request, f'Destinatär "{destinataer.get_full_name()}" wurde erfolgreich aktualisiert.')
return redirect('stiftung:destinataer_detail', pk=destinataer.pk)
else:
form = DestinataerForm(instance=destinataer)
context = {'form': form, 'destinataer': destinataer, 'title': f'Destinatär bearbeiten: {destinataer.get_full_name()}'}
return render(request, 'stiftung/destinataer_form.html', context)
@login_required
def destinataer_delete(request, pk):
destinataer = get_object_or_404(Destinataer, pk=pk)
if request.method == 'POST':
destinataer.delete()
messages.success(request, f'Destinatär "{destinataer.get_full_name()}" wurde erfolgreich gelöscht.')
return redirect('stiftung:destinataer_list')
context = {'destinataer': destinataer}
return render(request, 'stiftung/destinataer_confirm_delete.html', context)
# Paechter Views (Landpächter)
@login_required
def paechter_list(request):
search_query = request.GET.get('search', '')
ausbildung_filter = request.GET.get('ausbildung', '')
aktiv_filter = request.GET.get('aktiv', '')
sort = request.GET.get('sort', '')
direction = request.GET.get('dir', 'asc')
paechter = Paechter.objects.all()
if search_query:
paechter = paechter.filter(
Q(nachname__icontains=search_query) |
Q(vorname__icontains=search_query) |
Q(email__icontains=search_query) |
Q(pachtnummer__icontains=search_query)
)
if ausbildung_filter == 'true':
paechter = paechter.filter(landwirtschaftliche_ausbildung=True)
elif ausbildung_filter == 'false':
paechter = paechter.filter(landwirtschaftliche_ausbildung=False)
if aktiv_filter == 'true':
paechter = paechter.filter(aktiv=True)
elif aktiv_filter == 'false':
paechter = paechter.filter(aktiv=False)
# Annotate with total leased area and rent (coalesce nulls to Decimal for stable sorting)
paechter = paechter.annotate(
gesamt_flaeche=Coalesce(
Sum('verpachtung__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('verpachtung__pachtzins_jaehrlich'),
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')
# Legacy Verpachtungen für diesen Pächter laden
verpachtungen = Verpachtung.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, # Legacy
'gepachtete_laendereien': gepachtete_laendereien, # Neu
'total_flaeche_neu': total_flaeche_neu,
'total_pachtzins_neu': total_pachtzins_neu,
}
return render(request, 'stiftung/paechter_detail.html', context)
@login_required
def paechter_create(request):
if request.method == 'POST':
form = PaechterForm(request.POST)
if form.is_valid():
paechter = form.save()
messages.success(request, f'Pächter "{paechter.get_full_name()}" wurde erfolgreich erstellt.')
return redirect('stiftung:paechter_detail', pk=paechter.pk)
else:
# Debug: Log form errors and show them to user
print(f"Form errors: {form.errors}")
print(f"Form data: {request.POST}")
messages.error(request, f'Formular-Fehler: {form.errors}')
for field, errors in form.errors.items():
for error in errors:
messages.error(request, f'{field}: {error}')
else:
form = PaechterForm()
context = {'form': form, 'title': 'Neuen Pächter erstellen'}
return render(request, 'stiftung/paechter_form.html', context)
@login_required
def paechter_update(request, pk):
paechter = get_object_or_404(Paechter, pk=pk)
if request.method == 'POST':
form = PaechterForm(request.POST, instance=paechter)
if form.is_valid():
paechter = form.save()
messages.success(request, f'Pächter "{paechter.get_full_name()}" wurde erfolgreich aktualisiert.')
return redirect('stiftung:paechter_detail', pk=paechter.pk)
else:
# Debug: Log form errors and show them to user
print(f"Form errors: {form.errors}")
print(f"Form data: {request.POST}")
messages.error(request, f'Formular-Fehler: {form.errors}')
for field, errors in form.errors.items():
for error in errors:
messages.error(request, f'{field}: {error}')
else:
form = PaechterForm(instance=paechter)
context = {'form': form, 'paechter': paechter, 'title': f'Pächter bearbeiten: {paechter.get_full_name()}'}
return render(request, 'stiftung/paechter_form.html', context)
@login_required
def paechter_delete(request, pk):
paechter = get_object_or_404(Paechter, pk=pk)
if request.method == 'POST':
paechter.delete()
messages.success(request, f'Pächter "{paechter.get_full_name()}" wurde erfolgreich gelöscht.')
return redirect('stiftung:paechter_list')
context = {'paechter': paechter}
return render(request, 'stiftung/paechter_confirm_delete.html', context)
# Land Views
@login_required
def land_list(request):
search_query = request.GET.get('search', '')
gemeinde_filter = request.GET.get('gemeinde', '')
aktiv_filter = request.GET.get('aktiv', '')
sort = request.GET.get('sort', '')
direction = request.GET.get('dir', 'asc')
lands = Land.objects.all()
if search_query:
lands = lands.filter(
Q(lfd_nr__icontains=search_query) |
Q(gemeinde__icontains=search_query) |
Q(gemarkung__icontains=search_query) |
Q(flur__icontains=search_query) |
Q(flurstueck__icontains=search_query)
)
if gemeinde_filter:
lands = lands.filter(gemeinde=gemeinde_filter)
if aktiv_filter == 'true':
lands = lands.filter(aktiv=True)
elif aktiv_filter == 'false':
lands = lands.filter(aktiv=False)
# Annotate with verpachtungsgrad and numeric casts for natural sorting
# Prepare numeric versions of textual fields by stripping common non-digits
def digits_only(field_expr):
expr = Replace(field_expr, Value(' '), Value(''))
expr = Replace(expr, Value('-'), Value(''))
expr = Replace(expr, Value('.'), Value(''))
expr = Replace(expr, Value('/'), Value(''))
expr = Replace(expr, Value('L'), Value(''))
return expr
lands = lands.extra(
select={'verpachtungsgrad': 'CASE WHEN groesse_qm > 0 THEN (verp_flaeche_aktuell / groesse_qm) * 100 ELSE 0 END'}
).annotate(
lfd_nr_num=Cast(NullIf(digits_only(F('lfd_nr')), Value('')), IntegerField()),
flur_num=Cast(NullIf(digits_only(F('flur')), Value('')), IntegerField()),
flurstueck_num=Cast(NullIf(digits_only(F('flurstueck')), Value('')), IntegerField()),
)
# Sorting
sort_map = {
'lfd_nr': ['lfd_nr_num', 'lfd_nr'],
'gemeinde': ['gemeinde'],
'gemarkung': ['gemarkung'],
'flur': ['flur_num', 'flur'],
'flurstueck': ['flurstueck_num', 'flurstueck'],
'groesse': ['groesse_qm'],
'verp': ['verp_flaeche_aktuell'],
'grad': ['verpachtungsgrad'],
}
if sort in sort_map:
fields = sort_map[sort]
if direction == 'desc':
order_fields = [f'-{f}' for f in fields]
else:
order_fields = fields
lands = lands.order_by(*order_fields)
# Aggregated statistics for current filter set
aggregates = lands.aggregate(
sum_groesse_qm=Sum('groesse_qm'),
sum_gruenland_qm=Sum('gruenland_qm'),
sum_acker_qm=Sum('acker_qm'),
sum_wald_qm=Sum('wald_qm'),
sum_sonstiges_qm=Sum('sonstiges_qm'),
)
sum_gruenland_qm = float(aggregates.get('sum_gruenland_qm') or 0)
sum_acker_qm = float(aggregates.get('sum_acker_qm') or 0)
sum_wald_qm = float(aggregates.get('sum_wald_qm') or 0)
sum_sonstiges_qm = float(aggregates.get('sum_sonstiges_qm') or 0)
sum_total_use_qm = sum_gruenland_qm + sum_acker_qm + sum_wald_qm + sum_sonstiges_qm
def pct(part, total):
return round((part / total) * 100, 1) if total and part is not None else 0.0
stats = {
'sum_gruenland_qm': sum_gruenland_qm,
'sum_acker_qm': sum_acker_qm,
'sum_wald_qm': sum_wald_qm,
'sum_sonstiges_qm': sum_sonstiges_qm,
'sum_total_use_qm': sum_total_use_qm,
'pct_gruenland': pct(sum_gruenland_qm, sum_total_use_qm),
'pct_acker': pct(sum_acker_qm, sum_total_use_qm),
'pct_wald': pct(sum_wald_qm, sum_total_use_qm),
}
# Prepare size chart data (top 30 by size)
top_sizes = list(
lands.order_by('-groesse_qm').values_list('lfd_nr', 'groesse_qm')[:30]
)
size_chart_labels = [label or '' for label, _ in top_sizes]
size_chart_values = [float(val or 0) for _, val in top_sizes]
paginator = Paginator(lands, 20)
page_number = request.GET.get('page')
page_obj = paginator.get_page(page_number)
# Get unique gemeinden for filter
gemeinden = Land.objects.values_list('gemeinde', flat=True).distinct().order_by('gemeinde')
context = {
'page_obj': page_obj,
'search_query': search_query,
'gemeinde_filter': gemeinde_filter,
'aktiv_filter': aktiv_filter,
'gemeinden': gemeinden,
'stats': stats,
'size_chart_labels_json': json.dumps(size_chart_labels),
'size_chart_values_json': json.dumps(size_chart_values),
'sort': sort,
'dir': direction,
}
return render(request, 'stiftung/land_list.html', context)
@login_required
def land_detail(request, pk):
land = get_object_or_404(Land, pk=pk)
# Alle mit dieser Länderei verknüpften Dokumente laden
verknuepfte_dokumente = DokumentLink.objects.filter(
land_id=land.pk
).order_by('kontext', 'titel')
# Legacy Verpachtungen für diese Länderei laden
verpachtungen = Verpachtung.objects.filter(land=land).order_by('-pachtbeginn')
# 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': verpachtungen,
'neue_verpachtungen': neue_verpachtungen,
}
return render(request, 'stiftung/land_detail.html', context)
@login_required
def land_create(request):
if request.method == 'POST':
form = LandForm(request.POST)
if form.is_valid():
land = form.save()
messages.success(request, f'Länderei "{land}" wurde erfolgreich erstellt.')
return redirect('stiftung:land_detail', pk=land.pk)
else:
form = LandForm()
context = {'form': form, 'title': 'Neue Länderei erstellen'}
return render(request, 'stiftung/land_form.html', context)
@login_required
def land_update(request, pk):
land = get_object_or_404(Land, pk=pk)
if request.method == 'POST':
form = LandForm(request.POST, instance=land)
if form.is_valid():
land = form.save()
messages.success(request, f'Länderei "{land}" wurde erfolgreich aktualisiert.')
return redirect('stiftung:land_detail', pk=land.pk)
else:
form = LandForm(instance=land)
context = {'form': form, 'land': land, 'title': f'Länderei bearbeiten: {land}'}
return render(request, 'stiftung/land_form.html', context)
@login_required
def land_delete(request, pk):
land = get_object_or_404(Land, pk=pk)
if request.method == 'POST':
land.delete()
messages.success(request, f'Länderei "{land}" wurde erfolgreich gelöscht.')
return redirect('stiftung:land_list')
context = {'land': land}
return render(request, 'stiftung/land_confirm_delete.html', context)
# Verpachtung Views
@login_required
def verpachtung_list(request):
search_query = request.GET.get('search', '')
status_filter = request.GET.get('status', '')
gemeinde_filter = request.GET.get('gemeinde', '')
sort = request.GET.get('sort', '')
direction = request.GET.get('dir', 'asc')
verpachtungen = Verpachtung.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_jaehrlich'],
'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 = Verpachtung.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_jaehrlich')
)
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 = Verpachtung.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
def verpachtung_detail(request, pk):
verpachtung = get_object_or_404(Verpachtung, pk=pk)
# Alle mit dieser Verpachtung verknüpften Dokumente laden
verknuepfte_dokumente = DokumentLink.objects.filter(
verpachtung_id=verpachtung.pk
).order_by('kontext', 'titel')
context = {
'verpachtung': verpachtung,
'verknuepfte_dokumente': verknuepfte_dokumente,
}
return render(request, 'stiftung/verpachtung_detail.html', context)
@login_required
def verpachtung_create(request):
if request.method == 'POST':
form = VerpachtungForm(request.POST)
if form.is_valid():
verpachtung = form.save()
messages.success(request, f'Verpachtung "{verpachtung}" wurde erfolgreich erstellt.')
return redirect('stiftung:verpachtung_detail', pk=verpachtung.pk)
else:
form = VerpachtungForm()
context = {'form': form, 'title': 'Neue Verpachtung erstellen'}
return render(request, 'stiftung/verpachtung_form.html', context)
@login_required
def verpachtung_update(request, pk):
verpachtung = get_object_or_404(Verpachtung, pk=pk)
if request.method == 'POST':
form = VerpachtungForm(request.POST, instance=verpachtung)
if form.is_valid():
verpachtung = form.save()
messages.success(request, f'Verpachtung "{verpachtung}" wurde erfolgreich aktualisiert.')
return redirect('stiftung:verpachtung_list')
else:
form = VerpachtungForm(instance=verpachtung)
context = {'form': form, 'verpachtung': verpachtung, 'title': f'Verpachtung bearbeiten: {verpachtung}'}
return render(request, 'stiftung/verpachtung_form.html', context)
@login_required
def verpachtung_delete(request, pk):
verpachtung = get_object_or_404(Verpachtung, pk=pk)
if request.method == 'POST':
verpachtung.delete()
messages.success(request, f'Verpachtung "{verpachtung}" wurde erfolgreich gelöscht.')
return redirect('stiftung:verpachtung_list')
context = {'verpachtung': verpachtung}
return render(request, 'stiftung/verpachtung_confirm_delete.html', context)
# Förderung Views
@login_required
def foerderung_list(request):
"""List all funding grants with filtering and pagination"""
foerderungen = Foerderung.objects.select_related('person', 'verwendungsnachweis').all()
# Filtering
jahr = request.GET.get('jahr')
kategorie = request.GET.get('kategorie')
status = request.GET.get('status')
person = request.GET.get('person')
if jahr:
foerderungen = foerderungen.filter(jahr=int(jahr))
if kategorie:
foerderungen = foerderungen.filter(kategorie=kategorie)
if status:
foerderungen = foerderungen.filter(status=status)
if person:
foerderungen = foerderungen.filter(person__nachname__icontains=person)
# 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,
'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': person,
'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"""
if request.method == 'POST':
form = FoerderungForm(request.POST)
if form.is_valid():
foerderung = form.save()
messages.success(request, f'Förderung für {foerderung.person} wurde erfolgreich erstellt.')
return redirect('stiftung:foerderung_detail', pk=foerderung.pk)
else:
form = FoerderungForm()
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':
foerderung.delete()
messages.success(request, f'Förderung für {foerderung.person} 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
from django.conf import settings
import requests
url = getattr(settings, "PAPERLESS_API_URL", None)
token = getattr(settings, "PAPERLESS_API_TOKEN", None)
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 ['Stiftung_Destinatäre', 'Stiftung_Land_und_Pächter', 'Stiftung_Administration'] 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.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('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(Verpachtung.objects.values_list('pachtbeginn__year', flat=True))
), reverse=True)
# Statistics for overview tiles
total_persons = Person.objects.count()
total_destinataere = Destinataer.objects.count()
total_laendereien = Land.objects.count()
total_verpachtungen = Verpachtung.objects.count()
total_foerderungen = Foerderung.objects.count()
context = {
'jahre': jahre,
'title': 'Berichte',
'total_persons': total_persons,
'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 = Verpachtung.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_jaehrlich'))['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 weasyprint import HTML
from django.template.loader import render_to_string
# Get data for the year
foerderungen = Foerderung.objects.filter(jahr=jahr).select_related('person')
verpachtungen = Verpachtung.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_jaehrlich'))['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):
# Person statistics
total_persons = Person.objects.count()
active_persons = Person.objects.filter(aktiv=True).count()
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 = Verpachtung.objects.filter(status='aktiv').aggregate(
total=Sum('verpachtete_flaeche')
)['total'] or 0
# Verpachtung statistics
total_verpachtungen = Verpachtung.objects.count()
active_verpachtungen = Verpachtung.objects.filter(status='aktiv').count()
total_pachtzins = Verpachtung.objects.filter(status='aktiv').aggregate(
total=Sum('pachtzins_jaehrlich')
)['total'] or 0
# Recent activities
recent_lands = Land.objects.order_by('-erstellt_am')[:5]
recent_verpachtungen = Verpachtung.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 = []
url = getattr(settings, "PAPERLESS_API_URL", None)
token = getattr(settings, "PAPERLESS_API_TOKEN", None)
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 ['Stiftung_Destinatäre', 'Stiftung_Land_und_Pächter', 'Stiftung_Administration'] 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 = {
'total_persons': total_persons,
'active_persons': active_persons,
'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(Verpachtung.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):
url = getattr(settings, "PAPERLESS_API_URL", None)
token = getattr(settings, "PAPERLESS_API_TOKEN", None)
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 django.conf import settings
url = getattr(settings, "PAPERLESS_API_URL", None)
token = getattr(settings, "PAPERLESS_API_TOKEN", None)
required_tag = getattr(settings, "PAPERLESS_REQUIRED_TAG", "Stiftung_Destinatäre")
land_tag = getattr(settings, "PAPERLESS_LAND_TAG", "Stiftung_Land_und_Pächter")
admin_tag = getattr(settings, "PAPERLESS_ADMIN_TAG", "Stiftung_Administration")
destinaere_tag_id = getattr(settings, "PAPERLESS_DESTINATAERE_TAG_ID", "210")
land_tag_id = getattr(settings, "PAPERLESS_LAND_TAG_ID", "204")
admin_tag_id = getattr(settings, "PAPERLESS_ADMIN_TAG_ID", "216")
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:
return Response({
'error': f'API-Fehler: {e}',
'message': 'Could not connect to Paperless API. Please check your configuration.',
'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 django.conf import settings
url = getattr(settings, "PAPERLESS_API_URL", None)
token = getattr(settings, "PAPERLESS_API_TOKEN", None)
required_tag = getattr(settings, "PAPERLESS_REQUIRED_TAG", "Stiftung_Destinatäre")
land_tag = getattr(settings, "PAPERLESS_LAND_TAG", "Stiftung_Land_und_Pächter")
admin_tag = getattr(settings, "PAPERLESS_ADMIN_TAG", "Stiftung_Administration")
destinaere_tag_id = getattr(settings, "PAPERLESS_DESTINATAERE_TAG_ID", "210")
land_tag_id = getattr(settings, "PAPERLESS_LAND_TAG_ID", "204")
admin_tag_id = getattr(settings, "PAPERLESS_ADMIN_TAG_ID", "216")
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 django.conf import settings
url = getattr(settings, "PAPERLESS_API_URL", None)
token = getattr(settings, "PAPERLESS_API_TOKEN", None)
if not url or not token:
return Response({'error': 'Paperless API not configured'}, status=400)
try:
# Entferne /api vom Ende der URL falls vorhanden
base_url = url.rstrip('/api') if url.endswith('/api') else url
# Alle Tags abrufen (mit großer page_size)
headers = {'Authorization': f'Token {token}'}
# Erste Anfrage mit großer page_size
tags_response = requests.get(f"{base_url}/api/tags/?page_size=1000", headers=headers, timeout=10)
tags_response.raise_for_status()
tags_data = tags_response.json()
all_tags = []
# Erste Seite verarbeiten
for tag in tags_data.get('results', []):
tag_detail = {
'id': tag.get('id'),
'name': tag.get('name', ''),
'slug': tag.get('slug', ''),
'color': tag.get('color', ''),
'text_color': tag.get('text_color', ''),
'match': tag.get('match', ''),
'matching_algorithm': tag.get('matching_algorithm'),
'is_inbox_tag': tag.get('is_inbox_tag'),
'document_count': tag.get('document_count', 0)
}
all_tags.append(tag_detail)
# Weitere Seiten abrufen falls vorhanden
next_url = tags_data.get('next')
while next_url:
next_response = requests.get(next_url, headers=headers, timeout=10)
next_response.raise_for_status()
next_data = next_response.json()
for tag in next_data.get('results', []):
tag_detail = {
'id': tag.get('id'),
'name': tag.get('name', ''),
'slug': tag.get('slug', ''),
'color': tag.get('color', ''),
'text_color': tag.get('text_color', ''),
'match': tag.get('match', ''),
'matching_algorithm': tag.get('matching_algorithm'),
'is_inbox_tag': tag.get('is_inbox_tag'),
'document_count': tag.get('document_count', 0)
}
all_tags.append(tag_detail)
next_url = next_data.get('next')
# Nach ID sortieren
all_tags.sort(key=lambda x: x['id'])
return Response({
'total_tags': len(all_tags),
'tags': all_tags,
'tag_ids': [tag['id'] for tag in all_tags],
'tag_names': [tag['name'] for tag in all_tags],
'api_info': {
'page_size_used': 1000,
'total_count_from_api': tags_data.get('count', 0)
}
})
except requests.exceptions.RequestException as e:
return Response({'error': f'API-Fehler: {e}'}, status=500)
except Exception as e:
return Response({'error': f'Unerwarteter Fehler: {e}'}, status=500)
@api_view(['GET'])
def link_document_search(request):
"""Sucht nach Datensätzen für die Dokument-Verknüpfung"""
from django.db.models import Q
query = request.GET.get('q', '')
category = request.GET.get('category', 'all')
results = {}
if category in ['all', 'destinataer']:
# Suche nach Destinatären
destinataer_query = Q()
if query and query != 'all':
destinataer_query = (
Q(nachname__icontains=query) | Q(vorname__icontains=query) |
Q(email__icontains=query) | Q(telefon__icontains=query) |
Q(strasse__icontains=query) | Q(ort__icontains=query) | Q(plz__icontains=query) |
Q(institution__icontains=query) | Q(familienzweig__icontains=query) |
Q(notizen__icontains=query)
)
destinataer_results = Destinataer.objects.filter(destinataer_query)[:25]
results['destinataer'] = [
{
'id': d.id,
'name': f"{d.vorname} {d.nachname}".strip() if d.vorname else (d.institution or d.nachname),
'type': 'Destinatär',
'details': f"{d.strasse or ''}{(', ' + d.plz + ' ' + d.ort) if d.plz and d.ort else (' ' + d.ort) if d.ort else ''} {('' + d.email) if d.email else ''} {('' + d.institution) if d.institution else ''} {('' + d.familienzweig) if d.familienzweig else ''}".strip()
}
for d in destinataer_results
]
if category in ['all', 'land']:
# Suche nach Ländereien
land_query = Q()
if query and query != 'all':
# Extract numbers from search terms like "Flur 9" or "Flurstück 11"
import re
flur_match = re.search(r'flur\s*(\d+)', query, re.IGNORECASE)
flurstuck_match = re.search(r'flurstück\s*(\d+)', query, re.IGNORECASE)
land_query = (
Q(gemarkung__icontains=query) | Q(gemeinde__icontains=query) |
Q(flur__icontains=query) | Q(flurstueck__icontains=query) |
Q(lfd_nr__icontains=query) | Q(ew_nummer__icontains=query) |
Q(notizen__icontains=query)
)
# Add specific searches for extracted numbers
if flur_match:
land_query |= Q(flur__exact=flur_match.group(1))
if flurstuck_match:
land_query |= Q(flurstueck__exact=flurstuck_match.group(1))
land_results = Land.objects.filter(land_query)[:25]
results['land'] = [
{
'id': l.id,
'name': f"{l.gemarkung} - {l.gemeinde}, Flur {l.flur}, Flurstück {l.flurstueck or 'N/A'}",
'type': 'Land',
'details': f"Lfd.Nr: {l.lfd_nr or 'N/A'} • EW-Nr: {l.ew_nummer or 'N/A'} • Größe: {l.groesse_qm or 0}"
}
for l in land_results
]
if category in ['all', 'verpachtung']:
# Suche nach Verpachtungen
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(pachtzins_jaehrlich__icontains=query) | Q(notizen__icontains=query)
)
verpachtung_results = Verpachtung.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"Flurstück: {v.land.flurstueck or 'N/A'} • Pachtzins: {v.pachtzins_jaehrlich or 'N/A'} • Zeitraum: {v.pachtbeginn.strftime('%Y') if v.pachtbeginn else '?'}-{v.pachtende.strftime('%Y') if v.pachtende else '?'}"
}
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
]
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 Verpachtung und den zugehörigen Pächter
verpachtung = Verpachtung.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 (Verpachtung.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'
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':
dokument_link.verpachtung_id = link_id
elif link_type == 'paechter':
dokument_link.paechter_id = link_id
elif link_type == 'rentmeister':
dokument_link.rentmeister_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 == 'verpachtung':
from stiftung.models import Verpachtung
entity = Verpachtung.objects.get(id=link_id)
target_name = str(entity)
elif link_type == 'rentmeister':
from stiftung.models import Rentmeister
entity = Rentmeister.objects.get(id=link_id)
target_name = entity.get_full_name()
else:
target_name = f"ID {link_id}"
log_link(
request=request,
entity_type='dokumentlink',
entity_id=str(dokument_link.id),
entity_name=entity_name,
target_type=link_type,
target_name=target_name
)
except Exception as e:
# Don't fail the main operation if logging fails
print(f"Audit logging failed: {e}")
# Automatische Pächter-Verknüpfung NACH der Haupttransaktion
paechter_linked = False
if link_type == 'verpachtung':
paechter_linked = create_paechter_link_for_verpachtung(paperless_id, paperless_title, link_id)
message = f'Dokument erfolgreich mit {link_type} verknüpft'
if paechter_linked:
message += ' (automatisch auch mit Pächter verknüpft)'
return Response({
'success': True,
'message': message,
'dokument_id': dokument_link.id
})
except Exception as e:
return Response({'error': f'Fehler beim Erstellen der Verknüpfung: {str(e)}'}, status=500)
# Legacy dokument_verknuepfung view removed - use dokument_management instead
@api_view(['GET'])
def link_document_list(request):
"""Listet alle bestehenden Dokument-Verknüpfungen auf, gruppiert nach Paperless-Document-ID"""
try:
dokument_links = DokumentLink.objects.all().order_by('-id')
# Group links by paperless_document_id to show multiple links per document
links_by_document = {}
for link in dokument_links:
paperless_id = link.paperless_document_id
if paperless_id not in links_by_document:
links_by_document[paperless_id] = {
'paperless_id': paperless_id,
'title': link.titel,
'paperless_url': f"/api/paperless/documents/{paperless_id}/",
'links': []
}
# Create link info
link_info = {
'id': str(link.id), # Ensure UUID is stringified
'kontext': link.kontext,
'link_type': None,
'linked_object': None
}
# Determine link type and get linked object details
if link.destinataer_id:
link_info['link_type'] = 'destinataer'
try:
dest = Destinataer.objects.get(id=link.destinataer_id)
link_info['linked_object'] = {
'id': str(dest.id),
'type': 'Destinatär',
'name': f"{dest.vorname} {dest.nachname}".strip() if dest.vorname else dest.institution,
'details': f"Institution: {dest.institution}" if dest.institution else f"Person: {dest.vorname} {dest.nachname}".strip()
}
except Destinataer.DoesNotExist:
link_info['linked_object'] = {'type': 'Destinatär', 'name': 'Gelöscht', 'details': 'Datensatz nicht mehr verfügbar'}
elif link.land_id:
link_info['link_type'] = 'land'
try:
land = Land.objects.get(id=link.land_id)
link_info['linked_object'] = {
'id': str(land.id),
'type': 'Land',
'name': f"{land.gemarkung} - {land.gemeinde}",
'details': f"Flur: {land.flur}, Größe: {land.groesse_qm or 0}"
}
except Land.DoesNotExist:
link_info['linked_object'] = {'type': 'Land', 'name': 'Gelöscht', 'details': 'Datensatz nicht mehr verfügbar'}
elif link.paechter_id:
link_info['link_type'] = 'paechter'
try:
p = Paechter.objects.get(id=link.paechter_id)
link_info['linked_object'] = {
'id': str(p.id),
'type': 'Pächter',
'name': f"{(p.vorname + ' ') if p.vorname else ''}{p.nachname}",
'details': f"{p.ort or ''}"
}
except Paechter.DoesNotExist:
link_info['linked_object'] = {'type': 'Pächter', 'name': 'Gelöscht', 'details': 'Datensatz nicht mehr verfügbar'}
elif link.verpachtung_id:
link_info['link_type'] = 'verpachtung'
try:
verp = Verpachtung.objects.select_related('paechter', 'land').get(id=link.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'}"
}
except Verpachtung.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'}
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.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 == '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 = Verpachtung.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 (Verpachtung.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 = Verpachtung.objects.get(id=link.verpachtung_id)
target_name = str(entity)
except Verpachtung.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 = Verpachtung.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 (Verpachtung.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 stiftung.models import StiftungsKonto, Verwaltungskosten, Rentmeister
from django.db.models import Sum, Count
from datetime import datetime, timedelta
# 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 stiftung.models import StiftungsKonto
from django.db.models import Sum
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 stiftung.models import Verwaltungskosten
from django.core.paginator import Paginator
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 stiftung.models import Rentmeister, Verwaltungskosten
from django.db.models import Sum, Count, Q
from datetime import datetime, timedelta
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 stiftung.models import Rentmeister, Verwaltungskosten
from django.core.paginator import Paginator
from django.db.models import Sum, Count, Q
from django.db import models
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 stiftung.models import StiftungsKonto, BankTransaction
from django.db.models import Sum, Count, Max, Q
from django.db import models
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 stiftung.models import AuditLog, BackupJob
from django.db.models import Count
from datetime import datetime, timedelta
# 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 = request.GET.get('format', '')
qs = DestinataerUnterstuetzung.objects.select_related('destinataer', 'konto').order_by('-faellig_am', 'destinataer__nachname')
if status:
qs = qs.filter(status=status)
# CSV export
if export == 'csv':
import csv
from django.http import HttpResponse
response = HttpResponse(content_type='text/csv; charset=utf-8')
response['Content-Disposition'] = 'attachment; filename=unterstuetzungen.csv'
writer = csv.writer(response, delimiter=';')
writer.writerow(['Destinatär','Betrag','Fällig am','Status','Bank','IBAN','Kontoname','Beschreibung'])
for u in qs:
writer.writerow([
u.destinataer.get_full_name(),
f"{u.betrag:.2f}",
u.faellig_am.strftime('%d.%m.%Y'),
u.get_status_display(),
getattr(u.konto, 'bank_name', ''),
getattr(u.konto, 'iban', ''),
str(u.konto),
u.beschreibung or '',
])
return response
# 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': qs})
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
context = {
'unterstuetzungen': qs,
'status_filter': status,
}
return render(request, 'stiftung/unterstuetzungen_list.html', context)
@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)
if request.method == 'POST':
obj.delete()
messages.success(request, 'Unterstützung gelöscht.')
return redirect('stiftung:unterstuetzungen_list')
return render(request, 'stiftung/unterstuetzung_confirm_delete.html', {'obj': obj})
@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 zipfile
import json
import tempfile
import os
from django.http import HttpResponse
destinataer = get_object_or_404(Destinataer, pk=pk)
# Create a temporary file for the ZIP
temp_file = tempfile.NamedTemporaryFile(delete=False, suffix='.zip')
try:
with zipfile.ZipFile(temp_file.name, 'w', zipfile.ZIP_DEFLATED) as zipf:
# 1. Entity data as JSON
entity_data = {
'id': str(destinataer.id),
'vorname': destinataer.vorname,
'nachname': destinataer.nachname,
'geburtsdatum': destinataer.geburtsdatum.isoformat() if destinataer.geburtsdatum else None,
'email': destinataer.email,
'telefon': destinataer.telefon,
'iban': destinataer.iban,
'strasse': destinataer.strasse,
'plz': destinataer.plz,
'ort': destinataer.ort,
'familienzweig': destinataer.get_familienzweig_display(),
'berufsgruppe': destinataer.get_berufsgruppe_display(),
'ausbildungsstand': destinataer.ausbildungsstand,
'institution': destinataer.institution,
'projekt_beschreibung': destinataer.projekt_beschreibung,
'jaehrliches_einkommen': str(destinataer.jaehrliches_einkommen) if destinataer.jaehrliches_einkommen else None,
'finanzielle_notlage': destinataer.finanzielle_notlage,
'ist_abkoemmling': destinataer.ist_abkoemmling,
'haushaltsgroesse': destinataer.haushaltsgroesse,
'monatliche_bezuege': str(destinataer.monatliche_bezuege) if destinataer.monatliche_bezuege else None,
'vermoegen': str(destinataer.vermoegen) if destinataer.vermoegen else None,
'unterstuetzung_bestaetigt': destinataer.unterstuetzung_bestaetigt,
'vierteljaehrlicher_betrag': str(destinataer.vierteljaehrlicher_betrag) if destinataer.vierteljaehrlicher_betrag else None,
'standard_konto': str(destinataer.standard_konto) if destinataer.standard_konto else None,
'studiennachweis_erforderlich': destinataer.studiennachweis_erforderlich,
'letzter_studiennachweis': destinataer.letzter_studiennachweis.isoformat() if destinataer.letzter_studiennachweis else None,
'notizen': destinataer.notizen,
'aktiv': destinataer.aktiv,
'export_datum': timezone.now().isoformat(),
'export_user': request.user.username,
}
zipf.writestr('destinataer_data.json', json.dumps(entity_data, indent=2, ensure_ascii=False))
# 2. Notes with attachments
notizen = DestinataerNotiz.objects.filter(destinataer=destinataer).order_by('-erstellt_am')
notes_data = []
for note in notizen:
note_data = {
'titel': note.titel,
'text': note.text,
'erstellt_am': note.erstellt_am.isoformat(),
'erstellt_von': note.erstellt_von.username if note.erstellt_von else None,
'datei_name': note.datei.name if note.datei else None,
}
notes_data.append(note_data)
# Add attachment file if exists
if note.datei and os.path.exists(note.datei.path):
zipf.write(note.datei.path, f'notizen_anhaenge/{os.path.basename(note.datei.name)}')
if notes_data:
zipf.writestr('notizen.json', json.dumps(notes_data, indent=2, ensure_ascii=False))
# 3. Linked documents from Paperless
dokumente = DokumentLink.objects.filter(destinataer_id=destinataer.pk)
docs_data = []
for doc in dokumente:
doc_data = {
'paperless_id': doc.paperless_document_id,
'titel': doc.titel,
'kontext': doc.get_kontext_display(),
'beschreibung': doc.beschreibung,
}
docs_data.append(doc_data)
# Try to download document from Paperless
try:
if hasattr(settings, 'PAPERLESS_API_URL') and settings.PAPERLESS_API_URL:
doc_url = f"{settings.PAPERLESS_API_URL}/api/documents/{doc.paperless_document_id}/download/"
headers = {}
if hasattr(settings, 'PAPERLESS_API_TOKEN') and settings.PAPERLESS_API_TOKEN:
headers['Authorization'] = f'Token {settings.PAPERLESS_API_TOKEN}'
response = requests.get(doc_url, headers=headers, timeout=30)
if response.status_code == 200:
# Determine file extension from Content-Type or use .pdf as fallback
content_type = response.headers.get('content-type', '')
if 'pdf' in content_type:
ext = '.pdf'
elif 'jpeg' in content_type or 'jpg' in content_type:
ext = '.jpg'
elif 'png' in content_type:
ext = '.png'
else:
ext = '.pdf' # fallback
safe_filename = f"dokument_{doc.paperless_document_id}_{doc.titel.replace('/', '_')[:50]}{ext}"
zipf.writestr(f'dokumente/{safe_filename}', response.content)
doc_data['downloaded'] = True
else:
doc_data['download_error'] = f'HTTP {response.status_code}'
except Exception as e:
doc_data['download_error'] = str(e)
if docs_data:
zipf.writestr('dokumente.json', json.dumps(docs_data, indent=2, ensure_ascii=False))
# Prepare response
with open(temp_file.name, 'rb') as f:
response = HttpResponse(f.read(), content_type='application/zip')
filename = f"destinataer_{destinataer.nachname}_{destinataer.vorname}_{timezone.now().strftime('%Y%m%d_%H%M%S')}.zip"
response['Content-Disposition'] = f'attachment; filename="{filename}"'
return response
finally:
# Clean up temp file
try:
os.unlink(temp_file.name)
except:
pass
@login_required
def paechter_export(request, pk):
"""Export complete Pächter data as ZIP with documents"""
import zipfile
import json
import tempfile
import os
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 zipfile
import json
import tempfile
import os
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 zipfile
import json
import tempfile
import os
from django.http import HttpResponse
verpachtung = get_object_or_404(Verpachtung, 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_jaehrlich),
'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 stiftung.models import AuditLog
from django.core.paginator import Paginator
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 stiftung.models import BackupJob
from django.core.paginator import Paginator
# 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"""
from stiftung.models import BackupJob
from django.http import FileResponse, Http404
import os
try:
backup_job = BackupJob.objects.get(id=backup_id, status='completed')
except BackupJob.DoesNotExist:
raise Http404("Backup nicht gefunden oder nicht vollständig")
backup_path = os.path.join('/app/backups', backup_job.backup_filename)
if not os.path.exists(backup_path):
raise Http404("Backup-Datei nicht gefunden")
# Log download
from stiftung.audit import log_system_action
log_system_action(
request=request,
action='export',
description=f"Backup heruntergeladen: {backup_job.backup_filename}",
details={'backup_job_id': str(backup_job.id)}
)
response = FileResponse(
open(backup_path, 'rb'),
as_attachment=True,
filename=backup_job.backup_filename
)
return response
@login_required
def backup_restore(request):
"""Restore from backup"""
if request.method == 'POST':
from stiftung.models import BackupJob
backup_file = request.FILES.get('backup_file')
if not backup_file:
messages.error(request, 'Bitte wählen Sie eine Backup-Datei aus.')
return redirect('stiftung:backup_management')
# Validate file
if not backup_file.name.endswith('.tar.gz'):
messages.error(request, 'Ungültiges Backup-Format. Nur .tar.gz Dateien sind erlaubt.')
return redirect('stiftung:backup_management')
# Save uploaded file
import os
import tempfile
temp_dir = tempfile.mkdtemp()
backup_path = os.path.join(temp_dir, backup_file.name)
with open(backup_path, 'wb+') as destination:
for chunk in backup_file.chunks():
destination.write(chunk)
# Create restore job
restore_job = BackupJob.objects.create(
backup_type='full',
created_by=request.user,
backup_filename=backup_file.name
)
# Log restore initiation
from stiftung.audit import log_system_action
log_system_action(
request=request,
action='restore',
description=f"Wiederherstellung gestartet von: {backup_file.name}",
details={'restore_job_id': str(restore_job.id), 'filename': backup_file.name}
)
# Start restore process
import threading
from stiftung.backup_utils import run_restore
restore_thread = threading.Thread(target=run_restore, args=(str(restore_job.id), backup_path))
restore_thread.start()
messages.success(request, f'Wiederherstellung von "{backup_file.name}" wurde gestartet.')
return redirect('stiftung:backup_management')
return redirect('stiftung:backup_management')
# =============================================================================
# USER MANAGEMENT VIEWS
# =============================================================================
@login_required
def user_management(request):
"""User Management Dashboard"""
from django.contrib.auth.models import User
from django.core.paginator import Paginator
from django.db.models import Q
# Check permission
if not request.user.has_perm('stiftung.manage_users'):
messages.error(request, 'Sie haben keine Berechtigung für die Benutzerverwaltung.')
return redirect('stiftung:administration')
users = User.objects.all().order_by('username')
# Search functionality
search = request.GET.get('search')
if search:
users = users.filter(
Q(username__icontains=search) |
Q(email__icontains=search) |
Q(first_name__icontains=search) |
Q(last_name__icontains=search)
)
# Filter by status
status_filter = request.GET.get('status')
if status_filter == 'active':
users = users.filter(is_active=True)
elif status_filter == 'inactive':
users = users.filter(is_active=False)
elif status_filter == 'staff':
users = users.filter(is_staff=True)
# Pagination
paginator = Paginator(users, 20)
page_number = request.GET.get('page')
page_obj = paginator.get_page(page_number)
# Statistics
stats = {
'total_users': User.objects.count(),
'active_users': User.objects.filter(is_active=True).count(),
'staff_users': User.objects.filter(is_staff=True).count(),
'inactive_users': User.objects.filter(is_active=False).count(),
}
context = {
'page_obj': page_obj,
'stats': stats,
'search': search,
'status_filter': status_filter,
}
return render(request, 'stiftung/user_management.html', context)
@login_required
def user_create(request):
"""Create a new user"""
from stiftung.forms import UserCreationForm
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')
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 stiftung.forms import UserUpdateForm
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)
if request.method == 'POST':
form = UserUpdateForm(request.POST, instance=user)
if form.is_valid():
# Track changes
from stiftung.audit import track_model_changes, log_action
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 stiftung.forms import PasswordChangeForm
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)
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 stiftung.forms import UserPermissionForm
from django.contrib.auth.models import User, Permission
# 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)