- Fix undefined variable 'e' in PDF generator error handling - Replace undefined 'Verpachtung' model references with 'LandVerpachtung' - Fix all import and object access references to use correct model name - Resolve all flake8 F821 undefined name errors This addresses all code quality failures that were blocking CI pipeline.
5752 lines
237 KiB
Python
5752 lines
237 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, CSVImport, LandAbrechnung, LandVerpachtung, AppConfiguration, DestinataerUnterstuetzung, UnterstuetzungWiederkehrend
|
|
import json
|
|
|
|
def get_pdf_generator():
|
|
"""Lazy load PDF generator to handle missing dependencies gracefully"""
|
|
try:
|
|
from .utils.pdf_generator import pdf_generator
|
|
return pdf_generator
|
|
except ImportError as e:
|
|
# Store the error message for use in MockPDFGenerator
|
|
error_message = str(e)
|
|
|
|
# Return a mock generator if dependencies are missing
|
|
class MockPDFGenerator:
|
|
def is_available(self):
|
|
return False
|
|
def export_data_list_pdf(self, *args, **kwargs):
|
|
from django.http import HttpResponse
|
|
error_html = f"""
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head><title>PDF Not Available</title></head>
|
|
<body>
|
|
<h1>PDF Export Not Available</h1>
|
|
<p>PDF generation requires additional system dependencies that are not installed.</p>
|
|
<p>Error: {error_message}</p>
|
|
<p>Please install WeasyPrint dependencies or use CSV export instead.</p>
|
|
</body>
|
|
</html>
|
|
"""
|
|
response = HttpResponse(error_html, content_type='text/html')
|
|
response['Content-Disposition'] = 'inline; filename="pdf_not_available.html"'
|
|
return response
|
|
return MockPDFGenerator()
|
|
|
|
class GrampsClient:
|
|
"""Lightweight client for Gramps Web API."""
|
|
def __init__(self, base_url: str, token: str = '', username: str = '', password: str = ''):
|
|
self.base_url = base_url.rstrip('/')
|
|
self.session = requests.Session()
|
|
if token:
|
|
self.session.headers.update({"Authorization": f"Bearer {token}"})
|
|
self.username = username
|
|
self.password = password
|
|
self._cached_token = token
|
|
|
|
def search_people(self, query: str, limit: int = 5):
|
|
try:
|
|
r = self.session.get(f"{self.base_url}/api/people/", params={"q": query, "limit": limit}, timeout=10)
|
|
r.raise_for_status()
|
|
return r.json()
|
|
except Exception as e:
|
|
# try login-once if unauthorized and we have credentials
|
|
if self.username and self.password and '401' in str(e):
|
|
if self._login():
|
|
return self.search_people(query, limit)
|
|
return {"error": str(e)}
|
|
|
|
def get_person(self, handle_or_id: str):
|
|
try:
|
|
r = self.session.get(f"{self.base_url}/api/people/{handle_or_id}", timeout=10)
|
|
r.raise_for_status()
|
|
return r.json()
|
|
except Exception as e:
|
|
if self.username and self.password and '401' in str(e):
|
|
if self._login():
|
|
return self.get_person(handle_or_id)
|
|
return {"error": str(e)}
|
|
|
|
def _login(self) -> bool:
|
|
try:
|
|
# try common endpoints
|
|
endpoints = [
|
|
("/api/auth/login", {"username": self.username, "password": self.password}, "json"),
|
|
("/auth/login", {"username": self.username, "password": self.password}, "json"),
|
|
("/api/token", {"username": self.username, "password": self.password}, "form"),
|
|
("/login", {"username": self.username, "password": self.password}, "form"),
|
|
("/token", {"username": self.username, "password": self.password}, "form"),
|
|
("/api/login", {"username": self.username, "password": self.password}, "json"),
|
|
]
|
|
for path, payload, mode in endpoints:
|
|
url = f"{self.base_url}{path}"
|
|
if mode == 'json':
|
|
r = self.session.post(url, json=payload, timeout=10, allow_redirects=False)
|
|
else:
|
|
r = self.session.post(url, data=payload, timeout=10, allow_redirects=False)
|
|
# Success with token body
|
|
if r.status_code in (200, 201) and 'application/json' in r.headers.get('Content-Type',''):
|
|
data = r.json()
|
|
token = data.get('access_token') or data.get('token') or data.get('access') or data.get('jwt')
|
|
if token:
|
|
self._cached_token = token
|
|
self.session.headers.update({"Authorization": f"Bearer {token}"})
|
|
return True
|
|
# Success via session cookie and redirect
|
|
if r.status_code in (200, 302) and ('set-cookie' in {k.lower():v for k,v in r.headers.items()}):
|
|
return True
|
|
# Basic Auth fallback (some setups protect API with Basic)
|
|
try:
|
|
self.session.auth = (self.username, self.password)
|
|
r = self.session.get(f"{self.base_url}/api/people/?limit=1", timeout=10)
|
|
if r.status_code == 200:
|
|
return True
|
|
except Exception:
|
|
pass
|
|
return False
|
|
except Exception:
|
|
return False
|
|
|
|
def get_gramps_client() -> GrampsClient:
|
|
return GrampsClient(
|
|
getattr(settings, 'GRAMPS_URL', ''),
|
|
getattr(settings, 'GRAMPS_API_TOKEN', ''),
|
|
getattr(settings, 'GRAMPS_USERNAME', ''),
|
|
getattr(settings, 'GRAMPS_PASSWORD', ''),
|
|
)
|
|
|
|
@api_view(['GET'])
|
|
def gramps_debug_api(_request):
|
|
return Response({
|
|
'GRAMPS_URL': getattr(settings, 'GRAMPS_URL', ''),
|
|
'has_username': bool(getattr(settings, 'GRAMPS_USERNAME', '')),
|
|
'has_password': bool(getattr(settings, 'GRAMPS_PASSWORD', '')),
|
|
})
|
|
|
|
from .forms import PersonForm, PaechterForm, DestinataerForm, DokumentLinkForm, FoerderungForm, UnterstuetzungForm, UnterstuetzungMarkAsPaidForm, LandForm, 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."""
|
|
from stiftung.utils.config import get_paperless_config
|
|
|
|
config = get_paperless_config()
|
|
url = config['api_url']
|
|
if not url:
|
|
return Response({'error': 'Paperless API not configured'}, status=400)
|
|
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')
|
|
# Get new LandVerpachtungen for this person's Paechter instances
|
|
verpachtungen = LandVerpachtung.objects.filter(paechter__person=person).order_by('-pachtbeginn')
|
|
|
|
context = {
|
|
'person': person,
|
|
'foerderungen': foerderungen,
|
|
'verpachtungen': verpachtungen,
|
|
}
|
|
return render(request, 'stiftung/person_detail.html', context)
|
|
|
|
@login_required
|
|
def person_create(request):
|
|
if request.method == 'POST':
|
|
form = PersonForm(request.POST)
|
|
if form.is_valid():
|
|
person = form.save()
|
|
messages.success(request, f'Person "{person.get_full_name()}" wurde erfolgreich erstellt.')
|
|
return redirect('stiftung:person_detail', pk=person.pk)
|
|
else:
|
|
form = PersonForm()
|
|
|
|
context = {'form': form, 'title': 'Neue Person erstellen'}
|
|
return render(request, 'stiftung/person_form.html', context)
|
|
|
|
@login_required
|
|
def person_update(request, pk):
|
|
person = get_object_or_404(Person, pk=pk)
|
|
if request.method == 'POST':
|
|
form = PersonForm(request.POST, instance=person)
|
|
if form.is_valid():
|
|
person = form.save()
|
|
messages.success(request, f'Person "{person.get_full_name()}" wurde erfolgreich aktualisiert.')
|
|
return redirect('stiftung:person_detail', pk=person.pk)
|
|
else:
|
|
form = PersonForm(instance=person)
|
|
|
|
context = {'form': form, 'person': person, 'title': f'Person bearbeiten: {person.get_full_name()}'}
|
|
return render(request, 'stiftung/person_form.html', context)
|
|
|
|
@login_required
|
|
def person_delete(request, pk):
|
|
person = get_object_or_404(Person, pk=pk)
|
|
if request.method == 'POST':
|
|
person.delete()
|
|
messages.success(request, f'Person "{person.get_full_name()}" wurde erfolgreich gelöscht.')
|
|
return redirect('stiftung:person_list')
|
|
|
|
context = {'person': person}
|
|
return render(request, 'stiftung/person_confirm_delete.html', context)
|
|
|
|
# Destinatär Views (Förderungsempfänger)
|
|
@login_required
|
|
def destinataer_list(request):
|
|
search_query = request.GET.get('search', '')
|
|
familienzweig_filter = request.GET.get('familienzweig', '')
|
|
berufsgruppe_filter = request.GET.get('berufsgruppe', '')
|
|
aktiv_filter = request.GET.get('aktiv', '')
|
|
sort = request.GET.get('sort', '')
|
|
direction = request.GET.get('dir', 'asc')
|
|
|
|
destinataere = Destinataer.objects.all()
|
|
|
|
if search_query:
|
|
destinataere = destinataere.filter(
|
|
Q(nachname__icontains=search_query) |
|
|
Q(vorname__icontains=search_query) |
|
|
Q(email__icontains=search_query) |
|
|
Q(institution__icontains=search_query) |
|
|
Q(familienzweig__icontains=search_query)
|
|
)
|
|
|
|
if familienzweig_filter:
|
|
destinataere = destinataere.filter(familienzweig=familienzweig_filter)
|
|
|
|
if berufsgruppe_filter:
|
|
destinataere = destinataere.filter(berufsgruppe=berufsgruppe_filter)
|
|
|
|
if aktiv_filter == 'true':
|
|
destinataere = destinataere.filter(aktiv=True)
|
|
elif aktiv_filter == 'false':
|
|
destinataere = destinataere.filter(aktiv=False)
|
|
|
|
# Annotate with total funding (coalesce nulls to Decimal for stable sorting)
|
|
destinataere = destinataere.annotate(
|
|
total_foerderungen=Coalesce(
|
|
Sum('foerderung__betrag'),
|
|
Value(Decimal('0.00'), output_field=DecimalField(max_digits=12, decimal_places=2)),
|
|
output_field=DecimalField(max_digits=12, decimal_places=2),
|
|
)
|
|
)
|
|
|
|
# Sorting
|
|
sort_map = {
|
|
'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')
|
|
|
|
# Unterstützungen für diesen Destinatär laden
|
|
unterstuetzungen = DestinataerUnterstuetzung.objects.filter(destinataer=destinataer).order_by('-faellig_am')
|
|
|
|
# Notizen laden
|
|
notizen_eintraege = DestinataerNotiz.objects.filter(destinataer=destinataer).order_by('-erstellt_am')
|
|
|
|
context = {
|
|
'destinataer': destinataer,
|
|
'verknuepfte_dokumente': verknuepfte_dokumente,
|
|
'foerderungen': foerderungen,
|
|
'unterstuetzungen': unterstuetzungen,
|
|
'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('neue_verpachtungen__verpachtete_flaeche'),
|
|
Value(Decimal('0.00'), output_field=DecimalField(max_digits=12, decimal_places=2)),
|
|
output_field=DecimalField(max_digits=12, decimal_places=2),
|
|
),
|
|
gesamt_pachtzins=Coalesce(
|
|
Sum('neue_verpachtungen__pachtzins_pauschal'),
|
|
Value(Decimal('0.00'), output_field=DecimalField(max_digits=12, decimal_places=2)),
|
|
output_field=DecimalField(max_digits=12, decimal_places=2)),
|
|
)
|
|
|
|
# Sorting
|
|
sort_map = {
|
|
'name': ['nachname', 'vorname'],
|
|
'pachtnummer': ['pachtnummer'],
|
|
'ausbildung': ['landwirtschaftliche_ausbildung'],
|
|
'spezialisierung': ['spezialisierung'],
|
|
'flaeche': ['gesamt_flaeche'],
|
|
'pachtzins': ['gesamt_pachtzins'],
|
|
'status': ['aktiv'],
|
|
}
|
|
if sort in sort_map:
|
|
fields = sort_map[sort]
|
|
if direction == 'desc':
|
|
order_fields = [f'-{f}' for f in fields]
|
|
else:
|
|
order_fields = fields
|
|
paechter = paechter.order_by(*order_fields)
|
|
|
|
paginator = Paginator(paechter, 20)
|
|
page_number = request.GET.get('page')
|
|
page_obj = paginator.get_page(page_number)
|
|
|
|
context = {
|
|
'page_obj': page_obj,
|
|
'search_query': search_query,
|
|
'ausbildung_filter': ausbildung_filter,
|
|
'aktiv_filter': aktiv_filter,
|
|
'sort': sort,
|
|
'dir': direction,
|
|
}
|
|
return render(request, 'stiftung/paechter_list.html', context)
|
|
|
|
@login_required
|
|
def paechter_detail(request, pk):
|
|
paechter = get_object_or_404(Paechter, pk=pk)
|
|
|
|
# Alle mit diesem Pächter verknüpften Dokumente laden
|
|
verknuepfte_dokumente = DokumentLink.objects.filter(
|
|
paechter_id=paechter.pk
|
|
).order_by('kontext', 'titel')
|
|
|
|
# Neue LandVerpachtungen für diesen Pächter laden
|
|
verpachtungen = LandVerpachtung.objects.filter(paechter=paechter).order_by('-pachtbeginn')
|
|
|
|
# Neue gepachtete Ländereien (über aktueller_paechter)
|
|
gepachtete_laendereien = paechter.gepachtete_laendereien.filter(aktiv=True).order_by('gemeinde', 'gemarkung')
|
|
|
|
# Statistiken berechnen
|
|
total_flaeche_neu = sum(land.verp_flaeche_aktuell or 0 for land in gepachtete_laendereien)
|
|
total_pachtzins_neu = sum(land.pachtzins_pauschal or 0 for land in gepachtete_laendereien)
|
|
|
|
context = {
|
|
'paechter': paechter,
|
|
'verknuepfte_dokumente': verknuepfte_dokumente,
|
|
'verpachtungen': verpachtungen, # Now using LandVerpachtung
|
|
'gepachtete_laendereien': gepachtete_laendereien, # Neu
|
|
'total_flaeche_neu': total_flaeche_neu,
|
|
'total_pachtzins_neu': total_pachtzins_neu,
|
|
}
|
|
return render(request, 'stiftung/paechter_detail.html', context)
|
|
|
|
@login_required
|
|
def paechter_create(request):
|
|
if request.method == 'POST':
|
|
form = PaechterForm(request.POST)
|
|
if form.is_valid():
|
|
paechter = form.save()
|
|
messages.success(request, f'Pächter "{paechter.get_full_name()}" wurde erfolgreich erstellt.')
|
|
return redirect('stiftung:paechter_detail', pk=paechter.pk)
|
|
else:
|
|
# Debug: Log form errors and show them to user
|
|
print(f"Form errors: {form.errors}")
|
|
print(f"Form data: {request.POST}")
|
|
messages.error(request, f'Formular-Fehler: {form.errors}')
|
|
for field, errors in form.errors.items():
|
|
for error in errors:
|
|
messages.error(request, f'{field}: {error}')
|
|
else:
|
|
form = PaechterForm()
|
|
|
|
context = {'form': form, 'title': 'Neuen Pächter erstellen'}
|
|
return render(request, 'stiftung/paechter_form.html', context)
|
|
|
|
@login_required
|
|
def paechter_update(request, pk):
|
|
paechter = get_object_or_404(Paechter, pk=pk)
|
|
if request.method == 'POST':
|
|
form = PaechterForm(request.POST, instance=paechter)
|
|
if form.is_valid():
|
|
paechter = form.save()
|
|
messages.success(request, f'Pächter "{paechter.get_full_name()}" wurde erfolgreich aktualisiert.')
|
|
return redirect('stiftung:paechter_detail', pk=paechter.pk)
|
|
else:
|
|
# Debug: Log form errors and show them to user
|
|
print(f"Form errors: {form.errors}")
|
|
print(f"Form data: {request.POST}")
|
|
messages.error(request, f'Formular-Fehler: {form.errors}')
|
|
for field, errors in form.errors.items():
|
|
for error in errors:
|
|
messages.error(request, f'{field}: {error}')
|
|
else:
|
|
form = PaechterForm(instance=paechter)
|
|
|
|
context = {'form': form, 'paechter': paechter, 'title': f'Pächter bearbeiten: {paechter.get_full_name()}'}
|
|
return render(request, 'stiftung/paechter_form.html', context)
|
|
|
|
@login_required
|
|
def paechter_delete(request, pk):
|
|
paechter = get_object_or_404(Paechter, pk=pk)
|
|
if request.method == 'POST':
|
|
paechter.delete()
|
|
messages.success(request, f'Pächter "{paechter.get_full_name()}" wurde erfolgreich gelöscht.')
|
|
return redirect('stiftung:paechter_list')
|
|
|
|
context = {'paechter': paechter}
|
|
return render(request, 'stiftung/paechter_confirm_delete.html', context)
|
|
|
|
# Land Views
|
|
@login_required
|
|
def land_list(request):
|
|
search_query = request.GET.get('search', '')
|
|
gemeinde_filter = request.GET.get('gemeinde', '')
|
|
aktiv_filter = request.GET.get('aktiv', '')
|
|
sort = request.GET.get('sort', '')
|
|
direction = request.GET.get('dir', 'asc')
|
|
|
|
lands = Land.objects.all()
|
|
|
|
if search_query:
|
|
lands = lands.filter(
|
|
Q(lfd_nr__icontains=search_query) |
|
|
Q(gemeinde__icontains=search_query) |
|
|
Q(gemarkung__icontains=search_query) |
|
|
Q(flur__icontains=search_query) |
|
|
Q(flurstueck__icontains=search_query)
|
|
)
|
|
|
|
if gemeinde_filter:
|
|
lands = lands.filter(gemeinde=gemeinde_filter)
|
|
|
|
if aktiv_filter == 'true':
|
|
lands = lands.filter(aktiv=True)
|
|
elif aktiv_filter == 'false':
|
|
lands = lands.filter(aktiv=False)
|
|
|
|
# Annotate with verpachtungsgrad and numeric casts for natural sorting
|
|
# Prepare numeric versions of textual fields by stripping common non-digits
|
|
def digits_only(field_expr):
|
|
expr = Replace(field_expr, Value(' '), Value(''))
|
|
expr = Replace(expr, Value('-'), Value(''))
|
|
expr = Replace(expr, Value('.'), Value(''))
|
|
expr = Replace(expr, Value('/'), Value(''))
|
|
expr = Replace(expr, Value('L'), Value(''))
|
|
return expr
|
|
|
|
lands = lands.extra(
|
|
select={'verpachtungsgrad': 'CASE WHEN groesse_qm > 0 THEN (verp_flaeche_aktuell / groesse_qm) * 100 ELSE 0 END'}
|
|
).annotate(
|
|
lfd_nr_num=Cast(NullIf(digits_only(F('lfd_nr')), Value('')), IntegerField()),
|
|
flur_num=Cast(NullIf(digits_only(F('flur')), Value('')), IntegerField()),
|
|
flurstueck_num=Cast(NullIf(digits_only(F('flurstueck')), Value('')), IntegerField()),
|
|
)
|
|
|
|
# Sorting
|
|
sort_map = {
|
|
'lfd_nr': ['lfd_nr_num', 'lfd_nr'],
|
|
'gemeinde': ['gemeinde'],
|
|
'gemarkung': ['gemarkung'],
|
|
'flur': ['flur_num', 'flur'],
|
|
'flurstueck': ['flurstueck_num', 'flurstueck'],
|
|
'groesse': ['groesse_qm'],
|
|
'verp': ['verp_flaeche_aktuell'],
|
|
'grad': ['verpachtungsgrad'],
|
|
}
|
|
if sort in sort_map:
|
|
fields = sort_map[sort]
|
|
if direction == 'desc':
|
|
order_fields = [f'-{f}' for f in fields]
|
|
else:
|
|
order_fields = fields
|
|
lands = lands.order_by(*order_fields)
|
|
|
|
# Aggregated statistics for current filter set
|
|
aggregates = lands.aggregate(
|
|
sum_groesse_qm=Sum('groesse_qm'),
|
|
sum_gruenland_qm=Sum('gruenland_qm'),
|
|
sum_acker_qm=Sum('acker_qm'),
|
|
sum_wald_qm=Sum('wald_qm'),
|
|
sum_sonstiges_qm=Sum('sonstiges_qm'),
|
|
)
|
|
sum_gruenland_qm = float(aggregates.get('sum_gruenland_qm') or 0)
|
|
sum_acker_qm = float(aggregates.get('sum_acker_qm') or 0)
|
|
sum_wald_qm = float(aggregates.get('sum_wald_qm') or 0)
|
|
sum_sonstiges_qm = float(aggregates.get('sum_sonstiges_qm') or 0)
|
|
sum_total_use_qm = sum_gruenland_qm + sum_acker_qm + sum_wald_qm + sum_sonstiges_qm
|
|
def pct(part, total):
|
|
return round((part / total) * 100, 1) if total and part is not None else 0.0
|
|
stats = {
|
|
'sum_gruenland_qm': sum_gruenland_qm,
|
|
'sum_acker_qm': sum_acker_qm,
|
|
'sum_wald_qm': sum_wald_qm,
|
|
'sum_sonstiges_qm': sum_sonstiges_qm,
|
|
'sum_total_use_qm': sum_total_use_qm,
|
|
'pct_gruenland': pct(sum_gruenland_qm, sum_total_use_qm),
|
|
'pct_acker': pct(sum_acker_qm, sum_total_use_qm),
|
|
'pct_wald': pct(sum_wald_qm, sum_total_use_qm),
|
|
}
|
|
|
|
# Prepare size chart data (top 30 by size)
|
|
top_sizes = list(
|
|
lands.order_by('-groesse_qm').values_list('lfd_nr', 'groesse_qm')[:30]
|
|
)
|
|
size_chart_labels = [label or '' for label, _ in top_sizes]
|
|
size_chart_values = [float(val or 0) for _, val in top_sizes]
|
|
|
|
paginator = Paginator(lands, 20)
|
|
page_number = request.GET.get('page')
|
|
page_obj = paginator.get_page(page_number)
|
|
|
|
# Get unique gemeinden for filter
|
|
gemeinden = Land.objects.values_list('gemeinde', flat=True).distinct().order_by('gemeinde')
|
|
|
|
context = {
|
|
'page_obj': page_obj,
|
|
'search_query': search_query,
|
|
'gemeinde_filter': gemeinde_filter,
|
|
'aktiv_filter': aktiv_filter,
|
|
'gemeinden': gemeinden,
|
|
'stats': stats,
|
|
'size_chart_labels_json': json.dumps(size_chart_labels),
|
|
'size_chart_values_json': json.dumps(size_chart_values),
|
|
'sort': sort,
|
|
'dir': direction,
|
|
}
|
|
return render(request, 'stiftung/land_list.html', context)
|
|
|
|
@login_required
|
|
def land_detail(request, pk):
|
|
land = get_object_or_404(Land, pk=pk)
|
|
|
|
# Alle mit dieser Länderei verknüpften Dokumente laden
|
|
verknuepfte_dokumente = DokumentLink.objects.filter(
|
|
land_id=land.pk
|
|
).order_by('kontext', 'titel')
|
|
|
|
# Neue LandVerpachtungen laden (mit related data)
|
|
neue_verpachtungen = land.neue_verpachtungen.select_related('paechter').order_by('-pachtbeginn')
|
|
|
|
context = {
|
|
'land': land,
|
|
'verknuepfte_dokumente': verknuepfte_dokumente,
|
|
'verpachtungen': neue_verpachtungen, # Using only new system now
|
|
'neue_verpachtungen': neue_verpachtungen,
|
|
}
|
|
return render(request, 'stiftung/land_detail.html', context)
|
|
|
|
@login_required
|
|
def land_create(request):
|
|
if request.method == 'POST':
|
|
form = LandForm(request.POST)
|
|
if form.is_valid():
|
|
land = form.save()
|
|
messages.success(request, f'Länderei "{land}" wurde erfolgreich erstellt.')
|
|
return redirect('stiftung:land_detail', pk=land.pk)
|
|
else:
|
|
form = LandForm()
|
|
|
|
context = {'form': form, 'title': 'Neue Länderei erstellen'}
|
|
return render(request, 'stiftung/land_form.html', context)
|
|
|
|
@login_required
|
|
def land_update(request, pk):
|
|
land = get_object_or_404(Land, pk=pk)
|
|
if request.method == 'POST':
|
|
form = LandForm(request.POST, instance=land)
|
|
if form.is_valid():
|
|
land = form.save()
|
|
messages.success(request, f'Länderei "{land}" wurde erfolgreich aktualisiert.')
|
|
return redirect('stiftung:land_detail', pk=land.pk)
|
|
else:
|
|
form = LandForm(instance=land)
|
|
|
|
context = {'form': form, 'land': land, 'title': f'Länderei bearbeiten: {land}'}
|
|
return render(request, 'stiftung/land_form.html', context)
|
|
|
|
@login_required
|
|
def land_delete(request, pk):
|
|
land = get_object_or_404(Land, pk=pk)
|
|
if request.method == 'POST':
|
|
land.delete()
|
|
messages.success(request, f'Länderei "{land}" wurde erfolgreich gelöscht.')
|
|
return redirect('stiftung:land_list')
|
|
|
|
context = {'land': land}
|
|
return render(request, 'stiftung/land_confirm_delete.html', context)
|
|
|
|
# Verpachtung Views
|
|
@login_required
|
|
def verpachtung_list(request):
|
|
search_query = request.GET.get('search', '')
|
|
status_filter = request.GET.get('status', '')
|
|
gemeinde_filter = request.GET.get('gemeinde', '')
|
|
sort = request.GET.get('sort', '')
|
|
direction = request.GET.get('dir', 'asc')
|
|
|
|
verpachtungen = LandVerpachtung.objects.select_related('land', 'paechter').all()
|
|
|
|
if search_query:
|
|
verpachtungen = verpachtungen.filter(
|
|
Q(vertragsnummer__icontains=search_query) |
|
|
Q(land__gemeinde__icontains=search_query) |
|
|
Q(paechter__nachname__icontains=search_query) |
|
|
Q(paechter__vorname__icontains=search_query)
|
|
)
|
|
|
|
if status_filter:
|
|
verpachtungen = verpachtungen.filter(status=status_filter)
|
|
|
|
if gemeinde_filter:
|
|
verpachtungen = verpachtungen.filter(land__gemeinde=gemeinde_filter)
|
|
|
|
# Sorting
|
|
sort_map = {
|
|
'vertragsnummer': ['vertragsnummer'],
|
|
'land': ['land__gemeinde'],
|
|
'paechter': ['paechter__nachname', 'paechter__vorname'],
|
|
'beginn': ['pachtbeginn'],
|
|
'ende': ['pachtende'],
|
|
'flaeche': ['verpachtete_flaeche'],
|
|
'pachtzins': ['pachtzins_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 = LandVerpachtung.objects.select_related('land', 'paechter').all()
|
|
|
|
# Active verpachtungen count
|
|
aktive_verpachtungen = all_verpachtungen.filter(status='aktiv').count()
|
|
|
|
# Total leased area (only active verpachtungen)
|
|
gesamt_flaeche_result = all_verpachtungen.filter(status='aktiv').aggregate(
|
|
total=Sum('verpachtete_flaeche')
|
|
)
|
|
gesamt_flaeche = gesamt_flaeche_result['total'] if gesamt_flaeche_result['total'] is not None else 0
|
|
|
|
# Total annual rent (only active verpachtungen)
|
|
jaehrlicher_pachtzins_result = all_verpachtungen.filter(status='aktiv').aggregate(
|
|
total=Sum('pachtzins_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 = LandVerpachtung.STATUS_CHOICES
|
|
|
|
context = {
|
|
'page_obj': page_obj,
|
|
'search_query': search_query,
|
|
'status_filter': status_filter,
|
|
'gemeinde_filter': gemeinde_filter,
|
|
'gemeinden': gemeinden,
|
|
'status_choices': status_choices,
|
|
# Statistics for summary cards
|
|
'aktive_verpachtungen': aktive_verpachtungen,
|
|
'gesamt_flaeche': gesamt_flaeche,
|
|
'jaehrlicher_pachtzins': jaehrlicher_pachtzins,
|
|
'anzahl_verpachtungen': anzahl_verpachtungen,
|
|
'sort': sort,
|
|
'dir': direction,
|
|
}
|
|
return render(request, 'stiftung/verpachtung_list.html', context)
|
|
|
|
@login_required
|
|
@login_required
|
|
def land_verpachtung_detail(request, pk):
|
|
"""Detail view for LandVerpachtung"""
|
|
verpachtung = get_object_or_404(LandVerpachtung, pk=pk)
|
|
|
|
# Alle mit dieser Verpachtung verknüpften Dokumente laden
|
|
verknuepfte_dokumente = DokumentLink.objects.filter(
|
|
land_verpachtung_id=verpachtung.pk
|
|
).order_by('kontext', 'titel')
|
|
|
|
context = {
|
|
'verpachtung': verpachtung,
|
|
'landverpachtung': verpachtung, # Template expects this variable name
|
|
'verknuepfte_dokumente': verknuepfte_dokumente,
|
|
}
|
|
return render(request, 'stiftung/land_verpachtung_detail.html', context)
|
|
|
|
@login_required
|
|
def land_verpachtung_update(request, pk):
|
|
"""Update an existing LandVerpachtung by its primary key"""
|
|
verpachtung = get_object_or_404(LandVerpachtung, pk=pk)
|
|
|
|
if request.method == 'POST':
|
|
# Handle the update form submission
|
|
vertragsnummer = request.POST.get('vertragsnummer')
|
|
pachtbeginn = request.POST.get('pachtbeginn')
|
|
pachtende = request.POST.get('pachtende')
|
|
pachtzins_jaehrlich = request.POST.get('pachtzins_jaehrlich')
|
|
|
|
if vertragsnummer:
|
|
verpachtung.vertragsnummer = vertragsnummer
|
|
if pachtbeginn:
|
|
verpachtung.pachtbeginn = pachtbeginn
|
|
if pachtende:
|
|
verpachtung.pachtende = pachtende
|
|
if pachtzins_jaehrlich:
|
|
verpachtung.pachtzins_jaehrlich = pachtzins_jaehrlich
|
|
|
|
verpachtung.save()
|
|
messages.success(request, 'Verpachtung wurde erfolgreich aktualisiert.')
|
|
return redirect('stiftung:land_verpachtung_detail', pk=verpachtung.pk)
|
|
|
|
context = {
|
|
'verpachtung': verpachtung,
|
|
'landverpachtung': verpachtung, # Template expects this variable name
|
|
'is_edit': True,
|
|
'is_update': True, # Form template uses this flag
|
|
}
|
|
return render(request, 'stiftung/land_verpachtung_form.html', context)
|
|
|
|
@login_required
|
|
def land_verpachtung_end_direct(request, pk):
|
|
"""End a LandVerpachtung directly by its primary key"""
|
|
verpachtung = get_object_or_404(LandVerpachtung, pk=pk)
|
|
|
|
if request.method == 'POST':
|
|
verpachtung.status = 'beendet'
|
|
verpachtung.pachtende = timezone.now().date()
|
|
verpachtung.save()
|
|
messages.success(request, 'Verpachtung wurde erfolgreich beendet.')
|
|
return redirect('stiftung:land_detail', pk=verpachtung.land.pk)
|
|
|
|
context = {
|
|
'verpachtung': verpachtung,
|
|
}
|
|
return render(request, 'stiftung/land_verpachtung_end_confirm.html', context)
|
|
|
|
# Förderung Views
|
|
@login_required
|
|
def foerderung_list(request):
|
|
"""List all funding grants with filtering and pagination"""
|
|
foerderungen = Foerderung.objects.select_related('destinataer', 'verwendungsnachweis').all()
|
|
|
|
# Check for export request - handle both GET and POST
|
|
export_format = request.POST.get('format') if request.method == 'POST' else request.GET.get('format', '')
|
|
selected_ids_param = request.POST.get('selected_entries', '') if request.method == 'POST' else request.GET.get('selected_entries', '')
|
|
selected_ids = [id for id in selected_ids_param.split(',') if id] if selected_ids_param else []
|
|
|
|
# Filtering
|
|
jahr = request.GET.get('jahr')
|
|
kategorie = request.GET.get('kategorie')
|
|
status = request.GET.get('status')
|
|
destinataer = request.GET.get('destinataer')
|
|
|
|
if jahr:
|
|
foerderungen = foerderungen.filter(jahr=int(jahr))
|
|
if kategorie:
|
|
foerderungen = foerderungen.filter(kategorie=kategorie)
|
|
if status:
|
|
foerderungen = foerderungen.filter(status=status)
|
|
if destinataer:
|
|
foerderungen = foerderungen.filter(destinataer__nachname__icontains=destinataer)
|
|
|
|
# Handle exports
|
|
if export_format == 'csv':
|
|
return export_foerderungen_csv(request, foerderungen, selected_ids)
|
|
elif export_format == 'pdf':
|
|
return export_foerderungen_pdf(request, foerderungen, selected_ids)
|
|
|
|
# Pagination
|
|
paginator = Paginator(foerderungen, 25)
|
|
page_number = request.GET.get('page')
|
|
page_obj = paginator.get_page(page_number)
|
|
|
|
# Statistics
|
|
total_betrag = foerderungen.aggregate(total=Sum('betrag'))['total'] or 0
|
|
avg_betrag = foerderungen.aggregate(avg=Avg('betrag'))['avg'] or 0
|
|
|
|
# Year choices for filters
|
|
jahre = sorted(set(
|
|
list(Foerderung.objects.values_list('jahr', flat=True))
|
|
), reverse=True)
|
|
|
|
context = {
|
|
'page_obj': page_obj,
|
|
'foerderungen': foerderungen, # Add for counting
|
|
'total_betrag': total_betrag,
|
|
'avg_betrag': avg_betrag,
|
|
'kategorien': Foerderung.KATEGORIE_CHOICES,
|
|
'status_choices': Foerderung.STATUS_CHOICES,
|
|
'filter_jahr': jahr,
|
|
'filter_kategorie': kategorie,
|
|
'filter_status': status,
|
|
'filter_person': destinataer,
|
|
'jahre': jahre,
|
|
}
|
|
return render(request, 'stiftung/foerderung_list.html', context)
|
|
|
|
@login_required
|
|
def foerderung_detail(request, pk):
|
|
"""Show details of a specific funding grant"""
|
|
foerderung = get_object_or_404(Foerderung.objects.select_related('person', 'verwendungsnachweis'), pk=pk)
|
|
|
|
# Alle mit dieser Förderung verknüpften Dokumente laden
|
|
verknuepfte_dokumente = DokumentLink.objects.filter(
|
|
foerderung_id=foerderung.pk
|
|
).order_by('kontext', 'titel')
|
|
|
|
context = {
|
|
'foerderung': foerderung,
|
|
'verknuepfte_dokumente': verknuepfte_dokumente,
|
|
'title': f'Förderung: {foerderung}',
|
|
}
|
|
return render(request, 'stiftung/foerderung_detail.html', context)
|
|
|
|
@login_required
|
|
def foerderung_create(request):
|
|
"""Create a new funding grant"""
|
|
# Get destinataer from URL parameter if provided
|
|
destinataer_id = request.GET.get('destinataer')
|
|
initial = {}
|
|
if destinataer_id:
|
|
initial['destinataer'] = destinataer_id
|
|
|
|
if request.method == 'POST':
|
|
form = FoerderungForm(request.POST)
|
|
if form.is_valid():
|
|
foerderung = form.save()
|
|
messages.success(request, f'Förderung für {foerderung.destinataer} wurde erfolgreich erstellt.')
|
|
return redirect('stiftung:foerderung_detail', pk=foerderung.pk)
|
|
else:
|
|
form = FoerderungForm(initial=initial)
|
|
|
|
context = {
|
|
'form': form,
|
|
'title': 'Neue Förderung erstellen',
|
|
}
|
|
return render(request, 'stiftung/foerderung_form.html', context)
|
|
|
|
@login_required
|
|
def foerderung_update(request, pk):
|
|
"""Update an existing funding grant"""
|
|
foerderung = get_object_or_404(Foerderung, pk=pk)
|
|
|
|
if request.method == 'POST':
|
|
form = FoerderungForm(request.POST, instance=foerderung)
|
|
if form.is_valid():
|
|
form.save()
|
|
messages.success(request, f'Förderung für {foerderung.person} wurde erfolgreich aktualisiert.')
|
|
return redirect('stiftung:foerderung_detail', pk=foerderung.pk)
|
|
else:
|
|
form = FoerderungForm(instance=foerderung)
|
|
|
|
context = {
|
|
'form': form,
|
|
'foerderung': foerderung,
|
|
'title': f'Förderung bearbeiten: {foerderung}',
|
|
}
|
|
return render(request, 'stiftung/foerderung_form.html', context)
|
|
|
|
@login_required
|
|
def foerderung_delete(request, pk):
|
|
"""Delete a funding grant"""
|
|
foerderung = get_object_or_404(Foerderung, pk=pk)
|
|
|
|
if request.method == 'POST':
|
|
# Get the recipient name before deletion
|
|
recipient_name = foerderung.destinataer.get_full_name() if foerderung.destinataer else (
|
|
foerderung.person.get_full_name() if foerderung.person else "Unbekannter Empfänger"
|
|
)
|
|
|
|
foerderung.delete()
|
|
messages.success(request, f'Förderung für {recipient_name} wurde erfolgreich gelöscht.')
|
|
return redirect('stiftung:foerderung_list')
|
|
|
|
context = {
|
|
'foerderung': foerderung,
|
|
'title': f'Förderung löschen: {foerderung}',
|
|
}
|
|
return render(request, 'stiftung/foerderung_confirm_delete.html', context)
|
|
|
|
# DokumentLink Views
|
|
@login_required
|
|
def dokument_list(request):
|
|
"""Zeigt alle verknüpften Dokumente an"""
|
|
# Alle verknüpften Dokumente laden
|
|
dokumente = DokumentLink.objects.all().order_by('-id')
|
|
|
|
# Paperless-API-Konfiguration für verfügbare Dokumente
|
|
from stiftung.utils.config import get_paperless_config
|
|
import requests
|
|
|
|
config = get_paperless_config()
|
|
url = config['api_url']
|
|
token = config['api_token']
|
|
|
|
available_dokumente = []
|
|
if url and token:
|
|
try:
|
|
base_url = url.rstrip('/api') if url.endswith('/api') else url
|
|
headers = {'Authorization': f'Token {token}'}
|
|
|
|
# Alle verfügbaren Dokumente abrufen (mit Paginierung)
|
|
all_dokumente = []
|
|
page = 1
|
|
page_size = 100
|
|
|
|
while True:
|
|
response = requests.get(f"{base_url}/api/documents/?page={page}&page_size={page_size}", headers=headers, timeout=10)
|
|
response.raise_for_status()
|
|
data = response.json()
|
|
|
|
all_dokumente.extend(data.get('results', []))
|
|
|
|
if not data.get('next'):
|
|
break
|
|
page += 1
|
|
|
|
# Stiftung-Dokumente filtern
|
|
for doc in all_dokumente:
|
|
try:
|
|
tags = []
|
|
doc_tags = doc.get('tags', [])
|
|
|
|
if isinstance(doc_tags, list):
|
|
for tag in doc_tags:
|
|
if isinstance(tag, dict) and 'name' in tag:
|
|
tags.append(tag['name'])
|
|
elif isinstance(tag, str):
|
|
tags.append(tag)
|
|
elif isinstance(tag, int):
|
|
tags.append(f"Tag_{tag}")
|
|
elif isinstance(doc_tags, str):
|
|
tags = [tag.strip() for tag in doc_tags.split(',')]
|
|
|
|
if any(tag in [config['destinataere_tag'], config['land_tag'], config['admin_tag']] for tag in tags):
|
|
bereits_verknuepft = DokumentLink.objects.filter(
|
|
paperless_document_id=doc['id']
|
|
).exists()
|
|
|
|
if not bereits_verknuepft:
|
|
available_dokumente.append({
|
|
'id': doc['id'],
|
|
'title': doc.get('title', f'Dokument {doc["id"]}'),
|
|
'created_date': doc.get('created_date', ''),
|
|
'tags': tags,
|
|
'thumbnail_url': f"{base_url}/api/documents/{doc['id']}/thumb/",
|
|
'document_url': f"{base_url}/documents/{doc['id']}/",
|
|
})
|
|
except Exception:
|
|
continue
|
|
|
|
# Nach Erstellungsdatum sortieren
|
|
available_dokumente.sort(key=lambda x: x['created_date'], reverse=True)
|
|
|
|
except Exception:
|
|
pass
|
|
|
|
context = {
|
|
'dokumente': dokumente,
|
|
'available_dokumente': available_dokumente,
|
|
'title': 'Alle verknüpften Dokumente',
|
|
}
|
|
return render(request, 'stiftung/dokument_list.html', context)
|
|
|
|
@login_required
|
|
def dokument_detail(request, pk):
|
|
"""Show details of a specific document link"""
|
|
dokument = get_object_or_404(DokumentLink, pk=pk)
|
|
|
|
context = {
|
|
'dokument': dokument,
|
|
'title': f'Dokument: {dokument}',
|
|
}
|
|
return render(request, 'stiftung/dokument_detail.html', context)
|
|
|
|
@login_required
|
|
def dokument_create(request):
|
|
"""Create a new document link"""
|
|
if request.method == 'POST':
|
|
form = DokumentLinkForm(request.POST)
|
|
if form.is_valid():
|
|
dokument = form.save()
|
|
messages.success(request, f'Dokument "{dokument.titel}" wurde erfolgreich verknüpft.')
|
|
|
|
# Zurück zur verknüpften Entität leiten
|
|
if dokument.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(LandVerpachtung.objects.values_list('pachtbeginn__year', flat=True))
|
|
), reverse=True)
|
|
|
|
# Statistics for overview tiles (removed legacy Person and Verpachtung)
|
|
total_destinataere = Destinataer.objects.count()
|
|
total_laendereien = Land.objects.count()
|
|
total_verpachtungen = LandVerpachtung.objects.count()
|
|
total_foerderungen = Foerderung.objects.count()
|
|
|
|
context = {
|
|
'jahre': jahre,
|
|
'title': 'Berichte',
|
|
'total_destinataere': total_destinataere,
|
|
'total_laendereien': total_laendereien,
|
|
'total_verpachtungen': total_verpachtungen,
|
|
'total_foerderungen': total_foerderungen,
|
|
}
|
|
return render(request, 'stiftung/bericht_list.html', context)
|
|
|
|
@login_required
|
|
def jahresbericht_generate(request, jahr):
|
|
"""Generate annual report for a specific year"""
|
|
# Get data for the year
|
|
foerderungen = Foerderung.objects.filter(jahr=jahr).select_related('person')
|
|
verpachtungen = LandVerpachtung.objects.filter(
|
|
pachtbeginn__year__lte=jahr,
|
|
pachtende__year__gte=jahr
|
|
).select_related('land', 'paechter')
|
|
|
|
# Calculate statistics
|
|
total_foerderungen = foerderungen.aggregate(total=Sum('betrag'))['total'] or 0
|
|
total_pachtzins = verpachtungen.aggregate(total=Sum('pachtzins_pauschal'))['total'] or 0
|
|
|
|
context = {
|
|
'jahr': jahr,
|
|
'foerderungen': foerderungen,
|
|
'verpachtungen': verpachtungen,
|
|
'total_foerderungen': total_foerderungen,
|
|
'total_pachtzins': total_pachtzins,
|
|
'title': f'Jahresbericht {jahr}',
|
|
}
|
|
return render(request, 'stiftung/jahresbericht.html', context)
|
|
|
|
@login_required
|
|
def jahresbericht_generate_redirect(request):
|
|
"""Redirects the GET form without path param to the proper URL using the provided query param 'jahr'."""
|
|
jahr = request.GET.get('jahr')
|
|
if jahr and str(jahr).isdigit():
|
|
return redirect('stiftung:jahresbericht_generate', jahr=int(jahr))
|
|
messages.error(request, 'Bitte wählen Sie ein gültiges Jahr aus.')
|
|
return redirect('stiftung:bericht_list')
|
|
|
|
@login_required
|
|
def jahresbericht_pdf(request, jahr):
|
|
"""Generate PDF version of annual report"""
|
|
from django.http import HttpResponse
|
|
from 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 = LandVerpachtung.objects.filter(
|
|
pachtbeginn__year__lte=jahr,
|
|
pachtende__year__gte=jahr
|
|
).select_related('land', 'paechter')
|
|
|
|
# Calculate statistics
|
|
total_foerderungen = foerderungen.aggregate(total=Sum('betrag'))['total'] or 0
|
|
total_pachtzins = verpachtungen.aggregate(total=Sum('pachtzins_pauschal'))['total'] or 0
|
|
|
|
context = {
|
|
'jahr': jahr,
|
|
'foerderungen': foerderungen,
|
|
'verpachtungen': verpachtungen,
|
|
'total_foerderungen': total_foerderungen,
|
|
'total_pachtzins': total_pachtzins,
|
|
}
|
|
|
|
# Render HTML
|
|
html_string = render_to_string('stiftung/jahresbericht.html', context)
|
|
|
|
# Generate PDF
|
|
pdf = HTML(string=html_string).write_pdf()
|
|
|
|
# Create response
|
|
response = HttpResponse(pdf, content_type='application/pdf')
|
|
response['Content-Disposition'] = f'attachment; filename="jahresbericht_{jahr}.pdf"'
|
|
|
|
return response
|
|
|
|
# Dashboard Views
|
|
@login_required
|
|
def dashboard(request):
|
|
# Foerderung statistics (Person statistics removed - was legacy Verpachtung system)
|
|
total_foerderungen = Foerderung.objects.aggregate(total=Sum('betrag'))['total'] or 0
|
|
|
|
# Land statistics
|
|
total_land = Land.objects.count()
|
|
active_land = Land.objects.filter(aktiv=True).count()
|
|
total_flaeche = Land.objects.aggregate(total=Sum('groesse_qm'))['total'] or 0
|
|
|
|
# Calculate total verpachtet from active verpachtungen
|
|
total_verpachtet = LandVerpachtung.objects.filter(status='aktiv').aggregate(
|
|
total=Sum('verpachtete_flaeche')
|
|
)['total'] or 0
|
|
|
|
# Verpachtung statistics
|
|
total_verpachtungen = LandVerpachtung.objects.count()
|
|
active_verpachtungen = LandVerpachtung.objects.filter(status='aktiv').count()
|
|
total_pachtzins = LandVerpachtung.objects.filter(status='aktiv').aggregate(
|
|
total=Sum('pachtzins_pauschal')
|
|
)['total'] or 0
|
|
|
|
# Recent activities
|
|
recent_lands = Land.objects.order_by('-erstellt_am')[:5]
|
|
recent_verpachtungen = LandVerpachtung.objects.select_related('land', 'paechter').order_by('-erstellt_am')[:5]
|
|
|
|
# Dokumentenübersicht
|
|
dokumente_uebersicht = DokumentLink.objects.all().order_by('-id')[:10]
|
|
|
|
# Verfügbare Paperless-Dokumente für Dashboard
|
|
available_paperless_docs = []
|
|
from stiftung.utils.config import get_paperless_config
|
|
|
|
config = get_paperless_config()
|
|
url = config['api_url']
|
|
token = config['api_token']
|
|
|
|
if url and token:
|
|
try:
|
|
base_url = url.rstrip('/api') if url.endswith('/api') else url
|
|
headers = {'Authorization': f'Token {token}'}
|
|
|
|
# Alle verfügbaren Dokumente abrufen (mit Paginierung)
|
|
all_dokumente = []
|
|
page = 1
|
|
page_size = 100
|
|
|
|
while True:
|
|
response = requests.get(f"{base_url}/api/documents/?page={page}&page_size={page_size}", headers=headers, timeout=10)
|
|
response.raise_for_status()
|
|
data = response.json()
|
|
|
|
all_dokumente.extend(data.get('results', []))
|
|
|
|
if not data.get('next'):
|
|
break
|
|
page += 1
|
|
|
|
# Stiftung-Dokumente filtern
|
|
for doc in all_dokumente:
|
|
try:
|
|
tags = []
|
|
doc_tags = doc.get('tags', [])
|
|
|
|
if isinstance(doc_tags, list):
|
|
for tag in doc_tags:
|
|
if isinstance(tag, dict) and 'name' in tag:
|
|
tags.append(tag['name'])
|
|
elif isinstance(tag, str):
|
|
tags.append(tag)
|
|
elif isinstance(tag, int):
|
|
tags.append(f"Tag_{tag}")
|
|
elif isinstance(doc_tags, str):
|
|
tags = [tag.strip() for tag in doc_tags.split(',')]
|
|
|
|
if any(tag in [config['destinataere_tag'], config['land_tag'], config['admin_tag']] for tag in tags):
|
|
bereits_verknuepft = DokumentLink.objects.filter(
|
|
paperless_document_id=doc['id']
|
|
).exists()
|
|
|
|
if not bereits_verknuepft:
|
|
available_paperless_docs.append({
|
|
'id': doc['id'],
|
|
'title': doc.get('title', f'Dokument {doc["id"]}'),
|
|
'created_date': doc.get('created_date', ''),
|
|
'tags': tags,
|
|
'thumbnail_url': f"{base_url}/api/documents/{doc['id']}/thumb/",
|
|
'document_url': f"{base_url}/documents/{doc['id']}/",
|
|
})
|
|
except Exception:
|
|
continue
|
|
|
|
# Nach Erstellungsdatum sortieren (neueste zuerst)
|
|
available_paperless_docs.sort(key=lambda x: x['created_date'], reverse=True)
|
|
|
|
except Exception:
|
|
pass
|
|
|
|
context = {
|
|
# Person statistics removed - was legacy Verpachtung system
|
|
'total_foerderungen': total_foerderungen,
|
|
'total_land': total_land,
|
|
'active_land': active_land,
|
|
'total_flaeche': total_flaeche,
|
|
'total_verpachtet': total_verpachtet,
|
|
'total_verpachtungen': total_verpachtungen,
|
|
'active_verpachtungen': active_verpachtungen,
|
|
'total_pachtzins': total_pachtzins,
|
|
'recent_lands': recent_lands,
|
|
'recent_verpachtungen': recent_verpachtungen,
|
|
'dokumente_uebersicht': dokumente_uebersicht,
|
|
'available_paperless_docs': available_paperless_docs,
|
|
}
|
|
return render(request, 'stiftung/dashboard.html', context)
|
|
|
|
# API Views for AJAX
|
|
@login_required
|
|
def land_stats_api(request):
|
|
"""API endpoint for land statistics"""
|
|
if request.method == 'GET':
|
|
gemeinde = request.GET.get('gemeinde', '')
|
|
|
|
if gemeinde:
|
|
lands = Land.objects.filter(gemeinde=gemeinde)
|
|
else:
|
|
lands = Land.objects.all()
|
|
|
|
stats = {
|
|
'total_count': lands.count(),
|
|
'total_flaeche': float(lands.aggregate(total=Sum('groesse_qm'))['total'] or 0),
|
|
'total_verpachtet': float(LandVerpachtung.objects.filter(status='aktiv', land__in=lands).aggregate(
|
|
total=Sum('verpachtete_flaeche')
|
|
)['total'] or 0),
|
|
'avg_verpachtungsgrad': 0,
|
|
}
|
|
|
|
if stats['total_flaeche'] > 0:
|
|
stats['avg_verpachtungsgrad'] = (stats['total_verpachtet'] / stats['total_flaeche']) * 100
|
|
|
|
return JsonResponse(stats)
|
|
|
|
return JsonResponse({'error': 'Invalid request method'}, status=400)
|
|
|
|
@api_view(['GET'])
|
|
def health(_request):
|
|
return Response({'status': 'ok'})
|
|
|
|
@api_view(['GET'])
|
|
def paperless_ping(_request):
|
|
from stiftung.utils.config import get_paperless_config
|
|
|
|
config = get_paperless_config()
|
|
url = config['api_url']
|
|
token = config['api_token']
|
|
if not url or not token:
|
|
return Response({'ok': False, 'reason': 'Paperless API not configured'}, status=400)
|
|
try:
|
|
# Entferne /api vom Ende der URL falls vorhanden
|
|
base_url = url.rstrip('/api') if url.endswith('/api') else url
|
|
r = requests.get(f"{base_url}/api/tags/", headers={"Authorization": f"Token {token}"}, timeout=5)
|
|
return Response({'ok': r.ok, 'status_code': r.status_code})
|
|
except Exception as e:
|
|
return Response({'ok': False, 'error': str(e)}, status=500)
|
|
|
|
@api_view(['GET'])
|
|
def paperless_documents(request):
|
|
"""Holt Dokumente aus Paperless mit den erforderlichen Tags.
|
|
Optionales Polling: ?poll=1 wartet kurz und versucht erneut, damit frisch ingestete
|
|
Dokumente in Paperless erscheinen, bevor die Liste zurückgegeben wird.
|
|
"""
|
|
from stiftung.utils.config import get_paperless_config
|
|
|
|
config = get_paperless_config()
|
|
url = config['api_url']
|
|
token = config['api_token']
|
|
required_tag = config['destinataere_tag']
|
|
land_tag = config['land_tag']
|
|
admin_tag = config['admin_tag']
|
|
destinaere_tag_id = config['destinataere_tag_id']
|
|
land_tag_id = config['land_tag_id']
|
|
admin_tag_id = config['admin_tag_id']
|
|
|
|
if not url or not token:
|
|
return Response({
|
|
'error': 'Paperless API not configured',
|
|
'message': 'Please set PAPERLESS_API_URL and PAPERLESS_API_TOKEN in your environment variables',
|
|
'documents': [],
|
|
'total_destinaere': 0,
|
|
'total_land': 0,
|
|
'total_admin': 0,
|
|
'total_all': 0
|
|
}, status=400)
|
|
|
|
try:
|
|
# Entferne /api vom Ende der URL falls vorhanden
|
|
base_url = url.rstrip('/api') if url.endswith('/api') else url
|
|
headers = {'Authorization': f'Token {token}'}
|
|
|
|
def fetch_tagged():
|
|
# mit ordering=-created neueste zuerst
|
|
dest_resp = requests.get(
|
|
f"{base_url}/api/documents/?tags__id={destinaere_tag_id}&ordering=-created",
|
|
headers=headers, timeout=10
|
|
)
|
|
dest_resp.raise_for_status()
|
|
dest_docs = dest_resp.json()
|
|
|
|
land_resp = requests.get(
|
|
f"{base_url}/api/documents/?tags__id={land_tag_id}&ordering=-created",
|
|
headers=headers, timeout=10
|
|
)
|
|
land_resp.raise_for_status()
|
|
land_docs = land_resp.json()
|
|
|
|
admin_resp = requests.get(
|
|
f"{base_url}/api/documents/?tags__id={admin_tag_id}&ordering=-created",
|
|
headers=headers, timeout=10
|
|
)
|
|
admin_resp.raise_for_status()
|
|
admin_docs = admin_resp.json()
|
|
|
|
return dest_docs, land_docs, admin_docs
|
|
|
|
dest_docs, land_docs, admin_docs = fetch_tagged()
|
|
|
|
# Optionales kurzes Polling, wenn angefordert
|
|
if request.GET.get('poll') in ('1', 'true', 'yes'):
|
|
start_total = sum([
|
|
dest_docs.get('count', 0),
|
|
land_docs.get('count', 0),
|
|
admin_docs.get('count', 0),
|
|
])
|
|
deadline = time.time() + 6.0 # bis zu 6 Sekunden warten
|
|
while time.time() < deadline:
|
|
time.sleep(1.0)
|
|
d2, l2, a2 = fetch_tagged()
|
|
new_total = sum([d2.get('count', 0), l2.get('count', 0), a2.get('count', 0)])
|
|
if new_total > start_total:
|
|
dest_docs, land_docs, admin_docs = d2, l2, a2
|
|
break
|
|
|
|
# Alle Dokumente zusammenfassen
|
|
all_documents = []
|
|
for doc in dest_docs.get('results', []):
|
|
doc['tag_category'] = 'destinaere'
|
|
all_documents.append(doc)
|
|
for doc in land_docs.get('results', []):
|
|
doc['tag_category'] = 'land'
|
|
all_documents.append(doc)
|
|
for doc in admin_docs.get('results', []):
|
|
doc['tag_category'] = 'admin'
|
|
all_documents.append(doc)
|
|
|
|
return Response({
|
|
'documents': all_documents,
|
|
'total_destinaere': dest_docs.get('count', 0),
|
|
'total_land': land_docs.get('count', 0),
|
|
'total_admin': admin_docs.get('count', 0),
|
|
'total_all': len(all_documents)
|
|
})
|
|
|
|
except requests.exceptions.RequestException as e:
|
|
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 stiftung.utils.config import get_paperless_config
|
|
|
|
config = get_paperless_config()
|
|
url = config['api_url']
|
|
token = config['api_token']
|
|
required_tag = config['destinataere_tag']
|
|
land_tag = config['land_tag']
|
|
admin_tag = config['admin_tag']
|
|
destinaere_tag_id = config['destinataere_tag_id']
|
|
land_tag_id = config['land_tag_id']
|
|
admin_tag_id = config['admin_tag_id']
|
|
|
|
if not url or not token:
|
|
return Response({'error': 'Paperless API not configured'}, status=400)
|
|
|
|
try:
|
|
# Entferne /api vom Ende der URL falls vorhanden
|
|
base_url = url.rstrip('/api') if url.endswith('/api') else url
|
|
|
|
headers = {'Authorization': f'Token {token}'}
|
|
|
|
# Alle Tags abrufen
|
|
tags_response = requests.get(f"{base_url}/api/tags/?page_size=1000", headers=headers, timeout=10)
|
|
tags_response.raise_for_status()
|
|
tags_data = tags_response.json()
|
|
|
|
# Alle Tags durchsuchen
|
|
all_tags = tags_data.get('results', [])
|
|
exact_match_destinaere = None
|
|
exact_match_land = None
|
|
exact_match_admin = None
|
|
similar_tags = []
|
|
|
|
# Nach den neuen Tag-Namen suchen (mit Unterstrichen)
|
|
for tag in all_tags:
|
|
tag_name = tag.get('name', '')
|
|
tag_id = tag.get('id')
|
|
|
|
# Suche nach den neuen Tag-Namen
|
|
if tag_name == "Stiftung_Destinatäre":
|
|
exact_match_destinaere = {'id': tag_id, 'name': tag_name}
|
|
elif tag_name == "Stiftung_Land_und_Pächter":
|
|
exact_match_land = {'id': tag_id, 'name': tag_name}
|
|
elif tag_name == "Stiftung_Administration":
|
|
exact_match_admin = {'id': tag_id, 'name': tag_name}
|
|
|
|
# Ähnliche Tags finden
|
|
if 'stiftung' in tag_name.lower() or 'destinat' in tag_name.lower() or 'land' in tag_name.lower() or 'admin' in tag_name.lower():
|
|
similar_tags.append({'id': tag_id, 'name': tag_name})
|
|
|
|
# Alle Tag-Namen sammeln
|
|
all_tag_names = [tag.get('name', '') for tag in all_tags]
|
|
|
|
# Dokumente abrufen
|
|
documents_response = requests.get(f"{base_url}/api/documents/?page_size=10", headers=headers, timeout=10)
|
|
documents_response.raise_for_status()
|
|
documents_data = documents_response.json()
|
|
|
|
# Stiftung-Dokumente finden (mit Tag 21 "Stiftung")
|
|
stiftung_documents = []
|
|
for doc in documents_data.get('results', []):
|
|
doc_tags = doc.get('tags', [])
|
|
if 21 in doc_tags: # Tag 21 ist "Stiftung"
|
|
stiftung_documents.append(doc)
|
|
|
|
# Sample-Dokumente mit Tag-Namen anreichern
|
|
sample_documents = documents_data.get('results', [])[:5]
|
|
enriched_documents = []
|
|
|
|
for doc in sample_documents:
|
|
doc_copy = doc.copy()
|
|
tag_names = []
|
|
for tag_id in doc.get('tags', []):
|
|
# Tag-Namen aus der Tag-Liste finden
|
|
tag_name = next((tag.get('name', f'Unknown({tag_id})') for tag in all_tags if tag.get('id') == tag_id), f'Unknown({tag_id})')
|
|
tag_names.append(tag_name)
|
|
doc_copy['tag_names'] = tag_names
|
|
enriched_documents.append(doc_copy)
|
|
|
|
return Response({
|
|
'paperless_url': url,
|
|
'base_url': base_url,
|
|
'required_tag': required_tag,
|
|
'land_tag': land_tag,
|
|
'admin_tag': admin_tag,
|
|
'destinaere_tag_id': destinaere_tag_id,
|
|
'land_tag_id': land_tag_id,
|
|
'admin_tag_id': admin_tag_id,
|
|
'exact_match_destinaere': exact_match_destinaere,
|
|
'exact_match_land': exact_match_land,
|
|
'exact_match_admin': exact_match_admin,
|
|
'similar_tags': similar_tags,
|
|
'all_tag_names': all_tag_names,
|
|
'total_tags': len(all_tags),
|
|
'total_documents': documents_data.get('count', 0),
|
|
'sample_documents': sample_documents,
|
|
'api_token_length': len(token) if token else 0,
|
|
'enriched_documents': enriched_documents,
|
|
'stiftung_documents': stiftung_documents
|
|
})
|
|
|
|
except requests.exceptions.RequestException as e:
|
|
return Response({'error': f'API-Fehler: {e}'}, status=500)
|
|
except Exception as e:
|
|
return Response({'error': f'Unerwarteter Fehler: {e}'}, status=500)
|
|
|
|
@api_view(['GET'])
|
|
def paperless_tags_only(request):
|
|
"""Holt nur die Tag-Liste aus Paperless - ohne Dokumente"""
|
|
from stiftung.utils.config import get_paperless_config
|
|
|
|
config = get_paperless_config()
|
|
url = config['api_url']
|
|
token = config['api_token']
|
|
|
|
if not url or not token:
|
|
return Response({'error': 'Paperless API not configured'}, status=400)
|
|
|
|
try:
|
|
# Entferne /api vom Ende der URL falls vorhanden
|
|
base_url = url.rstrip('/api') if url.endswith('/api') else url
|
|
|
|
# Alle Tags abrufen (mit großer page_size)
|
|
headers = {'Authorization': f'Token {token}'}
|
|
|
|
# Erste Anfrage mit großer page_size
|
|
tags_response = requests.get(f"{base_url}/api/tags/?page_size=1000", headers=headers, timeout=10)
|
|
tags_response.raise_for_status()
|
|
tags_data = tags_response.json()
|
|
|
|
all_tags = []
|
|
|
|
# Erste Seite verarbeiten
|
|
for tag in tags_data.get('results', []):
|
|
tag_detail = {
|
|
'id': tag.get('id'),
|
|
'name': tag.get('name', ''),
|
|
'slug': tag.get('slug', ''),
|
|
'color': tag.get('color', ''),
|
|
'text_color': tag.get('text_color', ''),
|
|
'match': tag.get('match', ''),
|
|
'matching_algorithm': tag.get('matching_algorithm'),
|
|
'is_inbox_tag': tag.get('is_inbox_tag'),
|
|
'document_count': tag.get('document_count', 0)
|
|
}
|
|
all_tags.append(tag_detail)
|
|
|
|
# Weitere Seiten abrufen falls vorhanden
|
|
next_url = tags_data.get('next')
|
|
while next_url:
|
|
next_response = requests.get(next_url, headers=headers, timeout=10)
|
|
next_response.raise_for_status()
|
|
next_data = next_response.json()
|
|
|
|
for tag in next_data.get('results', []):
|
|
tag_detail = {
|
|
'id': tag.get('id'),
|
|
'name': tag.get('name', ''),
|
|
'slug': tag.get('slug', ''),
|
|
'color': tag.get('color', ''),
|
|
'text_color': tag.get('text_color', ''),
|
|
'match': tag.get('match', ''),
|
|
'matching_algorithm': tag.get('matching_algorithm'),
|
|
'is_inbox_tag': tag.get('is_inbox_tag'),
|
|
'document_count': tag.get('document_count', 0)
|
|
}
|
|
all_tags.append(tag_detail)
|
|
|
|
next_url = next_data.get('next')
|
|
|
|
# Nach ID sortieren
|
|
all_tags.sort(key=lambda x: x['id'])
|
|
|
|
return Response({
|
|
'total_tags': len(all_tags),
|
|
'tags': all_tags,
|
|
'tag_ids': [tag['id'] for tag in all_tags],
|
|
'tag_names': [tag['name'] for tag in all_tags],
|
|
'api_info': {
|
|
'page_size_used': 1000,
|
|
'total_count_from_api': tags_data.get('count', 0)
|
|
}
|
|
})
|
|
|
|
except requests.exceptions.RequestException as e:
|
|
return Response({'error': f'API-Fehler: {e}'}, status=500)
|
|
except Exception as e:
|
|
return Response({'error': f'Unerwarteter Fehler: {e}'}, status=500)
|
|
|
|
@api_view(['GET'])
|
|
def link_document_search(request):
|
|
"""Sucht nach Datensätzen für die Dokument-Verknüpfung"""
|
|
from django.db.models import Q
|
|
|
|
query = request.GET.get('q', '')
|
|
category = request.GET.get('category', 'all')
|
|
|
|
results = {}
|
|
|
|
if category in ['all', 'destinataer']:
|
|
# Suche nach Destinatären
|
|
destinataer_query = Q()
|
|
if query and query != 'all':
|
|
destinataer_query = (
|
|
Q(nachname__icontains=query) | Q(vorname__icontains=query) |
|
|
Q(email__icontains=query) | Q(telefon__icontains=query) |
|
|
Q(strasse__icontains=query) | Q(ort__icontains=query) | Q(plz__icontains=query) |
|
|
Q(institution__icontains=query) | Q(familienzweig__icontains=query) |
|
|
Q(notizen__icontains=query)
|
|
)
|
|
|
|
destinataer_results = Destinataer.objects.filter(destinataer_query)[:25]
|
|
results['destinataer'] = [
|
|
{
|
|
'id': d.id,
|
|
'name': f"{d.vorname} {d.nachname}".strip() if d.vorname else (d.institution or d.nachname),
|
|
'type': 'Destinatär',
|
|
'details': f"{d.strasse or ''}{(', ' + d.plz + ' ' + d.ort) if d.plz and d.ort else (' ' + d.ort) if d.ort else ''} {('• ' + d.email) if d.email else ''} {('• ' + d.institution) if d.institution else ''} {('• ' + d.familienzweig) if d.familienzweig else ''}".strip()
|
|
}
|
|
for d in destinataer_results
|
|
]
|
|
|
|
if category in ['all', 'land']:
|
|
# Suche nach Ländereien
|
|
land_query = Q()
|
|
if query and query != 'all':
|
|
# Extract numbers from search terms like "Flur 9" or "Flurstück 11"
|
|
import re
|
|
flur_match = re.search(r'flur\s*(\d+)', query, re.IGNORECASE)
|
|
flurstuck_match = re.search(r'flurstück\s*(\d+)', query, re.IGNORECASE)
|
|
|
|
land_query = (
|
|
Q(gemarkung__icontains=query) | Q(gemeinde__icontains=query) |
|
|
Q(flur__icontains=query) | Q(flurstueck__icontains=query) |
|
|
Q(lfd_nr__icontains=query) | Q(ew_nummer__icontains=query) |
|
|
Q(notizen__icontains=query)
|
|
)
|
|
|
|
# Add specific searches for extracted numbers
|
|
if flur_match:
|
|
land_query |= Q(flur__exact=flur_match.group(1))
|
|
if flurstuck_match:
|
|
land_query |= Q(flurstueck__exact=flurstuck_match.group(1))
|
|
|
|
land_results = Land.objects.filter(land_query)[:25]
|
|
results['land'] = [
|
|
{
|
|
'id': l.id,
|
|
'name': f"{l.gemarkung} - {l.gemeinde}, Flur {l.flur}, Flurstück {l.flurstueck or 'N/A'}",
|
|
'type': 'Land',
|
|
'details': f"Lfd.Nr: {l.lfd_nr or 'N/A'} • EW-Nr: {l.ew_nummer or 'N/A'} • Größe: {l.groesse_qm or 0} m²"
|
|
}
|
|
for l in land_results
|
|
]
|
|
|
|
if category in ['all', 'verpachtung']:
|
|
# Suche nach Verpachtungen (using new LandVerpachtung model)
|
|
verpachtung_query = Q()
|
|
if query and query != 'all':
|
|
verpachtung_query = (
|
|
Q(paechter__nachname__icontains=query) | Q(paechter__vorname__icontains=query) |
|
|
Q(paechter__ort__icontains=query) | Q(paechter__email__icontains=query) |
|
|
Q(paechter__pachtnummer__icontains=query) |
|
|
Q(land__gemarkung__icontains=query) | Q(land__gemeinde__icontains=query) |
|
|
Q(land__flur__icontains=query) | Q(land__flurstueck__icontains=query) |
|
|
Q(land__lfd_nr__icontains=query) |
|
|
Q(vertragsnummer__icontains=query) | Q(pachtzins_pauschal__icontains=query) | Q(bemerkungen__icontains=query)
|
|
)
|
|
|
|
verpachtung_results = LandVerpachtung.objects.filter(verpachtung_query).select_related('paechter', 'land')[:25]
|
|
results['verpachtung'] = [
|
|
{
|
|
'id': v.id,
|
|
'name': f"{(v.paechter.vorname + ' ') if v.paechter and v.paechter.vorname else ''}{v.paechter.nachname if v.paechter else 'Unbekannt'} → {v.land.gemarkung}, Flur {v.land.flur}",
|
|
'type': 'Verpachtung',
|
|
'details': f"Vertrag: {v.vertragsnummer} • Flurstück: {v.land.flurstueck or 'N/A'} • Pachtzins: {v.pachtzins_pauschal or 'N/A'} €/Jahr • Zeitraum: {v.pachtbeginn.strftime('%Y') if v.pachtbeginn else '?'}-{v.pachtende.strftime('%Y') if v.pachtende else 'laufend'}"
|
|
}
|
|
for v in verpachtung_results
|
|
]
|
|
|
|
if category in ['all', 'paechter']:
|
|
# Suche nach Pächtern
|
|
paechter_query = Q()
|
|
if query and query != 'all':
|
|
paechter_query = (
|
|
Q(nachname__icontains=query) | Q(vorname__icontains=query) |
|
|
Q(ort__icontains=query) | Q(email__icontains=query) |
|
|
Q(telefon__icontains=query) | Q(strasse__icontains=query) |
|
|
Q(pachtnummer__icontains=query) | Q(plz__icontains=query) |
|
|
Q(notizen__icontains=query)
|
|
)
|
|
paechter_results = Paechter.objects.filter(paechter_query)[:25]
|
|
results['paechter'] = [
|
|
{
|
|
'id': p.id,
|
|
'name': f"{(p.vorname + ' ') if p.vorname else ''}{p.nachname}" + (f" (#{p.pachtnummer})" if p.pachtnummer else ""),
|
|
'type': 'Pächter',
|
|
'details': f"{p.strasse or ''}{(', ' + p.plz + ' ' + p.ort) if p.plz and p.ort else (' ' + p.ort) if p.ort else ''} {('• ' + p.email) if p.email else ''} {('• Tel: ' + p.telefon) if p.telefon else ''}".strip()
|
|
}
|
|
for p in paechter_results
|
|
]
|
|
|
|
if category in ['all', 'rentmeister']:
|
|
# Suche nach Rentmeistern
|
|
from stiftung.models import Rentmeister
|
|
rentmeister_query = Q()
|
|
if query and query != 'all':
|
|
rentmeister_query = (
|
|
Q(nachname__icontains=query) | Q(vorname__icontains=query) |
|
|
Q(ort__icontains=query) | Q(email__icontains=query) |
|
|
Q(telefon__icontains=query) | Q(strasse__icontains=query) |
|
|
Q(plz__icontains=query) | Q(notizen__icontains=query) |
|
|
Q(titel__icontains=query) | Q(mobil__icontains=query)
|
|
)
|
|
rentmeister_results = Rentmeister.objects.filter(rentmeister_query)[:25]
|
|
results['rentmeister'] = [
|
|
{
|
|
'id': r.id,
|
|
'name': f"{(r.anrede + ' ') if r.anrede else ''}{(r.vorname + ' ') if r.vorname else ''}{r.nachname}" + (f" ({r.titel})" if r.titel else ""),
|
|
'type': 'Rentmeister',
|
|
'details': f"{r.strasse or ''}{(', ' + r.plz + ' ' + r.ort) if r.plz and r.ort else (' ' + r.ort) if r.ort else ''} {('• ' + r.email) if r.email else ''} {('• Tel: ' + r.telefon) if r.telefon else ''} {('• Mobil: ' + r.mobil) if r.mobil else ''}".strip()
|
|
}
|
|
for r in rentmeister_results
|
|
]
|
|
|
|
if category in ['all', 'abrechnung']:
|
|
# Suche nach Abrechnungen
|
|
abrechnung_query = Q()
|
|
if query and query != 'all':
|
|
abrechnung_query = (
|
|
Q(land__gemarkung__icontains=query) | Q(land__gemeinde__icontains=query) |
|
|
Q(land__flur__icontains=query) | Q(land__flurstueck__icontains=query) |
|
|
Q(land__lfd_nr__icontains=query) | Q(abrechnungsjahr__icontains=query) |
|
|
Q(bemerkungen__icontains=query)
|
|
)
|
|
|
|
abrechnung_results = LandAbrechnung.objects.filter(abrechnung_query).select_related('land')[:25]
|
|
results['abrechnung'] = [
|
|
{
|
|
'id': a.id,
|
|
'name': f"Abrechnung {a.abrechnungsjahr} - {a.land.gemarkung}, Flur {a.land.flur}",
|
|
'type': 'Abrechnung',
|
|
'details': f"Flurstück: {a.land.flurstueck or 'N/A'} • Jahr: {a.abrechnungsjahr} • Grundsteuer: {a.grundsteuer_betrag or 0} € • Versicherung: {a.versicherungen_betrag or 0} €"
|
|
}
|
|
for a in abrechnung_results
|
|
]
|
|
|
|
if category in ['all', 'foerderung']:
|
|
# Suche nach Förderungen
|
|
foerderung_query = Q()
|
|
if query and query != 'all':
|
|
foerderung_query = (
|
|
Q(destinataer__nachname__icontains=query) | Q(destinataer__vorname__icontains=query) |
|
|
Q(destinataer__institution__icontains=query) | Q(destinataer__email__icontains=query) |
|
|
Q(jahr__icontains=query) | Q(betrag__icontains=query) |
|
|
Q(kategorie__icontains=query) | Q(status__icontains=query) |
|
|
Q(bemerkungen__icontains=query)
|
|
)
|
|
|
|
foerderung_results = Foerderung.objects.filter(foerderung_query).select_related('destinataer')[:25]
|
|
results['foerderung'] = [
|
|
{
|
|
'id': str(f.id), # Convert UUID to string for JSON serialization
|
|
'name': f"{f.destinataer.get_full_name() if f.destinataer else 'Unbekannt'} - {f.jahr}",
|
|
'type': 'Förderung',
|
|
'details': f"Betrag: {f.betrag} € • Kategorie: {f.get_kategorie_display()} • Status: {f.get_status_display()} • Jahr: {f.jahr}"
|
|
}
|
|
for f in foerderung_results
|
|
]
|
|
|
|
|
|
return Response(results)
|
|
|
|
def create_paechter_link_for_verpachtung(paperless_id, paperless_title, verpachtung_id):
|
|
"""Hilfsfunktion: Erstellt automatisch eine Pächter-Verknüpfung für eine Verpachtung"""
|
|
try:
|
|
# Hole die LandVerpachtung und den zugehörigen Pächter
|
|
verpachtung = LandVerpachtung.objects.select_related('paechter').get(id=verpachtung_id)
|
|
if verpachtung.paechter:
|
|
# Prüfe, ob bereits eine Verknüpfung für dieses Dokument und diesen Pächter existiert
|
|
existing_link = DokumentLink.objects.filter(
|
|
paperless_document_id=paperless_id,
|
|
paechter_id=verpachtung.paechter.id
|
|
).first()
|
|
|
|
if not existing_link:
|
|
# Erstelle automatische Pächter-Verknüpfung
|
|
DokumentLink.objects.create(
|
|
paperless_document_id=paperless_id,
|
|
titel=paperless_title,
|
|
kontext='paechter',
|
|
paechter_id=verpachtung.paechter.id
|
|
)
|
|
return True
|
|
except (LandVerpachtung.DoesNotExist, Exception):
|
|
pass
|
|
return False
|
|
|
|
@csrf_exempt
|
|
@api_view(['POST'])
|
|
def link_document_create(request):
|
|
"""Erstellt eine Verknüpfung zwischen einem Paperless-Dokument und einem Datensatz"""
|
|
from django.db import transaction
|
|
|
|
try:
|
|
# Robust JSON parsing to tolerate Latin-1 encoded payloads (e.g., umlauts)
|
|
try:
|
|
payload = request.data
|
|
except Exception:
|
|
raw = request.body
|
|
try:
|
|
payload = json.loads(raw.decode('utf-8'))
|
|
except UnicodeDecodeError:
|
|
payload = json.loads(raw.decode('latin-1'))
|
|
|
|
paperless_id = payload.get('paperless_id')
|
|
paperless_title = payload.get('paperless_title')
|
|
paperless_url = payload.get('paperless_url')
|
|
link_type = payload.get('link_type') # 'destinataer', 'land', 'verpachtung', 'paechter', 'abrechnung'
|
|
link_id = payload.get('link_id')
|
|
|
|
if not all([paperless_id, paperless_title, paperless_url, link_type, link_id]):
|
|
return Response({'error': 'Alle Felder sind erforderlich'}, status=400)
|
|
|
|
with transaction.atomic():
|
|
# Erstelle den DokumentLink
|
|
dokument_link = DokumentLink.objects.create(
|
|
paperless_document_id=paperless_id, # Korrigiert: 'paperless_document_id' statt 'paperless_id'
|
|
titel=paperless_title, # Korrigiert: 'titel' statt 'title'
|
|
kontext='anderes'
|
|
)
|
|
|
|
# Setze die entsprechenden UUID-Felder basierend auf dem Link-Typ
|
|
if link_type == 'destinataer':
|
|
dokument_link.destinataer_id = link_id
|
|
elif link_type == 'land':
|
|
dokument_link.land_id = link_id
|
|
elif link_type == 'verpachtung':
|
|
# Use new LandVerpachtung field instead of legacy
|
|
dokument_link.land_verpachtung_id = link_id
|
|
elif link_type == 'paechter':
|
|
dokument_link.paechter_id = link_id
|
|
elif link_type == 'foerderung':
|
|
dokument_link.foerderung_id = link_id
|
|
elif link_type == 'rentmeister':
|
|
dokument_link.rentmeister_id = link_id
|
|
elif link_type == 'abrechnung':
|
|
dokument_link.abrechnung_id = link_id
|
|
|
|
dokument_link.save()
|
|
|
|
# Log the document linking action
|
|
from stiftung.audit import log_link
|
|
try:
|
|
# Get the linked entity name for logging
|
|
entity_name = paperless_title
|
|
if link_type == 'destinataer':
|
|
from stiftung.models import Destinataer
|
|
entity = Destinataer.objects.get(id=link_id)
|
|
target_name = entity.get_full_name()
|
|
elif link_type == 'land':
|
|
from stiftung.models import Land
|
|
entity = Land.objects.get(id=link_id)
|
|
target_name = str(entity)
|
|
elif link_type == 'paechter':
|
|
from stiftung.models import Paechter
|
|
entity = Paechter.objects.get(id=link_id)
|
|
target_name = f"{entity.vorname} {entity.nachname}".strip()
|
|
elif link_type == 'foerderung':
|
|
from stiftung.models import Foerderung
|
|
entity = Foerderung.objects.get(id=link_id)
|
|
target_name = f"{entity.destinataer.get_full_name() if entity.destinataer else 'Unbekannt'} - {entity.jahr}"
|
|
elif link_type == 'verpachtung':
|
|
entity = LandVerpachtung.objects.get(id=link_id)
|
|
target_name = str(entity)
|
|
elif link_type == 'rentmeister':
|
|
from stiftung.models import Rentmeister
|
|
entity = Rentmeister.objects.get(id=link_id)
|
|
target_name = entity.get_full_name()
|
|
else:
|
|
target_name = f"ID {link_id}"
|
|
|
|
log_link(
|
|
request=request,
|
|
entity_type='dokumentlink',
|
|
entity_id=str(dokument_link.id),
|
|
entity_name=entity_name,
|
|
target_type=link_type,
|
|
target_name=target_name
|
|
)
|
|
except Exception as e:
|
|
# Don't fail the main operation if logging fails
|
|
print(f"Audit logging failed: {e}")
|
|
|
|
# Automatische Pächter-Verknüpfung NACH der Haupttransaktion
|
|
paechter_linked = False
|
|
if link_type == 'verpachtung':
|
|
paechter_linked = create_paechter_link_for_verpachtung(paperless_id, paperless_title, link_id)
|
|
|
|
message = f'Dokument erfolgreich mit {link_type} verknüpft'
|
|
if paechter_linked:
|
|
message += ' (automatisch auch mit Pächter verknüpft)'
|
|
|
|
return Response({
|
|
'success': True,
|
|
'message': message,
|
|
'dokument_id': dokument_link.id
|
|
})
|
|
|
|
except Exception as e:
|
|
return Response({'error': f'Fehler beim Erstellen der Verknüpfung: {str(e)}'}, status=500)
|
|
|
|
# Legacy dokument_verknuepfung view removed - use dokument_management instead
|
|
|
|
@api_view(['GET'])
|
|
def link_document_list(request):
|
|
"""Listet alle bestehenden Dokument-Verknüpfungen auf, gruppiert nach Paperless-Document-ID"""
|
|
try:
|
|
dokument_links = DokumentLink.objects.all().order_by('-id')
|
|
|
|
# Group links by paperless_document_id to show multiple links per document
|
|
links_by_document = {}
|
|
|
|
for link in dokument_links:
|
|
paperless_id = link.paperless_document_id
|
|
|
|
if paperless_id not in links_by_document:
|
|
links_by_document[paperless_id] = {
|
|
'paperless_id': paperless_id,
|
|
'title': link.titel,
|
|
'paperless_url': f"/api/paperless/documents/{paperless_id}/",
|
|
'links': []
|
|
}
|
|
|
|
# Create link info
|
|
link_info = {
|
|
'id': str(link.id), # Ensure UUID is stringified
|
|
'kontext': link.kontext,
|
|
'link_type': None,
|
|
'linked_object': None
|
|
}
|
|
|
|
# Determine link type and get linked object details
|
|
if link.destinataer_id:
|
|
link_info['link_type'] = 'destinataer'
|
|
try:
|
|
dest = Destinataer.objects.get(id=link.destinataer_id)
|
|
link_info['linked_object'] = {
|
|
'id': str(dest.id),
|
|
'type': 'Destinatär',
|
|
'name': f"{dest.vorname} {dest.nachname}".strip() if dest.vorname else dest.institution,
|
|
'details': f"Institution: {dest.institution}" if dest.institution else f"Person: {dest.vorname} {dest.nachname}".strip()
|
|
}
|
|
except Destinataer.DoesNotExist:
|
|
link_info['linked_object'] = {'type': 'Destinatär', 'name': 'Gelöscht', 'details': 'Datensatz nicht mehr verfügbar'}
|
|
|
|
elif link.land_id:
|
|
link_info['link_type'] = 'land'
|
|
try:
|
|
land = Land.objects.get(id=link.land_id)
|
|
link_info['linked_object'] = {
|
|
'id': str(land.id),
|
|
'type': 'Land',
|
|
'name': f"{land.gemarkung} - {land.gemeinde}",
|
|
'details': f"Flur: {land.flur}, Größe: {land.groesse_qm or 0} m²"
|
|
}
|
|
except Land.DoesNotExist:
|
|
link_info['linked_object'] = {'type': 'Land', 'name': 'Gelöscht', 'details': 'Datensatz nicht mehr verfügbar'}
|
|
|
|
elif link.paechter_id:
|
|
link_info['link_type'] = 'paechter'
|
|
try:
|
|
p = Paechter.objects.get(id=link.paechter_id)
|
|
link_info['linked_object'] = {
|
|
'id': str(p.id),
|
|
'type': 'Pächter',
|
|
'name': f"{(p.vorname + ' ') if p.vorname else ''}{p.nachname}",
|
|
'details': f"{p.ort or ''}"
|
|
}
|
|
except Paechter.DoesNotExist:
|
|
link_info['linked_object'] = {'type': 'Pächter', 'name': 'Gelöscht', 'details': 'Datensatz nicht mehr verfügbar'}
|
|
|
|
elif link.land_verpachtung_id:
|
|
link_info['link_type'] = 'verpachtung'
|
|
try:
|
|
from stiftung.models import LandVerpachtung
|
|
verp = LandVerpachtung.objects.select_related('paechter', 'land').get(id=link.land_verpachtung_id)
|
|
link_info['linked_object'] = {
|
|
'id': str(verp.id),
|
|
'type': 'Verpachtung',
|
|
'name': f"Verpachtung {(verp.paechter.vorname + ' ') if verp.paechter and verp.paechter.vorname else ''}{verp.paechter.nachname if verp.paechter else 'Unbekannt'}",
|
|
'details': f"Land: {verp.land.gemarkung} - {verp.land.gemeinde}, Pächter: {(verp.paechter.vorname + ' ') if verp.paechter and verp.paechter.vorname else ''}{verp.paechter.nachname if verp.paechter else 'Unbekannt'}, Vertrag: {verp.vertragsnummer}"
|
|
}
|
|
except LandVerpachtung.DoesNotExist:
|
|
link_info['linked_object'] = {'type': 'Verpachtung', 'name': 'Gelöscht', 'details': 'Datensatz nicht mehr verfügbar'}
|
|
|
|
elif link.rentmeister_id:
|
|
link_info['link_type'] = 'rentmeister'
|
|
try:
|
|
from stiftung.models import Rentmeister
|
|
rentmeister = Rentmeister.objects.get(id=link.rentmeister_id)
|
|
link_info['linked_object'] = {
|
|
'id': str(rentmeister.id),
|
|
'type': 'Rentmeister',
|
|
'name': f"{(rentmeister.anrede + ' ') if rentmeister.anrede else ''}{(rentmeister.vorname + ' ') if rentmeister.vorname else ''}{rentmeister.nachname}" + (f" ({rentmeister.titel})" if rentmeister.titel else ""),
|
|
'details': f"Seit: {rentmeister.seit_datum.strftime('%d.%m.%Y') if rentmeister.seit_datum else 'Unbekannt'}" + (f", Tel: {rentmeister.telefon}" if rentmeister.telefon else "") + (f", {rentmeister.email}" if rentmeister.email else ""),
|
|
'url': f"/geschaeftsfuehrung/rentmeister/{rentmeister.id}/"
|
|
}
|
|
except Rentmeister.DoesNotExist:
|
|
link_info['linked_object'] = {'type': 'Rentmeister', 'name': 'Gelöscht', 'details': 'Datensatz nicht mehr verfügbar'}
|
|
|
|
elif link.abrechnung_id:
|
|
link_info['link_type'] = 'abrechnung'
|
|
try:
|
|
abrechnung = LandAbrechnung.objects.select_related('land').get(id=link.abrechnung_id)
|
|
link_info['linked_object'] = {
|
|
'id': str(abrechnung.id),
|
|
'type': 'Abrechnung',
|
|
'name': f"Abrechnung {abrechnung.abrechnungsjahr} - {abrechnung.land.gemarkung}",
|
|
'details': f"Land: {abrechnung.land.gemarkung} - {abrechnung.land.gemeinde}, Flur {abrechnung.land.flur}, Jahr {abrechnung.abrechnungsjahr}",
|
|
'url': f"/laendereien/abrechnungen/{abrechnung.id}/"
|
|
}
|
|
except LandAbrechnung.DoesNotExist:
|
|
link_info['linked_object'] = {'type': 'Abrechnung', 'name': 'Gelöscht', 'details': 'Datensatz nicht mehr verfügbar'}
|
|
|
|
links_by_document[paperless_id]['links'].append(link_info)
|
|
|
|
# Convert to list format for frontend
|
|
results = list(links_by_document.values())
|
|
|
|
return Response({
|
|
'total_documents': len(results),
|
|
'total_links': sum(len(doc['links']) for doc in results),
|
|
'links': results
|
|
})
|
|
|
|
except Exception as e:
|
|
return Response({'error': f'Fehler beim Abrufen der Verknüpfungen: {str(e)}'}, status=500)
|
|
|
|
|
|
@csrf_exempt
|
|
@api_view(['POST'])
|
|
def link_document_update(request):
|
|
"""Aktualisiert die Verknüpfung eines Dokuments (Re-Linking auf anderen Datensatz/Kontext)."""
|
|
from django.db import transaction
|
|
|
|
try:
|
|
# Robust JSON parsing to tolerate Latin-1 encoded payloads (e.g., umlauts)
|
|
try:
|
|
payload = request.data
|
|
except Exception:
|
|
raw = request.body
|
|
try:
|
|
payload = json.loads(raw.decode('utf-8'))
|
|
except UnicodeDecodeError:
|
|
payload = json.loads(raw.decode('latin-1'))
|
|
|
|
link_id = payload.get('link_id')
|
|
link_type = payload.get('link_type') # 'destinataer' | 'land' | 'verpachtung' | 'paechter' | 'rentmeister'
|
|
link_target_id = payload.get('link_id_target')
|
|
if not all([link_id, link_type, link_target_id]):
|
|
return Response({'error': 'link_id, link_type und link_id_target sind erforderlich'}, status=400)
|
|
|
|
with transaction.atomic():
|
|
link = DokumentLink.objects.get(id=link_id)
|
|
old_verpachtung_id = link.verpachtung_id # Merke alte Verpachtung für Cleanup
|
|
paperless_id_for_cleanup = link.paperless_document_id
|
|
titel_for_new_link = link.titel
|
|
|
|
# Reset all associations first
|
|
link.destinataer_id = None
|
|
link.land_id = None
|
|
link.verpachtung_id = None
|
|
link.paechter_id = None
|
|
link.foerderung_id = None
|
|
link.rentmeister_id = None
|
|
link.kontext = link_type
|
|
|
|
if link_type == 'destinataer':
|
|
link.destinataer_id = link_target_id
|
|
elif link_type == 'land':
|
|
link.land_id = link_target_id
|
|
elif link_type == 'verpachtung':
|
|
link.verpachtung_id = link_target_id
|
|
elif link_type == 'paechter':
|
|
link.paechter_id = link_target_id
|
|
elif link_type == 'foerderung':
|
|
link.foerderung_id = link_target_id
|
|
elif link_type == 'rentmeister':
|
|
link.rentmeister_id = link_target_id
|
|
else:
|
|
return Response({'error': 'Ungültiger link_type'}, status=400)
|
|
|
|
link.save()
|
|
|
|
# Automatische Pächter-Verknüpfung und Cleanup NACH der Haupttransaktion
|
|
paechter_linked = False
|
|
if link_type == 'verpachtung':
|
|
paechter_linked = create_paechter_link_for_verpachtung(
|
|
paperless_id_for_cleanup, titel_for_new_link, link_target_id
|
|
)
|
|
|
|
# Cleanup: Entferne verwaiste Pächter-Verknüpfungen wenn von Verpachtung weg geändert
|
|
if old_verpachtung_id and link_type != 'verpachtung':
|
|
try:
|
|
old_verpachtung = LandVerpachtung.objects.select_related('paechter').get(id=old_verpachtung_id)
|
|
if old_verpachtung.paechter:
|
|
# Prüfe ob noch andere Verpachtungs-Links für diesen Pächter existieren
|
|
other_verpachtung_links = DokumentLink.objects.filter(
|
|
paperless_document_id=paperless_id_for_cleanup,
|
|
verpachtung__paechter_id=old_verpachtung.paechter.id
|
|
).exists()
|
|
|
|
if not other_verpachtung_links:
|
|
# Entferne automatisch erstellte Pächter-Verknüpfung
|
|
DokumentLink.objects.filter(
|
|
paperless_document_id=paperless_id_for_cleanup,
|
|
paechter_id=old_verpachtung.paechter.id,
|
|
kontext='paechter'
|
|
).delete()
|
|
except (LandVerpachtung.DoesNotExist, Exception):
|
|
pass
|
|
|
|
message = 'Verknüpfung aktualisiert'
|
|
if paechter_linked:
|
|
message += ' (automatisch auch mit Pächter verknüpft)'
|
|
|
|
return Response({'success': True, 'message': message})
|
|
except DokumentLink.DoesNotExist:
|
|
return Response({'error': 'Verknüpfung nicht gefunden'}, status=404)
|
|
except Exception as e:
|
|
return Response({'error': f'Unerwarteter Fehler: {e}'}, status=500)
|
|
|
|
|
|
@csrf_exempt
|
|
@api_view(['DELETE'])
|
|
def link_document_delete(request, link_id):
|
|
"""Löscht eine bestehende Verknüpfung."""
|
|
from django.db import transaction
|
|
|
|
try:
|
|
with transaction.atomic():
|
|
link = DokumentLink.objects.get(id=link_id)
|
|
verpachtung_id_for_cleanup = link.verpachtung_id
|
|
paperless_id_for_cleanup = link.paperless_document_id
|
|
|
|
# Log the unlinking action before deletion
|
|
from stiftung.audit import log_unlink
|
|
try:
|
|
# Determine what entity this was linked to
|
|
target_type = "unknown"
|
|
target_name = "Unknown"
|
|
|
|
if link.destinataer_id:
|
|
target_type = "destinataer"
|
|
try:
|
|
entity = Destinataer.objects.get(id=link.destinataer_id)
|
|
target_name = entity.get_full_name()
|
|
except Destinataer.DoesNotExist:
|
|
target_name = f"Destinatär ID {link.destinataer_id}"
|
|
elif link.land_id:
|
|
target_type = "land"
|
|
try:
|
|
entity = Land.objects.get(id=link.land_id)
|
|
target_name = str(entity)
|
|
except Land.DoesNotExist:
|
|
target_name = f"Land ID {link.land_id}"
|
|
elif link.paechter_id:
|
|
target_type = "paechter"
|
|
try:
|
|
entity = Paechter.objects.get(id=link.paechter_id)
|
|
target_name = f"{entity.vorname} {entity.nachname}".strip()
|
|
except Paechter.DoesNotExist:
|
|
target_name = f"Pächter ID {link.paechter_id}"
|
|
elif link.verpachtung_id:
|
|
target_type = "verpachtung"
|
|
try:
|
|
entity = LandVerpachtung.objects.get(id=link.verpachtung_id)
|
|
target_name = str(entity)
|
|
except LandVerpachtung.DoesNotExist:
|
|
target_name = f"Verpachtung ID {link.verpachtung_id}"
|
|
elif link.rentmeister_id:
|
|
target_type = "rentmeister"
|
|
try:
|
|
from stiftung.models import Rentmeister
|
|
entity = Rentmeister.objects.get(id=link.rentmeister_id)
|
|
target_name = entity.get_full_name()
|
|
except Rentmeister.DoesNotExist:
|
|
target_name = f"Rentmeister ID {link.rentmeister_id}"
|
|
|
|
log_unlink(
|
|
request=request,
|
|
entity_type='dokumentlink',
|
|
entity_id=str(link.id),
|
|
entity_name=link.titel,
|
|
target_type=target_type,
|
|
target_name=target_name
|
|
)
|
|
except Exception as e:
|
|
# Don't fail the main operation if logging fails
|
|
print(f"Audit logging failed: {e}")
|
|
|
|
link.delete()
|
|
|
|
# Cleanup NACH der Haupttransaktion: Wenn eine Verpachtungs-Verknüpfung gelöscht wurde, prüfe Pächter-Links
|
|
if verpachtung_id_for_cleanup:
|
|
try:
|
|
verpachtung = LandVerpachtung.objects.select_related('paechter').get(id=verpachtung_id_for_cleanup)
|
|
if verpachtung.paechter:
|
|
# Prüfe ob noch andere Verpachtungs-Links für diesen Pächter existieren
|
|
other_verpachtung_links = DokumentLink.objects.filter(
|
|
paperless_document_id=paperless_id_for_cleanup,
|
|
verpachtung__paechter_id=verpachtung.paechter.id
|
|
).exists()
|
|
|
|
if not other_verpachtung_links:
|
|
# Entferne automatisch erstellte Pächter-Verknüpfung
|
|
DokumentLink.objects.filter(
|
|
paperless_document_id=paperless_id_for_cleanup,
|
|
paechter_id=verpachtung.paechter.id,
|
|
kontext='paechter'
|
|
).delete()
|
|
except (LandVerpachtung.DoesNotExist, Exception):
|
|
pass
|
|
|
|
return Response({'success': True})
|
|
except DokumentLink.DoesNotExist:
|
|
return Response({'error': 'Verknüpfung nicht gefunden'}, status=404)
|
|
except Exception as e:
|
|
return Response({'error': f'Unerwarteter Fehler: {e}'}, status=500)
|
|
|
|
|
|
@api_view(['GET'])
|
|
def gramps_search_api(request):
|
|
"""Probe-Endpoint: Suche Personen in Gramps Web nach q (Nachname, Vorname)."""
|
|
q = request.GET.get('q', '')
|
|
if not q:
|
|
return Response({'error': 'Parameter q erforderlich'}, status=400)
|
|
client = get_gramps_client()
|
|
result = client.search_people(q)
|
|
return Response(result)
|
|
|
|
|
|
# Geschäftsführung Views
|
|
@login_required
|
|
def geschaeftsfuehrung(request):
|
|
"""Hauptansicht für die Geschäftsführung mit Übersicht"""
|
|
from 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_format = request.POST.get('format') if request.method == 'POST' else request.GET.get('format', '')
|
|
selected_ids_param = request.POST.get('selected_entries', '') if request.method == 'POST' else request.GET.get('selected_entries', '')
|
|
selected_ids = [id for id in selected_ids_param.split(',') if id] if selected_ids_param else []
|
|
|
|
qs = DestinataerUnterstuetzung.objects.select_related(
|
|
'destinataer', 'konto', 'ausgezahlt_von', 'wiederkehrend_von'
|
|
).order_by('-faellig_am', 'destinataer__nachname')
|
|
|
|
if status:
|
|
qs = qs.filter(status=status)
|
|
|
|
# Enhanced CSV export with field selection
|
|
if export_format == 'csv':
|
|
return export_unterstuetzungen_csv(request, qs, selected_ids)
|
|
|
|
# Enhanced PDF export with corporate identity
|
|
elif export_format == 'pdf':
|
|
return export_unterstuetzungen_pdf(request, qs, selected_ids)
|
|
context = {
|
|
'unterstuetzungen': qs,
|
|
'status_filter': status,
|
|
}
|
|
return render(request, 'stiftung/unterstuetzungen_list.html', context)
|
|
|
|
|
|
def export_unterstuetzungen_csv(request, queryset, selected_ids=None):
|
|
"""Enhanced CSV export with field selection"""
|
|
import csv
|
|
from django.http import HttpResponse
|
|
from datetime import datetime
|
|
|
|
# If specific entries are selected, filter to only those
|
|
if selected_ids:
|
|
queryset = queryset.filter(id__in=selected_ids)
|
|
|
|
# Get selected fields from request (default to all if none specified)
|
|
selected_fields_param = request.POST.get('selected_fields', '') if request.method == 'POST' else request.GET.get('selected_fields', '')
|
|
selected_fields = selected_fields_param.split(',') if selected_fields_param else []
|
|
|
|
if not selected_fields:
|
|
# Default field set
|
|
selected_fields = [
|
|
'destinataer_name', 'betrag', 'faellig_am', 'status',
|
|
'empfaenger_iban', 'empfaenger_name', 'beschreibung'
|
|
]
|
|
|
|
# Field definitions with headers and data extraction
|
|
field_definitions = {
|
|
# Core payment fields
|
|
'id': ('ID', lambda u: str(u.id)),
|
|
'betrag': ('Betrag (€)', lambda u: f"{u.betrag:.2f}"),
|
|
'faellig_am': ('Fällig am', lambda u: u.faellig_am.strftime('%d.%m.%Y') if u.faellig_am else ''),
|
|
'status': ('Status', lambda u: u.get_status_display()),
|
|
'beschreibung': ('Beschreibung', lambda u: u.beschreibung or ''),
|
|
'ausgezahlt_am': ('Ausgezahlt am', lambda u: u.ausgezahlt_am.strftime('%d.%m.%Y') if u.ausgezahlt_am else ''),
|
|
'erstellt_am': ('Erstellt am', lambda u: u.erstellt_am.strftime('%d.%m.%Y %H:%M') if u.erstellt_am else ''),
|
|
'aktualisiert_am': ('Aktualisiert am', lambda u: u.aktualisiert_am.strftime('%d.%m.%Y %H:%M') if u.aktualisiert_am else ''),
|
|
|
|
# Destinataer fields
|
|
'destinataer_name': ('Destinatär Name', lambda u: u.destinataer.get_full_name() if u.destinataer else ''),
|
|
'destinataer_vorname': ('Vorname', lambda u: u.destinataer.vorname if u.destinataer else ''),
|
|
'destinataer_nachname': ('Nachname', lambda u: u.destinataer.nachname if u.destinataer else ''),
|
|
'familienzweig': ('Familienzweig', lambda u: u.destinataer.familienzweig if u.destinataer else ''),
|
|
'geburtsdatum': ('Geburtsdatum', lambda u: u.destinataer.geburtsdatum.strftime('%d.%m.%Y') if u.destinataer and u.destinataer.geburtsdatum else ''),
|
|
'email': ('E-Mail', lambda u: u.destinataer.email if u.destinataer else ''),
|
|
'telefon': ('Telefon', lambda u: u.destinataer.telefon if u.destinataer else ''),
|
|
'destinataer_iban': ('Destinatär IBAN', lambda u: u.destinataer.iban if u.destinataer else ''),
|
|
'strasse': ('Straße', lambda u: u.destinataer.strasse if u.destinataer else ''),
|
|
'plz': ('PLZ', lambda u: u.destinataer.plz if u.destinataer else ''),
|
|
'ort': ('Ort', lambda u: u.destinataer.ort if u.destinataer else ''),
|
|
'adresse': ('Adresse', lambda u: f"{u.destinataer.strasse}, {u.destinataer.plz} {u.destinataer.ort}".strip(', ') if u.destinataer else ''),
|
|
'berufsgruppe': ('Berufsgruppe', lambda u: u.destinataer.berufsgruppe if u.destinataer else ''),
|
|
'ausbildungsstand': ('Ausbildungsstand', lambda u: u.destinataer.ausbildungsstand if u.destinataer else ''),
|
|
'institution': ('Institution', lambda u: u.destinataer.institution if u.destinataer else ''),
|
|
'jaehrliches_einkommen': ('Jährliches Einkommen (€)', lambda u: f"{u.destinataer.jaehrliches_einkommen:.2f}" if u.destinataer and u.destinataer.jaehrliches_einkommen else ''),
|
|
'haushaltsgroesse': ('Haushaltsgröße', lambda u: str(u.destinataer.haushaltsgroesse) if u.destinataer and u.destinataer.haushaltsgroesse else ''),
|
|
'monatliche_bezuege': ('Monatliche Bezüge (€)', lambda u: f"{u.destinataer.monatliche_bezuege:.2f}" if u.destinataer and u.destinataer.monatliche_bezuege else ''),
|
|
'vermoegen': ('Vermögen (€)', lambda u: f"{u.destinataer.vermoegen:.2f}" if u.destinataer and u.destinataer.vermoegen else ''),
|
|
|
|
# Payment details
|
|
'empfaenger_iban': ('Empfänger IBAN', lambda u: u.empfaenger_iban or ''),
|
|
'empfaenger_name': ('Empfänger Name', lambda u: u.empfaenger_name or ''),
|
|
'verwendungszweck': ('Verwendungszweck', lambda u: u.verwendungszweck or ''),
|
|
|
|
# Account fields
|
|
'konto_name': ('Konto', lambda u: str(u.konto) if u.konto else ''),
|
|
'konto_bank': ('Bank', lambda u: u.konto.bank_name if u.konto else ''),
|
|
'konto_iban': ('Konto IBAN', lambda u: u.konto.iban if u.konto else ''),
|
|
|
|
# System fields
|
|
'ausgezahlt_von': ('Ausgezahlt von', lambda u: u.ausgezahlt_von.get_full_name() if u.ausgezahlt_von else ''),
|
|
'ist_wiederkehrend': ('Wiederkehrend', lambda u: 'Ja' if u.wiederkehrend_von else 'Nein'),
|
|
}
|
|
|
|
# Create CSV response
|
|
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
|
|
filename = f'unterstuetzungen_{timestamp}.csv'
|
|
|
|
response = HttpResponse(content_type='text/csv; charset=utf-8')
|
|
response['Content-Disposition'] = f'attachment; filename="{filename}"'
|
|
|
|
writer = csv.writer(response, delimiter=';', quoting=csv.QUOTE_ALL)
|
|
|
|
# Write headers
|
|
headers = [field_definitions[field][0] for field in selected_fields if field in field_definitions]
|
|
writer.writerow(headers)
|
|
|
|
# Write data rows
|
|
for u in queryset:
|
|
row = []
|
|
for field in selected_fields:
|
|
if field in field_definitions:
|
|
try:
|
|
value = field_definitions[field][1](u)
|
|
row.append(value)
|
|
except Exception:
|
|
row.append('') # Fallback for any errors
|
|
else:
|
|
row.append('') # Unknown field
|
|
writer.writerow(row)
|
|
|
|
return response
|
|
|
|
|
|
def export_unterstuetzungen_pdf(request, queryset, selected_ids=None):
|
|
"""Enhanced PDF export with corporate identity and field selection"""
|
|
# If specific entries are selected, filter to only those
|
|
if selected_ids:
|
|
queryset = queryset.filter(id__in=selected_ids)
|
|
|
|
# Get selected fields from request (default to key fields if none specified)
|
|
selected_fields_param = request.POST.get('selected_fields', '') if request.method == 'POST' else request.GET.get('selected_fields', '')
|
|
selected_fields = selected_fields_param.split(',') if selected_fields_param else []
|
|
|
|
if not selected_fields:
|
|
# Default field set for PDF (fewer fields than CSV for better readability)
|
|
selected_fields = [
|
|
'destinataer_name', 'betrag', 'faellig_am', 'status',
|
|
'beschreibung', 'ausgezahlt_am'
|
|
]
|
|
|
|
# Field definitions with display names (reuse from CSV but select PDF-appropriate subset)
|
|
field_definitions = {
|
|
# Core payment fields
|
|
'destinataer_name': 'Destinatär',
|
|
'betrag': 'Betrag (€)',
|
|
'faellig_am': 'Fällig am',
|
|
'status': 'Status',
|
|
'beschreibung': 'Beschreibung',
|
|
'ausgezahlt_am': 'Ausgezahlt am',
|
|
'erstellt_am': 'Erstellt am',
|
|
'empfaenger_iban': 'Empfänger IBAN',
|
|
'empfaenger_name': 'Empfänger',
|
|
'verwendungszweck': 'Verwendungszweck',
|
|
'konto_name': 'Konto',
|
|
'ist_wiederkehrend': 'Wiederkehrend',
|
|
}
|
|
|
|
# Filter to only include fields that are both selected and defined
|
|
filtered_fields = {k: v for k, v in field_definitions.items() if k in selected_fields}
|
|
|
|
# Prepare data with field extraction logic
|
|
data_for_pdf = []
|
|
for item in queryset:
|
|
row_data = {}
|
|
for field_key in filtered_fields.keys():
|
|
try:
|
|
if field_key == 'destinataer_name':
|
|
row_data[field_key] = item.destinataer.get_full_name() if item.destinataer else ''
|
|
elif field_key == 'betrag':
|
|
row_data[field_key] = f"€{item.betrag:.2f}" if item.betrag else ''
|
|
elif field_key == 'faellig_am':
|
|
row_data[field_key] = item.faellig_am.strftime('%d.%m.%Y') if item.faellig_am else ''
|
|
elif field_key == 'status':
|
|
row_data[field_key] = item.get_status_display()
|
|
elif field_key == 'beschreibung':
|
|
row_data[field_key] = item.beschreibung or ''
|
|
elif field_key == 'ausgezahlt_am':
|
|
row_data[field_key] = item.ausgezahlt_am.strftime('%d.%m.%Y') if item.ausgezahlt_am else ''
|
|
elif field_key == 'erstellt_am':
|
|
row_data[field_key] = item.erstellt_am.strftime('%d.%m.%Y') if item.erstellt_am else ''
|
|
elif field_key == 'empfaenger_iban':
|
|
row_data[field_key] = item.empfaenger_iban or ''
|
|
elif field_key == 'empfaenger_name':
|
|
row_data[field_key] = item.empfaenger_name or ''
|
|
elif field_key == 'verwendungszweck':
|
|
row_data[field_key] = item.verwendungszweck or ''
|
|
elif field_key == 'konto_name':
|
|
row_data[field_key] = str(item.konto) if item.konto else ''
|
|
elif field_key == 'ist_wiederkehrend':
|
|
row_data[field_key] = 'Ja' if item.wiederkehrend_von else 'Nein'
|
|
else:
|
|
# Generic field access
|
|
row_data[field_key] = getattr(item, field_key, '') or ''
|
|
except Exception:
|
|
row_data[field_key] = '' # Fallback for any errors
|
|
|
|
data_for_pdf.append(row_data)
|
|
|
|
# Use PDF generator
|
|
pdf_gen = get_pdf_generator()
|
|
return pdf_gen.export_data_list_pdf(
|
|
data=data_for_pdf,
|
|
fields_config=filtered_fields,
|
|
title="Unterstützungen Export",
|
|
filename_prefix="unterstuetzungen",
|
|
request_user=request.user
|
|
)
|
|
|
|
|
|
def export_foerderungen_csv(request, queryset, selected_ids=None):
|
|
"""Enhanced CSV export for Förderungen with field selection"""
|
|
import csv
|
|
from django.http import HttpResponse
|
|
from datetime import datetime
|
|
|
|
# If specific entries are selected, filter to only those
|
|
if selected_ids:
|
|
queryset = queryset.filter(id__in=selected_ids)
|
|
|
|
# Get selected fields from request (default to all if none specified)
|
|
selected_fields_param = request.POST.get('selected_fields', '') if request.method == 'POST' else request.GET.get('selected_fields', '')
|
|
selected_fields = selected_fields_param.split(',') if selected_fields_param else []
|
|
|
|
if not selected_fields:
|
|
# Default field set
|
|
selected_fields = [
|
|
'destinataer_name', 'jahr', 'betrag', 'kategorie', 'status',
|
|
'antragsdatum', 'beschreibung'
|
|
]
|
|
|
|
# Field definitions with headers and data extraction
|
|
field_definitions = {
|
|
# Core fields
|
|
'id': ('ID', lambda f: str(f.id)),
|
|
'destinataer_name': ('Destinatär Name', lambda f: f.destinataer.get_full_name() if f.destinataer else ''),
|
|
'jahr': ('Jahr', lambda f: str(f.jahr)),
|
|
'betrag': ('Betrag (€)', lambda f: f"{f.betrag:.2f}"),
|
|
'kategorie': ('Kategorie', lambda f: f.get_kategorie_display()),
|
|
'status': ('Status', lambda f: f.get_status_display()),
|
|
'antragsdatum': ('Antragsdatum', lambda f: f.antragsdatum.strftime('%d.%m.%Y') if f.antragsdatum else ''),
|
|
'bewilligungsdatum': ('Bewilligungsdatum', lambda f: f.bewilligungsdatum.strftime('%d.%m.%Y') if f.bewilligungsdatum else ''),
|
|
'auszahlungsdatum': ('Auszahlungsdatum', lambda f: f.auszahlungsdatum.strftime('%d.%m.%Y') if f.auszahlungsdatum else ''),
|
|
'beschreibung': ('Beschreibung', lambda f: f.beschreibung or ''),
|
|
'begruendung': ('Begründung', lambda f: f.begruendung or ''),
|
|
'verwendungsnachweis_datum': ('Verwendungsnachweis Datum', lambda f: f.verwendungsnachweis_datum.strftime('%d.%m.%Y') if f.verwendungsnachweis_datum else ''),
|
|
'verwendungsnachweis_status': ('Verwendungsnachweis Status', lambda f: f.get_verwendungsnachweis_status_display() if f.verwendungsnachweis_status else ''),
|
|
|
|
# Destinataer fields
|
|
'destinataer_vorname': ('Vorname', lambda f: f.destinataer.vorname if f.destinataer else ''),
|
|
'destinataer_nachname': ('Nachname', lambda f: f.destinataer.nachname if f.destinataer else ''),
|
|
'familienzweig': ('Familienzweig', lambda f: f.destinataer.familienzweig if f.destinataer else ''),
|
|
'email': ('E-Mail', lambda f: f.destinataer.email if f.destinataer else ''),
|
|
'telefon': ('Telefon', lambda f: f.destinataer.telefon if f.destinataer else ''),
|
|
'adresse': ('Adresse', lambda f: f"{f.destinataer.strasse}, {f.destinataer.plz} {f.destinataer.ort}".strip(', ') if f.destinataer else ''),
|
|
'berufsgruppe': ('Berufsgruppe', lambda f: f.destinataer.berufsgruppe if f.destinataer else ''),
|
|
'ausbildungsstand': ('Ausbildungsstand', lambda f: f.destinataer.ausbildungsstand if f.destinataer else ''),
|
|
'institution': ('Institution', lambda f: f.destinataer.institution if f.destinataer else ''),
|
|
|
|
# System fields
|
|
'erstellt_am': ('Erstellt am', lambda f: f.erstellt_am.strftime('%d.%m.%Y %H:%M') if f.erstellt_am else ''),
|
|
'aktualisiert_am': ('Aktualisiert am', lambda f: f.aktualisiert_am.strftime('%d.%m.%Y %H:%M') if f.aktualisiert_am else ''),
|
|
}
|
|
|
|
# Create CSV response
|
|
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
|
|
filename = f'foerderungen_{timestamp}.csv'
|
|
|
|
response = HttpResponse(content_type='text/csv; charset=utf-8')
|
|
response['Content-Disposition'] = f'attachment; filename="{filename}"'
|
|
|
|
writer = csv.writer(response, delimiter=';', quoting=csv.QUOTE_ALL)
|
|
|
|
# Write headers
|
|
headers = [field_definitions[field][0] for field in selected_fields if field in field_definitions]
|
|
writer.writerow(headers)
|
|
|
|
# Write data rows
|
|
for f in queryset:
|
|
row = []
|
|
for field in selected_fields:
|
|
if field in field_definitions:
|
|
try:
|
|
value = field_definitions[field][1](f)
|
|
row.append(value)
|
|
except Exception:
|
|
row.append('') # Fallback for any errors
|
|
else:
|
|
row.append('') # Unknown field
|
|
writer.writerow(row)
|
|
|
|
return response
|
|
|
|
|
|
def export_foerderungen_pdf(request, queryset, selected_ids=None):
|
|
"""Enhanced PDF export for Förderungen with corporate identity and field selection"""
|
|
# If specific entries are selected, filter to only those
|
|
if selected_ids:
|
|
queryset = queryset.filter(id__in=selected_ids)
|
|
|
|
# Get selected fields from request (default to key fields if none specified)
|
|
selected_fields_param = request.POST.get('selected_fields', '') if request.method == 'POST' else request.GET.get('selected_fields', '')
|
|
selected_fields = selected_fields_param.split(',') if selected_fields_param else []
|
|
|
|
if not selected_fields:
|
|
# Default field set for PDF (fewer fields than CSV for better readability)
|
|
selected_fields = [
|
|
'destinataer_name', 'jahr', 'betrag', 'kategorie', 'status',
|
|
'antragsdatum'
|
|
]
|
|
|
|
# Field definitions with display names
|
|
field_definitions = {
|
|
'destinataer_name': 'Destinatär',
|
|
'jahr': 'Jahr',
|
|
'betrag': 'Betrag (€)',
|
|
'kategorie': 'Kategorie',
|
|
'status': 'Status',
|
|
'antragsdatum': 'Antragsdatum',
|
|
'bewilligungsdatum': 'Bewilligungsdatum',
|
|
'auszahlungsdatum': 'Auszahlungsdatum',
|
|
'beschreibung': 'Beschreibung',
|
|
'begruendung': 'Begründung',
|
|
'verwendungsnachweis_status': 'Verwendungsnachweis',
|
|
}
|
|
|
|
# Filter to only include fields that are both selected and defined
|
|
filtered_fields = {k: v for k, v in field_definitions.items() if k in selected_fields}
|
|
|
|
# Prepare data with field extraction logic
|
|
data_for_pdf = []
|
|
for item in queryset:
|
|
row_data = {}
|
|
for field_key in filtered_fields.keys():
|
|
try:
|
|
if field_key == 'destinataer_name':
|
|
row_data[field_key] = item.destinataer.get_full_name() if item.destinataer else ''
|
|
elif field_key == 'jahr':
|
|
row_data[field_key] = str(item.jahr)
|
|
elif field_key == 'betrag':
|
|
row_data[field_key] = f"€{item.betrag:.2f}" if item.betrag else ''
|
|
elif field_key == 'kategorie':
|
|
row_data[field_key] = item.get_kategorie_display()
|
|
elif field_key == 'status':
|
|
row_data[field_key] = item.get_status_display()
|
|
elif field_key == 'antragsdatum':
|
|
row_data[field_key] = item.antragsdatum.strftime('%d.%m.%Y') if item.antragsdatum else ''
|
|
elif field_key == 'bewilligungsdatum':
|
|
row_data[field_key] = item.bewilligungsdatum.strftime('%d.%m.%Y') if item.bewilligungsdatum else ''
|
|
elif field_key == 'auszahlungsdatum':
|
|
row_data[field_key] = item.auszahlungsdatum.strftime('%d.%m.%Y') if item.auszahlungsdatum else ''
|
|
elif field_key == 'beschreibung':
|
|
row_data[field_key] = (item.beschreibung or '')[:100] + ('...' if len(item.beschreibung or '') > 100 else '')
|
|
elif field_key == 'begruendung':
|
|
row_data[field_key] = (item.begruendung or '')[:100] + ('...' if len(item.begruendung or '') > 100 else '')
|
|
elif field_key == 'verwendungsnachweis_status':
|
|
row_data[field_key] = item.get_verwendungsnachweis_status_display() if item.verwendungsnachweis_status else ''
|
|
else:
|
|
# Generic field access
|
|
row_data[field_key] = getattr(item, field_key, '') or ''
|
|
except Exception:
|
|
row_data[field_key] = '' # Fallback for any errors
|
|
|
|
data_for_pdf.append(row_data)
|
|
|
|
# Use PDF generator
|
|
pdf_gen = get_pdf_generator()
|
|
return pdf_gen.export_data_list_pdf(
|
|
data=data_for_pdf,
|
|
fields_config=filtered_fields,
|
|
title="Förderungen Export",
|
|
filename_prefix="foerderungen",
|
|
request_user=request.user
|
|
)
|
|
|
|
|
|
@login_required
|
|
def unterstuetzung_edit(request, pk):
|
|
obj = get_object_or_404(DestinataerUnterstuetzung, pk=pk)
|
|
if request.method == 'POST':
|
|
form = DestinataerUnterstuetzungForm(request.POST, instance=obj)
|
|
if form.is_valid():
|
|
form.save()
|
|
messages.success(request, 'Unterstützung aktualisiert.')
|
|
return redirect('stiftung:unterstuetzungen_list')
|
|
else:
|
|
form = DestinataerUnterstuetzungForm(instance=obj)
|
|
return render(request, 'stiftung/unterstuetzung_form.html', {'form': form, 'title': 'Unterstützung bearbeiten'})
|
|
|
|
|
|
@login_required
|
|
def unterstuetzung_delete(request, pk):
|
|
obj = get_object_or_404(DestinataerUnterstuetzung, pk=pk)
|
|
|
|
# Check if this will also delete the recurring template
|
|
will_delete_template = False
|
|
if obj.wiederkehrend_von:
|
|
andere_zahlungen = DestinataerUnterstuetzung.objects.filter(
|
|
wiederkehrend_von=obj.wiederkehrend_von
|
|
).exclude(pk=pk).exists()
|
|
will_delete_template = not andere_zahlungen
|
|
|
|
if request.method == 'POST':
|
|
# Check if this support payment is linked to a recurring payment template
|
|
wiederkehrend_template = obj.wiederkehrend_von
|
|
|
|
# Delete the support payment
|
|
obj.delete()
|
|
|
|
# If this was generated from a recurring template and there are no other
|
|
# payments from this template, delete the template too
|
|
if wiederkehrend_template:
|
|
# Check if there are other payments from this recurring template
|
|
andere_zahlungen = DestinataerUnterstuetzung.objects.filter(
|
|
wiederkehrend_von=wiederkehrend_template
|
|
).exists()
|
|
|
|
# If no other payments exist from this template, delete the template too
|
|
if not andere_zahlungen:
|
|
wiederkehrend_template.delete()
|
|
messages.success(request, 'Unterstützung und wiederkehrende Zahlungsvorlage gelöscht.')
|
|
else:
|
|
messages.success(request, 'Unterstützung gelöscht.')
|
|
else:
|
|
messages.success(request, 'Unterstützung gelöscht.')
|
|
|
|
return redirect('stiftung:unterstuetzungen_list')
|
|
|
|
context = {
|
|
'obj': obj,
|
|
'will_delete_template': will_delete_template,
|
|
}
|
|
return render(request, 'stiftung/unterstuetzung_confirm_delete.html', context)
|
|
|
|
|
|
@login_required
|
|
def destinataer_notiz_create(request, pk):
|
|
destinataer = get_object_or_404(Destinataer, pk=pk)
|
|
if request.method == 'POST':
|
|
form = DestinataerNotizForm(request.POST, request.FILES)
|
|
if form.is_valid():
|
|
note = form.save(commit=False)
|
|
note.destinataer = destinataer
|
|
note.erstellt_von = request.user
|
|
note.save()
|
|
messages.success(request, 'Notiz wurde gespeichert.')
|
|
return redirect('stiftung:destinataer_detail', pk=destinataer.pk)
|
|
else:
|
|
# Debug: show what validation failed
|
|
for field, errors in form.errors.items():
|
|
messages.error(request, f'Fehler in {field}: {", ".join(errors)}')
|
|
else:
|
|
form = DestinataerNotizForm()
|
|
return render(request, 'stiftung/destinataer_notiz_form.html', {'form': form, 'destinataer': destinataer, 'title': 'Notiz hinzufügen'})
|
|
|
|
|
|
@login_required
|
|
def destinataer_export(request, pk):
|
|
"""Export complete Destinatär data as ZIP with documents"""
|
|
import 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(LandVerpachtung, pk=pk)
|
|
|
|
# Create a temporary file for the ZIP
|
|
temp_file = tempfile.NamedTemporaryFile(delete=False, suffix='.zip')
|
|
|
|
try:
|
|
with zipfile.ZipFile(temp_file.name, 'w', zipfile.ZIP_DEFLATED) as zipf:
|
|
# 1. Entity data as JSON
|
|
entity_data = {
|
|
'id': str(verpachtung.id),
|
|
'vertragsnummer': verpachtung.vertragsnummer,
|
|
'land': str(verpachtung.land),
|
|
'land_id': str(verpachtung.land.id),
|
|
'paechter': str(verpachtung.paechter),
|
|
'paechter_id': str(verpachtung.paechter.id),
|
|
'pachtbeginn': verpachtung.pachtbeginn.isoformat(),
|
|
'pachtende': verpachtung.pachtende.isoformat(),
|
|
'verlaengerung': verpachtung.verlaengerung.isoformat() if verpachtung.verlaengerung else None,
|
|
'pachtzins_pro_qm': str(verpachtung.pachtzins_pro_qm),
|
|
'pachtzins_jaehrlich': str(verpachtung.pachtzins_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)
|
|
|
|
|
|
# Settings Management Views
|
|
@login_required
|
|
def app_settings(request):
|
|
"""Application settings management interface"""
|
|
|
|
# Group settings by category
|
|
categories = {}
|
|
for setting in AppConfiguration.objects.filter(is_active=True).order_by('category', 'order', 'display_name'):
|
|
if setting.category not in categories:
|
|
categories[setting.category] = []
|
|
categories[setting.category].append(setting)
|
|
|
|
if request.method == 'POST':
|
|
# Handle form submission
|
|
updated_count = 0
|
|
for key, value in request.POST.items():
|
|
if key.startswith('setting_'):
|
|
setting_key = key.replace('setting_', '')
|
|
try:
|
|
setting = AppConfiguration.objects.get(key=setting_key, is_active=True)
|
|
if not setting.is_system and setting.value != value:
|
|
setting.value = value
|
|
setting.save()
|
|
updated_count += 1
|
|
except AppConfiguration.DoesNotExist:
|
|
continue
|
|
|
|
if updated_count > 0:
|
|
messages.success(request, f'Successfully updated {updated_count} settings!')
|
|
else:
|
|
messages.info(request, 'No changes were made.')
|
|
|
|
return redirect('stiftung:app_settings')
|
|
|
|
context = {
|
|
'categories': categories,
|
|
'title': 'Application Settings',
|
|
}
|
|
return render(request, 'stiftung/app_settings.html', context)
|
|
|
|
|
|
# Unterstützungen Views (Destinataer-focused)
|
|
@login_required
|
|
def unterstuetzungen_all(request):
|
|
"""List all support payments - destinataer-focused view"""
|
|
status = request.GET.get('status')
|
|
destinataer_id = request.GET.get('destinataer')
|
|
export = request.GET.get('format', '')
|
|
selected_ids = request.POST.getlist('selected_entries') if request.method == 'POST' else []
|
|
|
|
unterstuetzungen = DestinataerUnterstuetzung.objects.select_related(
|
|
'destinataer', 'konto', 'ausgezahlt_von', 'wiederkehrend_von'
|
|
).order_by('-faellig_am')
|
|
|
|
# Filtering
|
|
if status:
|
|
unterstuetzungen = unterstuetzungen.filter(status=status)
|
|
if destinataer_id:
|
|
unterstuetzungen = unterstuetzungen.filter(destinataer_id=destinataer_id)
|
|
|
|
# Enhanced CSV export with field selection
|
|
if export == 'csv':
|
|
return export_unterstuetzungen_csv(request, unterstuetzungen, selected_ids)
|
|
|
|
# PDF export (simple table via WeasyPrint; graceful fallback if missing)
|
|
if export == 'pdf':
|
|
try:
|
|
from django.template.loader import render_to_string
|
|
from weasyprint import HTML
|
|
html = render_to_string('stiftung/unterstuetzungen_pdf.html', {'unterstuetzungen': unterstuetzungen})
|
|
from django.http import HttpResponse
|
|
pdf = HTML(string=html).write_pdf()
|
|
resp = HttpResponse(pdf, content_type='application/pdf')
|
|
resp['Content-Disposition'] = 'inline; filename=unterstuetzungen.pdf'
|
|
return resp
|
|
except Exception:
|
|
pass
|
|
|
|
# Statistics
|
|
total_betrag = unterstuetzungen.aggregate(total=Sum('betrag'))['total'] or 0
|
|
avg_betrag = unterstuetzungen.aggregate(avg=Avg('betrag'))['avg'] or 0
|
|
|
|
# Available destinataer for filter
|
|
destinataer = Destinataer.objects.all().order_by('nachname', 'vorname')
|
|
|
|
context = {
|
|
'page_obj': unterstuetzungen, # Use directly for now (pagination can be added later)
|
|
'unterstuetzungen': unterstuetzungen,
|
|
'title': 'Alle Unterstützungen',
|
|
'status_filter': status,
|
|
'total_betrag': total_betrag,
|
|
'avg_betrag': avg_betrag,
|
|
'status_choices': DestinataerUnterstuetzung.STATUS_CHOICES,
|
|
'destinataer': destinataer,
|
|
}
|
|
return render(request, 'stiftung/unterstuetzungen_all.html', context)
|
|
|
|
|
|
@login_required
|
|
def unterstuetzung_create(request):
|
|
"""Create a new support payment"""
|
|
# Get destinataer from URL parameter if provided
|
|
destinataer_id = request.GET.get('destinataer')
|
|
initial = {}
|
|
if destinataer_id:
|
|
initial['destinataer'] = destinataer_id
|
|
# Pre-populate IBAN and name if destinataer is specified
|
|
try:
|
|
destinataer = Destinataer.objects.get(pk=destinataer_id)
|
|
if hasattr(destinataer, 'iban') and destinataer.iban:
|
|
initial['empfaenger_iban'] = destinataer.iban
|
|
initial['empfaenger_name'] = destinataer.get_full_name()
|
|
except Destinataer.DoesNotExist:
|
|
pass
|
|
|
|
if request.method == 'POST':
|
|
form = UnterstuetzungForm(request.POST)
|
|
if form.is_valid():
|
|
ist_wiederkehrend = form.cleaned_data.get('ist_wiederkehrend', False)
|
|
|
|
if ist_wiederkehrend:
|
|
# Create recurring payment template
|
|
wiederkehrend = UnterstuetzungWiederkehrend.objects.create(
|
|
destinataer=form.cleaned_data['destinataer'],
|
|
konto=form.cleaned_data['konto'],
|
|
betrag=form.cleaned_data['betrag'],
|
|
intervall=form.cleaned_data['intervall'],
|
|
beschreibung=form.cleaned_data['beschreibung'],
|
|
empfaenger_iban=form.cleaned_data['empfaenger_iban'],
|
|
empfaenger_name=form.cleaned_data['empfaenger_name'],
|
|
verwendungszweck=form.cleaned_data['verwendungszweck'],
|
|
erste_zahlung_am=form.cleaned_data['faellig_am'],
|
|
letzte_zahlung_am=form.cleaned_data.get('letzte_zahlung_am'),
|
|
naechste_generierung=form.cleaned_data['faellig_am'],
|
|
erstellt_von=request.user
|
|
)
|
|
|
|
# Create the first payment
|
|
unterstuetzung = form.save(commit=False)
|
|
unterstuetzung.wiederkehrend_von = wiederkehrend
|
|
unterstuetzung.save()
|
|
|
|
messages.success(request, f'Wiederkehrende Unterstützung für {unterstuetzung.destinataer} wurde erfolgreich erstellt. Die erste Zahlung ist am {unterstuetzung.faellig_am} fällig.')
|
|
else:
|
|
# Create single payment
|
|
unterstuetzung = form.save()
|
|
messages.success(request, f'Unterstützung für {unterstuetzung.destinataer} wurde erfolgreich erstellt.')
|
|
|
|
return redirect('stiftung:unterstuetzung_detail', pk=unterstuetzung.pk)
|
|
else:
|
|
form = UnterstuetzungForm(initial=initial)
|
|
|
|
context = {
|
|
'form': form,
|
|
'title': 'Neue Unterstützung erstellen',
|
|
}
|
|
return render(request, 'stiftung/unterstuetzung_form.html', context)
|
|
|
|
|
|
@login_required
|
|
def get_destinataer_info(request, destinataer_id):
|
|
"""AJAX endpoint to get Destinataer IBAN and name information"""
|
|
try:
|
|
destinataer = Destinataer.objects.get(pk=destinataer_id)
|
|
data = {
|
|
'success': True,
|
|
'name': destinataer.get_full_name(),
|
|
'iban': getattr(destinataer, 'iban', '') or '',
|
|
}
|
|
except Destinataer.DoesNotExist:
|
|
data = {
|
|
'success': False,
|
|
'error': 'Destinataer not found'
|
|
}
|
|
|
|
return JsonResponse(data)
|
|
|
|
|
|
@login_required
|
|
def unterstuetzung_detail(request, pk):
|
|
"""View support payment details"""
|
|
unterstuetzung = get_object_or_404(DestinataerUnterstuetzung, pk=pk)
|
|
|
|
# Check if this payment can be marked as paid
|
|
can_mark_paid = unterstuetzung.can_be_marked_paid()
|
|
|
|
context = {
|
|
'unterstuetzung': unterstuetzung,
|
|
'title': f'Unterstützung für {unterstuetzung.destinataer.get_full_name()}',
|
|
'can_mark_paid': can_mark_paid,
|
|
}
|
|
return render(request, 'stiftung/unterstuetzung_detail.html', context)
|
|
|
|
|
|
@login_required
|
|
def unterstuetzung_mark_paid(request, pk):
|
|
"""Mark a support payment as paid"""
|
|
unterstuetzung = get_object_or_404(DestinataerUnterstuetzung, pk=pk)
|
|
|
|
if not unterstuetzung.can_be_marked_paid():
|
|
messages.error(request, 'Diese Unterstützung kann nicht als bezahlt markiert werden.')
|
|
return redirect('stiftung:unterstuetzung_detail', pk=pk)
|
|
|
|
if request.method == 'POST':
|
|
form = UnterstuetzungMarkAsPaidForm(request.POST)
|
|
if form.is_valid():
|
|
unterstuetzung.status = 'ausgezahlt'
|
|
unterstuetzung.ausgezahlt_am = form.cleaned_data['ausgezahlt_am']
|
|
unterstuetzung.ausgezahlt_von = request.user
|
|
|
|
# Add optional note to description
|
|
bemerkung = form.cleaned_data.get('bemerkung')
|
|
if bemerkung:
|
|
if unterstuetzung.beschreibung:
|
|
unterstuetzung.beschreibung += f' | Zahlung: {bemerkung}'
|
|
else:
|
|
unterstuetzung.beschreibung = f'Zahlung: {bemerkung}'
|
|
|
|
unterstuetzung.save()
|
|
messages.success(request, f'Unterstützung wurde als bezahlt markiert.')
|
|
return redirect('stiftung:unterstuetzung_detail', pk=pk)
|
|
else:
|
|
form = UnterstuetzungMarkAsPaidForm()
|
|
|
|
context = {
|
|
'form': form,
|
|
'unterstuetzung': unterstuetzung,
|
|
'title': f'Zahlung markieren - {unterstuetzung.destinataer.get_full_name()}',
|
|
}
|
|
return render(request, 'stiftung/unterstuetzung_mark_paid.html', context)
|
|
|
|
|
|
@login_required
|
|
def wiederkehrende_unterstuetzungen(request):
|
|
"""List all recurring support payment templates"""
|
|
from django.db.models import Count
|
|
|
|
# Check for cleanup request
|
|
if request.GET.get('cleanup') == '1':
|
|
# Find templates with no associated payments
|
|
verwaiste_templates = UnterstuetzungWiederkehrend.objects.annotate(
|
|
zahlung_count=Count('destinataerunterstuetzung')
|
|
).filter(zahlung_count=0)
|
|
|
|
if verwaiste_templates.exists():
|
|
anzahl_geloescht = verwaiste_templates.count()
|
|
template_namen = list(verwaiste_templates.values_list('destinataer__nachname', flat=True))
|
|
verwaiste_templates.delete()
|
|
messages.success(
|
|
request,
|
|
f'{anzahl_geloescht} verwaiste Zahlungsvorlagen bereinigt: {", ".join(template_namen[:5])}{"..." if len(template_namen) > 5 else ""}'
|
|
)
|
|
else:
|
|
messages.info(request, 'Keine verwaisten Zahlungsvorlagen gefunden.')
|
|
|
|
return redirect('stiftung:wiederkehrende_unterstuetzungen')
|
|
|
|
# Get all templates with payment counts
|
|
templates = UnterstuetzungWiederkehrend.objects.select_related(
|
|
'destinataer', 'konto'
|
|
).annotate(
|
|
aktive_zahlungen=Count('destinataerunterstuetzung')
|
|
).all()
|
|
|
|
context = {
|
|
'templates': templates,
|
|
'title': 'Wiederkehrende Unterstützungen',
|
|
}
|
|
return render(request, 'stiftung/wiederkehrende_unterstuetzungen.html', context)
|
|
|
|
|
|
@login_required
|
|
def edit_help_box(request):
|
|
"""Bearbeite oder erstelle eine Hilfs-Infobox"""
|
|
from .models import HelpBox
|
|
|
|
# Nur root oder Superuser dürfen bearbeiten
|
|
if request.user.username != 'root' and not request.user.is_superuser:
|
|
messages.error(request, 'Sie haben keine Berechtigung, Hilfsboxen zu bearbeiten.')
|
|
return redirect('stiftung:dashboard')
|
|
|
|
if request.method == 'POST':
|
|
page_key = request.POST.get('page_key')
|
|
title = request.POST.get('title')
|
|
content = request.POST.get('content')
|
|
is_active = request.POST.get('is_active') == 'on'
|
|
|
|
if not page_key or not title or not content:
|
|
messages.error(request, 'Alle Felder sind erforderlich.')
|
|
return redirect(request.META.get('HTTP_REFERER', 'stiftung:dashboard'))
|
|
|
|
# Hilfsbox erstellen oder aktualisieren
|
|
help_box, created = HelpBox.objects.get_or_create(
|
|
page_key=page_key,
|
|
defaults={
|
|
'title': title,
|
|
'content': content,
|
|
'is_active': is_active,
|
|
'created_by': request.user.username,
|
|
'updated_by': request.user.username,
|
|
}
|
|
)
|
|
|
|
if not created:
|
|
# Existierende Hilfsbox aktualisieren
|
|
help_box.title = title
|
|
help_box.content = content
|
|
help_box.is_active = is_active
|
|
help_box.updated_by = request.user.username
|
|
help_box.save()
|
|
|
|
messages.success(request, f'Hilfsbox "{title}" wurde aktualisiert.')
|
|
else:
|
|
messages.success(request, f'Hilfsbox "{title}" wurde erstellt.')
|
|
|
|
# Zurück zur vorherigen Seite
|
|
return redirect(request.META.get('HTTP_REFERER', 'stiftung:dashboard'))
|
|
|
|
# GET Request - Zeige Admin-Übersicht der Hilfsboxen
|
|
help_boxes = HelpBox.objects.all().order_by('page_key', '-updated_at')
|
|
|
|
# Statistiken berechnen
|
|
active_count = help_boxes.filter(is_active=True).count()
|
|
inactive_count = help_boxes.filter(is_active=False).count()
|
|
existing_pages = set(help_boxes.values_list('page_key', flat=True))
|
|
|
|
# Verfügbare Seiten aus dem Model holen
|
|
available_pages = HelpBox.PAGE_CHOICES
|
|
|
|
context = {
|
|
'help_boxes': help_boxes,
|
|
'active_count': active_count,
|
|
'inactive_count': inactive_count,
|
|
'existing_pages': existing_pages,
|
|
'available_pages': available_pages,
|
|
'title': 'Hilfs-Infoboxen verwalten',
|
|
}
|
|
return render(request, 'stiftung/help_boxes_admin.html', context)
|