- Set initial value for disabled faellig_am field in auto-generated payments - Explicitly set widget value attribute to display current date - Ensures the field shows the correct date even when disabled
1597 lines
61 KiB
Python
1597 lines
61 KiB
Python
import re
|
|
|
|
from django import forms
|
|
from django.core.exceptions import ValidationError
|
|
from django.utils import timezone
|
|
|
|
from .models import (BankTransaction, Destinataer, DestinataerNotiz,
|
|
DestinataerUnterstuetzung, DokumentLink, Foerderung, Land,
|
|
LandAbrechnung, LandVerpachtung, Paechter, Person, Rentmeister,
|
|
StiftungsKonto, UnterstuetzungWiederkehrend,
|
|
Verwaltungskosten, VierteljahresNachweis)
|
|
|
|
|
|
class RentmeisterForm(forms.ModelForm):
|
|
"""Form für das Erstellen und Bearbeiten von Rentmeistern"""
|
|
|
|
class Meta:
|
|
model = Rentmeister
|
|
fields = [
|
|
"anrede",
|
|
"vorname",
|
|
"nachname",
|
|
"titel",
|
|
"email",
|
|
"telefon",
|
|
"mobil",
|
|
"strasse",
|
|
"plz",
|
|
"ort",
|
|
"iban",
|
|
"bic",
|
|
"bank_name",
|
|
"seit_datum",
|
|
"bis_datum",
|
|
"aktiv",
|
|
"monatliche_verguetung",
|
|
"km_pauschale",
|
|
"notizen",
|
|
]
|
|
|
|
widgets = {
|
|
"anrede": forms.Select(attrs={"class": "form-select"}),
|
|
"vorname": forms.TextInput(attrs={"class": "form-control"}),
|
|
"nachname": forms.TextInput(attrs={"class": "form-control"}),
|
|
"titel": forms.TextInput(attrs={"class": "form-control"}),
|
|
"email": forms.EmailInput(attrs={"class": "form-control"}),
|
|
"telefon": forms.TextInput(attrs={"class": "form-control"}),
|
|
"mobil": forms.TextInput(attrs={"class": "form-control"}),
|
|
"strasse": forms.TextInput(attrs={"class": "form-control"}),
|
|
"plz": forms.TextInput(attrs={"class": "form-control"}),
|
|
"ort": forms.TextInput(attrs={"class": "form-control"}),
|
|
"iban": forms.TextInput(
|
|
attrs={"class": "form-control", "placeholder": "DE89370400440532013000"}
|
|
),
|
|
"bic": forms.TextInput(
|
|
attrs={"class": "form-control", "placeholder": "COBADEFFXXX"}
|
|
),
|
|
"bank_name": forms.TextInput(attrs={"class": "form-control"}),
|
|
"seit_datum": forms.DateInput(
|
|
attrs={"class": "form-control", "type": "date"}
|
|
),
|
|
"bis_datum": forms.DateInput(
|
|
attrs={"class": "form-control", "type": "date"}
|
|
),
|
|
"aktiv": forms.CheckboxInput(attrs={"class": "form-check-input"}),
|
|
"monatliche_verguetung": forms.NumberInput(
|
|
attrs={"class": "form-control", "step": "0.01"}
|
|
),
|
|
"km_pauschale": forms.NumberInput(
|
|
attrs={"class": "form-control", "step": "0.01", "value": "0.30"}
|
|
),
|
|
"notizen": forms.Textarea(attrs={"class": "form-control", "rows": 4}),
|
|
}
|
|
|
|
labels = {
|
|
"anrede": "Anrede",
|
|
"vorname": "Vorname *",
|
|
"nachname": "Nachname *",
|
|
"titel": "Titel",
|
|
"email": "E-Mail",
|
|
"telefon": "Telefon",
|
|
"mobil": "Mobil",
|
|
"strasse": "Straße",
|
|
"plz": "PLZ",
|
|
"ort": "Ort",
|
|
"iban": "IBAN",
|
|
"bic": "BIC",
|
|
"bank_name": "Bank",
|
|
"seit_datum": "Rentmeister seit *",
|
|
"bis_datum": "Rentmeister bis",
|
|
"aktiv": "Aktiv",
|
|
"monatliche_verguetung": "Monatliche Vergütung (€)",
|
|
"km_pauschale": "Kilometerpauschale (€/km)",
|
|
"notizen": "Notizen",
|
|
}
|
|
|
|
help_texts = {
|
|
"iban": "Internationale Bankkontonummer für Abrechnungen",
|
|
"km_pauschale": "Standard: 0,30 € pro Kilometer",
|
|
"seit_datum": "Datum des Amtsantritts als Rentmeister",
|
|
"bis_datum": "Leer lassen für aktive Rentmeister",
|
|
}
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
super().__init__(*args, **kwargs)
|
|
# Markiere Pflichtfelder
|
|
self.fields["vorname"].required = True
|
|
self.fields["nachname"].required = True
|
|
self.fields["seit_datum"].required = True
|
|
|
|
def clean_iban(self):
|
|
"""Validierung der IBAN"""
|
|
iban = self.cleaned_data.get("iban")
|
|
if iban:
|
|
# Entferne Leerzeichen und konvertiere zu Großbuchstaben
|
|
iban = re.sub(r"\s+", "", iban.upper())
|
|
|
|
# Einfache IBAN-Längenvalidierung für deutsche IBANs
|
|
if iban.startswith("DE") and len(iban) != 22:
|
|
raise ValidationError("Deutsche IBANs müssen 22 Zeichen lang sein.")
|
|
|
|
# Speichere die bereinigte IBAN
|
|
return iban
|
|
return iban
|
|
|
|
def clean_plz(self):
|
|
"""Validierung der PLZ"""
|
|
plz = self.cleaned_data.get("plz")
|
|
if plz and not re.match(r"^\d{5}$", plz):
|
|
raise ValidationError("PLZ muss aus 5 Ziffern bestehen.")
|
|
return plz
|
|
|
|
def clean(self):
|
|
"""Übergreifende Validierung"""
|
|
from django.utils.dateparse import parse_date
|
|
|
|
cleaned_data = super().clean()
|
|
seit_datum = cleaned_data.get("seit_datum")
|
|
bis_datum = cleaned_data.get("bis_datum")
|
|
|
|
# Helper function to ensure we have date objects
|
|
def ensure_date(date_value):
|
|
if not date_value:
|
|
return None
|
|
if isinstance(date_value, str):
|
|
return parse_date(date_value)
|
|
return date_value
|
|
|
|
# Convert to date objects if they're strings
|
|
seit_datum = ensure_date(seit_datum)
|
|
bis_datum = ensure_date(bis_datum)
|
|
|
|
# Prüfe Datum-Logik
|
|
if seit_datum and bis_datum and bis_datum <= seit_datum:
|
|
raise ValidationError("Das End-Datum muss nach dem Start-Datum liegen.")
|
|
|
|
return cleaned_data
|
|
|
|
|
|
class StiftungsKontoForm(forms.ModelForm):
|
|
"""Form für das Erstellen und Bearbeiten von Stiftungskonten"""
|
|
|
|
class Meta:
|
|
model = StiftungsKonto
|
|
fields = [
|
|
"kontoname",
|
|
"bank_name",
|
|
"iban",
|
|
"bic",
|
|
"konto_typ",
|
|
"saldo",
|
|
"saldo_datum",
|
|
"zinssatz",
|
|
"laufzeit_bis",
|
|
"aktiv",
|
|
"notizen",
|
|
]
|
|
|
|
widgets = {
|
|
"kontoname": forms.TextInput(attrs={"class": "form-control"}),
|
|
"bank_name": forms.TextInput(attrs={"class": "form-control"}),
|
|
"iban": forms.TextInput(
|
|
attrs={"class": "form-control", "placeholder": "DE89370400440532013000"}
|
|
),
|
|
"bic": forms.TextInput(
|
|
attrs={"class": "form-control", "placeholder": "COBADEFFXXX"}
|
|
),
|
|
"konto_typ": forms.Select(attrs={"class": "form-select"}),
|
|
"saldo": forms.NumberInput(attrs={"class": "form-control", "step": "0.01"}),
|
|
"saldo_datum": forms.DateInput(
|
|
attrs={"class": "form-control", "type": "date"}
|
|
),
|
|
"zinssatz": forms.NumberInput(
|
|
attrs={"class": "form-control", "step": "0.01"}
|
|
),
|
|
"laufzeit_bis": forms.DateInput(
|
|
attrs={"class": "form-control", "type": "date"}
|
|
),
|
|
"aktiv": forms.CheckboxInput(attrs={"class": "form-check-input"}),
|
|
"notizen": forms.Textarea(attrs={"class": "form-control", "rows": 3}),
|
|
}
|
|
|
|
|
|
class VerwaltungskostenForm(forms.ModelForm):
|
|
"""Form für das Erstellen und Bearbeiten von Verwaltungskosten"""
|
|
|
|
class Meta:
|
|
model = Verwaltungskosten
|
|
fields = [
|
|
"bezeichnung",
|
|
"kategorie",
|
|
"betrag",
|
|
"datum",
|
|
"status",
|
|
"rentmeister",
|
|
"zahlungskonto",
|
|
"quellkonto",
|
|
"lieferant_firma",
|
|
"rechnungsnummer",
|
|
"km_anzahl",
|
|
"km_satz",
|
|
"von_ort",
|
|
"nach_ort",
|
|
"zweck",
|
|
"beschreibung",
|
|
"notizen",
|
|
]
|
|
|
|
widgets = {
|
|
"bezeichnung": forms.TextInput(attrs={"class": "form-control"}),
|
|
"kategorie": forms.Select(attrs={"class": "form-select"}),
|
|
"betrag": forms.NumberInput(
|
|
attrs={"class": "form-control", "step": "0.01"}
|
|
),
|
|
"datum": forms.DateInput(attrs={"class": "form-control", "type": "date"}),
|
|
"status": forms.Select(attrs={"class": "form-select"}),
|
|
"rentmeister": forms.Select(attrs={"class": "form-select"}),
|
|
"zahlungskonto": forms.Select(attrs={"class": "form-select"}),
|
|
"quellkonto": forms.Select(attrs={"class": "form-select"}),
|
|
"lieferant_firma": forms.TextInput(attrs={"class": "form-control"}),
|
|
"rechnungsnummer": forms.TextInput(attrs={"class": "form-control"}),
|
|
"km_anzahl": forms.NumberInput(
|
|
attrs={"class": "form-control", "step": "0.1"}
|
|
),
|
|
"km_satz": forms.NumberInput(
|
|
attrs={"class": "form-control", "step": "0.01"}
|
|
),
|
|
"von_ort": forms.TextInput(attrs={"class": "form-control"}),
|
|
"nach_ort": forms.TextInput(attrs={"class": "form-control"}),
|
|
"zweck": forms.TextInput(attrs={"class": "form-control"}),
|
|
"beschreibung": forms.Textarea(attrs={"class": "form-control", "rows": 3}),
|
|
"notizen": forms.Textarea(attrs={"class": "form-control", "rows": 2}),
|
|
}
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
super().__init__(*args, **kwargs)
|
|
# Filtere nur aktive Rentmeister und Konten
|
|
self.fields["rentmeister"].queryset = Rentmeister.objects.filter(aktiv=True)
|
|
self.fields["zahlungskonto"].queryset = StiftungsKonto.objects.filter(
|
|
aktiv=True
|
|
)
|
|
self.fields["quellkonto"].queryset = StiftungsKonto.objects.filter(aktiv=True)
|
|
|
|
# Standardwerte setzen
|
|
if not self.instance.pk: # Nur bei neuen Objekten
|
|
# Standard km_satz auf 0.30 Euro setzen
|
|
self.fields["km_satz"].initial = 0.30
|
|
|
|
|
|
class PersonForm(forms.ModelForm):
|
|
"""Form für das Erstellen und Bearbeiten von Personen (Legacy)"""
|
|
|
|
class Meta:
|
|
model = Person
|
|
fields = [
|
|
"familienzweig",
|
|
"vorname",
|
|
"nachname",
|
|
"geburtsdatum",
|
|
"email",
|
|
"telefon",
|
|
"iban",
|
|
"adresse",
|
|
"notizen",
|
|
"aktiv",
|
|
]
|
|
|
|
widgets = {
|
|
"familienzweig": forms.Select(attrs={"class": "form-select"}),
|
|
"vorname": forms.TextInput(attrs={"class": "form-control"}),
|
|
"nachname": forms.TextInput(attrs={"class": "form-control"}),
|
|
"geburtsdatum": forms.DateInput(
|
|
attrs={"class": "form-control", "type": "date"}
|
|
),
|
|
"email": forms.EmailInput(attrs={"class": "form-control"}),
|
|
"telefon": forms.TextInput(attrs={"class": "form-control"}),
|
|
"iban": forms.TextInput(
|
|
attrs={"class": "form-control", "placeholder": "DE89370400440532013000"}
|
|
),
|
|
"adresse": forms.Textarea(attrs={"class": "form-control", "rows": 3}),
|
|
"notizen": forms.Textarea(attrs={"class": "form-control", "rows": 3}),
|
|
"aktiv": forms.CheckboxInput(attrs={"class": "form-check-input"}),
|
|
}
|
|
|
|
labels = {
|
|
"familienzweig": "Familienzweig",
|
|
"vorname": "Vorname *",
|
|
"nachname": "Nachname *",
|
|
"geburtsdatum": "Geburtsdatum",
|
|
"email": "E-Mail",
|
|
"telefon": "Telefon",
|
|
"iban": "IBAN",
|
|
"adresse": "Adresse",
|
|
"notizen": "Notizen",
|
|
"aktiv": "Aktiv",
|
|
}
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
super().__init__(*args, **kwargs)
|
|
# Markiere Pflichtfelder
|
|
self.fields["vorname"].required = True
|
|
self.fields["nachname"].required = True
|
|
|
|
|
|
class PaechterForm(forms.ModelForm):
|
|
"""Form für das Erstellen und Bearbeiten von Pächtern"""
|
|
|
|
class Meta:
|
|
model = Paechter
|
|
fields = "__all__"
|
|
widgets = {
|
|
"anrede": forms.Select(attrs={"class": "form-select"}),
|
|
"vorname": forms.TextInput(attrs={"class": "form-control"}),
|
|
"nachname": forms.TextInput(attrs={"class": "form-control"}),
|
|
"email": forms.EmailInput(attrs={"class": "form-control"}),
|
|
"telefon": forms.TextInput(attrs={"class": "form-control"}),
|
|
"mobil": forms.TextInput(attrs={"class": "form-control"}),
|
|
"geburtsdatum": forms.DateInput(
|
|
attrs={"class": "form-control", "type": "date"}
|
|
),
|
|
"strasse": forms.TextInput(attrs={"class": "form-control"}),
|
|
"plz": forms.TextInput(attrs={"class": "form-control"}),
|
|
"ort": forms.TextInput(attrs={"class": "form-control"}),
|
|
"aktiv": forms.CheckboxInput(attrs={"class": "form-check-input"}),
|
|
"notizen": forms.Textarea(attrs={"class": "form-control", "rows": 3}),
|
|
}
|
|
|
|
|
|
class DestinataerForm(forms.ModelForm):
|
|
"""Form für das Erstellen und Bearbeiten von Destinatären"""
|
|
|
|
class Meta:
|
|
model = Destinataer
|
|
fields = "__all__"
|
|
widgets = {
|
|
"anrede": forms.Select(attrs={"class": "form-select"}),
|
|
"vorname": forms.TextInput(attrs={"class": "form-control"}),
|
|
"nachname": forms.TextInput(attrs={"class": "form-control"}),
|
|
"titel": forms.TextInput(attrs={"class": "form-control"}),
|
|
"strasse": forms.TextInput(attrs={"class": "form-control"}),
|
|
"plz": forms.TextInput(attrs={"class": "form-control"}),
|
|
"ort": forms.TextInput(attrs={"class": "form-control"}),
|
|
"telefon": forms.TextInput(attrs={"class": "form-control"}),
|
|
"mobil": forms.TextInput(attrs={"class": "form-control"}),
|
|
"email": forms.EmailInput(attrs={"class": "form-control"}),
|
|
"aktiv": forms.CheckboxInput(attrs={"class": "form-check-input"}),
|
|
"notizen": forms.Textarea(attrs={"class": "form-control", "rows": 3}),
|
|
"ist_abkoemmling": forms.CheckboxInput(attrs={"class": "form-check-input"}),
|
|
"haushaltsgroesse": forms.NumberInput(
|
|
attrs={"class": "form-control", "min": 1}
|
|
),
|
|
"vermoegen": forms.NumberInput(
|
|
attrs={"class": "form-control", "step": "0.01"}
|
|
),
|
|
"unterstuetzung_bestaetigt": forms.CheckboxInput(
|
|
attrs={"class": "form-check-input"}
|
|
),
|
|
"standard_konto": forms.Select(attrs={"class": "form-select"}, choices=[(None, "---")] + [(c.pk, str(c)) for c in getattr(Destinataer, 'konten_queryset', lambda: [])()]),
|
|
"vierteljaehrlicher_betrag": forms.NumberInput(
|
|
attrs={"class": "form-control", "step": "0.01"}
|
|
),
|
|
"studiennachweis_erforderlich": forms.CheckboxInput(
|
|
attrs={"class": "form-check-input"}
|
|
),
|
|
"letzter_studiennachweis": forms.DateInput(
|
|
attrs={"class": "form-control", "type": "date"}
|
|
),
|
|
"familienzweig": forms.Select(attrs={"class": "form-select"}),
|
|
"berufsgruppe": forms.Select(attrs={"class": "form-select"}),
|
|
}
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
super().__init__(*args, **kwargs)
|
|
for field_name, field in self.fields.items():
|
|
if field_name not in ["vorname", "nachname"]:
|
|
field.required = False
|
|
# Set choices for familienzweig and berufsgruppe to match model
|
|
self.fields["familienzweig"].choices = [("", "Bitte wählen...")] + list(Destinataer.FAMILIENZWIG_CHOICES)
|
|
self.fields["berufsgruppe"].choices = [("", "Bitte wählen...")] + list(Destinataer.BERUFSGRUPPE_CHOICES)
|
|
# Set choices for standard_konto to allow blank
|
|
self.fields["standard_konto"].empty_label = "---"
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
super().__init__(*args, **kwargs)
|
|
for field_name, field in self.fields.items():
|
|
if field_name not in ["vorname", "nachname"]:
|
|
field.required = False
|
|
|
|
|
|
class LandForm(forms.ModelForm):
|
|
"""Form für das Erstellen und Bearbeiten von Ländern"""
|
|
|
|
class Meta:
|
|
model = Land
|
|
fields = [
|
|
# Grundlegende Identifikation
|
|
"lfd_nr",
|
|
"ew_nummer",
|
|
"grundbuchblatt",
|
|
# Gerichtliche Zuständigkeit
|
|
"amtsgericht",
|
|
# Verwaltungsstruktur
|
|
"gemeinde",
|
|
"gemarkung",
|
|
"flur",
|
|
"flurstueck",
|
|
"adresse",
|
|
# Flächenangaben
|
|
"groesse_qm",
|
|
"gruenland_qm",
|
|
"acker_qm",
|
|
"wald_qm",
|
|
"sonstiges_qm",
|
|
# Legacy Verpachtung (für Kompatibilität)
|
|
"verpachtete_gesamtflaeche",
|
|
"flaeche_alte_liste",
|
|
"verp_flaeche_aktuell",
|
|
# Aktuelle Verpachtung
|
|
"aktueller_paechter",
|
|
"paechter_name",
|
|
"paechter_anschrift",
|
|
"pachtbeginn",
|
|
"pachtende",
|
|
"verlaengerung_klausel",
|
|
"zahlungsweise",
|
|
"pachtzins_pro_ha",
|
|
"pachtzins_pauschal",
|
|
# Umsatzsteuer
|
|
"ust_option",
|
|
"ust_satz",
|
|
# Umlagen
|
|
"grundsteuer_umlage",
|
|
"versicherungen_umlage",
|
|
"verbandsbeitraege_umlage",
|
|
"jagdpacht_anteil_umlage",
|
|
# Legacy Steuern
|
|
"anteil_grundsteuer",
|
|
"anteil_lwk",
|
|
# Status
|
|
"aktiv",
|
|
"notizen",
|
|
]
|
|
widgets = {
|
|
# Grundlegende Identifikation
|
|
"lfd_nr": forms.TextInput(attrs={"class": "form-control"}),
|
|
"ew_nummer": forms.TextInput(attrs={"class": "form-control"}),
|
|
"grundbuchblatt": forms.TextInput(attrs={"class": "form-control"}),
|
|
# Gerichtliche Zuständigkeit
|
|
"amtsgericht": forms.TextInput(attrs={"class": "form-control"}),
|
|
# Verwaltungsstruktur
|
|
"gemeinde": forms.TextInput(attrs={"class": "form-control"}),
|
|
"gemarkung": forms.TextInput(attrs={"class": "form-control"}),
|
|
"flur": forms.TextInput(attrs={"class": "form-control"}),
|
|
"flurstueck": forms.TextInput(attrs={"class": "form-control"}),
|
|
"adresse": forms.TextInput(attrs={"class": "form-control"}),
|
|
# Flächenangaben
|
|
"groesse_qm": forms.NumberInput(
|
|
attrs={"class": "form-control", "step": "0.01"}
|
|
),
|
|
"gruenland_qm": forms.NumberInput(
|
|
attrs={"class": "form-control", "step": "0.01"}
|
|
),
|
|
"acker_qm": forms.NumberInput(
|
|
attrs={"class": "form-control", "step": "0.01"}
|
|
),
|
|
"wald_qm": forms.NumberInput(
|
|
attrs={"class": "form-control", "step": "0.01"}
|
|
),
|
|
"sonstiges_qm": forms.NumberInput(
|
|
attrs={"class": "form-control", "step": "0.01"}
|
|
),
|
|
# Legacy Verpachtung
|
|
"verpachtete_gesamtflaeche": forms.NumberInput(
|
|
attrs={"class": "form-control", "step": "0.01"}
|
|
),
|
|
"flaeche_alte_liste": forms.NumberInput(
|
|
attrs={"class": "form-control", "step": "0.01"}
|
|
),
|
|
"verp_flaeche_aktuell": forms.NumberInput(
|
|
attrs={"class": "form-control", "step": "0.01"}
|
|
),
|
|
# Aktuelle Verpachtung
|
|
"aktueller_paechter": forms.Select(attrs={"class": "form-select"}),
|
|
"paechter_name": forms.TextInput(attrs={"class": "form-control"}),
|
|
"paechter_anschrift": forms.Textarea(
|
|
attrs={"class": "form-control", "rows": 3}
|
|
),
|
|
"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"}
|
|
),
|
|
"zahlungsweise": forms.Select(attrs={"class": "form-select"}),
|
|
"pachtzins_pro_ha": forms.NumberInput(
|
|
attrs={"class": "form-control", "step": "0.01"}
|
|
),
|
|
"pachtzins_pauschal": forms.NumberInput(
|
|
attrs={"class": "form-control", "step": "0.01"}
|
|
),
|
|
# Umsatzsteuer
|
|
"ust_option": forms.CheckboxInput(attrs={"class": "form-check-input"}),
|
|
"ust_satz": forms.NumberInput(
|
|
attrs={"class": "form-control", "step": "0.01"}
|
|
),
|
|
# Umlagen
|
|
"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"}
|
|
),
|
|
# Legacy
|
|
"anteil_grundsteuer": forms.NumberInput(
|
|
attrs={"class": "form-control", "step": "0.01"}
|
|
),
|
|
"anteil_lwk": forms.NumberInput(
|
|
attrs={"class": "form-control", "step": "0.01"}
|
|
),
|
|
# Status
|
|
"aktiv": forms.CheckboxInput(attrs={"class": "form-check-input"}),
|
|
"notizen": forms.Textarea(attrs={"class": "form-control", "rows": 3}),
|
|
}
|
|
|
|
|
|
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"""
|
|
|
|
class Meta:
|
|
model = LandAbrechnung
|
|
fields = [
|
|
"land",
|
|
"abrechnungsjahr",
|
|
# Einnahmen
|
|
"pacht_vereinnahmt",
|
|
"umlagen_vereinnahmt",
|
|
"sonstige_einnahmen",
|
|
# Ausgaben
|
|
"grundsteuer_bescheid_nr",
|
|
"grundsteuer_betrag",
|
|
"versicherungen_betrag",
|
|
"verbandsbeitraege_betrag",
|
|
"sonstige_abgaben_betrag",
|
|
"instandhaltung_betrag",
|
|
"verwaltung_recht_betrag",
|
|
# Umsatzsteuer
|
|
"vorsteuer_aus_umlagen",
|
|
# Sonstiges
|
|
"offene_posten",
|
|
"bemerkungen",
|
|
# Dokumente werden über Paperless verknüpft, nicht hochgeladen
|
|
]
|
|
widgets = {
|
|
"land": forms.Select(attrs={"class": "form-select"}),
|
|
"abrechnungsjahr": forms.NumberInput(
|
|
attrs={"class": "form-control", "min": "2000", "max": "2050"}
|
|
),
|
|
# Einnahmen
|
|
"pacht_vereinnahmt": forms.NumberInput(
|
|
attrs={"class": "form-control", "step": "0.01"}
|
|
),
|
|
"umlagen_vereinnahmt": forms.NumberInput(
|
|
attrs={"class": "form-control", "step": "0.01"}
|
|
),
|
|
"sonstige_einnahmen": forms.NumberInput(
|
|
attrs={"class": "form-control", "step": "0.01"}
|
|
),
|
|
# Ausgaben
|
|
"grundsteuer_bescheid_nr": forms.TextInput(attrs={"class": "form-control"}),
|
|
"grundsteuer_betrag": forms.NumberInput(
|
|
attrs={"class": "form-control", "step": "0.01"}
|
|
),
|
|
"versicherungen_betrag": forms.NumberInput(
|
|
attrs={"class": "form-control", "step": "0.01"}
|
|
),
|
|
"verbandsbeitraege_betrag": forms.NumberInput(
|
|
attrs={"class": "form-control", "step": "0.01"}
|
|
),
|
|
"sonstige_abgaben_betrag": forms.NumberInput(
|
|
attrs={"class": "form-control", "step": "0.01"}
|
|
),
|
|
"instandhaltung_betrag": forms.NumberInput(
|
|
attrs={"class": "form-control", "step": "0.01"}
|
|
),
|
|
"verwaltung_recht_betrag": forms.NumberInput(
|
|
attrs={"class": "form-control", "step": "0.01"}
|
|
),
|
|
# Umsatzsteuer
|
|
"vorsteuer_aus_umlagen": forms.NumberInput(
|
|
attrs={"class": "form-control", "step": "0.01"}
|
|
),
|
|
# Sonstiges
|
|
"offene_posten": forms.NumberInput(
|
|
attrs={"class": "form-control", "step": "0.01"}
|
|
),
|
|
"bemerkungen": forms.Textarea(attrs={"class": "form-control", "rows": 4}),
|
|
}
|
|
|
|
|
|
class DokumentLinkForm(forms.ModelForm):
|
|
"""Form für das Erstellen und Bearbeiten von Dokumentverknüpfungen"""
|
|
|
|
class Meta:
|
|
model = DokumentLink
|
|
fields = "__all__"
|
|
widgets = {
|
|
"paperless_id": forms.NumberInput(attrs={"class": "form-control"}),
|
|
"content_type": forms.Select(attrs={"class": "form-select"}),
|
|
"object_id": forms.TextInput(attrs={"class": "form-control"}),
|
|
"verknuepft_am": forms.DateTimeInput(
|
|
attrs={"class": "form-control", "type": "datetime-local"}
|
|
),
|
|
}
|
|
|
|
|
|
class FoerderungForm(forms.ModelForm):
|
|
"""Form für das Erstellen und Bearbeiten von Förderungen"""
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
super().__init__(*args, **kwargs)
|
|
# Add empty option for optional fields
|
|
self.fields["verwendungsnachweis"].empty_label = (
|
|
"--- Kein Dokument verknüpfen ---"
|
|
)
|
|
# Ensure destinataer has proper choices
|
|
from django.utils import timezone
|
|
|
|
from .models import Destinataer, DokumentLink
|
|
|
|
self.fields["destinataer"].queryset = Destinataer.objects.all().order_by(
|
|
"nachname", "vorname"
|
|
)
|
|
self.fields["verwendungsnachweis"].queryset = (
|
|
DokumentLink.objects.all().order_by("titel")
|
|
)
|
|
# Set current year as default for new forms
|
|
if not self.instance.pk:
|
|
self.fields["jahr"].initial = timezone.now().year
|
|
|
|
class Meta:
|
|
model = Foerderung
|
|
fields = [
|
|
"destinataer",
|
|
"jahr",
|
|
"betrag",
|
|
"kategorie",
|
|
"status",
|
|
"antragsdatum",
|
|
"entscheidungsdatum",
|
|
"verwendungsnachweis",
|
|
"bemerkungen",
|
|
]
|
|
widgets = {
|
|
"destinataer": forms.Select(attrs={"class": "form-select"}),
|
|
"jahr": forms.NumberInput(attrs={"class": "form-control"}),
|
|
"betrag": forms.NumberInput(
|
|
attrs={"class": "form-control", "step": "0.01"}
|
|
),
|
|
"kategorie": forms.Select(attrs={"class": "form-select"}),
|
|
"status": forms.Select(attrs={"class": "form-select"}),
|
|
"antragsdatum": forms.DateInput(
|
|
attrs={"class": "form-control", "type": "date"}
|
|
),
|
|
"entscheidungsdatum": forms.DateInput(
|
|
attrs={"class": "form-control", "type": "date"}
|
|
),
|
|
"verwendungsnachweis": forms.Select(attrs={"class": "form-select"}),
|
|
"bemerkungen": forms.Textarea(attrs={"class": "form-control", "rows": 3}),
|
|
}
|
|
|
|
labels = {
|
|
"destinataer": "Destinatär",
|
|
"verwendungsnachweis": "Verknüpftes Dokument",
|
|
"bemerkungen": "Bemerkungen/Beschreibung",
|
|
"antragsdatum": "Antragsdatum",
|
|
"entscheidungsdatum": "Entscheidungsdatum",
|
|
}
|
|
|
|
help_texts = {
|
|
"verwendungsnachweis": "Optionale Verknüpfung zu einem Dokument aus dem Paperless-System",
|
|
"entscheidungsdatum": "Datum der Bewilligung/Ablehnung (optional)",
|
|
"bemerkungen": "Zusätzliche Informationen zur Förderung",
|
|
}
|
|
|
|
|
|
class UnterstuetzungForm(forms.ModelForm):
|
|
"""Form für das Erstellen und Bearbeiten von Unterstützungen"""
|
|
|
|
# Special field for creating recurring payments
|
|
ist_wiederkehrend = forms.BooleanField(
|
|
required=False,
|
|
label="Wiederkehrende Zahlung",
|
|
help_text="Aktivieren Sie diese Option um automatisch wiederkehrende Zahlungen zu erstellen",
|
|
)
|
|
intervall = forms.ChoiceField(
|
|
choices=[("", "--- Wählen Sie ein Intervall ---")]
|
|
+ UnterstuetzungWiederkehrend.INTERVALL_CHOICES,
|
|
required=False,
|
|
widget=forms.Select(attrs={"class": "form-select"}),
|
|
label="Zahlungsintervall",
|
|
)
|
|
letzte_zahlung_am = forms.DateField(
|
|
required=False,
|
|
widget=forms.DateInput(attrs={"class": "form-control", "type": "date"}),
|
|
label="Letzte Zahlung am (optional)",
|
|
help_text="Leer lassen für unbegrenzte Wiederholung",
|
|
)
|
|
|
|
class Meta:
|
|
model = DestinataerUnterstuetzung
|
|
fields = [
|
|
"destinataer",
|
|
"konto",
|
|
"faellig_am",
|
|
"betrag",
|
|
"status",
|
|
"beschreibung",
|
|
"empfaenger_iban",
|
|
"empfaenger_name",
|
|
"verwendungszweck",
|
|
]
|
|
|
|
widgets = {
|
|
"destinataer": forms.Select(attrs={"class": "form-select"}),
|
|
"konto": forms.Select(attrs={"class": "form-select"}),
|
|
"faellig_am": forms.DateInput(
|
|
attrs={"class": "form-control", "type": "date"}
|
|
),
|
|
"betrag": forms.NumberInput(
|
|
attrs={"class": "form-control", "step": "0.01"}
|
|
),
|
|
"status": forms.Select(attrs={"class": "form-select"}),
|
|
"beschreibung": forms.TextInput(attrs={"class": "form-control"}),
|
|
"empfaenger_iban": forms.TextInput(
|
|
attrs={
|
|
"class": "form-control",
|
|
"placeholder": "DE89 3704 0044 0532 0130 00",
|
|
}
|
|
),
|
|
"empfaenger_name": forms.TextInput(attrs={"class": "form-control"}),
|
|
"verwendungszweck": forms.TextInput(
|
|
attrs={"class": "form-control", "maxlength": "140"}
|
|
),
|
|
}
|
|
|
|
labels = {
|
|
"destinataer": "Destinatär",
|
|
"konto": "Zahlungskonto",
|
|
"faellig_am": "Fällig am",
|
|
"betrag": "Betrag (€)",
|
|
"status": "Status",
|
|
"beschreibung": "Beschreibung",
|
|
"empfaenger_iban": "Empfänger IBAN",
|
|
"empfaenger_name": "Empfänger Name",
|
|
"verwendungszweck": "Verwendungszweck",
|
|
}
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
super().__init__(*args, **kwargs)
|
|
# Add onchange event to destinataer field for AJAX IBAN fetching
|
|
self.fields["destinataer"].widget.attrs["onchange"] = "updateDestinataerInfo()"
|
|
|
|
def clean(self):
|
|
cleaned_data = super().clean()
|
|
ist_wiederkehrend = cleaned_data.get("ist_wiederkehrend")
|
|
intervall = cleaned_data.get("intervall")
|
|
|
|
if ist_wiederkehrend and not intervall:
|
|
raise forms.ValidationError(
|
|
"Bitte wählen Sie ein Zahlungsintervall für wiederkehrende Zahlungen."
|
|
)
|
|
|
|
return cleaned_data
|
|
|
|
|
|
class UnterstuetzungWiederkehrendForm(forms.ModelForm):
|
|
"""Form für das Bearbeiten von wiederkehrenden Unterstützungsvorlagen"""
|
|
|
|
class Meta:
|
|
model = UnterstuetzungWiederkehrend
|
|
fields = [
|
|
"destinataer",
|
|
"konto",
|
|
"betrag",
|
|
"intervall",
|
|
"beschreibung",
|
|
"empfaenger_iban",
|
|
"empfaenger_name",
|
|
"verwendungszweck",
|
|
"erste_zahlung_am",
|
|
"letzte_zahlung_am",
|
|
"aktiv",
|
|
]
|
|
|
|
widgets = {
|
|
"destinataer": forms.Select(attrs={"class": "form-select"}),
|
|
"konto": forms.Select(attrs={"class": "form-select"}),
|
|
"betrag": forms.NumberInput(
|
|
attrs={"class": "form-control", "step": "0.01"}
|
|
),
|
|
"intervall": forms.Select(attrs={"class": "form-select"}),
|
|
"beschreibung": forms.TextInput(attrs={"class": "form-control"}),
|
|
"empfaenger_iban": forms.TextInput(
|
|
attrs={
|
|
"class": "form-control",
|
|
"placeholder": "DE89 3704 0044 0532 0130 00",
|
|
}
|
|
),
|
|
"empfaenger_name": forms.TextInput(attrs={"class": "form-control"}),
|
|
"verwendungszweck": forms.TextInput(
|
|
attrs={"class": "form-control", "maxlength": "140"}
|
|
),
|
|
"erste_zahlung_am": forms.DateInput(
|
|
attrs={"class": "form-control", "type": "date"}
|
|
),
|
|
"letzte_zahlung_am": forms.DateInput(
|
|
attrs={"class": "form-control", "type": "date"}
|
|
),
|
|
"aktiv": forms.CheckboxInput(attrs={"class": "form-check-input"}),
|
|
}
|
|
|
|
|
|
class UnterstuetzungMarkAsPaidForm(forms.Form):
|
|
"""Simple form to mark an Unterstützung as paid"""
|
|
|
|
ausgezahlt_am = forms.DateField(
|
|
widget=forms.DateInput(attrs={"class": "form-control", "type": "date"}),
|
|
label="Ausgezahlt am",
|
|
initial=timezone.now().date(),
|
|
)
|
|
|
|
bemerkung = forms.CharField(
|
|
widget=forms.Textarea(attrs={"class": "form-control", "rows": 3}),
|
|
label="Bemerkung (optional)",
|
|
required=False,
|
|
help_text="Optionale Notiz zur Zahlung",
|
|
)
|
|
|
|
|
|
class BankTransactionForm(forms.ModelForm):
|
|
"""Form für das Bearbeiten von Banktransaktionen"""
|
|
|
|
class Meta:
|
|
model = BankTransaction
|
|
fields = [
|
|
"konto",
|
|
"datum",
|
|
"valuta",
|
|
"betrag",
|
|
"waehrung",
|
|
"verwendungszweck",
|
|
"empfaenger_zahlungspflichtiger",
|
|
"iban_gegenpartei",
|
|
"bic_gegenpartei",
|
|
"transaction_type",
|
|
"status",
|
|
"kommentare",
|
|
"verwaltungskosten",
|
|
]
|
|
|
|
widgets = {
|
|
"konto": forms.Select(attrs={"class": "form-select"}),
|
|
"datum": forms.DateInput(attrs={"class": "form-control", "type": "date"}),
|
|
"valuta": forms.DateInput(attrs={"class": "form-control", "type": "date"}),
|
|
"betrag": forms.NumberInput(
|
|
attrs={"class": "form-control", "step": "0.01"}
|
|
),
|
|
"waehrung": forms.TextInput(attrs={"class": "form-control"}),
|
|
"verwendungszweck": forms.Textarea(
|
|
attrs={"class": "form-control", "rows": 3}
|
|
),
|
|
"empfaenger_zahlungspflichtiger": forms.TextInput(
|
|
attrs={"class": "form-control"}
|
|
),
|
|
"iban_gegenpartei": forms.TextInput(attrs={"class": "form-control"}),
|
|
"bic_gegenpartei": forms.TextInput(attrs={"class": "form-control"}),
|
|
"transaction_type": forms.Select(attrs={"class": "form-select"}),
|
|
"status": forms.Select(attrs={"class": "form-select"}),
|
|
"kommentare": forms.Textarea(attrs={"class": "form-control", "rows": 4}),
|
|
"verwaltungskosten": forms.Select(attrs={"class": "form-select"}),
|
|
}
|
|
|
|
|
|
class DestinataerUnterstuetzungForm(forms.ModelForm):
|
|
"""Form für geplante/ausgeführte Destinatärunterstützungen"""
|
|
|
|
class Meta:
|
|
model = DestinataerUnterstuetzung
|
|
fields = [
|
|
"destinataer",
|
|
"konto",
|
|
"betrag",
|
|
"faellig_am",
|
|
"status",
|
|
"beschreibung",
|
|
"empfaenger_iban",
|
|
"empfaenger_name",
|
|
"verwendungszweck",
|
|
]
|
|
widgets = {
|
|
"destinataer": forms.Select(attrs={"class": "form-select"}),
|
|
"konto": forms.Select(attrs={"class": "form-select"}),
|
|
"betrag": forms.NumberInput(
|
|
attrs={"class": "form-control", "step": "0.01"}
|
|
),
|
|
"faellig_am": forms.DateInput(
|
|
attrs={"class": "form-control", "type": "date"}
|
|
),
|
|
"status": forms.Select(attrs={"class": "form-select"}),
|
|
"beschreibung": forms.TextInput(attrs={"class": "form-control"}),
|
|
"empfaenger_iban": forms.TextInput(
|
|
attrs={"class": "form-control", "placeholder": "DE89 3704 0044 0532 0130 00"}
|
|
),
|
|
"empfaenger_name": forms.TextInput(
|
|
attrs={"class": "form-control", "placeholder": "Max Mustermann"}
|
|
),
|
|
"verwendungszweck": forms.TextInput(
|
|
attrs={"class": "form-control", "placeholder": "Vierteljährliche Unterstützung Q1/2025"}
|
|
),
|
|
}
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
super().__init__(*args, **kwargs)
|
|
|
|
# Make faellig_am disabled for automatically generated quarterly payments
|
|
self.is_auto_generated = False
|
|
if self.instance and self.instance.pk and self.instance.beschreibung:
|
|
if "Vierteljährliche Unterstützung" in self.instance.beschreibung and "(automatisch erstellt)" in self.instance.beschreibung:
|
|
self.is_auto_generated = True
|
|
self.fields['faellig_am'].disabled = True
|
|
self.fields['faellig_am'].help_text = "Fälligkeitsdatum wird automatisch basierend auf Quartal berechnet"
|
|
# Ensure the initial value is set for disabled field and widget shows the value
|
|
self.fields['faellig_am'].initial = self.instance.faellig_am
|
|
self.fields['faellig_am'].widget.attrs.update({
|
|
'value': self.instance.faellig_am.strftime('%Y-%m-%d') if self.instance.faellig_am else ''
|
|
})
|
|
|
|
def clean(self):
|
|
cleaned_data = super().clean()
|
|
|
|
# For auto-generated payments, preserve the original due date
|
|
if self.is_auto_generated and self.instance and self.instance.pk:
|
|
cleaned_data['faellig_am'] = self.instance.faellig_am
|
|
|
|
return cleaned_data
|
|
|
|
|
|
class DestinataerNotizForm(forms.ModelForm):
|
|
class Meta:
|
|
model = DestinataerNotiz
|
|
fields = ["titel", "text", "datei"]
|
|
widgets = {
|
|
"titel": forms.TextInput(
|
|
attrs={
|
|
"class": "form-control",
|
|
"placeholder": "z.B. Telefonat vom 29.08.2025",
|
|
}
|
|
),
|
|
"text": forms.Textarea(
|
|
attrs={
|
|
"class": "form-control",
|
|
"rows": 5,
|
|
"placeholder": "Notiztext...",
|
|
}
|
|
),
|
|
"datei": forms.ClearableFileInput(attrs={"class": "form-control"}),
|
|
}
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
super().__init__(*args, **kwargs)
|
|
# Make all fields optional
|
|
self.fields["datei"].required = False
|
|
self.fields["titel"].required = False
|
|
self.fields["text"].required = False
|
|
|
|
def clean(self):
|
|
cleaned = super().clean()
|
|
titel = cleaned.get("titel", "").strip()
|
|
text = cleaned.get("text", "").strip()
|
|
if not (titel or text):
|
|
raise forms.ValidationError(
|
|
"Bitte geben Sie einen Titel oder einen Text ein."
|
|
)
|
|
return cleaned
|
|
|
|
|
|
class BankImportForm(forms.Form):
|
|
"""Form für den Import von Bankdaten"""
|
|
|
|
konto = forms.ModelChoiceField(
|
|
queryset=StiftungsKonto.objects.filter(aktiv=True),
|
|
widget=forms.Select(attrs={"class": "form-select"}),
|
|
label="Zielkonto",
|
|
)
|
|
|
|
datei = forms.FileField(
|
|
widget=forms.FileInput(attrs={"class": "form-control", "accept": ".csv,.txt"}),
|
|
label="Bankdatei",
|
|
help_text="Unterstützte Formate: CSV, TXT (Sparkasse, Volksbank, etc.)",
|
|
)
|
|
|
|
encoding = forms.ChoiceField(
|
|
choices=[
|
|
("utf-8", "UTF-8"),
|
|
("latin1", "Latin-1 / ISO-8859-1"),
|
|
("cp1252", "Windows-1252"),
|
|
],
|
|
initial="utf-8",
|
|
widget=forms.Select(attrs={"class": "form-select"}),
|
|
label="Zeichenkodierung",
|
|
)
|
|
|
|
delimiter = forms.ChoiceField(
|
|
choices=[
|
|
(";", "Semikolon (;)"),
|
|
(",", "Komma (,)"),
|
|
("\t", "Tab"),
|
|
],
|
|
initial=";",
|
|
widget=forms.Select(attrs={"class": "form-select"}),
|
|
label="Trennzeichen",
|
|
)
|
|
|
|
skip_header = forms.BooleanField(
|
|
initial=True,
|
|
required=False,
|
|
widget=forms.CheckboxInput(attrs={"class": "form-check-input"}),
|
|
label="Erste Zeile überspringen (Spaltenüberschriften)",
|
|
)
|
|
|
|
|
|
# =============================================================================
|
|
# USER MANAGEMENT FORMS
|
|
# =============================================================================
|
|
|
|
|
|
class UserCreationForm(forms.Form):
|
|
"""Form für die Erstellung neuer Benutzer"""
|
|
|
|
username = forms.CharField(
|
|
label="Benutzername",
|
|
max_length=150,
|
|
help_text="Eindeutiger Benutzername für die Anmeldung",
|
|
widget=forms.TextInput(attrs={"class": "form-control"}),
|
|
)
|
|
|
|
email = forms.EmailField(
|
|
label="E-Mail-Adresse",
|
|
help_text="E-Mail-Adresse des Benutzers",
|
|
widget=forms.EmailInput(attrs={"class": "form-control"}),
|
|
)
|
|
|
|
first_name = forms.CharField(
|
|
label="Vorname",
|
|
max_length=30,
|
|
required=False,
|
|
widget=forms.TextInput(attrs={"class": "form-control"}),
|
|
)
|
|
|
|
last_name = forms.CharField(
|
|
label="Nachname",
|
|
max_length=150,
|
|
required=False,
|
|
widget=forms.TextInput(attrs={"class": "form-control"}),
|
|
)
|
|
|
|
password1 = forms.CharField(
|
|
label="Passwort",
|
|
widget=forms.PasswordInput(attrs={"class": "form-control"}),
|
|
help_text="Mindestens 8 Zeichen",
|
|
)
|
|
|
|
password2 = forms.CharField(
|
|
label="Passwort bestätigen",
|
|
widget=forms.PasswordInput(attrs={"class": "form-control"}),
|
|
help_text="Geben Sie das Passwort zur Bestätigung erneut ein",
|
|
)
|
|
|
|
is_active = forms.BooleanField(
|
|
label="Aktiv",
|
|
required=False,
|
|
initial=True,
|
|
help_text="Benutzer kann sich anmelden",
|
|
widget=forms.CheckboxInput(attrs={"class": "form-check-input"}),
|
|
)
|
|
|
|
is_staff = forms.BooleanField(
|
|
label="Staff-Status",
|
|
required=False,
|
|
help_text="Benutzer kann auf Django Admin zugreifen",
|
|
widget=forms.CheckboxInput(attrs={"class": "form-check-input"}),
|
|
)
|
|
|
|
def clean_username(self):
|
|
username = self.cleaned_data["username"]
|
|
from django.contrib.auth.models import User
|
|
|
|
if User.objects.filter(username=username).exists():
|
|
raise forms.ValidationError(
|
|
"Ein Benutzer mit diesem Namen existiert bereits."
|
|
)
|
|
return username
|
|
|
|
def clean_email(self):
|
|
email = self.cleaned_data["email"]
|
|
from django.contrib.auth.models import User
|
|
|
|
if User.objects.filter(email=email).exists():
|
|
raise forms.ValidationError(
|
|
"Ein Benutzer mit dieser E-Mail-Adresse existiert bereits."
|
|
)
|
|
return email
|
|
|
|
def clean(self):
|
|
cleaned_data = super().clean()
|
|
password1 = cleaned_data.get("password1")
|
|
password2 = cleaned_data.get("password2")
|
|
|
|
if password1 and password2:
|
|
if password1 != password2:
|
|
raise forms.ValidationError("Die Passwörter stimmen nicht überein.")
|
|
if len(password1) < 8:
|
|
raise forms.ValidationError(
|
|
"Das Passwort muss mindestens 8 Zeichen lang sein."
|
|
)
|
|
|
|
return cleaned_data
|
|
|
|
|
|
class UserUpdateForm(forms.ModelForm):
|
|
"""Form für die Bearbeitung bestehender Benutzer"""
|
|
|
|
class Meta:
|
|
from django.contrib.auth.models import User
|
|
|
|
model = User
|
|
fields = [
|
|
"username",
|
|
"email",
|
|
"first_name",
|
|
"last_name",
|
|
"is_active",
|
|
"is_staff",
|
|
]
|
|
widgets = {
|
|
"username": forms.TextInput(attrs={"class": "form-control"}),
|
|
"email": forms.EmailInput(attrs={"class": "form-control"}),
|
|
"first_name": forms.TextInput(attrs={"class": "form-control"}),
|
|
"last_name": forms.TextInput(attrs={"class": "form-control"}),
|
|
"is_active": forms.CheckboxInput(attrs={"class": "form-check-input"}),
|
|
"is_staff": forms.CheckboxInput(attrs={"class": "form-check-input"}),
|
|
}
|
|
labels = {
|
|
"username": "Benutzername",
|
|
"email": "E-Mail-Adresse",
|
|
"first_name": "Vorname",
|
|
"last_name": "Nachname",
|
|
"is_active": "Aktiv",
|
|
"is_staff": "Staff-Status",
|
|
}
|
|
help_texts = {
|
|
"username": "Eindeutiger Benutzername für die Anmeldung",
|
|
"email": "E-Mail-Adresse des Benutzers",
|
|
"is_active": "Benutzer kann sich anmelden",
|
|
"is_staff": "Benutzer kann auf Django Admin zugreifen",
|
|
}
|
|
|
|
|
|
class PasswordChangeForm(forms.Form):
|
|
"""Form für Passwort-Änderungen"""
|
|
|
|
new_password1 = forms.CharField(
|
|
label="Neues Passwort",
|
|
widget=forms.PasswordInput(attrs={"class": "form-control"}),
|
|
help_text="Mindestens 8 Zeichen",
|
|
)
|
|
|
|
new_password2 = forms.CharField(
|
|
label="Neues Passwort bestätigen",
|
|
widget=forms.PasswordInput(attrs={"class": "form-control"}),
|
|
help_text="Geben Sie das neue Passwort zur Bestätigung erneut ein",
|
|
)
|
|
|
|
def clean(self):
|
|
cleaned_data = super().clean()
|
|
password1 = cleaned_data.get("new_password1")
|
|
password2 = cleaned_data.get("new_password2")
|
|
|
|
if password1 and password2:
|
|
if password1 != password2:
|
|
raise forms.ValidationError("Die Passwörter stimmen nicht überein.")
|
|
if len(password1) < 8:
|
|
raise forms.ValidationError(
|
|
"Das Passwort muss mindestens 8 Zeichen lang sein."
|
|
)
|
|
|
|
return cleaned_data
|
|
|
|
|
|
class UserPermissionForm(forms.Form):
|
|
"""Form für die Zuweisung von Berechtigungen"""
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
user = kwargs.pop("user", None)
|
|
super().__init__(*args, **kwargs)
|
|
|
|
from django.contrib.auth.models import Permission
|
|
|
|
# Get all custom permissions for stiftung app
|
|
app_permissions = Permission.objects.filter(
|
|
content_type__app_label="stiftung"
|
|
).order_by("name")
|
|
|
|
# Create checkbox fields for each permission
|
|
for perm in app_permissions:
|
|
field_name = f"perm_{perm.id}"
|
|
self.fields[field_name] = forms.BooleanField(
|
|
label=perm.name,
|
|
required=False,
|
|
widget=forms.CheckboxInput(attrs={"class": "form-check-input"}),
|
|
)
|
|
|
|
# Set initial values if user is provided
|
|
if user:
|
|
self.fields[field_name].initial = user.has_perm(
|
|
f"stiftung.{perm.codename}"
|
|
)
|
|
|
|
def get_permission_groups(self):
|
|
"""Group permissions by functionality for template rendering"""
|
|
from django.contrib.auth.models import Permission
|
|
|
|
groups = {
|
|
"entities": {
|
|
"name": "Entitäten verwalten",
|
|
"permissions": [],
|
|
"icon": "fas fa-users",
|
|
},
|
|
"documents": {
|
|
"name": "Dokumentenverwaltung",
|
|
"permissions": [],
|
|
"icon": "fas fa-folder-open",
|
|
},
|
|
"financial": {
|
|
"name": "Finanzverwaltung",
|
|
"permissions": [],
|
|
"icon": "fas fa-euro-sign",
|
|
},
|
|
"administration": {
|
|
"name": "Administration",
|
|
"permissions": [],
|
|
"icon": "fas fa-cogs",
|
|
},
|
|
"system": {"name": "System", "permissions": [], "icon": "fas fa-server"},
|
|
}
|
|
|
|
# Get all permissions to properly categorize them
|
|
for field_name, field in self.fields.items():
|
|
if field_name.startswith("perm_"):
|
|
# Extract permission ID from field name
|
|
perm_id = field_name.replace("perm_", "")
|
|
try:
|
|
permission = Permission.objects.get(id=perm_id)
|
|
label = permission.name.lower()
|
|
codename = permission.codename.lower()
|
|
|
|
# More precise categorization based on both name and codename
|
|
if (
|
|
any(
|
|
word in codename
|
|
for word in [
|
|
"destinataer",
|
|
"land",
|
|
"paechter",
|
|
"verpachtung",
|
|
"foerderung",
|
|
]
|
|
)
|
|
and "manage_" in codename
|
|
or "view_" in codename
|
|
):
|
|
groups["entities"]["permissions"].append(
|
|
(field_name, field, permission)
|
|
)
|
|
elif (
|
|
any(
|
|
word in codename for word in ["documents", "link_documents"]
|
|
)
|
|
or "dokument" in label
|
|
):
|
|
groups["documents"]["permissions"].append(
|
|
(field_name, field, permission)
|
|
)
|
|
elif any(
|
|
word in codename
|
|
for word in [
|
|
"verwaltungskosten",
|
|
"konten",
|
|
"rentmeister",
|
|
"approve_payments",
|
|
]
|
|
) or any(
|
|
word in label
|
|
for word in [
|
|
"verwaltungskosten",
|
|
"konto",
|
|
"rentmeister",
|
|
"zahlung",
|
|
]
|
|
):
|
|
groups["financial"]["permissions"].append(
|
|
(field_name, field, permission)
|
|
)
|
|
elif any(
|
|
word in codename
|
|
for word in [
|
|
"administration",
|
|
"audit",
|
|
"backup",
|
|
"manage_users",
|
|
"manage_permissions",
|
|
]
|
|
) or any(
|
|
word in label
|
|
for word in [
|
|
"administration",
|
|
"audit",
|
|
"backup",
|
|
"benutzer",
|
|
"berechtigung",
|
|
]
|
|
):
|
|
groups["administration"]["permissions"].append(
|
|
(field_name, field, permission)
|
|
)
|
|
else:
|
|
groups["system"]["permissions"].append(
|
|
(field_name, field, permission)
|
|
)
|
|
except Permission.DoesNotExist:
|
|
# Fallback for permissions that don't exist
|
|
groups["system"]["permissions"].append((field_name, field, None))
|
|
|
|
return groups
|
|
|
|
|
|
class VierteljahresNachweisForm(forms.ModelForm):
|
|
"""Form for quarterly confirmations (Vierteljahresnachweise)"""
|
|
|
|
class Meta:
|
|
model = VierteljahresNachweis
|
|
fields = [
|
|
'studiennachweis_eingereicht',
|
|
'studiennachweis_datei',
|
|
'studiennachweis_bemerkung',
|
|
'einkommenssituation_bestaetigt',
|
|
'einkommenssituation_text',
|
|
'einkommenssituation_datei',
|
|
'vermogenssituation_bestaetigt',
|
|
'vermogenssituation_text',
|
|
'vermogenssituation_datei',
|
|
'weitere_dokumente',
|
|
'weitere_dokumente_beschreibung',
|
|
'interne_notizen',
|
|
]
|
|
|
|
widgets = {
|
|
'studiennachweis_eingereicht': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
|
|
'studiennachweis_datei': forms.FileInput(attrs={'class': 'form-control', 'accept': '.pdf,.jpg,.jpeg,.png,.doc,.docx'}),
|
|
'studiennachweis_bemerkung': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}),
|
|
'einkommenssituation_bestaetigt': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
|
|
'einkommenssituation_text': forms.Textarea(attrs={'class': 'form-control', 'rows': 3, 'placeholder': 'Z.B. "Keine Änderungen seit letzter Meldung"'}),
|
|
'einkommenssituation_datei': forms.FileInput(attrs={'class': 'form-control', 'accept': '.pdf,.jpg,.jpeg,.png,.doc,.docx'}),
|
|
'vermogenssituation_bestaetigt': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
|
|
'vermogenssituation_text': forms.Textarea(attrs={'class': 'form-control', 'rows': 3, 'placeholder': 'Z.B. "Keine Änderungen seit letzter Meldung"'}),
|
|
'vermogenssituation_datei': forms.FileInput(attrs={'class': 'form-control', 'accept': '.pdf,.jpg,.jpeg,.png,.doc,.docx'}),
|
|
'weitere_dokumente': forms.FileInput(attrs={'class': 'form-control', 'accept': '.pdf,.jpg,.jpeg,.png,.doc,.docx'}),
|
|
'weitere_dokumente_beschreibung': forms.Textarea(attrs={'class': 'form-control', 'rows': 2}),
|
|
'interne_notizen': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}),
|
|
}
|
|
|
|
labels = {
|
|
'studiennachweis_erforderlich': 'Studiennachweis erforderlich',
|
|
'studiennachweis_eingereicht': 'Studiennachweis eingereicht',
|
|
'studiennachweis_datei': 'Studiennachweis (Datei)',
|
|
'studiennachweis_bemerkung': 'Bemerkung zum Studiennachweis',
|
|
'einkommenssituation_bestaetigt': 'Einkommenssituation bestätigt',
|
|
'einkommenssituation_text': 'Einkommenssituation (Text)',
|
|
'einkommenssituation_datei': 'Einkommenssituation (Datei)',
|
|
'vermogenssituation_bestaetigt': 'Vermögenssituation bestätigt',
|
|
'vermogenssituation_text': 'Vermögenssituation (Text)',
|
|
'vermogenssituation_datei': 'Vermögenssituation (Datei)',
|
|
'weitere_dokumente': 'Weitere Dokumente',
|
|
'weitere_dokumente_beschreibung': 'Beschreibung weitere Dokumente',
|
|
'interne_notizen': 'Interne Notizen (nur für Verwaltung)',
|
|
}
|
|
|
|
help_texts = {
|
|
'einkommenssituation_text': 'Z.B. "Keine Änderungen seit letzter Meldung" oder Details zu Änderungen',
|
|
'vermogenssituation_text': 'Z.B. "Keine Änderungen seit letzter Meldung" oder Details zu Änderungen',
|
|
'interne_notizen': 'Diese Notizen sind nur für die interne Verwaltung sichtbar',
|
|
}
|
|
|
|
def clean(self):
|
|
cleaned_data = super().clean()
|
|
|
|
# Validate that at least one form of confirmation is provided for income situation
|
|
einkommenssituation_text = cleaned_data.get('einkommenssituation_text')
|
|
einkommenssituation_datei = cleaned_data.get('einkommenssituation_datei')
|
|
einkommenssituation_bestaetigt = cleaned_data.get('einkommenssituation_bestaetigt')
|
|
|
|
if einkommenssituation_bestaetigt and not einkommenssituation_text and not einkommenssituation_datei:
|
|
raise ValidationError(
|
|
'Wenn die Einkommenssituation bestätigt wird, muss entweder ein Text oder eine Datei angegeben werden.'
|
|
)
|
|
|
|
# Validate that at least one form of confirmation is provided for asset situation
|
|
vermogenssituation_text = cleaned_data.get('vermogenssituation_text')
|
|
vermogenssituation_datei = cleaned_data.get('vermogenssituation_datei')
|
|
vermogenssituation_bestaetigt = cleaned_data.get('vermogenssituation_bestaetigt')
|
|
|
|
if vermogenssituation_bestaetigt and not vermogenssituation_text and not vermogenssituation_datei:
|
|
raise ValidationError(
|
|
'Wenn die Vermögenssituation bestätigt wird, muss entweder ein Text oder eine Datei angegeben werden.'
|
|
)
|
|
|
|
# Validate study proof if required and marked as submitted
|
|
studiennachweis_erforderlich = cleaned_data.get('studiennachweis_erforderlich')
|
|
studiennachweis_eingereicht = cleaned_data.get('studiennachweis_eingereicht')
|
|
studiennachweis_datei = cleaned_data.get('studiennachweis_datei')
|
|
studiennachweis_bemerkung = cleaned_data.get('studiennachweis_bemerkung')
|
|
|
|
if studiennachweis_erforderlich and studiennachweis_eingereicht:
|
|
if not studiennachweis_datei and not studiennachweis_bemerkung:
|
|
raise ValidationError(
|
|
'Wenn der Studiennachweis als eingereicht markiert wird, muss entweder eine Datei oder eine Bemerkung angegeben werden.'
|
|
)
|
|
|
|
return cleaned_data
|
|
|
|
|
|
# Two-Factor Authentication Forms
|
|
|
|
class TwoFactorSetupForm(forms.Form):
|
|
"""Form for setting up 2FA with TOTP verification"""
|
|
token = forms.CharField(
|
|
max_length=6,
|
|
min_length=6,
|
|
widget=forms.TextInput(attrs={
|
|
'class': 'form-control text-center',
|
|
'placeholder': '000000',
|
|
'autocomplete': 'off',
|
|
'pattern': '[0-9]{6}',
|
|
'inputmode': 'numeric'
|
|
}),
|
|
label='Bestätigungscode',
|
|
help_text='6-stelliger Code aus Ihrer Authenticator-App'
|
|
)
|
|
|
|
def clean_token(self):
|
|
token = self.cleaned_data.get('token')
|
|
if token and not token.isdigit():
|
|
raise ValidationError('Der Code darf nur Zahlen enthalten.')
|
|
return token
|
|
|
|
|
|
class TwoFactorVerifyForm(forms.Form):
|
|
"""Form for verifying 2FA during login"""
|
|
otp_token = forms.CharField(
|
|
max_length=8,
|
|
min_length=6,
|
|
widget=forms.TextInput(attrs={
|
|
'class': 'form-control form-control-lg text-center',
|
|
'placeholder': '000000',
|
|
'autocomplete': 'off',
|
|
'autofocus': True
|
|
}),
|
|
label='Authentifizierungscode',
|
|
help_text='6-stelliger Code aus der App oder 8-stelliger Backup-Code'
|
|
)
|
|
|
|
def clean_otp_token(self):
|
|
token = self.cleaned_data.get('otp_token')
|
|
if token:
|
|
token = token.strip().lower()
|
|
# Allow 6-digit TOTP codes or 8-character backup codes
|
|
if len(token) == 6 and token.isdigit():
|
|
return token
|
|
elif len(token) == 8 and re.match(r'^[a-f0-9]{8}$', token):
|
|
return token
|
|
else:
|
|
raise ValidationError(
|
|
'Bitte geben Sie einen 6-stelligen Authenticator-Code oder 8-stelligen Backup-Code ein.'
|
|
)
|
|
return token
|
|
|
|
|
|
class TwoFactorDisableForm(forms.Form):
|
|
"""Form for disabling 2FA with password confirmation"""
|
|
password = forms.CharField(
|
|
widget=forms.PasswordInput(attrs={
|
|
'class': 'form-control',
|
|
'autocomplete': 'current-password',
|
|
'autofocus': True
|
|
}),
|
|
label='Passwort',
|
|
help_text='Geben Sie Ihr aktuelles Passwort zur Bestätigung ein'
|
|
)
|
|
|
|
|
|
class BackupTokenRegenerateForm(forms.Form):
|
|
"""Form for regenerating backup tokens"""
|
|
password = forms.CharField(
|
|
widget=forms.PasswordInput(attrs={
|
|
'class': 'form-control',
|
|
'autocomplete': 'current-password'
|
|
}),
|
|
label='Passwort',
|
|
help_text='Geben Sie Ihr Passwort ein, um neue Backup-Codes zu generieren'
|
|
)
|