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 # Lazy import for PDF generator to avoid startup errors 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: # 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"""
PDF generation requires additional system dependencies that are not installed.
Error: {str(e)}
Please install WeasyPrint dependencies or use CSV export instead.
""" 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 = Verpachtung.STATUS_CHOICES context = { 'page_obj': page_obj, 'search_query': search_query, 'status_filter': status_filter, 'gemeinde_filter': gemeinde_filter, 'gemeinden': gemeinden, 'status_choices': status_choices, # Statistics for summary cards 'aktive_verpachtungen': aktive_verpachtungen, 'gesamt_flaeche': gesamt_flaeche, 'jaehrlicher_pachtzins': jaehrlicher_pachtzins, 'anzahl_verpachtungen': anzahl_verpachtungen, 'sort': sort, 'dir': direction, } return render(request, 'stiftung/verpachtung_list.html', context) @login_required @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(Verpachtung.objects.filter(status='aktiv', land__in=lands).aggregate( total=Sum('verpachtete_flaeche') )['total'] or 0), 'avg_verpachtungsgrad': 0, } if stats['total_flaeche'] > 0: stats['avg_verpachtungsgrad'] = (stats['total_verpachtet'] / stats['total_flaeche']) * 100 return JsonResponse(stats) return JsonResponse({'error': 'Invalid request method'}, status=400) @api_view(['GET']) def health(_request): return Response({'status': 'ok'}) @api_view(['GET']) def paperless_ping(_request): 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': from stiftung.models import Verpachtung entity = Verpachtung.objects.get(id=link_id) target_name = str(entity) elif link_type == 'rentmeister': from stiftung.models import Rentmeister entity = Rentmeister.objects.get(id=link_id) target_name = entity.get_full_name() else: target_name = f"ID {link_id}" log_link( request=request, entity_type='dokumentlink', entity_id=str(dokument_link.id), entity_name=entity_name, target_type=link_type, target_name=target_name ) except Exception as e: # Don't fail the main operation if logging fails print(f"Audit logging failed: {e}") # Automatische Pächter-Verknüpfung NACH der Haupttransaktion paechter_linked = False if link_type == 'verpachtung': paechter_linked = create_paechter_link_for_verpachtung(paperless_id, paperless_title, link_id) message = f'Dokument erfolgreich mit {link_type} verknüpft' if paechter_linked: message += ' (automatisch auch mit Pächter verknüpft)' return Response({ 'success': True, 'message': message, 'dokument_id': dokument_link.id }) except Exception as e: return Response({'error': f'Fehler beim Erstellen der Verknüpfung: {str(e)}'}, status=500) # Legacy dokument_verknuepfung view removed - use dokument_management instead @api_view(['GET']) def link_document_list(request): """Listet alle bestehenden Dokument-Verknüpfungen auf, gruppiert nach Paperless-Document-ID""" try: dokument_links = DokumentLink.objects.all().order_by('-id') # Group links by paperless_document_id to show multiple links per document links_by_document = {} for link in dokument_links: paperless_id = link.paperless_document_id if paperless_id not in links_by_document: links_by_document[paperless_id] = { 'paperless_id': paperless_id, 'title': link.titel, 'paperless_url': f"/api/paperless/documents/{paperless_id}/", 'links': [] } # Create link info link_info = { 'id': str(link.id), # Ensure UUID is stringified 'kontext': link.kontext, 'link_type': None, 'linked_object': None } # Determine link type and get linked object details if link.destinataer_id: link_info['link_type'] = 'destinataer' try: dest = Destinataer.objects.get(id=link.destinataer_id) link_info['linked_object'] = { 'id': str(dest.id), 'type': 'Destinatär', 'name': f"{dest.vorname} {dest.nachname}".strip() if dest.vorname else dest.institution, 'details': f"Institution: {dest.institution}" if dest.institution else f"Person: {dest.vorname} {dest.nachname}".strip() } except Destinataer.DoesNotExist: link_info['linked_object'] = {'type': 'Destinatär', 'name': 'Gelöscht', 'details': 'Datensatz nicht mehr verfügbar'} elif link.land_id: link_info['link_type'] = 'land' try: land = Land.objects.get(id=link.land_id) link_info['linked_object'] = { 'id': str(land.id), 'type': 'Land', 'name': f"{land.gemarkung} - {land.gemeinde}", 'details': f"Flur: {land.flur}, Größe: {land.groesse_qm or 0} 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 = Verpachtung.objects.select_related('paechter').get(id=old_verpachtung_id) if old_verpachtung.paechter: # Prüfe ob noch andere Verpachtungs-Links für diesen Pächter existieren other_verpachtung_links = DokumentLink.objects.filter( paperless_document_id=paperless_id_for_cleanup, verpachtung__paechter_id=old_verpachtung.paechter.id ).exists() if not other_verpachtung_links: # Entferne automatisch erstellte Pächter-Verknüpfung DokumentLink.objects.filter( paperless_document_id=paperless_id_for_cleanup, paechter_id=old_verpachtung.paechter.id, kontext='paechter' ).delete() except (Verpachtung.DoesNotExist, Exception): pass message = 'Verknüpfung aktualisiert' if paechter_linked: message += ' (automatisch auch mit Pächter verknüpft)' return Response({'success': True, 'message': message}) except DokumentLink.DoesNotExist: return Response({'error': 'Verknüpfung nicht gefunden'}, status=404) except Exception as e: return Response({'error': f'Unerwarteter Fehler: {e}'}, status=500) @csrf_exempt @api_view(['DELETE']) def link_document_delete(request, link_id): """Löscht eine bestehende Verknüpfung.""" from django.db import transaction try: with transaction.atomic(): link = DokumentLink.objects.get(id=link_id) verpachtung_id_for_cleanup = link.verpachtung_id paperless_id_for_cleanup = link.paperless_document_id # Log the unlinking action before deletion from stiftung.audit import log_unlink try: # Determine what entity this was linked to target_type = "unknown" target_name = "Unknown" if link.destinataer_id: target_type = "destinataer" try: entity = Destinataer.objects.get(id=link.destinataer_id) target_name = entity.get_full_name() except Destinataer.DoesNotExist: target_name = f"Destinatär ID {link.destinataer_id}" elif link.land_id: target_type = "land" try: entity = Land.objects.get(id=link.land_id) target_name = str(entity) except Land.DoesNotExist: target_name = f"Land ID {link.land_id}" elif link.paechter_id: target_type = "paechter" try: entity = Paechter.objects.get(id=link.paechter_id) target_name = f"{entity.vorname} {entity.nachname}".strip() except Paechter.DoesNotExist: target_name = f"Pächter ID {link.paechter_id}" elif link.verpachtung_id: target_type = "verpachtung" try: entity = Verpachtung.objects.get(id=link.verpachtung_id) target_name = str(entity) except Verpachtung.DoesNotExist: target_name = f"Verpachtung ID {link.verpachtung_id}" elif link.rentmeister_id: target_type = "rentmeister" try: from stiftung.models import Rentmeister entity = Rentmeister.objects.get(id=link.rentmeister_id) target_name = entity.get_full_name() except Rentmeister.DoesNotExist: target_name = f"Rentmeister ID {link.rentmeister_id}" log_unlink( request=request, entity_type='dokumentlink', entity_id=str(link.id), entity_name=link.titel, target_type=target_type, target_name=target_name ) except Exception as e: # Don't fail the main operation if logging fails print(f"Audit logging failed: {e}") link.delete() # Cleanup NACH der Haupttransaktion: Wenn eine Verpachtungs-Verknüpfung gelöscht wurde, prüfe Pächter-Links if verpachtung_id_for_cleanup: try: verpachtung = Verpachtung.objects.select_related('paechter').get(id=verpachtung_id_for_cleanup) if verpachtung.paechter: # Prüfe ob noch andere Verpachtungs-Links für diesen Pächter existieren other_verpachtung_links = DokumentLink.objects.filter( paperless_document_id=paperless_id_for_cleanup, verpachtung__paechter_id=verpachtung.paechter.id ).exists() if not other_verpachtung_links: # Entferne automatisch erstellte Pächter-Verknüpfung DokumentLink.objects.filter( paperless_document_id=paperless_id_for_cleanup, paechter_id=verpachtung.paechter.id, kontext='paechter' ).delete() except (Verpachtung.DoesNotExist, Exception): pass return Response({'success': True}) except DokumentLink.DoesNotExist: return Response({'error': 'Verknüpfung nicht gefunden'}, status=404) except Exception as e: return Response({'error': f'Unerwarteter Fehler: {e}'}, status=500) @api_view(['GET']) def gramps_search_api(request): """Probe-Endpoint: Suche Personen in Gramps Web nach q (Nachname, Vorname).""" q = request.GET.get('q', '') if not q: return Response({'error': 'Parameter q erforderlich'}, status=400) client = get_gramps_client() result = client.search_people(q) return Response(result) # Geschäftsführung Views @login_required def geschaeftsfuehrung(request): """Hauptansicht für die Geschäftsführung mit Übersicht""" from stiftung.models import StiftungsKonto, Verwaltungskosten, Rentmeister from django.db.models import Sum, Count from datetime import datetime, timedelta # Rentmeister-Übersicht rentmeister = Rentmeister.objects.filter(aktiv=True).order_by('nachname', 'vorname') # Konten-Übersicht konten = StiftungsKonto.objects.filter(aktiv=True).order_by('bank_name', 'kontoname') gesamtsaldo = konten.aggregate(total=Sum('saldo'))['total'] or 0 # Aktuelle Kosten (letzten 30 Tage) heute = datetime.now().date() vor_30_tagen = heute - timedelta(days=30) aktuelle_kosten = Verwaltungskosten.objects.filter( datum__gte=vor_30_tagen ).order_by('-datum')[:10] # Statistiken kosten_summe_monat = Verwaltungskosten.objects.filter( datum__gte=vor_30_tagen ).aggregate(total=Sum('betrag'))['total'] or 0 kosten_statistik = Verwaltungskosten.objects.filter( datum__gte=vor_30_tagen ).values('kategorie').annotate( summe=Sum('betrag'), anzahl=Count('id') ).order_by('-summe') context = { 'rentmeister': rentmeister, 'konten': konten, 'gesamtsaldo': gesamtsaldo, 'aktuelle_kosten': aktuelle_kosten, 'kosten_summe_monat': kosten_summe_monat, 'kosten_statistik': kosten_statistik, } return render(request, 'stiftung/geschaeftsfuehrung.html', context) @login_required def konto_list(request): """Liste aller Stiftungskonten""" from stiftung.models import StiftungsKonto from django.db.models import Sum konten = StiftungsKonto.objects.all().order_by('bank_name', 'kontoname') gesamtsaldo = konten.aggregate(total=Sum('saldo'))['total'] or 0 context = { 'konten': konten, 'gesamtsaldo': gesamtsaldo, } return render(request, 'stiftung/konto_list.html', context) @login_required def verwaltungskosten_list(request): """Liste aller Verwaltungskosten""" from stiftung.models import Verwaltungskosten from django.core.paginator import Paginator kosten = Verwaltungskosten.objects.all().order_by('-datum', '-erstellt_am') # Filter nach Kategorie kategorie_filter = request.GET.get('kategorie') if kategorie_filter: kosten = kosten.filter(kategorie=kategorie_filter) # Filter nach Status status_filter = request.GET.get('status') if status_filter: kosten = kosten.filter(status=status_filter) # Pagination paginator = Paginator(kosten, 25) page_number = request.GET.get('page') page_obj = paginator.get_page(page_number) # Für Filter-Dropdowns kategorien = Verwaltungskosten.KATEGORIE_CHOICES status_choices = Verwaltungskosten.STATUS_CHOICES context = { 'page_obj': page_obj, 'kategorien': kategorien, 'status_choices': status_choices, 'kategorie_filter': kategorie_filter, 'status_filter': status_filter, } return render(request, 'stiftung/verwaltungskosten_list.html', context) @login_required def rentmeister_list(request): """Liste aller Rentmeister""" from stiftung.models import Rentmeister rentmeister = Rentmeister.objects.all().order_by('nachname', 'vorname') # Aktive/Inaktive aufteilen aktive_rentmeister = rentmeister.filter(aktiv=True) ehemalige_rentmeister = rentmeister.filter(aktiv=False) context = { 'aktive_rentmeister': aktive_rentmeister, 'ehemalige_rentmeister': ehemalige_rentmeister, 'total_count': rentmeister.count(), } return render(request, 'stiftung/rentmeister_list.html', context) @login_required def rentmeister_detail(request, pk): """Detailansicht eines Rentmeisters mit seinen Ausgaben""" from stiftung.models import Rentmeister, Verwaltungskosten from django.db.models import Sum, Count, Q from datetime import datetime, timedelta rentmeister = get_object_or_404(Rentmeister, pk=pk) # Ausgaben des Rentmeisters ausgaben = Verwaltungskosten.objects.filter(rentmeister=rentmeister).order_by('-datum') # Statistiken heute = datetime.now().date() aktueller_monat = heute.replace(day=1) aktuelles_jahr = heute.replace(month=1, day=1) stats = { 'gesamt_ausgaben': ausgaben.aggregate(total=Sum('betrag'))['total'] or 0, 'monat_ausgaben': ausgaben.filter(datum__gte=aktueller_monat).aggregate(total=Sum('betrag'))['total'] or 0, 'jahr_ausgaben': ausgaben.filter(datum__gte=aktuelles_jahr).aggregate(total=Sum('betrag'))['total'] or 0, 'anzahl_ausgaben': ausgaben.count(), 'offene_ausgaben': ausgaben.exclude(status='bezahlt').count(), } # Kategorie-Aufschlüsselung kategorie_stats = ausgaben.values('kategorie').annotate( summe=Sum('betrag'), anzahl=Count('id') ).order_by('-summe') # Aktuelle Ausgaben (letzten 30 Tage) vor_30_tagen = heute - timedelta(days=30) aktuelle_ausgaben = ausgaben.filter(datum__gte=vor_30_tagen)[:10] # Verknüpfte Dokumente laden from stiftung.models import DokumentLink verknuepfte_dokumente = DokumentLink.objects.filter( rentmeister_id=rentmeister.id ).order_by('-id')[:10] # Neueste 10 Dokumente context = { 'rentmeister': rentmeister, 'ausgaben': ausgaben[:20], # Nur erste 20 für Übersicht 'stats': stats, 'kategorie_stats': kategorie_stats, 'aktuelle_ausgaben': aktuelle_ausgaben, 'verknuepfte_dokumente': verknuepfte_dokumente, } return render(request, 'stiftung/rentmeister_detail.html', context) @login_required def rentmeister_ausgaben(request, pk): """Vollständige Ausgabenliste eines Rentmeisters mit PDF Export""" from stiftung.models import Rentmeister, Verwaltungskosten from django.core.paginator import Paginator from django.db.models import Sum, Count, Q from django.db import models rentmeister = get_object_or_404(Rentmeister, pk=pk) # Handle PDF export request if request.method == 'POST' and 'export_pdf' in request.POST: selected_ids = request.POST.getlist('selected_expenses') if selected_ids: # Update status to 'in_bearbeitung' and log each change from stiftung.audit import log_action expenses_to_update = Verwaltungskosten.objects.filter( id__in=selected_ids, rentmeister=rentmeister ) updated_count = 0 for expense in expenses_to_update: old_status = expense.status expense.status = 'in_bearbeitung' expense.save() updated_count += 1 # Log the status change log_action( request=request, action='update', entity_type='verwaltungskosten', entity_id=str(expense.pk), entity_name=expense.bezeichnung, description=f'Ausgaben-Status für PDF-Export geändert von "{old_status}" zu "in_bearbeitung"', changes={'status': {'old': old_status, 'new': 'in_bearbeitung'}} ) messages.success(request, f'{updated_count} Ausgaben wurden zur Bearbeitung markiert und sind bereit für PDF Export.') return redirect('stiftung:rentmeister_ausgaben_pdf', pk=pk, expense_ids=','.join(selected_ids)) # Get expenses grouped by status ausgaben_by_status = {} for status_code, status_name in Verwaltungskosten.STATUS_CHOICES: ausgaben_by_status[status_code] = { 'name': status_name, 'ausgaben': Verwaltungskosten.objects.filter( rentmeister=rentmeister, status=status_code ).order_by('-datum', '-erstellt_am'), 'total': Verwaltungskosten.objects.filter( rentmeister=rentmeister, status=status_code ).aggregate(total=Sum('betrag'))['total'] or 0 } # Get statistics stats = Verwaltungskosten.objects.filter(rentmeister=rentmeister).aggregate( total_count=Count('id'), total_amount=Sum('betrag'), geplant_count=Count('id', filter=Q(status='geplant')), geplant_amount=Sum('betrag', filter=Q(status='geplant')), in_bearbeitung_count=Count('id', filter=Q(status='in_bearbeitung')), in_bearbeitung_amount=Sum('betrag', filter=Q(status='in_bearbeitung')), bezahlt_count=Count('id', filter=Q(status='bezahlt')), bezahlt_amount=Sum('betrag', filter=Q(status='bezahlt')), ) context = { 'rentmeister': rentmeister, 'ausgaben_by_status': ausgaben_by_status, 'stats': stats, 'kategorien': Verwaltungskosten.KATEGORIE_CHOICES, 'status_choices': Verwaltungskosten.STATUS_CHOICES, } return render(request, 'stiftung/rentmeister_ausgaben.html', context) @login_required def rentmeister_create(request): """Erstelle einen neuen Rentmeister""" from stiftung.forms import RentmeisterForm if request.method == 'POST': form = RentmeisterForm(request.POST) if form.is_valid(): rentmeister = form.save() messages.success(request, f'Rentmeister {rentmeister.get_full_name()} wurde erfolgreich angelegt.') return redirect('stiftung:rentmeister_detail', pk=rentmeister.pk) else: form = RentmeisterForm() context = { 'form': form, 'title': 'Neuen Rentmeister anlegen', 'submit_text': 'Rentmeister anlegen', } return render(request, 'stiftung/rentmeister_form.html', context) @login_required def rentmeister_edit(request, pk): """Bearbeite einen bestehenden Rentmeister""" from stiftung.forms import RentmeisterForm from stiftung.models import Rentmeister rentmeister = get_object_or_404(Rentmeister, pk=pk) if request.method == 'POST': form = RentmeisterForm(request.POST, instance=rentmeister) if form.is_valid(): rentmeister = form.save() messages.success(request, f'Rentmeister {rentmeister.get_full_name()} wurde erfolgreich aktualisiert.') return redirect('stiftung:rentmeister_detail', pk=rentmeister.pk) else: form = RentmeisterForm(instance=rentmeister) context = { 'form': form, 'rentmeister': rentmeister, 'title': f'{rentmeister.get_full_name()} bearbeiten', 'submit_text': 'Änderungen speichern', } return render(request, 'stiftung/rentmeister_form.html', context) @login_required def konto_create(request): """Erstelle ein neues Stiftungskonto""" from stiftung.forms import StiftungsKontoForm if request.method == 'POST': form = StiftungsKontoForm(request.POST) if form.is_valid(): konto = form.save() messages.success(request, f'Konto {konto.kontoname} wurde erfolgreich angelegt.') return redirect('stiftung:konto_list') else: form = StiftungsKontoForm() context = { 'form': form, 'title': 'Neues Konto anlegen', 'submit_text': 'Konto anlegen', } return render(request, 'stiftung/konto_form.html', context) @login_required def konto_edit(request, pk): """Bearbeite ein bestehendes Stiftungskonto""" from stiftung.forms import StiftungsKontoForm from stiftung.models import StiftungsKonto konto = get_object_or_404(StiftungsKonto, pk=pk) if request.method == 'POST': form = StiftungsKontoForm(request.POST, instance=konto) if form.is_valid(): konto = form.save() messages.success(request, f'Konto {konto.kontoname} wurde erfolgreich aktualisiert.') return redirect('stiftung:konto_list') else: form = StiftungsKontoForm(instance=konto) context = { 'form': form, 'konto': konto, 'title': f'Konto {konto.kontoname} bearbeiten', 'submit_text': 'Änderungen speichern', } return render(request, 'stiftung/konto_form.html', context) @login_required def konto_detail(request, pk): """Zeige Details eines Stiftungskontos""" from stiftung.models import StiftungsKonto, BankTransaction from django.db.models import Sum, Count, Max, Q from django.db import models konto = get_object_or_404(StiftungsKonto, pk=pk) # Get transaction statistics transactions = BankTransaction.objects.filter(konto=konto) transaction_stats = transactions.aggregate( total_count=Count('id'), total_eingang=Sum('betrag', filter=Q(betrag__gt=0)), total_ausgang=Sum('betrag', filter=Q(betrag__lt=0)), last_transaction_date=Max('datum') ) # Recent transactions recent_transactions = transactions.order_by('-datum', '-importiert_am')[:10] context = { 'konto': konto, 'transaction_stats': transaction_stats, 'recent_transactions': recent_transactions, } return render(request, 'stiftung/konto_detail.html', context) @login_required def verwaltungskosten_create(request): """Erstelle neue Verwaltungskosten""" from stiftung.forms import VerwaltungskostenForm from stiftung.models import Rentmeister # Check if we're coming from a specific Rentmeister rentmeister_id = request.GET.get('rentmeister') initial_data = {} redirect_url = 'stiftung:verwaltungskosten_list' if rentmeister_id: try: rentmeister = Rentmeister.objects.get(pk=rentmeister_id) initial_data['rentmeister'] = rentmeister redirect_url = 'stiftung:rentmeister_detail' redirect_args = [rentmeister_id] except Rentmeister.DoesNotExist: pass if request.method == 'POST': form = VerwaltungskostenForm(request.POST) if form.is_valid(): kosten = form.save() messages.success(request, f'Verwaltungskosten "{kosten.bezeichnung}" wurden erfolgreich angelegt.') if rentmeister_id: return redirect(redirect_url, pk=rentmeister_id) return redirect('stiftung:verwaltungskosten_list') else: form = VerwaltungskostenForm(initial=initial_data) context = { 'form': form, 'title': 'Neue Verwaltungskosten anlegen', 'submit_text': 'Kosten anlegen', } return render(request, 'stiftung/verwaltungskosten_form.html', context) @login_required def verwaltungskosten_edit(request, pk): """Bearbeite bestehende Verwaltungskosten""" from stiftung.forms import VerwaltungskostenForm from stiftung.models import Verwaltungskosten verwaltungskosten = get_object_or_404(Verwaltungskosten, pk=pk) if request.method == 'POST': form = VerwaltungskostenForm(request.POST, instance=verwaltungskosten) if form.is_valid(): verwaltungskosten = form.save() messages.success(request, f'Verwaltungskosten "{verwaltungskosten.bezeichnung}" wurden erfolgreich aktualisiert.') return redirect('stiftung:verwaltungskosten_list') else: form = VerwaltungskostenForm(instance=verwaltungskosten) context = { 'form': form, 'verwaltungskosten': verwaltungskosten, 'title': f'Verwaltungskosten bearbeiten: {verwaltungskosten.bezeichnung}', 'submit_text': 'Änderungen speichern', } return render(request, 'stiftung/verwaltungskosten_form.html', context) @login_required def mark_expense_paid(request): """Markiere eine Ausgabe als bezahlt""" if request.method == 'POST': expense_id = request.POST.get('expense_id') if expense_id: try: from stiftung.models import Verwaltungskosten expense = Verwaltungskosten.objects.get(pk=expense_id) old_status = expense.status expense.status = 'bezahlt' expense.save() # Log the status change from stiftung.audit import log_action log_action( request=request, action='update', entity_type='verwaltungskosten', entity_id=str(expense.pk), entity_name=expense.bezeichnung, description=f'Ausgaben-Status geändert von "{old_status}" zu "bezahlt"', changes={'status': {'old': old_status, 'new': 'bezahlt'}} ) messages.success(request, f'Ausgabe "{expense.bezeichnung}" wurde als bezahlt markiert.') return redirect('stiftung:rentmeister_ausgaben', pk=expense.rentmeister.pk) except Verwaltungskosten.DoesNotExist: messages.error(request, 'Ausgabe nicht gefunden.') return redirect('stiftung:verwaltungskosten_list') # ============================================================================= # ADMINISTRATION VIEWS # ============================================================================= @login_required def administration(request): """Administration Dashboard""" from stiftung.models import AuditLog, BackupJob from django.db.models import Count from datetime import datetime, timedelta # Recent audit activity recent_audit = AuditLog.objects.all()[:10] # Audit statistics heute = datetime.now().date() stats = { 'total_logs': AuditLog.objects.count(), 'logs_today': AuditLog.objects.filter(timestamp__date=heute).count(), 'logs_week': AuditLog.objects.filter(timestamp__gte=heute - timedelta(days=7)).count(), 'recent_backups': BackupJob.objects.all()[:5], 'last_backup': BackupJob.objects.filter(status='completed').first(), } # User activity summary user_activity = AuditLog.objects.values('username').annotate( count=Count('id') ).order_by('-count')[:10] context = { 'recent_audit': recent_audit, 'stats': stats, 'user_activity': user_activity, } return render(request, 'stiftung/administration.html', context) @login_required def unterstuetzungen_list(request): """Liste der Destinatärunterstützungen (Administration).""" status = request.GET.get('status', '') export_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(Verpachtung, pk=pk) # Create a temporary file for the ZIP temp_file = tempfile.NamedTemporaryFile(delete=False, suffix='.zip') try: with zipfile.ZipFile(temp_file.name, 'w', zipfile.ZIP_DEFLATED) as zipf: # 1. Entity data as JSON entity_data = { 'id': str(verpachtung.id), 'vertragsnummer': verpachtung.vertragsnummer, 'land': str(verpachtung.land), 'land_id': str(verpachtung.land.id), 'paechter': str(verpachtung.paechter), 'paechter_id': str(verpachtung.paechter.id), 'pachtbeginn': verpachtung.pachtbeginn.isoformat(), 'pachtende': verpachtung.pachtende.isoformat(), 'verlaengerung': verpachtung.verlaengerung.isoformat() if verpachtung.verlaengerung else None, 'pachtzins_pro_qm': str(verpachtung.pachtzins_pro_qm), 'pachtzins_jaehrlich': str(verpachtung.pachtzins_jaehrlich), 'verpachtete_flaeche': str(verpachtung.verpachtete_flaeche), 'status': verpachtung.get_status_display(), 'verwendungsnachweis': str(verpachtung.verwendungsnachweis) if verpachtung.verwendungsnachweis else None, 'bemerkungen': verpachtung.bemerkungen, 'erstellt_am': verpachtung.erstellt_am.isoformat(), 'aktualisiert_am': verpachtung.aktualisiert_am.isoformat(), 'vertragsdauer_tage': verpachtung.get_vertragsdauer_tage(), 'restlaufzeit_tage': verpachtung.get_restlaufzeit_tage(), 'ist_aktiv': verpachtung.is_aktiv(), 'export_datum': timezone.now().isoformat(), 'export_user': request.user.username, } zipf.writestr('verpachtung_data.json', json.dumps(entity_data, indent=2, ensure_ascii=False)) # 2. Linked documents from Paperless dokumente = DokumentLink.objects.filter(verpachtung_id=verpachtung.pk) docs_data = [] for doc in dokumente: doc_data = { 'paperless_id': doc.paperless_document_id, 'titel': doc.titel, 'kontext': doc.get_kontext_display(), 'beschreibung': doc.beschreibung, } docs_data.append(doc_data) # Try to download document from Paperless try: if hasattr(settings, 'PAPERLESS_API_URL') and settings.PAPERLESS_API_URL: doc_url = f"{settings.PAPERLESS_API_URL}/api/documents/{doc.paperless_document_id}/download/" headers = {} if hasattr(settings, 'PAPERLESS_API_TOKEN') and settings.PAPERLESS_API_TOKEN: headers['Authorization'] = f'Token {settings.PAPERLESS_API_TOKEN}' response = requests.get(doc_url, headers=headers, timeout=30) if response.status_code == 200: content_type = response.headers.get('content-type', '') if 'pdf' in content_type: ext = '.pdf' elif 'jpeg' in content_type or 'jpg' in content_type: ext = '.jpg' elif 'png' in content_type: ext = '.png' else: ext = '.pdf' safe_filename = f"dokument_{doc.paperless_document_id}_{doc.titel.replace('/', '_')[:50]}{ext}" zipf.writestr(f'dokumente/{safe_filename}', response.content) doc_data['downloaded'] = True else: doc_data['download_error'] = f'HTTP {response.status_code}' except Exception as e: doc_data['download_error'] = str(e) if docs_data: zipf.writestr('dokumente.json', json.dumps(docs_data, indent=2, ensure_ascii=False)) # Prepare response with open(temp_file.name, 'rb') as f: response = HttpResponse(f.read(), content_type='application/zip') filename = f"verpachtung_{verpachtung.vertragsnummer}_{timezone.now().strftime('%Y%m%d_%H%M%S')}.zip" response['Content-Disposition'] = f'attachment; filename="{filename}"' return response finally: try: os.unlink(temp_file.name) except: pass @login_required def audit_log_list(request): """Liste aller Audit Log Einträge""" from stiftung.models import AuditLog from django.core.paginator import Paginator logs = AuditLog.objects.all() # Filter user_filter = request.GET.get('user') if user_filter: logs = logs.filter(username__icontains=user_filter) action_filter = request.GET.get('action') if action_filter: logs = logs.filter(action=action_filter) entity_filter = request.GET.get('entity_type') if entity_filter: logs = logs.filter(entity_type=entity_filter) date_from = request.GET.get('date_from') if date_from: logs = logs.filter(timestamp__date__gte=date_from) date_to = request.GET.get('date_to') if date_to: logs = logs.filter(timestamp__date__lte=date_to) # Pagination paginator = Paginator(logs, 50) page_number = request.GET.get('page') page_obj = paginator.get_page(page_number) context = { 'page_obj': page_obj, 'action_choices': AuditLog.ACTION_TYPES, 'entity_choices': AuditLog.ENTITY_TYPES, 'user_filter': user_filter, 'action_filter': action_filter, 'entity_filter': entity_filter, 'date_from': date_from, 'date_to': date_to, } return render(request, 'stiftung/audit_log_list.html', context) @login_required def backup_management(request): """Backup Management Interface""" from stiftung.models import BackupJob from django.core.paginator import Paginator # Handle backup creation if request.method == 'POST': backup_type = request.POST.get('backup_type', 'full') # Create backup job backup_job = BackupJob.objects.create( backup_type=backup_type, created_by=request.user ) # Log the backup initiation from stiftung.audit import log_system_action log_system_action( request=request, action='backup', description=f"Backup-Job erstellt: {backup_job.get_backup_type_display()}", details={'backup_job_id': str(backup_job.id), 'backup_type': backup_type} ) # Start backup process asynchronously (we'll create a simple version for now) import threading from stiftung.backup_utils import run_backup backup_thread = threading.Thread(target=run_backup, args=(str(backup_job.id),)) backup_thread.start() messages.success(request, f'Backup-Job "{backup_job.get_backup_type_display()}" wurde gestartet.') return redirect('stiftung:backup_management') # List backup jobs backup_jobs = BackupJob.objects.all() # Pagination paginator = Paginator(backup_jobs, 20) page_number = request.GET.get('page') page_obj = paginator.get_page(page_number) context = { 'page_obj': page_obj, 'backup_types': BackupJob.TYPE_CHOICES, } return render(request, 'stiftung/backup_management.html', context) @login_required def backup_download(request, backup_id): """Download a backup file""" from stiftung.models import BackupJob from django.http import FileResponse, Http404 import os try: backup_job = BackupJob.objects.get(id=backup_id, status='completed') except BackupJob.DoesNotExist: raise Http404("Backup nicht gefunden oder nicht vollständig") backup_path = os.path.join('/app/backups', backup_job.backup_filename) if not os.path.exists(backup_path): raise Http404("Backup-Datei nicht gefunden") # Log download from stiftung.audit import log_system_action log_system_action( request=request, action='export', description=f"Backup heruntergeladen: {backup_job.backup_filename}", details={'backup_job_id': str(backup_job.id)} ) response = FileResponse( open(backup_path, 'rb'), as_attachment=True, filename=backup_job.backup_filename ) return response @login_required def backup_restore(request): """Restore from backup""" if request.method == 'POST': from stiftung.models import BackupJob backup_file = request.FILES.get('backup_file') if not backup_file: messages.error(request, 'Bitte wählen Sie eine Backup-Datei aus.') return redirect('stiftung:backup_management') # Validate file if not backup_file.name.endswith('.tar.gz'): messages.error(request, 'Ungültiges Backup-Format. Nur .tar.gz Dateien sind erlaubt.') return redirect('stiftung:backup_management') # Save uploaded file import os import tempfile temp_dir = tempfile.mkdtemp() backup_path = os.path.join(temp_dir, backup_file.name) with open(backup_path, 'wb+') as destination: for chunk in backup_file.chunks(): destination.write(chunk) # Create restore job restore_job = BackupJob.objects.create( backup_type='full', created_by=request.user, backup_filename=backup_file.name ) # Log restore initiation from stiftung.audit import log_system_action log_system_action( request=request, action='restore', description=f"Wiederherstellung gestartet von: {backup_file.name}", details={'restore_job_id': str(restore_job.id), 'filename': backup_file.name} ) # Start restore process import threading from stiftung.backup_utils import run_restore restore_thread = threading.Thread(target=run_restore, args=(str(restore_job.id), backup_path)) restore_thread.start() messages.success(request, f'Wiederherstellung von "{backup_file.name}" wurde gestartet.') return redirect('stiftung:backup_management') return redirect('stiftung:backup_management') # ============================================================================= # USER MANAGEMENT VIEWS # ============================================================================= @login_required def user_management(request): """User Management Dashboard""" from django.contrib.auth.models import User from django.core.paginator import Paginator from django.db.models import Q # Check permission if not request.user.has_perm('stiftung.manage_users'): messages.error(request, 'Sie haben keine Berechtigung für die Benutzerverwaltung.') return redirect('stiftung:administration') users = User.objects.all().order_by('username') # Search functionality search = request.GET.get('search') if search: users = users.filter( Q(username__icontains=search) | Q(email__icontains=search) | Q(first_name__icontains=search) | Q(last_name__icontains=search) ) # Filter by status status_filter = request.GET.get('status') if status_filter == 'active': users = users.filter(is_active=True) elif status_filter == 'inactive': users = users.filter(is_active=False) elif status_filter == 'staff': users = users.filter(is_staff=True) # Pagination paginator = Paginator(users, 20) page_number = request.GET.get('page') page_obj = paginator.get_page(page_number) # Statistics stats = { 'total_users': User.objects.count(), 'active_users': User.objects.filter(is_active=True).count(), 'staff_users': User.objects.filter(is_staff=True).count(), 'inactive_users': User.objects.filter(is_active=False).count(), } context = { 'page_obj': page_obj, 'stats': stats, 'search': search, 'status_filter': status_filter, } return render(request, 'stiftung/user_management.html', context) @login_required def user_create(request): """Create a new user""" from stiftung.forms import UserCreationForm from django.contrib.auth.models import User # Check permission if not request.user.has_perm('stiftung.manage_users'): messages.error(request, 'Sie haben keine Berechtigung für die Benutzerverwaltung.') return redirect('stiftung:administration') if request.method == 'POST': form = UserCreationForm(request.POST) if form.is_valid(): # Create user user = User.objects.create_user( username=form.cleaned_data['username'], email=form.cleaned_data['email'], password=form.cleaned_data['password1'], first_name=form.cleaned_data['first_name'], last_name=form.cleaned_data['last_name'], is_active=form.cleaned_data['is_active'], is_staff=form.cleaned_data['is_staff'] ) # Log user creation from stiftung.audit import log_action log_action( request=request, action='create', entity_type='user', entity_id=str(user.pk), entity_name=user.username, description=f'Neuer Benutzer "{user.username}" wurde erstellt' ) messages.success(request, f'Benutzer "{user.username}" wurde erfolgreich erstellt.') return redirect('stiftung:user_detail', pk=user.pk) else: form = UserCreationForm() context = { 'form': form, 'title': 'Neuen Benutzer erstellen' } return render(request, 'stiftung/user_form.html', context) @login_required def user_detail(request, pk): """User detail view""" from django.contrib.auth.models import User # Check permission if not request.user.has_perm('stiftung.manage_users'): messages.error(request, 'Sie haben keine Berechtigung für die Benutzerverwaltung.') return redirect('stiftung:administration') user = get_object_or_404(User, pk=pk) # Get user's permissions user_permissions = user.get_all_permissions() stiftung_permissions = [perm for perm in user_permissions if perm.startswith('stiftung.')] # Get recent audit activity from stiftung.models import AuditLog recent_activity = AuditLog.objects.filter(user=user).order_by('-timestamp')[:10] context = { 'user_obj': user, # Use user_obj to avoid conflict with request.user 'stiftung_permissions': stiftung_permissions, 'recent_activity': recent_activity, } return render(request, 'stiftung/user_detail.html', context) @login_required def user_edit(request, pk): """Edit user""" from stiftung.forms import UserUpdateForm from django.contrib.auth.models import User # Check permission if not request.user.has_perm('stiftung.manage_users'): messages.error(request, 'Sie haben keine Berechtigung für die Benutzerverwaltung.') return redirect('stiftung:administration') user = get_object_or_404(User, pk=pk) if request.method == 'POST': form = UserUpdateForm(request.POST, instance=user) if form.is_valid(): # Track changes from stiftung.audit import track_model_changes, log_action old_user = User.objects.get(pk=user.pk) updated_user = form.save() # Log changes changes = track_model_changes(old_user, updated_user) if changes: log_action( request=request, action='update', entity_type='user', entity_id=str(updated_user.pk), entity_name=updated_user.username, description=f'Benutzer "{updated_user.username}" wurde aktualisiert', changes=changes ) messages.success(request, f'Benutzer "{updated_user.username}" wurde erfolgreich aktualisiert.') return redirect('stiftung:user_detail', pk=updated_user.pk) else: form = UserUpdateForm(instance=user) context = { 'form': form, 'user_obj': user, 'title': f'Benutzer "{user.username}" bearbeiten' } return render(request, 'stiftung/user_form.html', context) @login_required def user_change_password(request, pk): """Change user password""" from stiftung.forms import PasswordChangeForm from django.contrib.auth.models import User # Check permission if not request.user.has_perm('stiftung.manage_users'): messages.error(request, 'Sie haben keine Berechtigung für die Benutzerverwaltung.') return redirect('stiftung:administration') user = get_object_or_404(User, pk=pk) if request.method == 'POST': form = PasswordChangeForm(request.POST) if form.is_valid(): user.set_password(form.cleaned_data['new_password1']) user.save() # Log password change from stiftung.audit import log_action log_action( request=request, action='update', entity_type='user', entity_id=str(user.pk), entity_name=user.username, description=f'Passwort für Benutzer "{user.username}" wurde geändert' ) messages.success(request, f'Passwort für Benutzer "{user.username}" wurde erfolgreich geändert.') return redirect('stiftung:user_detail', pk=user.pk) else: form = PasswordChangeForm() context = { 'form': form, 'user_obj': user, 'title': f'Passwort für "{user.username}" ändern' } return render(request, 'stiftung/user_change_password.html', context) @login_required def user_permissions(request, pk): """Manage user permissions""" from stiftung.forms import UserPermissionForm from django.contrib.auth.models import User, Permission # Check permission if not request.user.has_perm('stiftung.manage_permissions'): messages.error(request, 'Sie haben keine Berechtigung für die Berechtigungsverwaltung.') return redirect('stiftung:administration') user = get_object_or_404(User, pk=pk) if request.method == 'POST': form = UserPermissionForm(request.POST, user=user) if form.is_valid(): # Get selected permissions selected_perms = [] for field_name, value in form.cleaned_data.items(): if field_name.startswith('perm_') and value: perm_id = field_name.replace('perm_', '') selected_perms.append(int(perm_id)) # Get current stiftung permissions current_perms = user.user_permissions.filter(content_type__app_label='stiftung') current_perm_ids = set(current_perms.values_list('id', flat=True)) selected_perm_ids = set(selected_perms) # Remove permissions that are no longer selected to_remove = current_perm_ids - selected_perm_ids if to_remove: user.user_permissions.remove(*Permission.objects.filter(id__in=to_remove)) # Add new permissions to_add = selected_perm_ids - current_perm_ids if to_add: user.user_permissions.add(*Permission.objects.filter(id__in=to_add)) # Log permission changes from stiftung.audit import log_action if to_remove or to_add: changes = { 'removed_permissions': list(Permission.objects.filter(id__in=to_remove).values_list('name', flat=True)), 'added_permissions': list(Permission.objects.filter(id__in=to_add).values_list('name', flat=True)) } log_action( request=request, action='update', entity_type='user', entity_id=str(user.pk), entity_name=user.username, description=f'Berechtigungen für Benutzer "{user.username}" wurden aktualisiert', changes=changes ) messages.success(request, f'Berechtigungen für Benutzer "{user.username}" wurden erfolgreich aktualisiert.') return redirect('stiftung:user_detail', pk=user.pk) else: form = UserPermissionForm(user=user) context = { 'form': form, 'user_obj': user, 'permission_groups': form.get_permission_groups(), 'title': f'Berechtigungen für "{user.username}"' } return render(request, 'stiftung/user_permissions.html', context) @login_required def user_delete(request, pk): """Delete user""" from django.contrib.auth.models import User # Check permission if not request.user.has_perm('stiftung.manage_users'): messages.error(request, 'Sie haben keine Berechtigung für die Benutzerverwaltung.') return redirect('stiftung:administration') user = get_object_or_404(User, pk=pk) # Prevent deletion of current user if user == request.user: messages.error(request, 'Sie können sich nicht selbst löschen.') return redirect('stiftung:user_detail', pk=pk) if request.method == 'POST': username = user.username # Log deletion before deleting from stiftung.audit import log_action log_action( request=request, action='delete', entity_type='user', entity_id=str(user.pk), entity_name=username, description=f'Benutzer "{username}" wurde gelöscht' ) user.delete() messages.success(request, f'Benutzer "{username}" wurde erfolgreich gelöscht.') return redirect('stiftung:user_management') context = { 'user_obj': user, 'title': f'Benutzer "{user.username}" löschen' } return render(request, 'stiftung/user_delete.html', context) # ============================================================================= # AUTHENTICATION VIEWS # ============================================================================= def user_login(request): """User login view""" from django.contrib.auth import authenticate, login from django.contrib.auth.forms import AuthenticationForm if request.user.is_authenticated: return redirect('stiftung:dashboard') if request.method == 'POST': form = AuthenticationForm(request, data=request.POST) if form.is_valid(): username = form.cleaned_data.get('username') password = form.cleaned_data.get('password') user = authenticate(username=username, password=password) if user is not None: login(request, user) # Log the login from stiftung.audit import log_login log_login(request, user) messages.success(request, f'Willkommen zurück, {user.username}!') # Redirect to safe next URL path or dashboard next_param = request.GET.get('next') or request.POST.get('next') if next_param and next_param.startswith('/'): return redirect(next_param) return redirect('stiftung:dashboard') else: messages.error(request, 'Ungültige Anmeldedaten.') else: messages.error(request, 'Bitte korrigieren Sie die Fehler im Formular.') else: form = AuthenticationForm() context = { 'form': form, 'next': request.GET.get('next', '') } return render(request, 'stiftung/login.html', context) @login_required def user_logout(request): """User logout view""" from django.contrib.auth import logout # Log the logout before actually logging out from stiftung.audit import log_logout log_logout(request, request.user) username = request.user.username logout(request) messages.success(request, f'Sie wurden erfolgreich abgemeldet, {username}.') return redirect('stiftung:login') # ============================================================================ # LANDABRECHNUNGS VIEWS # ============================================================================ @login_required def land_abrechnung_list(request): """Liste aller Landabrechnungen""" abrechnungen = LandAbrechnung.objects.select_related('land').all() # Filter jahr_filter = request.GET.get('jahr') land_filter = request.GET.get('land') if jahr_filter: abrechnungen = abrechnungen.filter(abrechnungsjahr=jahr_filter) if land_filter: abrechnungen = abrechnungen.filter(land__pk=land_filter) # Pagination paginator = Paginator(abrechnungen, 20) page_number = request.GET.get('page') abrechnungen = paginator.get_page(page_number) # Statistiken stats = LandAbrechnung.objects.aggregate( total_einnahmen=Sum('pacht_vereinnahmt'), total_ausgaben=Sum('grundsteuer_betrag'), anzahl_abrechnungen=Count('id') ) context = { 'abrechnungen': abrechnungen, 'stats': stats, 'jahre': LandAbrechnung.objects.values_list('abrechnungsjahr', flat=True).distinct().order_by('-abrechnungsjahr'), 'laendereien': Land.objects.filter(aktiv=True).order_by('gemeinde', 'gemarkung'), 'jahr_filter': jahr_filter, 'land_filter': land_filter, } return render(request, 'stiftung/land_abrechnung_list.html', context) @login_required def land_abrechnung_detail(request, pk): """Detail-Ansicht einer Landabrechnung""" abrechnung = get_object_or_404(LandAbrechnung, pk=pk) context = { 'abrechnung': abrechnung, 'land': abrechnung.land, } return render(request, 'stiftung/land_abrechnung_detail.html', context) @login_required def land_abrechnung_create(request): """Neue Landabrechnung erstellen""" from .forms import LandAbrechnungForm land_pk = request.GET.get('land') initial = {} land = None if land_pk: land = get_object_or_404(Land, pk=land_pk) initial['land'] = land initial['abrechnungsjahr'] = datetime.now().year # Automatische Vorausfüllung aus Verpachtungsdaten if land.pachtzins_pauschal: initial['pacht_vereinnahmt'] = land.pachtzins_pauschal if request.method == 'POST': form = LandAbrechnungForm(request.POST, request.FILES) if form.is_valid(): abrechnung = form.save() messages.success(request, f'Landabrechnung für {abrechnung.land} ({abrechnung.abrechnungsjahr}) wurde erfolgreich erstellt.') return redirect('stiftung:land_abrechnung_detail', pk=abrechnung.pk) else: form = LandAbrechnungForm(initial=initial) context = { 'form': form, 'title': 'Neue Landabrechnung', 'land': land, } return render(request, 'stiftung/land_abrechnung_form.html', context) @login_required def land_abrechnung_update(request, pk): """Landabrechnung bearbeiten""" from .forms import LandAbrechnungForm abrechnung = get_object_or_404(LandAbrechnung, pk=pk) if request.method == 'POST': form = LandAbrechnungForm(request.POST, request.FILES, instance=abrechnung) if form.is_valid(): abrechnung = form.save() messages.success(request, f'Landabrechnung für {abrechnung.land} ({abrechnung.abrechnungsjahr}) wurde erfolgreich aktualisiert.') return redirect('stiftung:land_abrechnung_detail', pk=abrechnung.pk) else: form = LandAbrechnungForm(instance=abrechnung) context = { 'form': form, 'abrechnung': abrechnung, 'title': f'Abrechnung bearbeiten - {abrechnung.land} ({abrechnung.abrechnungsjahr})', } return render(request, 'stiftung/land_abrechnung_form.html', context) @login_required def land_abrechnung_delete(request, pk): """Landabrechnung löschen""" abrechnung = get_object_or_404(LandAbrechnung, pk=pk) land = abrechnung.land if request.method == 'POST': abrechnung.delete() messages.success(request, f'Landabrechnung für {land} ({abrechnung.abrechnungsjahr}) wurde erfolgreich gelöscht.') return redirect('stiftung:land_detail', pk=land.pk) context = { 'abrechnung': abrechnung, 'land': land, } return render(request, 'stiftung/land_abrechnung_confirm_delete.html', context) # ============================================================================ # VEREINHEITLICHTE VERPACHTUNGS VIEWS # ============================================================================ @login_required def land_verpachtung_create(request, land_pk): """Erstelle eine neue Verpachtung direkt im Land-Model""" from datetime import datetime as dt land = get_object_or_404(Land, pk=land_pk) if request.method == 'POST': # Einfaches Formular für die wichtigsten Verpachtungsfelder aktueller_paechter_id = request.POST.get('aktueller_paechter') pachtbeginn = request.POST.get('pachtbeginn') pachtende = request.POST.get('pachtende') pachtzins_pauschal = request.POST.get('pachtzins_pauschal') zahlungsweise = request.POST.get('zahlungsweise') ust_option = request.POST.get('ust_option') == 'on' if aktueller_paechter_id and pachtbeginn: paechter = get_object_or_404(Paechter, pk=aktueller_paechter_id) verpachtete_flaeche = request.POST.get('verpachtete_flaeche') # Validiere verpachtete Fläche if not verpachtete_flaeche: verpachtete_flaeche = land.groesse_qm # Standard: gesamte Fläche else: verpachtete_flaeche = float(verpachtete_flaeche) if verpachtete_flaeche > land.groesse_qm: messages.error(request, f'Die verpachtete Fläche ({verpachtete_flaeche} qm) kann nicht größer als die Gesamtfläche ({land.groesse_qm} qm) sein.') # Erstelle context für Fehlerfall paechter_list = Paechter.objects.filter(aktiv=True).order_by('nachname', 'vorname') verfuegbare_flaeche = land.groesse_qm if land.verp_flaeche_aktuell and land.verp_flaeche_aktuell > 0: verfuegbare_flaeche = land.groesse_qm - land.verp_flaeche_aktuell context = { 'land': land, 'paechter_list': paechter_list, 'current_year': dt.now().year, 'is_edit': False, 'verfuegbare_flaeche': verfuegbare_flaeche, } return render(request, 'stiftung/land_verpachtung_form.html', context) # Land aktualisieren land.aktueller_paechter = paechter land.paechter_name = paechter.get_full_name() land.paechter_anschrift = f"{paechter.strasse or ''}\n{paechter.plz or ''} {paechter.ort or ''}".strip() land.pachtbeginn = pachtbeginn land.pachtende = pachtende if pachtende else None land.pachtzins_pauschal = pachtzins_pauschal if pachtzins_pauschal else None land.zahlungsweise = zahlungsweise land.ust_option = ust_option land.verp_flaeche_aktuell = verpachtete_flaeche land.verpachtete_gesamtflaeche = verpachtete_flaeche land.save() # Erstelle LandVerpachtung-Objekt für bessere Nachverfolgung land_verpachtung = LandVerpachtung.objects.create( land=land, paechter=paechter, vertragsnummer=f"V-{land.lfd_nr}-{dt.now().year}", pachtbeginn=pachtbeginn, pachtende=pachtende if pachtende else None, verpachtete_flaeche=verpachtete_flaeche, pachtzins_pauschal=pachtzins_pauschal if pachtzins_pauschal else 0, zahlungsweise=zahlungsweise, ust_option=ust_option, status='aktiv' ) # Erstelle automatisch eine Abrechnung für das aktuelle Jahr current_year = dt.now().year # Berechne erwartete jährliche Pacht basierend auf Zahlungsweise expected_annual_rent = pachtzins_pauschal if pachtzins_pauschal else 0 abrechnung, created = LandAbrechnung.objects.get_or_create( land=land, abrechnungsjahr=current_year, defaults={ 'pacht_vereinnahmt': expected_annual_rent, # Setze erwartete Jahrespacht 'umlagen_vereinnahmt': 0, 'grundsteuer_betrag': 0, 'versicherungen_betrag': 0, } ) # Falls Abrechnung bereits existiert, aktualisiere die Pacht wenn höher if not created and expected_annual_rent > abrechnung.pacht_vereinnahmt: abrechnung.pacht_vereinnahmt = expected_annual_rent abrechnung.save() success_msg = f'Verpachtung von {land} an {paechter.get_full_name()} wurde erfolgreich erstellt.' if created: success_msg += f' Abrechnung für {current_year} wurde automatisch angelegt' if expected_annual_rent > 0: success_msg += f' (Erwartete Jahrespacht: {expected_annual_rent}€)' success_msg += '.' elif expected_annual_rent > 0: success_msg += f' Erwartete Jahrespacht in Abrechnung {current_year} wurde aktualisiert ({expected_annual_rent}€).' messages.success(request, success_msg) return redirect('stiftung:land_detail', pk=land.pk) else: messages.error(request, 'Bitte füllen Sie alle Pflichtfelder aus.') # Verfügbare Pächter paechter_list = Paechter.objects.filter(aktiv=True).order_by('nachname', 'vorname') # Berechne verfügbare Fläche (Gesamtfläche minus bereits verpachtete Fläche) verfuegbare_flaeche = land.groesse_qm if land.verp_flaeche_aktuell and land.verp_flaeche_aktuell > 0: verfuegbare_flaeche = land.groesse_qm - land.verp_flaeche_aktuell context = { 'land': land, 'paechter_list': paechter_list, 'current_year': dt.now().year, 'is_edit': False, 'verfuegbare_flaeche': verfuegbare_flaeche, } return render(request, 'stiftung/land_verpachtung_form.html', context) @login_required def land_verpachtung_end(request, land_pk): """Beende die aktuelle Verpachtung eines Landes""" land = get_object_or_404(Land, pk=land_pk) if request.method == 'POST': # Verpachtung beenden land.aktueller_paechter = None land.paechter_name = None land.paechter_anschrift = None land.pachtende = datetime.now().date() land.save() messages.success(request, f'Verpachtung von {land} wurde beendet.') return redirect('stiftung:land_detail', pk=land.pk) context = { 'land': land, } return render(request, 'stiftung/land_verpachtung_end.html', context) @login_required def land_verpachtung_edit(request, land_pk): """Bearbeite eine bestehende Verpachtung direkt im Land-Model""" land = get_object_or_404(Land, pk=land_pk) if request.method == 'POST': # Einfaches Formular für die wichtigsten Verpachtungsfelder aktueller_paechter_id = request.POST.get('aktueller_paechter') pachtbeginn = request.POST.get('pachtbeginn') pachtende = request.POST.get('pachtende') pachtzins_pauschal = request.POST.get('pachtzins_pauschal') zahlungsweise = request.POST.get('zahlungsweise') ust_option = request.POST.get('ust_option') == 'on' verpachtete_flaeche = request.POST.get('verpachtete_flaeche') if aktueller_paechter_id and pachtbeginn: paechter = get_object_or_404(Paechter, pk=aktueller_paechter_id) # Land aktualisieren land.aktueller_paechter = paechter land.paechter_name = paechter.get_full_name() land.paechter_anschrift = f"{paechter.strasse or ''}\n{paechter.plz or ''} {paechter.ort or ''}".strip() land.pachtbeginn = pachtbeginn land.pachtende = pachtende if pachtende else None land.pachtzins_pauschal = pachtzins_pauschal if pachtzins_pauschal else None land.zahlungsweise = zahlungsweise land.ust_option = ust_option if verpachtete_flaeche: land.verp_flaeche_aktuell = verpachtete_flaeche land.save() messages.success(request, f'Verpachtung von {land} an {paechter.get_full_name()} wurde erfolgreich aktualisiert.') return redirect('stiftung:land_detail', pk=land.pk) else: messages.error(request, 'Bitte füllen Sie alle Pflichtfelder aus.') # Verfügbare Pächter paechter_list = Paechter.objects.filter(aktiv=True).order_by('nachname', 'vorname') # Berechne verfügbare Fläche (Gesamtfläche minus bereits verpachtete Fläche) verfuegbare_flaeche = land.groesse_qm if land.verp_flaeche_aktuell and land.verp_flaeche_aktuell > 0: verfuegbare_flaeche = land.groesse_qm - land.verp_flaeche_aktuell context = { 'land': land, 'paechter_list': paechter_list, 'current_year': datetime.now().year, 'is_edit': True, 'verfuegbare_flaeche': verfuegbare_flaeche, } return render(request, 'stiftung/land_verpachtung_form.html', context) # 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)