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' ) class GeschichteSeiteForm(forms.ModelForm): """Form for creating and editing history pages""" class Meta: from .models import GeschichteSeite model = GeschichteSeite fields = ['titel', 'slug', 'inhalt', 'ist_veroeffentlicht', 'sortierung'] widgets = { 'titel': forms.TextInput(attrs={ 'class': 'form-control', 'placeholder': 'z.B. Gründung der Stiftung' }), 'slug': forms.TextInput(attrs={ 'class': 'form-control', 'placeholder': 'z.B. gruendung-der-stiftung' }), 'inhalt': forms.Textarea(attrs={ 'class': 'form-control rich-text-editor', 'rows': 20, 'placeholder': 'Schreiben Sie hier den Inhalt der Geschichtsseite...' }), 'ist_veroeffentlicht': forms.CheckboxInput(attrs={ 'class': 'form-check-input' }), 'sortierung': forms.NumberInput(attrs={ 'class': 'form-control', 'min': 0 }) } help_texts = { 'slug': 'URL-freundliche Version des Titels (nur Buchstaben, Zahlen und Bindestriche)', 'inhalt': 'Unterstützt Rich-Text-Formatierung, Bilder und Videos', 'sortierung': 'Niedrigere Zahlen erscheinen zuerst in der Navigation' } def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # Auto-generate slug from title if not provided if not self.instance.pk: self.fields['slug'].required = False def clean_slug(self): slug = self.cleaned_data.get('slug') titel = self.cleaned_data.get('titel') if not slug and titel: # Auto-generate slug from title from django.utils.text import slugify slug = slugify(titel) return slug class GeschichteBildForm(forms.ModelForm): """Form for uploading images to history pages""" class Meta: from .models import GeschichteBild model = GeschichteBild fields = ['titel', 'bild', 'beschreibung', 'alt_text', 'sortierung'] widgets = { 'titel': forms.TextInput(attrs={ 'class': 'form-control', 'placeholder': 'z.B. Gründungsurkunde 1895' }), 'bild': forms.ClearableFileInput(attrs={ 'class': 'form-control' }), 'beschreibung': forms.Textarea(attrs={ 'class': 'form-control', 'rows': 3, 'placeholder': 'Beschreibung des Bildes...' }), 'alt_text': forms.TextInput(attrs={ 'class': 'form-control', 'placeholder': 'Alternativtext für Bildschirmleser' }), 'sortierung': forms.NumberInput(attrs={ 'class': 'form-control', 'min': 0 }) } help_texts = { 'bild': 'Unterstützte Formate: JPG, PNG, GIF (max. 10MB)', 'alt_text': 'Wichtig für Barrierefreiheit', 'sortierung': 'Reihenfolge in der Bildergalerie' }