Files
stiftung-management-system/app/stiftung/forms.py
Stiftung Development 35ba089a84 fix: configure CI database connection properly
- Add dotenv loading to Django settings
- Update CI workflow to use correct environment variables
- Set POSTGRES_* variables instead of DATABASE_URL
- Add environment variables to all Django management commands
- Fixes CI test failures due to database connection issues
2025-09-06 18:47:23 +02:00

943 lines
42 KiB
Python

from django import forms
from django.core.exceptions import ValidationError
from django.utils import timezone
from .models import (
Rentmeister, StiftungsKonto, Verwaltungskosten, Person,
Paechter, Destinataer, Land, DokumentLink, Foerderung, BankTransaction,
DestinataerUnterstuetzung, UnterstuetzungWiederkehrend, DestinataerNotiz, LandAbrechnung,
)
import re
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 .models import Destinataer, DokumentLink
from django.utils import timezone
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