Files
stiftung-management-system/app/stiftung/forms/destinataere.py
SysAdmin Agent e0b377014c
Some checks failed
CI/CD Pipeline / test (push) Has been cancelled
CI/CD Pipeline / deploy (push) Has been cancelled
Code Quality / quality (push) Has been cancelled
v4.1.0: DMS email documents, category-specific Nachweis linking, version system
- Save cover email body as DMS document with new 'email' context type
- Show email body separately from attachments in email detail view
- Add per-category DMS document assignment in quarterly confirmation
  (Studiennachweis, Einkommenssituation, Vermögenssituation)
- Add VERSION file and context processor for automatic version display
- Add MCP server, agent system, import/export, and new migrations
- Update compose files and production environment template

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-15 18:48:52 +00:00

444 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 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')
# 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