Files
stiftung-management-system/app/stiftung/forms.py
Jan Remmer Siebels 390bf697ee Fix date field display with readonly TextInput widget
- Changed from disabled DateInput to readonly TextInput for auto-generated payments
- Uses German date format (dd.mm.yyyy) for better user experience
- Added visual styling to indicate readonly state
- Preserves original date value through clean() method
2025-10-02 21:40:01 +02:00

1606 lines
62 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 read-only 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
# Use a TextInput widget with readonly attribute to display the date
from django import forms
current_date = self.instance.faellig_am
if current_date:
self.fields['faellig_am'].widget = forms.TextInput(
attrs={
"class": "form-control",
"readonly": True,
"value": current_date.strftime('%d.%m.%Y'), # German date format
"style": "background-color: #f8f9fa; cursor: not-allowed;"
}
)
self.fields['faellig_am'].initial = current_date
self.fields['faellig_am'].help_text = "Fälligkeitsdatum wird automatisch basierend auf Quartal berechnet"
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'
)