Files
stiftung-management-system/app/stiftung/forms/destinataere.py
SysAdmin Agent 3ca2706e5d Phase 0: forms.py, admin.py und views.py in Domain-Packages aufteilen
- forms.py → forms/ Package (8 Domänen: destinataere, land, finanzen,
  foerderung, dokumente, veranstaltung, system, geschichte)
- admin.py → admin/ Package (7 Domänen, alle 22 @admin.register dekoriert)
- views.py (8845 Zeilen) → views/ Package (10 Domänen: dashboard, destinataere,
  land, paechter, finanzen, foerderung, dokumente, unterstuetzungen,
  veranstaltung, geschichte, system)
- __init__.py in jedem Package re-exportiert alle Symbole für Rückwärtskompatibilität
- urls.py bleibt unverändert (funktioniert durch Re-Exports)
- Django system check: 0 Fehler, alle URL-Auflösungen funktionieren

Keine funktionalen Änderungen – reine Strukturverbesserung für Vision 2026.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-11 09:55:15 +00:00

429 lines
18 KiB
Python

from django import forms
from django.utils import timezone
from ..models import (Destinataer, DestinataerNotiz, DestinataerUnterstuetzung,
UnterstuetzungWiederkehrend, VierteljahresNachweis)
from django.core.exceptions import ValidationError
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 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 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 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