Files
SysAdmin Agent e0b377014c
Some checks failed
CI/CD Pipeline / test (push) Has been cancelled
CI/CD Pipeline / deploy (push) Has been cancelled
Code Quality / quality (push) Has been cancelled
v4.1.0: DMS email documents, category-specific Nachweis linking, version system
- Save cover email body as DMS document with new 'email' context type
- Show email body separately from attachments in email detail view
- Add per-category DMS document assignment in quarterly confirmation
  (Studiennachweis, Einkommenssituation, Vermögenssituation)
- Add VERSION file and context processor for automatic version display
- Add MCP server, agent system, import/export, and new migrations
- Update compose files and production environment template

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-15 18:48:52 +00:00

1577 lines
58 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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, BigIntegerField, 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
# Use regexp_replace to strip ALL non-digit characters for safe integer casting
from django.db.models import Func
class RegexpReplace(Func):
function = "REGEXP_REPLACE"
template = "%(function)s(%(expressions)s, '[^0-9]', '', 'g')"
def digits_only(field_expr):
return RegexpReplace(field_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("")), BigIntegerField()),
flur_num=Cast(NullIf(digits_only(F("flur")), Value("")), BigIntegerField()),
flurstueck_num=Cast(
NullIf(digits_only(F("flurstueck")), Value("")), BigIntegerField()
),
)
# 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 = [] # 624 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 (624 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)