Getrennte Fristen für Studiennachweis und Zahlung implementieren
Some checks failed
Code Quality / quality (push) Has been cancelled
CI/CD Pipeline / test (push) Has been cancelled
CI/CD Pipeline / deploy (push) Has been cancelled

- Neue Felder: studiennachweis_faelligkeitsdatum (semesterbasiert) und zahlung_faelligkeitsdatum (vierteljährlich im Voraus)
- Studiennachweis-Fristen: Q1/Q2 → 15. März, Q3/Q4 → 15. September
- Zahlungsfälligkeiten: Q1 → 15. Dez (Vorjahr), Q2 → 15. Mär, Q3 → 15. Jun, Q4 → 15. Sep
- Auto-Freigabe: Q1 freigeben → Q2 Studiennachweis auto-freigegeben, Q3 → Q4
- Unterstützungserstellung: Verhindert Duplikate durch präzise Suche nach zahlung_faelligkeitsdatum
- Quartalserstellung: Modal-Formular funktioniert korrekt
- UI: Beide Fristen in Tabelle angezeigt, separate Überfälligkeits-Indikatoren
- Migration: Neue Felder hinzugefügt und bestehende Datensätze befüllt
This commit is contained in:
2025-12-30 20:20:33 +01:00
parent 24435660f5
commit 6c8ddbb4f0
6 changed files with 542 additions and 187 deletions

View File

@@ -2665,7 +2665,23 @@ class VierteljahresNachweis(models.Model):
faelligkeitsdatum = models.DateField(
null=True,
blank=True,
verbose_name="Fälligkeitsdatum"
verbose_name="Fälligkeitsdatum",
help_text="Veraltet - wird durch studiennachweis_faelligkeitsdatum und zahlung_faelligkeitsdatum ersetzt"
)
# Separate deadlines for study proof (semester-based) and payment (quarterly)
studiennachweis_faelligkeitsdatum = models.DateField(
null=True,
blank=True,
verbose_name="Studiennachweis Fälligkeitsdatum",
help_text="Semesterbasierte Frist: Q1/Q2 → 15. März, Q3/Q4 → 15. September"
)
zahlung_faelligkeitsdatum = models.DateField(
null=True,
blank=True,
verbose_name="Zahlungsfälligkeit",
help_text="Vierteljährliche Zahlungsfälligkeit im Voraus: Q1→15. Dez (Vorjahr), Q2→15. Mär, Q3→15. Jun, Q4→15. Sep"
)
class Meta:
@@ -2746,18 +2762,67 @@ class VierteljahresNachweis(models.Model):
return int((completed_requirements / total_requirements) * 100) if total_requirements > 0 else 0
def get_study_proof_deadline(self):
"""Calculate semester-based study proof deadline"""
from datetime import date
# Q1, Q2 → March 15 (same year)
# Q3, Q4 → September 15 (same year)
if self.quartal in [1, 2]:
return date(self.jahr, 3, 15)
else: # Q3, Q4
return date(self.jahr, 9, 15)
def get_payment_due_date(self):
"""Calculate quarterly payment due date (paid in advance)"""
from datetime import date
# Q1 → December 15 (previous year)
# Q2 → March 15 (same year)
# Q3 → June 15 (same year)
# Q4 → September 15 (same year)
if self.quartal == 1:
return date(self.jahr - 1, 12, 15)
elif self.quartal == 2:
return date(self.jahr, 3, 15)
elif self.quartal == 3:
return date(self.jahr, 6, 15)
else: # Q4
return date(self.jahr, 9, 15)
def is_study_proof_overdue(self):
"""Check if study proof deadline has passed"""
if not self.studiennachweis_faelligkeitsdatum:
return False
from django.utils import timezone
return timezone.now().date() > self.studiennachweis_faelligkeitsdatum and not self.studiennachweis_eingereicht
def is_payment_overdue(self):
"""Check if payment due date has passed"""
if not self.zahlung_faelligkeitsdatum:
return False
from django.utils import timezone
# Payment is overdue if due date passed and no payment exists or payment is not completed
payment = self.get_related_support_payment()
if payment and payment.status in ['bezahlt', 'in_bearbeitung']:
return False
return timezone.now().date() > self.zahlung_faelligkeitsdatum
def is_overdue(self):
"""Check if either deadline has passed"""
return self.is_study_proof_overdue() or self.is_payment_overdue()
def save(self, *args, **kwargs):
"""Override save to auto-update status and timestamps"""
# Auto-set deadline if not provided (semester-based deadlines)
# Set study proof deadline (semester-based) if not provided
if not self.studiennachweis_faelligkeitsdatum:
self.studiennachweis_faelligkeitsdatum = self.get_study_proof_deadline()
# Set payment due date (quarterly, advance) if not provided
if not self.zahlung_faelligkeitsdatum:
self.zahlung_faelligkeitsdatum = self.get_payment_due_date()
# Backward compatibility: set faelligkeitsdatum from study proof deadline if not set
if not self.faelligkeitsdatum:
from datetime import date
quarter_deadlines = {
1: date(self.jahr, 3, 15), # Q1 deadline: March 15 (Spring semester)
2: date(self.jahr, 3, 15), # Q2 deadline: March 15 (Spring semester, same as Q1)
3: date(self.jahr, 9, 15), # Q3 deadline: September 15 (Fall semester)
4: date(self.jahr, 9, 15), # Q4 deadline: September 15 (Fall semester, same as Q3)
}
self.faelligkeitsdatum = quarter_deadlines.get(self.quartal)
self.faelligkeitsdatum = self.studiennachweis_faelligkeitsdatum
# Auto-update status based on completion
if self.is_complete():
@@ -2773,18 +2838,71 @@ class VierteljahresNachweis(models.Model):
def get_related_support_payment(self):
"""Get the related support payment for this quarterly confirmation"""
from datetime import datetime
from datetime import date, timedelta
from django.db.models import Q
quarter_start = datetime(self.jahr, (self.quartal - 1) * 3 + 1, 1).date()
quarter_end = datetime(self.jahr, self.quartal * 3, 1).date()
# Use payment due date from quarterly confirmation for accurate search
# This is more accurate than using quarter date range, especially for Q1 (Dec 15 prev year)
payment_due_date = self.zahlung_faelligkeitsdatum
if not payment_due_date:
# Fallback: calculate if not set
if self.quartal == 1:
payment_due_date = date(self.jahr - 1, 12, 15)
elif self.quartal == 2:
payment_due_date = date(self.jahr, 3, 15)
elif self.quartal == 3:
payment_due_date = date(self.jahr, 6, 15)
else: # Q4
payment_due_date = date(self.jahr, 9, 15)
# Search for existing payment - match by payment due date and description
# Use a date range around the due date (±30 days) to catch any variations
date_start = payment_due_date - timedelta(days=30)
date_end = payment_due_date + timedelta(days=30)
return DestinataerUnterstuetzung.objects.filter(
destinataer=self.destinataer,
faellig_am__gte=quarter_start,
faellig_am__lt=quarter_end,
beschreibung__contains=f"Q{self.quartal}/{self.jahr}"
faellig_am__gte=date_start,
faellig_am__lte=date_end
).filter(
Q(beschreibung__contains=f"Q{self.quartal}/{self.jahr}") |
Q(beschreibung__contains=f"Vierteljährliche Unterstützung Q{self.quartal}/{self.jahr}")
).first()
def auto_approve_next_quarter(self):
"""Auto-approve the next quarter when Q1 or Q3 is approved (semester-based logic)"""
if self.quartal in [1, 3] and self.status == "geprueft":
next_quarter = self.quartal + 1
try:
next_nachweis = VierteljahresNachweis.objects.get(
destinataer=self.destinataer,
jahr=self.jahr,
quartal=next_quarter
)
if next_nachweis.status in ["offen", "teilweise"]:
# Copy study proof confirmations from current quarter (semester-based)
next_nachweis.studiennachweis_eingereicht = self.studiennachweis_eingereicht
next_nachweis.studiennachweis_datei = self.studiennachweis_datei
next_nachweis.studiennachweis_bemerkung = self.studiennachweis_bemerkung
# Set study proof deadline for next quarter (same semester)
next_nachweis.studiennachweis_faelligkeitsdatum = next_nachweis.get_study_proof_deadline()
# Set auto-approved status
next_nachweis.status = "auto_geprueft"
next_nachweis.geprueft_am = timezone.now()
next_nachweis.geprueft_von = self.geprueft_von
next_nachweis.save(update_fields=[
'studiennachweis_eingereicht', 'studiennachweis_datei', 'studiennachweis_bemerkung',
'studiennachweis_faelligkeitsdatum', 'status', 'geprueft_am', 'geprueft_von'
])
return next_nachweis
except VierteljahresNachweis.DoesNotExist:
pass
return None
@classmethod
def get_or_create_for_period(cls, destinataer, jahr, quartal):
"""Get or create a quarterly confirmation for a specific period"""