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