Adds optional salutation (Herr/Frau/Divers) to the Destinatär model with migration, form support, admin integration and template display. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
440 lines
19 KiB
Python
440 lines
19 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, berufsgruppe and anrede 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)
|
|
if "anrede" in self.fields:
|
|
self.fields["anrede"].choices = [("", "Bitte wählen...")] + list(Destinataer.ANREDE_CHOICES)
|
|
# Set choices for standard_konto to allow blank
|
|
self.fields["standard_konto"].empty_label = "---"
|
|
|
|
|
|
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')
|
|
|
|
# DMS-Dokumente aus POST-Daten beruecksichtigen (werden parallel zum Formular gesendet)
|
|
has_einkommens_dms = (
|
|
self.instance and self.instance.pk and
|
|
bool(self.instance.einkommenssituation_dms_dokument_id)
|
|
)
|
|
if einkommenssituation_bestaetigt and not einkommenssituation_text and not einkommenssituation_datei and not has_einkommens_dms:
|
|
raise ValidationError(
|
|
'Wenn die Einkommenssituation bestätigt wird, muss entweder ein Text, eine Datei oder ein DMS-Dokument 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')
|
|
|
|
has_vermogens_dms = (
|
|
self.instance and self.instance.pk and
|
|
bool(self.instance.vermogenssituation_dms_dokument_id)
|
|
)
|
|
if vermogenssituation_bestaetigt and not vermogenssituation_text and not vermogenssituation_datei and not has_vermogens_dms:
|
|
raise ValidationError(
|
|
'Wenn die Vermögenssituation bestätigt wird, muss entweder ein Text, eine Datei oder ein DMS-Dokument 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:
|
|
has_dms_studiennachweis = (
|
|
self.instance and self.instance.pk and (
|
|
bool(self.instance.studiennachweis_dms_dokument_id)
|
|
or self.instance.nachweis_dokumente.filter(kontext="studiennachweis").exists()
|
|
)
|
|
)
|
|
if not studiennachweis_datei and not studiennachweis_bemerkung and not has_dms_studiennachweis:
|
|
raise ValidationError(
|
|
'Wenn der Studiennachweis als eingereicht markiert wird, muss entweder eine Datei, eine Bemerkung oder ein DMS-Dokument angegeben werden.'
|
|
)
|
|
|
|
return cleaned_data
|