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, Paechter, Person, Rentmeister, StiftungsKonto, UnterstuetzungWiederkehrend, Verwaltungskosten, ) 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} ), # renamed in UI: use vierteljaehrlicher_betrag field "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"}), "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"} ), } 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 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", ] 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"}), } 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