feat: Implement quarterly confirmation system with automatic support payments
- Add VierteljahresNachweis model for quarterly document tracking - Remove studiennachweis_erforderlich field (now always required) - Fix modal edit view to include studiennachweis section - Implement automatic DestinataerUnterstuetzung creation when requirements met - Set payment due dates to exact quarter end dates (Mar 31, Jun 30, Sep 30, Dec 31) - Add quarterly confirmation CRUD views with modal and full-screen editing - Update templates with comprehensive quarterly management interface - Include proper validation, status tracking, and progress indicators
This commit is contained in:
@@ -3,7 +3,7 @@ import io
|
||||
import json
|
||||
import os
|
||||
import time
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timedelta
|
||||
from decimal import Decimal
|
||||
|
||||
import requests
|
||||
@@ -24,7 +24,7 @@ from rest_framework.response import Response
|
||||
from .models import (AppConfiguration, CSVImport, Destinataer,
|
||||
DestinataerUnterstuetzung, DokumentLink, Foerderung, Land,
|
||||
LandAbrechnung, LandVerpachtung, Paechter, Person,
|
||||
StiftungsKonto, UnterstuetzungWiederkehrend)
|
||||
StiftungsKonto, UnterstuetzungWiederkehrend, VierteljahresNachweis)
|
||||
|
||||
|
||||
def get_pdf_generator():
|
||||
@@ -214,7 +214,7 @@ from stiftung.models import DestinataerNotiz, DestinataerUnterstuetzung
|
||||
from .forms import (DestinataerForm, DestinataerNotizForm,
|
||||
DestinataerUnterstuetzungForm, DokumentLinkForm,
|
||||
FoerderungForm, LandForm, PaechterForm, PersonForm,
|
||||
UnterstuetzungForm, UnterstuetzungMarkAsPaidForm)
|
||||
UnterstuetzungForm, UnterstuetzungMarkAsPaidForm, VierteljahresNachweisForm)
|
||||
|
||||
|
||||
def home(request):
|
||||
@@ -1233,6 +1233,30 @@ def destinataer_detail(request, pk):
|
||||
destinataer=destinataer
|
||||
).order_by("-erstellt_am")
|
||||
|
||||
# Quarterly confirmations - load for current and next year
|
||||
from datetime import date
|
||||
current_year = date.today().year
|
||||
quarterly_confirmations = VierteljahresNachweis.objects.filter(
|
||||
destinataer=destinataer,
|
||||
jahr__in=[current_year, current_year + 1]
|
||||
).order_by('-jahr', '-quartal')
|
||||
|
||||
# Create missing quarterly confirmations for current year if destinataer requires study proof
|
||||
if destinataer.studiennachweis_erforderlich:
|
||||
for quartal in range(1, 5): # Q1-Q4
|
||||
nachweis, created = VierteljahresNachweis.get_or_create_for_period(
|
||||
destinataer, current_year, quartal
|
||||
)
|
||||
|
||||
# Reload to get any newly created confirmations
|
||||
quarterly_confirmations = VierteljahresNachweis.objects.filter(
|
||||
destinataer=destinataer,
|
||||
jahr__in=[current_year, current_year + 1]
|
||||
).order_by('-jahr', '-quartal')
|
||||
|
||||
# Generate available years for the add quarter dropdown (current year + next 5 years)
|
||||
available_years = list(range(current_year, current_year + 6))
|
||||
|
||||
# Alle verfügbaren StiftungsKonten für das Select-Feld laden
|
||||
stiftungskonten = StiftungsKonto.objects.all().order_by("kontoname")
|
||||
|
||||
@@ -1243,6 +1267,9 @@ def destinataer_detail(request, pk):
|
||||
"unterstuetzungen": unterstuetzungen,
|
||||
"notizen_eintraege": notizen_eintraege,
|
||||
"stiftungskonten": stiftungskonten,
|
||||
"quarterly_confirmations": quarterly_confirmations,
|
||||
"available_years": available_years,
|
||||
"current_year": current_year,
|
||||
}
|
||||
return render(request, "stiftung/destinataer_detail.html", context)
|
||||
|
||||
@@ -7195,3 +7222,318 @@ def verpachtung_delete(request, pk):
|
||||
'title': f'Verpachtung {verpachtung.vertragsnummer} löschen',
|
||||
}
|
||||
return render(request, 'stiftung/verpachtung_confirm_delete.html', context)
|
||||
|
||||
|
||||
@login_required
|
||||
def quarterly_confirmation_update(request, pk):
|
||||
"""Update quarterly confirmation for destinataer"""
|
||||
nachweis = get_object_or_404(VierteljahresNachweis, pk=pk)
|
||||
|
||||
if request.method == "POST":
|
||||
form = VierteljahresNachweisForm(request.POST, request.FILES, instance=nachweis)
|
||||
if form.is_valid():
|
||||
quarterly_proof = form.save(commit=False)
|
||||
|
||||
# Calculate current status before saving
|
||||
old_status = nachweis.status
|
||||
|
||||
# Auto-update status based on completion
|
||||
if quarterly_proof.is_complete():
|
||||
if quarterly_proof.status in ['offen', 'teilweise']:
|
||||
quarterly_proof.status = 'eingereicht'
|
||||
quarterly_proof.eingereicht_am = timezone.now()
|
||||
else:
|
||||
# If not complete, set to teilweise if some fields are filled
|
||||
has_partial_data = (
|
||||
quarterly_proof.einkommenssituation_bestaetigt or
|
||||
quarterly_proof.vermogenssituation_bestaetigt or
|
||||
quarterly_proof.studiennachweis_eingereicht
|
||||
)
|
||||
if has_partial_data and quarterly_proof.status == 'offen':
|
||||
quarterly_proof.status = 'teilweise'
|
||||
|
||||
quarterly_proof.save()
|
||||
|
||||
# Try to create automatic support payment if complete
|
||||
if quarterly_proof.is_complete() and quarterly_proof.status == 'eingereicht':
|
||||
support_payment = create_quarterly_support_payment(quarterly_proof)
|
||||
if support_payment:
|
||||
messages.success(
|
||||
request,
|
||||
f"Automatische Unterstützung über {support_payment.betrag}€ für {support_payment.destinataer.get_full_name()} wurde erstellt (fällig am {support_payment.faellig_am.strftime('%d.%m.%Y')})."
|
||||
)
|
||||
else:
|
||||
# Log why payment wasn't created
|
||||
reasons = []
|
||||
if not quarterly_proof.destinataer.vierteljaehrlicher_betrag:
|
||||
reasons.append("kein vierteljährlicher Betrag hinterlegt")
|
||||
if not quarterly_proof.destinataer.iban:
|
||||
reasons.append("keine IBAN hinterlegt")
|
||||
if not quarterly_proof.destinataer.standard_konto and not StiftungsKonto.objects.exists():
|
||||
reasons.append("kein Auszahlungskonto verfügbar")
|
||||
|
||||
if reasons:
|
||||
messages.warning(
|
||||
request,
|
||||
f"Automatische Unterstützung konnte nicht erstellt werden: {', '.join(reasons)}"
|
||||
)
|
||||
|
||||
# Debug message to see what happened
|
||||
status_changed = old_status != quarterly_proof.status
|
||||
status_msg = f" (Status: {old_status} → {quarterly_proof.status})" if status_changed else f" (Status: {quarterly_proof.status})"
|
||||
|
||||
messages.success(
|
||||
request,
|
||||
f"Vierteljahresnachweis für {nachweis.destinataer.get_full_name()} "
|
||||
f"({nachweis.jahr} Q{nachweis.quartal}) wurde erfolgreich aktualisiert{status_msg}."
|
||||
)
|
||||
return redirect("stiftung:destinataer_detail", pk=nachweis.destinataer.pk)
|
||||
else:
|
||||
# Add form errors to messages
|
||||
for field, errors in form.errors.items():
|
||||
for error in errors:
|
||||
messages.error(request, f"Fehler in {field}: {error}")
|
||||
|
||||
# If GET request or form errors, redirect back to destinataer detail
|
||||
return redirect("stiftung:destinataer_detail", pk=nachweis.destinataer.pk)
|
||||
|
||||
|
||||
def create_quarterly_support_payment(nachweis):
|
||||
"""
|
||||
Create an automatic support payment when all quarterly requirements are met
|
||||
"""
|
||||
destinataer = nachweis.destinataer
|
||||
|
||||
# Check if all requirements are met
|
||||
if not nachweis.is_complete():
|
||||
return None
|
||||
|
||||
# Check if destinataer has required payment info
|
||||
if not destinataer.vierteljaehrlicher_betrag or destinataer.vierteljaehrlicher_betrag <= 0:
|
||||
return None
|
||||
|
||||
if not destinataer.iban:
|
||||
return None
|
||||
|
||||
# Check if a payment for this quarter already exists
|
||||
quarter_start = datetime(nachweis.jahr, (nachweis.quartal - 1) * 3 + 1, 1).date()
|
||||
quarter_end = datetime(nachweis.jahr, nachweis.quartal * 3, 1).date()
|
||||
|
||||
existing_payment = DestinataerUnterstuetzung.objects.filter(
|
||||
destinataer=destinataer,
|
||||
faellig_am__gte=quarter_start,
|
||||
faellig_am__lt=quarter_end,
|
||||
beschreibung__contains=f"Q{nachweis.quartal}/{nachweis.jahr}"
|
||||
).first()
|
||||
|
||||
if existing_payment:
|
||||
return existing_payment
|
||||
|
||||
# Get default payment account
|
||||
default_konto = destinataer.standard_konto
|
||||
if not default_konto:
|
||||
# Try to get any StiftungsKonto
|
||||
default_konto = StiftungsKonto.objects.first()
|
||||
if not default_konto:
|
||||
return None
|
||||
|
||||
# Calculate payment due date (last day of quarter)
|
||||
# Quarter end months and their last days:
|
||||
# Q1: March (31), Q2: June (30), Q3: September (30), Q4: December (31)
|
||||
quarter_end_month = nachweis.quartal * 3
|
||||
|
||||
if nachweis.quartal == 1: # Q1: January-March (ends March 31)
|
||||
quarter_end_day = 31
|
||||
elif nachweis.quartal == 2: # Q2: April-June (ends June 30)
|
||||
quarter_end_day = 30
|
||||
elif nachweis.quartal == 3: # Q3: July-September (ends September 30)
|
||||
quarter_end_day = 30
|
||||
else: # Q4: October-December (ends December 31)
|
||||
quarter_end_day = 31
|
||||
|
||||
payment_due_date = datetime(nachweis.jahr, quarter_end_month, quarter_end_day).date()
|
||||
|
||||
# Create the support payment
|
||||
payment = DestinataerUnterstuetzung.objects.create(
|
||||
destinataer=destinataer,
|
||||
konto=default_konto,
|
||||
betrag=destinataer.vierteljaehrlicher_betrag,
|
||||
faellig_am=payment_due_date,
|
||||
status='geplant',
|
||||
beschreibung=f"Vierteljährliche Unterstützung Q{nachweis.quartal}/{nachweis.jahr} (automatisch erstellt)",
|
||||
empfaenger_iban=destinataer.iban,
|
||||
empfaenger_name=destinataer.get_full_name(),
|
||||
verwendungszweck=f"Vierteljährliche Unterstützung Q{nachweis.quartal}/{nachweis.jahr} - {destinataer.get_full_name()}",
|
||||
erstellt_am=timezone.now(),
|
||||
aktualisiert_am=timezone.now()
|
||||
)
|
||||
|
||||
return payment
|
||||
|
||||
|
||||
@login_required
|
||||
def quarterly_confirmation_create(request, destinataer_id):
|
||||
"""Create a new quarterly confirmation for a destinataer"""
|
||||
destinataer = get_object_or_404(Destinataer, pk=destinataer_id)
|
||||
|
||||
if request.method == "POST":
|
||||
jahr = request.POST.get('jahr')
|
||||
quartal = request.POST.get('quartal')
|
||||
|
||||
if jahr and quartal:
|
||||
try:
|
||||
jahr = int(jahr)
|
||||
quartal = int(quartal)
|
||||
|
||||
# Check if this quarter already exists
|
||||
existing = VierteljahresNachweis.objects.filter(
|
||||
destinataer=destinataer,
|
||||
jahr=jahr,
|
||||
quartal=quartal
|
||||
).exists()
|
||||
|
||||
if existing:
|
||||
messages.warning(
|
||||
request,
|
||||
f"Quartal {jahr} Q{quartal} existiert bereits für {destinataer.get_full_name()}."
|
||||
)
|
||||
else:
|
||||
# Create new quarterly confirmation
|
||||
nachweis = VierteljahresNachweis.objects.create(
|
||||
destinataer=destinataer,
|
||||
jahr=jahr,
|
||||
quartal=quartal,
|
||||
studiennachweis_erforderlich=True, # Always required now
|
||||
)
|
||||
|
||||
# Set deadline (15th of second month of quarter)
|
||||
deadline_months = {1: 5, 2: 8, 3: 11, 4: 2} # Q1->May, Q2->Aug, Q3->Nov, Q4->Feb(next year)
|
||||
deadline_month = deadline_months[quartal]
|
||||
deadline_year = jahr if quartal != 4 else jahr + 1
|
||||
|
||||
from datetime import date
|
||||
nachweis.faelligkeitsdatum = date(deadline_year, deadline_month, 15)
|
||||
nachweis.save()
|
||||
|
||||
messages.success(
|
||||
request,
|
||||
f"Quartal {jahr} Q{quartal} wurde erfolgreich für {destinataer.get_full_name()} erstellt."
|
||||
)
|
||||
|
||||
except (ValueError, TypeError):
|
||||
messages.error(request, "Ungültige Jahr- oder Quartalswerte.")
|
||||
else:
|
||||
messages.error(request, "Jahr und Quartal müssen angegeben werden.")
|
||||
|
||||
return redirect("stiftung:destinataer_detail", pk=destinataer.pk)
|
||||
|
||||
|
||||
@login_required
|
||||
def quarterly_confirmation_edit(request, pk):
|
||||
"""Standalone edit view for quarterly confirmation"""
|
||||
nachweis = get_object_or_404(VierteljahresNachweis, pk=pk)
|
||||
|
||||
if request.method == "POST":
|
||||
form = VierteljahresNachweisForm(request.POST, request.FILES, instance=nachweis)
|
||||
if form.is_valid():
|
||||
quarterly_proof = form.save(commit=False)
|
||||
|
||||
# Calculate current status before saving
|
||||
old_status = nachweis.status
|
||||
|
||||
# Auto-update status based on completion
|
||||
if quarterly_proof.is_complete():
|
||||
if quarterly_proof.status in ['offen', 'teilweise']:
|
||||
quarterly_proof.status = 'eingereicht'
|
||||
quarterly_proof.eingereicht_am = timezone.now()
|
||||
else:
|
||||
# If not complete, set to teilweise if some fields are filled
|
||||
has_partial_data = (
|
||||
quarterly_proof.einkommenssituation_bestaetigt or
|
||||
quarterly_proof.vermogenssituation_bestaetigt or
|
||||
quarterly_proof.studiennachweis_eingereicht
|
||||
)
|
||||
if has_partial_data and quarterly_proof.status == 'offen':
|
||||
quarterly_proof.status = 'teilweise'
|
||||
|
||||
quarterly_proof.save()
|
||||
|
||||
# Try to create automatic support payment if complete
|
||||
if quarterly_proof.is_complete() and quarterly_proof.status == 'eingereicht':
|
||||
support_payment = create_quarterly_support_payment(quarterly_proof)
|
||||
if support_payment:
|
||||
messages.success(
|
||||
request,
|
||||
f"Automatische Unterstützung über {support_payment.betrag}€ für {support_payment.destinataer.get_full_name()} wurde erstellt (fällig am {support_payment.faellig_am.strftime('%d.%m.%Y')})."
|
||||
)
|
||||
else:
|
||||
# Log why payment wasn't created
|
||||
reasons = []
|
||||
if not quarterly_proof.destinataer.vierteljaehrlicher_betrag:
|
||||
reasons.append("kein vierteljährlicher Betrag hinterlegt")
|
||||
if not quarterly_proof.destinataer.iban:
|
||||
reasons.append("keine IBAN hinterlegt")
|
||||
if not quarterly_proof.destinataer.standard_konto and not StiftungsKonto.objects.exists():
|
||||
reasons.append("kein Auszahlungskonto verfügbar")
|
||||
|
||||
if reasons:
|
||||
messages.warning(
|
||||
request,
|
||||
f"Automatische Unterstützung konnte nicht erstellt werden: {', '.join(reasons)}"
|
||||
)
|
||||
|
||||
# Debug message to see what happened
|
||||
status_changed = old_status != quarterly_proof.status
|
||||
status_msg = f" (Status: {old_status} → {quarterly_proof.status})" if status_changed else f" (Status: {quarterly_proof.status})"
|
||||
|
||||
messages.success(
|
||||
request,
|
||||
f"Vierteljahresnachweis für {nachweis.destinataer.get_full_name()} "
|
||||
f"({nachweis.jahr} Q{nachweis.quartal}) wurde erfolgreich aktualisiert{status_msg}."
|
||||
)
|
||||
return redirect("stiftung:destinataer_detail", pk=nachweis.destinataer.pk)
|
||||
else:
|
||||
# Add form errors to messages
|
||||
for field, errors in form.errors.items():
|
||||
for error in errors:
|
||||
messages.error(request, f"Fehler in {field}: {error}")
|
||||
else:
|
||||
form = VierteljahresNachweisForm(instance=nachweis)
|
||||
|
||||
context = {
|
||||
'form': form,
|
||||
'nachweis': nachweis,
|
||||
'destinataer': nachweis.destinataer,
|
||||
'title': f'Vierteljahresnachweis bearbeiten - {nachweis.destinataer.get_full_name()} {nachweis.jahr} Q{nachweis.quartal}',
|
||||
}
|
||||
return render(request, 'stiftung/quarterly_confirmation_edit.html', context)
|
||||
|
||||
|
||||
@login_required
|
||||
def quarterly_confirmation_approve(request, pk):
|
||||
"""Approve quarterly confirmation (staff only)"""
|
||||
if not request.user.is_staff:
|
||||
messages.error(request, "Sie haben keine Berechtigung für diese Aktion.")
|
||||
return redirect("stiftung:destinataer_list")
|
||||
|
||||
nachweis = get_object_or_404(VierteljahresNachweis, pk=pk)
|
||||
|
||||
if request.method == "POST":
|
||||
if nachweis.status == 'eingereicht':
|
||||
nachweis.status = 'geprueft'
|
||||
nachweis.geprueft_am = timezone.now()
|
||||
nachweis.geprueft_von = request.user
|
||||
nachweis.save()
|
||||
|
||||
messages.success(
|
||||
request,
|
||||
f"Vierteljahresnachweis für {nachweis.destinataer.get_full_name()} "
|
||||
f"({nachweis.jahr} Q{nachweis.quartal}) wurde freigegeben."
|
||||
)
|
||||
else:
|
||||
messages.error(
|
||||
request,
|
||||
"Nur eingereichte Nachweise können freigegeben werden."
|
||||
)
|
||||
|
||||
return redirect("stiftung:destinataer_detail", pk=nachweis.destinataer.pk)
|
||||
|
||||
Reference in New Issue
Block a user