# views/land.py # Phase 0: Vision 2026 – Code-Refactoring import csv import io import json import os import time from datetime import datetime, timedelta, date from decimal import Decimal import qrcode import qrcode.image.svg from django.contrib import messages from django.contrib.auth.decorators import login_required from django.core.paginator import Paginator from django.db.models import (Avg, Count, DecimalField, F, IntegerField, Q, Sum, Value) from django.db.models.functions import Cast, Coalesce, NullIf, Replace from django.http import HttpResponse, JsonResponse from django.shortcuts import get_object_or_404, redirect, render from django.urls import reverse from django.utils import timezone from django.views.decorators.csrf import csrf_exempt from django_otp.decorators import otp_required from django_otp.plugins.otp_totp.models import TOTPDevice from django_otp.plugins.otp_static.models import StaticDevice, StaticToken from django_otp.util import random_hex from rest_framework.decorators import api_view from rest_framework.response import Response from stiftung.models import (AppConfiguration, AuditLog, BackupJob, BankTransaction, BriefVorlage, CSVImport, Destinataer, DestinataerEmailEingang, DestinataerNotiz, DestinataerUnterstuetzung, DokumentDatei, DokumentLink, Foerderung, GeschichteBild, GeschichteSeite, Land, LandAbrechnung, LandVerpachtung, Paechter, Person, Rentmeister, StiftungsKalenderEintrag, StiftungsKonto, UnterstuetzungWiederkehrend, Veranstaltung, Veranstaltungsteilnehmer, Verwaltungskosten, VierteljahresNachweis) from stiftung.forms import ( DestinataerForm, DestinataerUnterstuetzungForm, DestinataerNotizForm, FoerderungForm, GeschichteBildForm, GeschichteSeiteForm, LandForm, LandVerpachtungForm, LandAbrechnungForm, PaechterForm, DokumentLinkForm, RentmeisterForm, StiftungsKontoForm, VerwaltungskostenForm, BankTransactionForm, BankImportForm, UnterstuetzungForm, UnterstuetzungWiederkehrendForm, UnterstuetzungMarkAsPaidForm, VierteljahresNachweisForm, UserCreationForm, UserUpdateForm, PasswordChangeForm, UserPermissionForm, TwoFactorSetupForm, TwoFactorVerifyForm, TwoFactorDisableForm, BackupTokenRegenerateForm, PersonForm, VeranstaltungForm, VeranstaltungsteilnehmerForm, ) @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 = DokumentDatei.objects.filter( paechter=paechter ).order_by("kontext", "titel") # Neue LandVerpachtungen für diesen Pächter laden verpachtungen = LandVerpachtung.objects.filter(paechter=paechter).order_by( "-pachtbeginn" ) # Neue gepachtete Ländereien (über aktueller_paechter) gepachtete_laendereien = paechter.gepachtete_laendereien.filter( aktiv=True ).order_by("gemeinde", "gemarkung") # Statistiken berechnen total_flaeche_neu = sum( land.verp_flaeche_aktuell or 0 for land in gepachtete_laendereien ) total_pachtzins_neu = sum( land.pachtzins_pauschal or 0 for land in gepachtete_laendereien ) context = { "paechter": paechter, "verknuepfte_dokumente": verknuepfte_dokumente, "verpachtungen": verpachtungen, # Now using LandVerpachtung "gepachtete_laendereien": gepachtete_laendereien, # Neu "total_flaeche_neu": total_flaeche_neu, "total_pachtzins_neu": total_pachtzins_neu, } return render(request, "stiftung/paechter_detail.html", context) @login_required def paechter_create(request): if request.method == "POST": form = PaechterForm(request.POST) if form.is_valid(): paechter = form.save() messages.success( request, f'Pächter "{paechter.get_full_name()}" wurde erfolgreich erstellt.', ) return redirect("stiftung:paechter_detail", pk=paechter.pk) else: # Debug: Log form errors and show them to user print(f"Form errors: {form.errors}") print(f"Form data: {request.POST}") messages.error(request, f"Formular-Fehler: {form.errors}") for field, errors in form.errors.items(): for error in errors: messages.error(request, f"{field}: {error}") else: form = PaechterForm() context = {"form": form, "title": "Neuen Pächter erstellen"} return render(request, "stiftung/paechter_form.html", context) @login_required def paechter_update(request, pk): paechter = get_object_or_404(Paechter, pk=pk) if request.method == "POST": form = PaechterForm(request.POST, instance=paechter) if form.is_valid(): paechter = form.save() messages.success( request, f'Pächter "{paechter.get_full_name()}" wurde erfolgreich aktualisiert.', ) return redirect("stiftung:paechter_detail", pk=paechter.pk) else: # Debug: Log form errors and show them to user print(f"Form errors: {form.errors}") print(f"Form data: {request.POST}") messages.error(request, f"Formular-Fehler: {form.errors}") for field, errors in form.errors.items(): for error in errors: messages.error(request, f"{field}: {error}") else: form = PaechterForm(instance=paechter) context = { "form": form, "paechter": paechter, "title": f"Pächter bearbeiten: {paechter.get_full_name()}", } return render(request, "stiftung/paechter_form.html", context) @login_required def paechter_delete(request, pk): paechter = get_object_or_404(Paechter, pk=pk) if request.method == "POST": paechter.delete() messages.success( request, f'Pächter "{paechter.get_full_name()}" wurde erfolgreich gelöscht.' ) return redirect("stiftung:paechter_list") context = {"paechter": paechter} return render(request, "stiftung/paechter_confirm_delete.html", context) # Land Views @login_required def land_list(request): search_query = request.GET.get("search", "") gemeinde_filter = request.GET.get("gemeinde", "") aktiv_filter = request.GET.get("aktiv", "") sort = request.GET.get("sort", "") direction = request.GET.get("dir", "asc") lands = Land.objects.all() if search_query: lands = lands.filter( Q(lfd_nr__icontains=search_query) | Q(gemeinde__icontains=search_query) | Q(gemarkung__icontains=search_query) | Q(flur__icontains=search_query) | Q(flurstueck__icontains=search_query) ) if gemeinde_filter: lands = lands.filter(gemeinde=gemeinde_filter) if aktiv_filter == "true": lands = lands.filter(aktiv=True) elif aktiv_filter == "false": lands = lands.filter(aktiv=False) # Annotate with verpachtungsgrad and numeric casts for natural sorting # Prepare numeric versions of textual fields by stripping common non-digits def digits_only(field_expr): expr = Replace(field_expr, Value(" "), Value("")) expr = Replace(expr, Value("-"), Value("")) expr = Replace(expr, Value("."), Value("")) expr = Replace(expr, Value("/"), Value("")) expr = Replace(expr, Value("L"), Value("")) return expr lands = lands.extra( select={ "verpachtungsgrad": "CASE WHEN groesse_qm > 0 THEN (verp_flaeche_aktuell / groesse_qm) * 100 ELSE 0 END" } ).annotate( lfd_nr_num=Cast(NullIf(digits_only(F("lfd_nr")), Value("")), IntegerField()), flur_num=Cast(NullIf(digits_only(F("flur")), Value("")), IntegerField()), flurstueck_num=Cast( NullIf(digits_only(F("flurstueck")), Value("")), IntegerField() ), ) # Sorting sort_map = { "lfd_nr": ["lfd_nr_num", "lfd_nr"], "gemeinde": ["gemeinde"], "gemarkung": ["gemarkung"], "flur": ["flur_num", "flur"], "flurstueck": ["flurstueck_num", "flurstueck"], "groesse": ["groesse_qm"], "verp": ["verp_flaeche_aktuell"], "grad": ["verpachtungsgrad"], } if sort in sort_map: fields = sort_map[sort] if direction == "desc": order_fields = [f"-{f}" for f in fields] else: order_fields = fields lands = lands.order_by(*order_fields) # Aggregated statistics for current filter set aggregates = lands.aggregate( sum_groesse_qm=Sum("groesse_qm"), sum_gruenland_qm=Sum("gruenland_qm"), sum_acker_qm=Sum("acker_qm"), sum_wald_qm=Sum("wald_qm"), sum_sonstiges_qm=Sum("sonstiges_qm"), ) sum_groesse_qm = float(aggregates.get("sum_groesse_qm") or 0) sum_gruenland_qm = float(aggregates.get("sum_gruenland_qm") or 0) sum_acker_qm = float(aggregates.get("sum_acker_qm") or 0) sum_wald_qm = float(aggregates.get("sum_wald_qm") or 0) sum_sonstiges_qm = float(aggregates.get("sum_sonstiges_qm") or 0) sum_total_use_qm = sum_gruenland_qm + sum_acker_qm + sum_wald_qm + sum_sonstiges_qm # Calculate verpachtung statistics total_plots = lands.count() verpachtete_plots = lands.filter(verp_flaeche_aktuell__gt=0).count() unveerpachtete_plots = total_plots - verpachtete_plots def pct(part, total): return round((part / total) * 100, 1) if total and part is not None else 0.0 stats = { "sum_groesse_qm": sum_groesse_qm, "sum_gruenland_qm": sum_gruenland_qm, "sum_acker_qm": sum_acker_qm, "sum_wald_qm": sum_wald_qm, "sum_sonstiges_qm": sum_sonstiges_qm, "sum_total_use_qm": sum_total_use_qm, "pct_gruenland": pct(sum_gruenland_qm, sum_total_use_qm), "pct_acker": pct(sum_acker_qm, sum_total_use_qm), "pct_wald": pct(sum_wald_qm, sum_total_use_qm), "total_plots": total_plots, "verpachtete_plots": verpachtete_plots, "unveerpachtete_plots": unveerpachtete_plots, "pct_verpachtet": pct(verpachtete_plots, total_plots), "pct_unveerpachtet": pct(unveerpachtete_plots, total_plots), } # Prepare size chart data (top 30 by size) top_sizes = list( lands.order_by("-groesse_qm").values_list("lfd_nr", "groesse_qm")[:30] ) size_chart_labels = [label or "" for label, _ in top_sizes] size_chart_values = [float(val or 0) for _, val in top_sizes] paginator = Paginator(lands, 20) page_number = request.GET.get("page") page_obj = paginator.get_page(page_number) # Get unique gemeinden for filter gemeinden = ( Land.objects.values_list("gemeinde", flat=True).distinct().order_by("gemeinde") ) context = { "page_obj": page_obj, "search_query": search_query, "gemeinde_filter": gemeinde_filter, "aktiv_filter": aktiv_filter, "gemeinden": gemeinden, "stats": stats, "size_chart_labels_json": json.dumps(size_chart_labels), "size_chart_values_json": json.dumps(size_chart_values), "sort": sort, "dir": direction, } return render(request, "stiftung/land_list.html", context) @login_required def land_detail(request, pk): land = get_object_or_404(Land, pk=pk) # Alle mit dieser Länderei verknüpften Dokumente laden verknuepfte_dokumente = DokumentDatei.objects.filter(land=land).order_by( "kontext", "titel" ) # Neue LandVerpachtungen laden (mit related data) neue_verpachtungen = land.neue_verpachtungen.select_related("paechter").order_by( "-pachtbeginn" ) context = { "land": land, "verknuepfte_dokumente": verknuepfte_dokumente, "verpachtungen": neue_verpachtungen, # Using only new system now "neue_verpachtungen": neue_verpachtungen, } return render(request, "stiftung/land_detail.html", context) @login_required def land_create(request): if request.method == "POST": form = LandForm(request.POST) # Debug: Print form data print("=== LAND CREATE DEBUG ===") print(f"POST data: {dict(request.POST)}") print(f"Form is valid: {form.is_valid()}") if not form.is_valid(): print(f"Form errors: {form.errors}") print(f"Form non-field errors: {form.non_field_errors()}") # Add error messages for debugging for field, errors in form.errors.items(): for error in errors: messages.error(request, f"{field}: {error}") if form.is_valid(): try: land = form.save() messages.success(request, f'Länderei "{land}" wurde erfolgreich erstellt.') print(f"Successfully created land: {land}") return redirect("stiftung:land_detail", pk=land.pk) except Exception as e: print(f"Error saving land: {e}") messages.error(request, f"Fehler beim Speichern: {str(e)}") else: messages.error(request, "Bitte korrigieren Sie die Fehler im Formular.") else: form = LandForm() context = {"form": form, "title": "Neue Länderei erstellen"} return render(request, "stiftung/land_form.html", context) @login_required def land_update(request, pk): land = get_object_or_404(Land, pk=pk) if request.method == "POST": form = LandForm(request.POST, instance=land) if form.is_valid(): land = form.save() messages.success( request, f'Länderei "{land}" wurde erfolgreich aktualisiert.' ) return redirect("stiftung:land_detail", pk=land.pk) else: form = LandForm(instance=land) context = {"form": form, "land": land, "title": f"Länderei bearbeiten: {land}"} return render(request, "stiftung/land_form.html", context) @login_required def land_delete(request, pk): land = get_object_or_404(Land, pk=pk) if request.method == "POST": land.delete() messages.success(request, f'Länderei "{land}" wurde erfolgreich gelöscht.') return redirect("stiftung:land_list") context = {"land": land} return render(request, "stiftung/land_confirm_delete.html", context) # Verpachtung Views @login_required def verpachtung_list(request): search_query = request.GET.get("search", "") status_filter = request.GET.get("status", "") gemeinde_filter = request.GET.get("gemeinde", "") sort = request.GET.get("sort", "") direction = request.GET.get("dir", "asc") verpachtungen = LandVerpachtung.objects.select_related("land", "paechter").all() if search_query: verpachtungen = verpachtungen.filter( Q(vertragsnummer__icontains=search_query) | Q(land__gemeinde__icontains=search_query) | Q(paechter__nachname__icontains=search_query) | Q(paechter__vorname__icontains=search_query) ) if status_filter: verpachtungen = verpachtungen.filter(status=status_filter) if gemeinde_filter: verpachtungen = verpachtungen.filter(land__gemeinde=gemeinde_filter) # Sorting sort_map = { "vertragsnummer": ["vertragsnummer"], "land": ["land__gemeinde"], "paechter": ["paechter__nachname", "paechter__vorname"], "beginn": ["pachtbeginn"], "ende": ["pachtende"], "flaeche": ["verpachtete_flaeche"], "pachtzins": ["pachtzins_pauschal"], "status": ["status"], } if sort in sort_map: fields = sort_map[sort] if direction == "desc": order_fields = [f"-{f}" for f in fields] else: order_fields = fields verpachtungen = verpachtungen.order_by(*order_fields) paginator = Paginator(verpachtungen, 20) page_number = request.GET.get("page") page_obj = paginator.get_page(page_number) # Calculate statistics for the summary cards # Get ALL verpachtungen (not filtered) for accurate statistics all_verpachtungen = LandVerpachtung.objects.select_related("land", "paechter").all() # Active verpachtungen count aktive_verpachtungen = all_verpachtungen.filter(status="aktiv").count() # Total leased area (only active verpachtungen) gesamt_flaeche_result = all_verpachtungen.filter(status="aktiv").aggregate( total=Sum("verpachtete_flaeche") ) gesamt_flaeche = ( gesamt_flaeche_result["total"] if gesamt_flaeche_result["total"] is not None else 0 ) # Total annual rent (only active verpachtungen) jaehrlicher_pachtzins_result = all_verpachtungen.filter(status="aktiv").aggregate( total=Sum("pachtzins_pauschal") ) jaehrlicher_pachtzins = ( jaehrlicher_pachtzins_result["total"] if jaehrlicher_pachtzins_result["total"] is not None else 0 ) # Total count of all verpachtungen anzahl_verpachtungen = all_verpachtungen.count() # Get unique gemeinden and statuses for filters gemeinden = ( Land.objects.values_list("gemeinde", flat=True).distinct().order_by("gemeinde") ) status_choices = LandVerpachtung.STATUS_CHOICES context = { "page_obj": page_obj, "search_query": search_query, "status_filter": status_filter, "gemeinde_filter": gemeinde_filter, "gemeinden": gemeinden, "status_choices": status_choices, # Statistics for summary cards "aktive_verpachtungen": aktive_verpachtungen, "gesamt_flaeche": gesamt_flaeche, "jaehrlicher_pachtzins": jaehrlicher_pachtzins, "anzahl_verpachtungen": anzahl_verpachtungen, "sort": sort, "dir": direction, } return render(request, "stiftung/verpachtung_list.html", context) @login_required 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 = DokumentDatei.objects.filter( verpachtung=verpachtung ).order_by("kontext", "titel") context = { "verpachtung": verpachtung, "landverpachtung": verpachtung, # Template expects this variable name "verknuepfte_dokumente": verknuepfte_dokumente, } return render(request, "stiftung/land_verpachtung_detail.html", context) @login_required def land_verpachtung_update(request, pk): """Update an existing LandVerpachtung by its primary key""" verpachtung = get_object_or_404(LandVerpachtung, pk=pk) if request.method == "POST": # Handle the update form submission vertragsnummer = request.POST.get("vertragsnummer") pachtbeginn = request.POST.get("pachtbeginn") pachtende = request.POST.get("pachtende") pachtzins_pauschal = request.POST.get("pachtzins_pauschal") if vertragsnummer: verpachtung.vertragsnummer = vertragsnummer if pachtbeginn: verpachtung.pachtbeginn = pachtbeginn if pachtende: verpachtung.pachtende = pachtende if pachtzins_pauschal: verpachtung.pachtzins_pauschal = pachtzins_pauschal verpachtung.save() messages.success(request, "Verpachtung wurde erfolgreich aktualisiert.") return redirect("stiftung:land_verpachtung_detail", pk=verpachtung.pk) context = { "verpachtung": verpachtung, "landverpachtung": verpachtung, # Template expects this variable name "is_edit": True, "is_update": True, # Form template uses this flag } return render(request, "stiftung/land_verpachtung_form.html", context) @login_required def land_verpachtung_end_direct(request, pk): """End a LandVerpachtung directly by its primary key""" verpachtung = get_object_or_404(LandVerpachtung, pk=pk) if request.method == "POST": verpachtung.status = "beendet" verpachtung.pachtende = timezone.now().date() verpachtung.save() messages.success(request, "Verpachtung wurde erfolgreich beendet.") return redirect("stiftung:land_detail", pk=verpachtung.land.pk) context = { "verpachtung": verpachtung, } return render(request, "stiftung/land_verpachtung_end_confirm.html", context) # Förderung Views @login_required def land_stats_api(request): """API endpoint for land statistics""" if request.method == "GET": gemeinde = request.GET.get("gemeinde", "") if gemeinde: lands = Land.objects.filter(gemeinde=gemeinde) else: lands = Land.objects.all() stats = { "total_count": lands.count(), "total_flaeche": float( lands.aggregate(total=Sum("groesse_qm"))["total"] or 0 ), "total_verpachtet": float( LandVerpachtung.objects.filter( status="aktiv", land__in=lands ).aggregate(total=Sum("verpachtete_flaeche"))["total"] or 0 ), "avg_verpachtungsgrad": 0, } if stats["total_flaeche"] > 0: stats["avg_verpachtungsgrad"] = ( stats["total_verpachtet"] / stats["total_flaeche"] ) * 100 return JsonResponse(stats) return JsonResponse({"error": "Invalid request method"}, status=400) @login_required def paechter_export(request, pk): """Export complete Pächter data as ZIP with documents""" import json import os import tempfile import zipfile from django.http import HttpResponse paechter = get_object_or_404(Paechter, pk=pk) # Create a temporary file for the ZIP temp_file = tempfile.NamedTemporaryFile(delete=False, suffix=".zip") try: with zipfile.ZipFile(temp_file.name, "w", zipfile.ZIP_DEFLATED) as zipf: # 1. Entity data as JSON entity_data = { "id": str(paechter.id), "vorname": paechter.vorname, "nachname": paechter.nachname, "geburtsdatum": ( paechter.geburtsdatum.isoformat() if paechter.geburtsdatum else None ), "email": paechter.email, "telefon": paechter.telefon, "iban": paechter.iban, "strasse": paechter.strasse, "plz": paechter.plz, "ort": paechter.ort, "personentyp": paechter.get_personentyp_display(), "pachtnummer": paechter.pachtnummer, "pachtbeginn_erste": ( paechter.pachtbeginn_erste.isoformat() if paechter.pachtbeginn_erste else None ), "pachtende_letzte": ( paechter.pachtende_letzte.isoformat() if paechter.pachtende_letzte else None ), "pachtzins_aktuell": ( str(paechter.pachtzins_aktuell) if paechter.pachtzins_aktuell else None ), "landwirtschaftliche_ausbildung": paechter.landwirtschaftliche_ausbildung, "berufserfahrung_jahre": paechter.berufserfahrung_jahre, "spezialisierung": paechter.spezialisierung, "notizen": paechter.notizen, "aktiv": paechter.aktiv, "gesamt_pachtflaeche": float(paechter.get_gesamt_pachtflaeche()), "gesamt_pachtzins": float(paechter.get_gesamt_pachtzins()), "export_datum": timezone.now().isoformat(), "export_user": request.user.username, } zipf.writestr( "paechter_data.json", json.dumps(entity_data, indent=2, ensure_ascii=False), ) # 2. DMS-Dokumente (Django-natives DMS) dokumente = DokumentDatei.objects.filter(paechter=paechter) docs_data = [] for doc in dokumente: doc_data = { "dms_id": str(doc.id), "titel": doc.titel, "kontext": doc.get_kontext_display(), "beschreibung": doc.beschreibung, "dateiname": doc.dateiname_original, } docs_data.append(doc_data) try: if doc.datei: safe_filename = doc.dateiname_original or str(doc.id) zipf.writestr(f"dokumente/{safe_filename}", doc.datei.read()) doc_data["included"] = True except Exception as e: doc_data["error"] = str(e) if docs_data: zipf.writestr( "dokumente.json", json.dumps(docs_data, indent=2, ensure_ascii=False), ) # Prepare response with open(temp_file.name, "rb") as f: response = HttpResponse(f.read(), content_type="application/zip") filename = f"paechter_{paechter.nachname}_{paechter.vorname}_{timezone.now().strftime('%Y%m%d_%H%M%S')}.zip" response["Content-Disposition"] = f'attachment; filename="{filename}"' return response finally: try: os.unlink(temp_file.name) except: pass @login_required def land_export(request, pk): """Export complete Land data as ZIP with documents""" import json import os import tempfile import zipfile from django.http import HttpResponse land = get_object_or_404(Land, pk=pk) # Create a temporary file for the ZIP temp_file = tempfile.NamedTemporaryFile(delete=False, suffix=".zip") try: with zipfile.ZipFile(temp_file.name, "w", zipfile.ZIP_DEFLATED) as zipf: # 1. Entity data as JSON entity_data = { "id": str(land.id), "lfd_nr": land.lfd_nr, "ew_nummer": land.ew_nummer, "amtsgericht": land.amtsgericht, "gemeinde": land.gemeinde, "gemarkung": land.gemarkung, "flur": land.flur, "flurstueck": land.flurstueck, "groesse_qm": str(land.groesse_qm), "gruenland_qm": str(land.gruenland_qm), "acker_qm": str(land.acker_qm), "wald_qm": str(land.wald_qm), "sonstiges_qm": str(land.sonstiges_qm), "verpachtete_gesamtflaeche": str(land.verpachtete_gesamtflaeche), "flaeche_alte_liste": ( str(land.flaeche_alte_liste) if land.flaeche_alte_liste else None ), "verp_flaeche_aktuell": str(land.verp_flaeche_aktuell), "anteil_grundsteuer": ( str(land.anteil_grundsteuer) if land.anteil_grundsteuer else None ), "anteil_lwk": str(land.anteil_lwk) if land.anteil_lwk else None, "aktiv": land.aktiv, "notizen": land.notizen, "erstellt_am": land.erstellt_am.isoformat(), "aktualisiert_am": land.aktualisiert_am.isoformat(), "gesamtflaeche_berechnet": float(land.get_gesamtflaeche()), "verpachtungsgrad": float(land.get_verpachtungsgrad()), "export_datum": timezone.now().isoformat(), "export_user": request.user.username, } zipf.writestr( "land_data.json", json.dumps(entity_data, indent=2, ensure_ascii=False) ) # 2. DMS-Dokumente (Django-natives DMS) dokumente = DokumentDatei.objects.filter(land=land) docs_data = [] for doc in dokumente: doc_data = { "dms_id": str(doc.id), "titel": doc.titel, "kontext": doc.get_kontext_display(), "beschreibung": doc.beschreibung, "dateiname": doc.dateiname_original, } docs_data.append(doc_data) try: if doc.datei: safe_filename = doc.dateiname_original or str(doc.id) zipf.writestr(f"dokumente/{safe_filename}", doc.datei.read()) doc_data["included"] = True except Exception as e: doc_data["error"] = str(e) if docs_data: zipf.writestr( "dokumente.json", json.dumps(docs_data, indent=2, ensure_ascii=False), ) # Prepare response with open(temp_file.name, "rb") as f: response = HttpResponse(f.read(), content_type="application/zip") filename = f"land_{land.gemeinde}_{land.gemarkung}_flur{land.flur}_{timezone.now().strftime('%Y%m%d_%H%M%S')}.zip" response["Content-Disposition"] = f'attachment; filename="{filename}"' return response finally: try: os.unlink(temp_file.name) except: pass @login_required def verpachtung_export(request, pk): """Export complete Verpachtung data as ZIP with documents""" import json import os import tempfile import zipfile from django.http import HttpResponse verpachtung = get_object_or_404(LandVerpachtung, pk=pk) # Create a temporary file for the ZIP temp_file = tempfile.NamedTemporaryFile(delete=False, suffix=".zip") try: with zipfile.ZipFile(temp_file.name, "w", zipfile.ZIP_DEFLATED) as zipf: # 1. Entity data as JSON entity_data = { "id": str(verpachtung.id), "vertragsnummer": verpachtung.vertragsnummer, "land": str(verpachtung.land), "land_id": str(verpachtung.land.id), "paechter": str(verpachtung.paechter), "paechter_id": str(verpachtung.paechter.id), "pachtbeginn": verpachtung.pachtbeginn.isoformat(), "pachtende": verpachtung.pachtende.isoformat(), "verlaengerung": ( verpachtung.verlaengerung.isoformat() if verpachtung.verlaengerung else None ), "pachtzins_pro_qm": str(verpachtung.pachtzins_pro_qm), "pachtzins_jaehrlich": str(verpachtung.pachtzins_pauschal), "verpachtete_flaeche": str(verpachtung.verpachtete_flaeche), "status": verpachtung.get_status_display(), "verwendungsnachweis": ( str(verpachtung.verwendungsnachweis) if verpachtung.verwendungsnachweis else None ), "bemerkungen": verpachtung.bemerkungen, "erstellt_am": verpachtung.erstellt_am.isoformat(), "aktualisiert_am": verpachtung.aktualisiert_am.isoformat(), "vertragsdauer_tage": verpachtung.get_vertragsdauer_tage(), "restlaufzeit_tage": verpachtung.get_restlaufzeit_tage(), "ist_aktiv": verpachtung.is_aktiv(), "export_datum": timezone.now().isoformat(), "export_user": request.user.username, } zipf.writestr( "verpachtung_data.json", json.dumps(entity_data, indent=2, ensure_ascii=False), ) # 2. DMS-Dokumente (Django-natives DMS) dokumente = DokumentDatei.objects.filter(verpachtung=verpachtung) docs_data = [] for doc in dokumente: doc_data = { "dms_id": str(doc.id), "titel": doc.titel, "kontext": doc.get_kontext_display(), "beschreibung": doc.beschreibung, "dateiname": doc.dateiname_original, } docs_data.append(doc_data) try: if doc.datei: safe_filename = doc.dateiname_original or str(doc.id) zipf.writestr(f"dokumente/{safe_filename}", doc.datei.read()) doc_data["included"] = True except Exception as e: doc_data["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 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 stiftung.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 stiftung.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 verpachtung_detail(request, pk): """Standalone detail view for verpachtung""" verpachtung = get_object_or_404(LandVerpachtung, pk=pk) # Alle mit dieser Verpachtung verknüpften Dokumente laden verknuepfte_dokumente = DokumentDatei.objects.filter( verpachtung=verpachtung ).order_by("kontext", "titel") context = { "verpachtung": verpachtung, "landverpachtung": verpachtung, # Template compatibility "verknuepfte_dokumente": verknuepfte_dokumente, "title": f"Verpachtung {verpachtung.vertragsnummer}", } return render(request, "stiftung/verpachtung_detail.html", context) @login_required def verpachtung_create(request): """Standalone create view for verpachtung""" from stiftung.forms import LandVerpachtungForm from datetime import datetime as dt if request.method == 'POST': form = LandVerpachtungForm(request.POST, request.FILES) if form.is_valid(): verpachtung = form.save() # Update the Land model to reflect this verpachtung land = verpachtung.land land.aktueller_paechter = verpachtung.paechter land.paechter_name = verpachtung.paechter.get_full_name() land.paechter_anschrift = f"{verpachtung.paechter.strasse or ''}\n{verpachtung.paechter.plz or ''} {verpachtung.paechter.ort or ''}".strip() land.pachtbeginn = verpachtung.pachtbeginn land.pachtende = verpachtung.pachtende land.pachtzins_pauschal = verpachtung.pachtzins_pauschal land.zahlungsweise = verpachtung.zahlungsweise land.ust_option = verpachtung.ust_option land.verpachtete_gesamtflaeche = verpachtung.verpachtete_flaeche land.verp_flaeche_aktuell = verpachtung.verpachtete_flaeche land.save() # Create automatic abrechnung current_year = dt.now().year expected_annual_rent = verpachtung.pachtzins_pauschal if verpachtung.pachtzins_pauschal else 0 abrechnung, created = LandAbrechnung.objects.get_or_create( land=land, abrechnungsjahr=current_year, defaults={ "pacht_vereinnahmt": expected_annual_rent, "umlagen_vereinnahmt": 0, "grundsteuer_betrag": 0, "versicherungen_betrag": 0, }, ) if not created and expected_annual_rent > abrechnung.pacht_vereinnahmt: abrechnung.pacht_vereinnahmt = expected_annual_rent abrechnung.save() success_msg = f'Verpachtung "{verpachtung.vertragsnummer}" wurde erfolgreich erstellt.' if created: success_msg += f" Abrechnung für {current_year} wurde automatisch angelegt" if expected_annual_rent > 0: success_msg += f" (Erwartete Jahrespacht: {expected_annual_rent}€)" success_msg += "." elif expected_annual_rent > 0: success_msg += f" Erwartete Jahrespacht in Abrechnung {current_year} wurde aktualisiert ({expected_annual_rent}€)." messages.success(request, success_msg) return redirect('stiftung:verpachtung_detail', pk=verpachtung.pk) else: form = LandVerpachtungForm() # Get available Länder and Pächter for the template laender_list = Land.objects.all().order_by('lfd_nr') paechter_list = Paechter.objects.filter(aktiv=True).order_by('nachname', 'vorname') context = { 'form': form, 'title': 'Neue Verpachtung erstellen', 'laender_list': laender_list, 'paechter_list': paechter_list, 'current_year': dt.now().year, 'is_edit': False, } return render(request, 'stiftung/verpachtung_form.html', context) @login_required def verpachtung_update(request, pk): """Standalone update view for verpachtung""" return land_verpachtung_update(request, pk) @login_required def verpachtung_delete(request, pk): """Standalone delete view for verpachtung""" verpachtung = get_object_or_404(LandVerpachtung, pk=pk) if request.method == 'POST': vertragsnummer = verpachtung.vertragsnummer verpachtung.delete() messages.success( request, f'Verpachtung "{vertragsnummer}" wurde erfolgreich gelöscht.' ) return redirect('stiftung:verpachtung_list') context = { 'verpachtung': verpachtung, 'title': f'Verpachtung {verpachtung.vertragsnummer} löschen', } return render(request, 'stiftung/verpachtung_confirm_delete.html', context) # ============================================================ # Phase 2d: Pächter-Workflow-Verbesserung # ============================================================ @login_required def paechter_workflow(request): """2d: Pipeline-Ansicht für Pächter – Vertragsfristen, Pachtanpassungen, Abrechnungen.""" heute = date.today() # Aktive Verpachtungen abrufen mit Restlaufzeit aktive_verpachtungen = LandVerpachtung.objects.filter( status="aktiv" ).select_related("land", "paechter").order_by("pachtende") # Kategorisieren abgelaufen = [] demnachst = [] # < 6 Monate mittelfrisitig = [] # 6–24 Monate langfristig = [] # > 24 Monate kein_enddatum = [] for v in aktive_verpachtungen: if not v.pachtende: kein_enddatum.append(v) continue restlaufzeit = (v.pachtende - heute).days if restlaufzeit < 0: abgelaufen.append(v) elif restlaufzeit <= 180: demnachst.append(v) elif restlaufzeit <= 730: mittelfrisitig.append(v) else: langfristig.append(v) # Ausstehende Jahresabrechnungen (letztes Jahr ohne Abrechnung) letztes_jahr = heute.year - 1 laender_ohne_abrechnung = Land.objects.filter( aktiv=True ).exclude( abrechnungen__abrechnungsjahr=letztes_jahr ).order_by("lfd_nr")[:20] # Pächter mit hoher Gesamtfläche (Top-Pächter) top_paechter = Paechter.objects.annotate( flaeche=Sum("neue_verpachtungen__verpachtete_flaeche"), anzahl_vertraege=Count("neue_verpachtungen") ).filter(flaeche__gt=0).order_by("-flaeche")[:10] # Anstehende Pachtanpassungen (> 5 Jahre laufend, keine Erhöhung dokumentiert) fuenf_jahre_ago = date(heute.year - 5, heute.month, heute.day) lang_laufend = LandVerpachtung.objects.filter( status="aktiv", pachtbeginn__lte=fuenf_jahre_ago, ).select_related("land", "paechter").order_by("pachtbeginn")[:20] pipeline_stages = [ { "key": "abgelaufen", "label": "Abgelaufen / Handlungsbedarf", "farbe": "danger", "icon": "fa-exclamation-triangle", "verpachtungen": abgelaufen, "count": len(abgelaufen), }, { "key": "demnachst", "label": "Bald fällig (< 6 Monate)", "farbe": "warning", "icon": "fa-clock", "verpachtungen": demnachst, "count": len(demnachst), }, { "key": "mittelfristig", "label": "Mittelfristig (6–24 Monate)", "farbe": "info", "icon": "fa-calendar", "verpachtungen": mittelfrisitig, "count": len(mittelfrisitig), }, { "key": "langfristig", "label": "Langfristig (> 24 Monate)", "farbe": "success", "icon": "fa-check", "verpachtungen": langfristig, "count": len(langfristig), }, { "key": "unbefristet", "label": "Unbefristet / Kein Enddatum", "farbe": "secondary", "icon": "fa-infinity", "verpachtungen": kein_enddatum, "count": len(kein_enddatum), }, ] context = { "pipeline_stages": pipeline_stages, "laender_ohne_abrechnung": laender_ohne_abrechnung, "top_paechter": top_paechter, "lang_laufend": lang_laufend, "letztes_jahr": letztes_jahr, "heute": heute, } return render(request, "stiftung/paechter_workflow.html", context)