feat: Add complete Verpachtung management system

- Add LandVerpachtung model with Land and Paechter relationships
- Implement full CRUD operations for Verpachtungen
- Add responsive Bootstrap templates with JavaScript calculations
- Integrate document linking functionality similar to other entities
- Add navigation links and URL patterns
- Include CSV import support for Paechter data
- Fix template encoding issues for proper UTF-8 support
- Enhance administration interface with Verpachtung CSV import

This implements the complete Verpachtung management feature requested,
allowing users to manage land lease agreements with proper relationships
to properties (Laenderei) and tenants (Paechter), following the same
patterns as Foerderung/Destinataer relationships.
This commit is contained in:
Stiftung Development
2025-09-15 21:18:01 +02:00
parent c8bef800c8
commit 4be6be203e
14 changed files with 1743 additions and 127 deletions

View File

@@ -6,7 +6,7 @@ from django.utils import timezone
from .models import (BankTransaction, Destinataer, DestinataerNotiz,
DestinataerUnterstuetzung, DokumentLink, Foerderung, Land,
LandAbrechnung, Paechter, Person, Rentmeister,
LandAbrechnung, LandVerpachtung, Paechter, Person, Rentmeister,
StiftungsKonto, UnterstuetzungWiederkehrend,
Verwaltungskosten)
@@ -534,6 +534,53 @@ class LandForm(forms.ModelForm):
}
class LandVerpachtungForm(forms.ModelForm):
"""Form für das Erstellen und Bearbeiten von Verpachtungen"""
class Meta:
model = LandVerpachtung
fields = [
'land',
'paechter',
'vertragsnummer',
'pachtbeginn',
'pachtende',
'verlaengerung_klausel',
'verpachtete_flaeche',
'pachtzins_pauschal',
'pachtzins_pro_ha',
'zahlungsweise',
'ust_option',
'ust_satz',
'grundsteuer_umlage',
'versicherungen_umlage',
'verbandsbeitraege_umlage',
'jagdpacht_anteil_umlage',
'status',
'bemerkungen'
]
widgets = {
'land': forms.Select(attrs={'class': 'form-select'}),
'paechter': forms.Select(attrs={'class': 'form-select'}),
'vertragsnummer': forms.TextInput(attrs={'class': 'form-control'}),
'pachtbeginn': forms.DateInput(attrs={'class': 'form-control', 'type': 'date'}),
'pachtende': forms.DateInput(attrs={'class': 'form-control', 'type': 'date'}),
'verlaengerung_klausel': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
'verpachtete_flaeche': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.01'}),
'pachtzins_pauschal': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.01'}),
'pachtzins_pro_ha': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.01'}),
'zahlungsweise': forms.Select(attrs={'class': 'form-select'}),
'ust_option': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
'ust_satz': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.01'}),
'grundsteuer_umlage': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
'versicherungen_umlage': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
'verbandsbeitraege_umlage': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
'jagdpacht_anteil_umlage': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
'status': forms.Select(attrs={'class': 'form-select'}),
'bemerkungen': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}),
}
class LandAbrechnungForm(forms.ModelForm):
"""Form für das Erstellen und Bearbeiten von Landabrechnungen"""

View File

@@ -109,6 +109,20 @@ urlpatterns = [
views.land_verpachtung_end_direct,
name="land_verpachtung_end_direct",
),
# Verpachtung URLs (Management Overview)
path("verpachtungen/", views.verpachtung_list, name="verpachtung_list"),
path("verpachtungen/<uuid:pk>/", views.verpachtung_detail, name="verpachtung_detail"),
path("verpachtungen/neu/", views.verpachtung_create, name="verpachtung_create"),
path(
"verpachtungen/<uuid:pk>/bearbeiten/",
views.verpachtung_update,
name="verpachtung_update",
),
path(
"verpachtungen/<uuid:pk>/loeschen/",
views.verpachtung_delete,
name="verpachtung_delete",
),
# Förderung URLs
path("foerderungen/", views.foerderung_list, name="foerderung_list"),
path("foerderungen/<uuid:pk>/", views.foerderung_detail, name="foerderung_detail"),

View File

@@ -1689,7 +1689,7 @@ def verpachtung_list(request):
"beginn": ["pachtbeginn"],
"ende": ["pachtende"],
"flaeche": ["verpachtete_flaeche"],
"pachtzins": ["pachtzins_jaehrlich"],
"pachtzins": ["pachtzins_pauschal"],
"status": ["status"],
}
if sort in sort_map:
@@ -1723,7 +1723,7 @@ def verpachtung_list(request):
# Total annual rent (only active verpachtungen)
jaehrlicher_pachtzins_result = all_verpachtungen.filter(status="aktiv").aggregate(
total=Sum("pachtzins_jaehrlich")
total=Sum("pachtzins_pauschal")
)
jaehrlicher_pachtzins = (
jaehrlicher_pachtzins_result["total"]
@@ -1787,7 +1787,7 @@ def land_verpachtung_update(request, pk):
vertragsnummer = request.POST.get("vertragsnummer")
pachtbeginn = request.POST.get("pachtbeginn")
pachtende = request.POST.get("pachtende")
pachtzins_jaehrlich = request.POST.get("pachtzins_jaehrlich")
pachtzins_pauschal = request.POST.get("pachtzins_pauschal")
if vertragsnummer:
verpachtung.vertragsnummer = vertragsnummer
@@ -1795,8 +1795,8 @@ def land_verpachtung_update(request, pk):
verpachtung.pachtbeginn = pachtbeginn
if pachtende:
verpachtung.pachtende = pachtende
if pachtzins_jaehrlich:
verpachtung.pachtzins_jaehrlich = pachtzins_jaehrlich
if pachtzins_pauschal:
verpachtung.pachtzins_pauschal = pachtzins_pauschal
verpachtung.save()
messages.success(request, "Verpachtung wurde erfolgreich aktualisiert.")
@@ -2130,7 +2130,11 @@ def dokument_create(request):
)
# Zurück zur verknüpften Entität leiten
if dokument.verpachtung_id:
if dokument.land_verpachtung_id:
return redirect(
"stiftung:verpachtung_detail", pk=dokument.land_verpachtung_id
)
elif dokument.verpachtung_id:
return redirect(
"stiftung:verpachtung_detail", pk=dokument.verpachtung_id
)
@@ -2149,6 +2153,8 @@ def dokument_create(request):
else:
# Initial-Werte aus GET-Parametern setzen
initial_data = {}
if request.GET.get("land_verpachtung_id"):
initial_data["land_verpachtung_id"] = request.GET.get("land_verpachtung_id")
if request.GET.get("verpachtung"):
initial_data["verpachtung_id"] = request.GET.get("verpachtung")
if request.GET.get("land"):
@@ -5435,7 +5441,7 @@ def verpachtung_export(request, pk):
else None
),
"pachtzins_pro_qm": str(verpachtung.pachtzins_pro_qm),
"pachtzins_jaehrlich": str(verpachtung.pachtzins_jaehrlich),
"pachtzins_jaehrlich": str(verpachtung.pachtzins_pauschal),
"verpachtete_flaeche": str(verpachtung.verpachtete_flaeche),
"status": verpachtung.get_status_display(),
"verwendungsnachweis": (
@@ -6914,3 +6920,126 @@ def edit_help_box(request):
"title": "Hilfs-Infoboxen verwalten",
}
return render(request, "stiftung/help_boxes_admin.html", context)
# =============================================================================
# Verpachtung Management Views (Standalone CRUD)
# =============================================================================
@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 .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)