The Land model's ForeignKey from LandAbrechnung uses related_name='abrechnungen', not the default 'landabrechnung'. Fixed the exclude() query in paechter_workflow view. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1665 lines
62 KiB
Python
1665 lines
62 KiB
Python
# 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
|
||
import requests
|
||
from django.conf import settings
|
||
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,
|
||
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 = DokumentLink.objects.filter(
|
||
paechter_id=paechter.pk
|
||
).order_by("kontext", "titel")
|
||
|
||
# Neue LandVerpachtungen für diesen Pächter laden
|
||
verpachtungen = LandVerpachtung.objects.filter(paechter=paechter).order_by(
|
||
"-pachtbeginn"
|
||
)
|
||
|
||
# Neue gepachtete Ländereien (über aktueller_paechter)
|
||
gepachtete_laendereien = paechter.gepachtete_laendereien.filter(
|
||
aktiv=True
|
||
).order_by("gemeinde", "gemarkung")
|
||
|
||
# Statistiken berechnen
|
||
total_flaeche_neu = sum(
|
||
land.verp_flaeche_aktuell or 0 for land in gepachtete_laendereien
|
||
)
|
||
total_pachtzins_neu = sum(
|
||
land.pachtzins_pauschal or 0 for land in gepachtete_laendereien
|
||
)
|
||
|
||
context = {
|
||
"paechter": paechter,
|
||
"verknuepfte_dokumente": verknuepfte_dokumente,
|
||
"verpachtungen": verpachtungen, # Now using LandVerpachtung
|
||
"gepachtete_laendereien": gepachtete_laendereien, # Neu
|
||
"total_flaeche_neu": total_flaeche_neu,
|
||
"total_pachtzins_neu": total_pachtzins_neu,
|
||
}
|
||
return render(request, "stiftung/paechter_detail.html", context)
|
||
|
||
|
||
@login_required
|
||
def paechter_create(request):
|
||
if request.method == "POST":
|
||
form = PaechterForm(request.POST)
|
||
if form.is_valid():
|
||
paechter = form.save()
|
||
messages.success(
|
||
request,
|
||
f'Pächter "{paechter.get_full_name()}" wurde erfolgreich erstellt.',
|
||
)
|
||
return redirect("stiftung:paechter_detail", pk=paechter.pk)
|
||
else:
|
||
# Debug: Log form errors and show them to user
|
||
print(f"Form errors: {form.errors}")
|
||
print(f"Form data: {request.POST}")
|
||
messages.error(request, f"Formular-Fehler: {form.errors}")
|
||
for field, errors in form.errors.items():
|
||
for error in errors:
|
||
messages.error(request, f"{field}: {error}")
|
||
else:
|
||
form = PaechterForm()
|
||
|
||
context = {"form": form, "title": "Neuen Pächter erstellen"}
|
||
return render(request, "stiftung/paechter_form.html", context)
|
||
|
||
|
||
@login_required
|
||
def paechter_update(request, pk):
|
||
paechter = get_object_or_404(Paechter, pk=pk)
|
||
if request.method == "POST":
|
||
form = PaechterForm(request.POST, instance=paechter)
|
||
if form.is_valid():
|
||
paechter = form.save()
|
||
messages.success(
|
||
request,
|
||
f'Pächter "{paechter.get_full_name()}" wurde erfolgreich aktualisiert.',
|
||
)
|
||
return redirect("stiftung:paechter_detail", pk=paechter.pk)
|
||
else:
|
||
# Debug: Log form errors and show them to user
|
||
print(f"Form errors: {form.errors}")
|
||
print(f"Form data: {request.POST}")
|
||
messages.error(request, f"Formular-Fehler: {form.errors}")
|
||
for field, errors in form.errors.items():
|
||
for error in errors:
|
||
messages.error(request, f"{field}: {error}")
|
||
else:
|
||
form = PaechterForm(instance=paechter)
|
||
|
||
context = {
|
||
"form": form,
|
||
"paechter": paechter,
|
||
"title": f"Pächter bearbeiten: {paechter.get_full_name()}",
|
||
}
|
||
return render(request, "stiftung/paechter_form.html", context)
|
||
|
||
|
||
@login_required
|
||
def paechter_delete(request, pk):
|
||
paechter = get_object_or_404(Paechter, pk=pk)
|
||
if request.method == "POST":
|
||
paechter.delete()
|
||
messages.success(
|
||
request, f'Pächter "{paechter.get_full_name()}" wurde erfolgreich gelöscht.'
|
||
)
|
||
return redirect("stiftung:paechter_list")
|
||
|
||
context = {"paechter": paechter}
|
||
return render(request, "stiftung/paechter_confirm_delete.html", context)
|
||
|
||
|
||
# Land Views
|
||
@login_required
|
||
def land_list(request):
|
||
search_query = request.GET.get("search", "")
|
||
gemeinde_filter = request.GET.get("gemeinde", "")
|
||
aktiv_filter = request.GET.get("aktiv", "")
|
||
sort = request.GET.get("sort", "")
|
||
direction = request.GET.get("dir", "asc")
|
||
|
||
lands = Land.objects.all()
|
||
|
||
if search_query:
|
||
lands = lands.filter(
|
||
Q(lfd_nr__icontains=search_query)
|
||
| Q(gemeinde__icontains=search_query)
|
||
| Q(gemarkung__icontains=search_query)
|
||
| Q(flur__icontains=search_query)
|
||
| Q(flurstueck__icontains=search_query)
|
||
)
|
||
|
||
if gemeinde_filter:
|
||
lands = lands.filter(gemeinde=gemeinde_filter)
|
||
|
||
if aktiv_filter == "true":
|
||
lands = lands.filter(aktiv=True)
|
||
elif aktiv_filter == "false":
|
||
lands = lands.filter(aktiv=False)
|
||
|
||
# Annotate with verpachtungsgrad and numeric casts for natural sorting
|
||
# Prepare numeric versions of textual fields by stripping common non-digits
|
||
def digits_only(field_expr):
|
||
expr = Replace(field_expr, Value(" "), Value(""))
|
||
expr = Replace(expr, Value("-"), Value(""))
|
||
expr = Replace(expr, Value("."), Value(""))
|
||
expr = Replace(expr, Value("/"), Value(""))
|
||
expr = Replace(expr, Value("L"), Value(""))
|
||
return expr
|
||
|
||
lands = lands.extra(
|
||
select={
|
||
"verpachtungsgrad": "CASE WHEN groesse_qm > 0 THEN (verp_flaeche_aktuell / groesse_qm) * 100 ELSE 0 END"
|
||
}
|
||
).annotate(
|
||
lfd_nr_num=Cast(NullIf(digits_only(F("lfd_nr")), Value("")), IntegerField()),
|
||
flur_num=Cast(NullIf(digits_only(F("flur")), Value("")), IntegerField()),
|
||
flurstueck_num=Cast(
|
||
NullIf(digits_only(F("flurstueck")), Value("")), IntegerField()
|
||
),
|
||
)
|
||
|
||
# Sorting
|
||
sort_map = {
|
||
"lfd_nr": ["lfd_nr_num", "lfd_nr"],
|
||
"gemeinde": ["gemeinde"],
|
||
"gemarkung": ["gemarkung"],
|
||
"flur": ["flur_num", "flur"],
|
||
"flurstueck": ["flurstueck_num", "flurstueck"],
|
||
"groesse": ["groesse_qm"],
|
||
"verp": ["verp_flaeche_aktuell"],
|
||
"grad": ["verpachtungsgrad"],
|
||
}
|
||
if sort in sort_map:
|
||
fields = sort_map[sort]
|
||
if direction == "desc":
|
||
order_fields = [f"-{f}" for f in fields]
|
||
else:
|
||
order_fields = fields
|
||
lands = lands.order_by(*order_fields)
|
||
|
||
# Aggregated statistics for current filter set
|
||
aggregates = lands.aggregate(
|
||
sum_groesse_qm=Sum("groesse_qm"),
|
||
sum_gruenland_qm=Sum("gruenland_qm"),
|
||
sum_acker_qm=Sum("acker_qm"),
|
||
sum_wald_qm=Sum("wald_qm"),
|
||
sum_sonstiges_qm=Sum("sonstiges_qm"),
|
||
)
|
||
sum_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 = DokumentLink.objects.filter(land_id=land.pk).order_by(
|
||
"kontext", "titel"
|
||
)
|
||
|
||
# Neue LandVerpachtungen laden (mit related data)
|
||
neue_verpachtungen = land.neue_verpachtungen.select_related("paechter").order_by(
|
||
"-pachtbeginn"
|
||
)
|
||
|
||
context = {
|
||
"land": land,
|
||
"verknuepfte_dokumente": verknuepfte_dokumente,
|
||
"verpachtungen": neue_verpachtungen, # Using only new system now
|
||
"neue_verpachtungen": neue_verpachtungen,
|
||
}
|
||
return render(request, "stiftung/land_detail.html", context)
|
||
|
||
|
||
@login_required
|
||
def land_create(request):
|
||
if request.method == "POST":
|
||
form = LandForm(request.POST)
|
||
|
||
# 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 = DokumentLink.objects.filter(
|
||
land_verpachtung_id=verpachtung.pk
|
||
).order_by("kontext", "titel")
|
||
|
||
context = {
|
||
"verpachtung": verpachtung,
|
||
"landverpachtung": verpachtung, # Template expects this variable name
|
||
"verknuepfte_dokumente": verknuepfte_dokumente,
|
||
}
|
||
return render(request, "stiftung/land_verpachtung_detail.html", context)
|
||
|
||
|
||
@login_required
|
||
def land_verpachtung_update(request, pk):
|
||
"""Update an existing LandVerpachtung by its primary key"""
|
||
verpachtung = get_object_or_404(LandVerpachtung, pk=pk)
|
||
|
||
if request.method == "POST":
|
||
# Handle the update form submission
|
||
vertragsnummer = request.POST.get("vertragsnummer")
|
||
pachtbeginn = request.POST.get("pachtbeginn")
|
||
pachtende = request.POST.get("pachtende")
|
||
pachtzins_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. Linked documents from Paperless
|
||
dokumente = DokumentLink.objects.filter(paechter_id=paechter.pk)
|
||
docs_data = []
|
||
for doc in dokumente:
|
||
doc_data = {
|
||
"paperless_id": doc.paperless_document_id,
|
||
"titel": doc.titel,
|
||
"kontext": doc.get_kontext_display(),
|
||
"beschreibung": doc.beschreibung,
|
||
}
|
||
docs_data.append(doc_data)
|
||
|
||
# Try to download document from Paperless
|
||
try:
|
||
if (
|
||
hasattr(settings, "PAPERLESS_API_URL")
|
||
and settings.PAPERLESS_API_URL
|
||
):
|
||
doc_url = f"{settings.PAPERLESS_API_URL}/api/documents/{doc.paperless_document_id}/download/"
|
||
headers = {}
|
||
if (
|
||
hasattr(settings, "PAPERLESS_API_TOKEN")
|
||
and settings.PAPERLESS_API_TOKEN
|
||
):
|
||
headers["Authorization"] = (
|
||
f"Token {settings.PAPERLESS_API_TOKEN}"
|
||
)
|
||
|
||
response = requests.get(doc_url, headers=headers, timeout=30)
|
||
if response.status_code == 200:
|
||
content_type = response.headers.get("content-type", "")
|
||
if "pdf" in content_type:
|
||
ext = ".pdf"
|
||
elif "jpeg" in content_type or "jpg" in content_type:
|
||
ext = ".jpg"
|
||
elif "png" in content_type:
|
||
ext = ".png"
|
||
else:
|
||
ext = ".pdf"
|
||
|
||
safe_filename = f"dokument_{doc.paperless_document_id}_{doc.titel.replace('/', '_')[:50]}{ext}"
|
||
zipf.writestr(
|
||
f"dokumente/{safe_filename}", response.content
|
||
)
|
||
doc_data["downloaded"] = True
|
||
else:
|
||
doc_data["download_error"] = f"HTTP {response.status_code}"
|
||
except Exception as e:
|
||
doc_data["download_error"] = str(e)
|
||
|
||
if docs_data:
|
||
zipf.writestr(
|
||
"dokumente.json",
|
||
json.dumps(docs_data, indent=2, ensure_ascii=False),
|
||
)
|
||
|
||
# Prepare response
|
||
with open(temp_file.name, "rb") as f:
|
||
response = HttpResponse(f.read(), content_type="application/zip")
|
||
filename = f"paechter_{paechter.nachname}_{paechter.vorname}_{timezone.now().strftime('%Y%m%d_%H%M%S')}.zip"
|
||
response["Content-Disposition"] = f'attachment; filename="{filename}"'
|
||
return response
|
||
|
||
finally:
|
||
try:
|
||
os.unlink(temp_file.name)
|
||
except:
|
||
pass
|
||
|
||
|
||
@login_required
|
||
def land_export(request, pk):
|
||
"""Export complete Land data as ZIP with documents"""
|
||
import 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. Linked documents from Paperless
|
||
dokumente = DokumentLink.objects.filter(land_id=land.pk)
|
||
docs_data = []
|
||
for doc in dokumente:
|
||
doc_data = {
|
||
"paperless_id": doc.paperless_document_id,
|
||
"titel": doc.titel,
|
||
"kontext": doc.get_kontext_display(),
|
||
"beschreibung": doc.beschreibung,
|
||
}
|
||
docs_data.append(doc_data)
|
||
|
||
# Try to download document from Paperless
|
||
try:
|
||
if (
|
||
hasattr(settings, "PAPERLESS_API_URL")
|
||
and settings.PAPERLESS_API_URL
|
||
):
|
||
doc_url = f"{settings.PAPERLESS_API_URL}/api/documents/{doc.paperless_document_id}/download/"
|
||
headers = {}
|
||
if (
|
||
hasattr(settings, "PAPERLESS_API_TOKEN")
|
||
and settings.PAPERLESS_API_TOKEN
|
||
):
|
||
headers["Authorization"] = (
|
||
f"Token {settings.PAPERLESS_API_TOKEN}"
|
||
)
|
||
|
||
response = requests.get(doc_url, headers=headers, timeout=30)
|
||
if response.status_code == 200:
|
||
content_type = response.headers.get("content-type", "")
|
||
if "pdf" in content_type:
|
||
ext = ".pdf"
|
||
elif "jpeg" in content_type or "jpg" in content_type:
|
||
ext = ".jpg"
|
||
elif "png" in content_type:
|
||
ext = ".png"
|
||
else:
|
||
ext = ".pdf"
|
||
|
||
safe_filename = f"dokument_{doc.paperless_document_id}_{doc.titel.replace('/', '_')[:50]}{ext}"
|
||
zipf.writestr(
|
||
f"dokumente/{safe_filename}", response.content
|
||
)
|
||
doc_data["downloaded"] = True
|
||
else:
|
||
doc_data["download_error"] = f"HTTP {response.status_code}"
|
||
except Exception as e:
|
||
doc_data["download_error"] = str(e)
|
||
|
||
if docs_data:
|
||
zipf.writestr(
|
||
"dokumente.json",
|
||
json.dumps(docs_data, indent=2, ensure_ascii=False),
|
||
)
|
||
|
||
# Prepare response
|
||
with open(temp_file.name, "rb") as f:
|
||
response = HttpResponse(f.read(), content_type="application/zip")
|
||
filename = f"land_{land.gemeinde}_{land.gemarkung}_flur{land.flur}_{timezone.now().strftime('%Y%m%d_%H%M%S')}.zip"
|
||
response["Content-Disposition"] = f'attachment; filename="{filename}"'
|
||
return response
|
||
|
||
finally:
|
||
try:
|
||
os.unlink(temp_file.name)
|
||
except:
|
||
pass
|
||
|
||
|
||
@login_required
|
||
def verpachtung_export(request, pk):
|
||
"""Export complete Verpachtung data as ZIP with documents"""
|
||
import 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. Linked documents from Paperless
|
||
dokumente = DokumentLink.objects.filter(verpachtung_id=verpachtung.pk)
|
||
docs_data = []
|
||
for doc in dokumente:
|
||
doc_data = {
|
||
"paperless_id": doc.paperless_document_id,
|
||
"titel": doc.titel,
|
||
"kontext": doc.get_kontext_display(),
|
||
"beschreibung": doc.beschreibung,
|
||
}
|
||
docs_data.append(doc_data)
|
||
|
||
# Try to download document from Paperless
|
||
try:
|
||
if (
|
||
hasattr(settings, "PAPERLESS_API_URL")
|
||
and settings.PAPERLESS_API_URL
|
||
):
|
||
doc_url = f"{settings.PAPERLESS_API_URL}/api/documents/{doc.paperless_document_id}/download/"
|
||
headers = {}
|
||
if (
|
||
hasattr(settings, "PAPERLESS_API_TOKEN")
|
||
and settings.PAPERLESS_API_TOKEN
|
||
):
|
||
headers["Authorization"] = (
|
||
f"Token {settings.PAPERLESS_API_TOKEN}"
|
||
)
|
||
|
||
response = requests.get(doc_url, headers=headers, timeout=30)
|
||
if response.status_code == 200:
|
||
content_type = response.headers.get("content-type", "")
|
||
if "pdf" in content_type:
|
||
ext = ".pdf"
|
||
elif "jpeg" in content_type or "jpg" in content_type:
|
||
ext = ".jpg"
|
||
elif "png" in content_type:
|
||
ext = ".png"
|
||
else:
|
||
ext = ".pdf"
|
||
|
||
safe_filename = f"dokument_{doc.paperless_document_id}_{doc.titel.replace('/', '_')[:50]}{ext}"
|
||
zipf.writestr(
|
||
f"dokumente/{safe_filename}", response.content
|
||
)
|
||
doc_data["downloaded"] = True
|
||
else:
|
||
doc_data["download_error"] = f"HTTP {response.status_code}"
|
||
except Exception as e:
|
||
doc_data["download_error"] = str(e)
|
||
|
||
if docs_data:
|
||
zipf.writestr(
|
||
"dokumente.json",
|
||
json.dumps(docs_data, indent=2, ensure_ascii=False),
|
||
)
|
||
|
||
# Prepare response
|
||
with open(temp_file.name, "rb") as f:
|
||
response = HttpResponse(f.read(), content_type="application/zip")
|
||
filename = f"verpachtung_{verpachtung.vertragsnummer}_{timezone.now().strftime('%Y%m%d_%H%M%S')}.zip"
|
||
response["Content-Disposition"] = f'attachment; filename="{filename}"'
|
||
return response
|
||
|
||
finally:
|
||
try:
|
||
os.unlink(temp_file.name)
|
||
except:
|
||
pass
|
||
|
||
|
||
@login_required
|
||
def 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 = DokumentLink.objects.filter(
|
||
land_verpachtung_id=verpachtung.pk
|
||
).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("landverpachtung__verpachtete_flaeche"),
|
||
anzahl_vertraege=Count("landverpachtung")
|
||
).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)
|
||
|
||
|