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:
2025-09-23 23:52:44 +02:00
parent 0184982f8c
commit 126f68ec68
9 changed files with 2177 additions and 56 deletions

View File

@@ -2472,3 +2472,296 @@ class HelpBox(models.Model):
return cls.objects.get(page_key=page_key, is_active=True)
except cls.DoesNotExist:
return None
class VierteljahresNachweis(models.Model):
"""Quarterly confirmation system for Destinatäre"""
QUARTAL_CHOICES = [
(1, "Q1 (Jan-Mär)"),
(2, "Q2 (Apr-Jun)"),
(3, "Q3 (Jul-Sep)"),
(4, "Q4 (Okt-Dez)"),
]
STATUS_CHOICES = [
("offen", "Nachweis ausstehend"),
("teilweise", "Teilweise eingereicht"),
("eingereicht", "Vollständig eingereicht"),
("geprueft", "Geprüft & Freigegeben"),
("nachbesserung", "Nachbesserung erforderlich"),
("abgelehnt", "Abgelehnt"),
]
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
destinataer = models.ForeignKey(
Destinataer,
on_delete=models.CASCADE,
related_name="quartalseinreichungen",
verbose_name="Destinatär"
)
# Time period
jahr = models.IntegerField(
verbose_name="Jahr",
validators=[MinValueValidator(2020), MaxValueValidator(2050)]
)
quartal = models.IntegerField(
choices=QUARTAL_CHOICES,
verbose_name="Quartal"
)
# Study proof (if required)
studiennachweis_erforderlich = models.BooleanField(
default=True,
verbose_name="Studiennachweis erforderlich"
)
studiennachweis_eingereicht = models.BooleanField(
default=False,
verbose_name="Studiennachweis eingereicht"
)
studiennachweis_datei = models.FileField(
upload_to="quarterly_proofs/studies/%Y/Q%m/",
null=True,
blank=True,
verbose_name="Studiennachweis (Datei)"
)
studiennachweis_bemerkung = models.TextField(
null=True,
blank=True,
verbose_name="Bemerkung zum Studiennachweis"
)
# Income/situation confirmation
einkommenssituation_bestaetigt = models.BooleanField(
default=False,
verbose_name="Einkommenssituation bestätigt"
)
einkommenssituation_text = models.TextField(
null=True,
blank=True,
verbose_name="Einkommenssituation (Text)",
help_text="Z.B. 'Keine Änderungen seit letzter Meldung' oder Details zu Änderungen"
)
einkommenssituation_datei = models.FileField(
upload_to="quarterly_proofs/income/%Y/Q%m/",
null=True,
blank=True,
verbose_name="Einkommenssituation (Datei)"
)
# Asset/wealth confirmation
vermogenssituation_bestaetigt = models.BooleanField(
default=False,
verbose_name="Vermögenssituation bestätigt"
)
vermogenssituation_text = models.TextField(
null=True,
blank=True,
verbose_name="Vermögenssituation (Text)",
help_text="Z.B. 'Keine Änderungen seit letzter Meldung' oder Details zu Änderungen"
)
vermogenssituation_datei = models.FileField(
upload_to="quarterly_proofs/assets/%Y/Q%m/",
null=True,
blank=True,
verbose_name="Vermögenssituation (Datei)"
)
# Additional documents
weitere_dokumente = models.FileField(
upload_to="quarterly_proofs/additional/%Y/Q%m/",
null=True,
blank=True,
verbose_name="Weitere Dokumente"
)
weitere_dokumente_beschreibung = models.TextField(
null=True,
blank=True,
verbose_name="Beschreibung weitere Dokumente"
)
# Review and approval
status = models.CharField(
max_length=20,
choices=STATUS_CHOICES,
default="offen",
verbose_name="Status"
)
interne_notizen = models.TextField(
null=True,
blank=True,
verbose_name="Interne Notizen (nur für Verwaltung)"
)
# Timestamps and tracking
erstellt_am = models.DateTimeField(auto_now_add=True, verbose_name="Erstellt am")
aktualisiert_am = models.DateTimeField(auto_now=True, verbose_name="Aktualisiert am")
eingereicht_am = models.DateTimeField(
null=True,
blank=True,
verbose_name="Eingereicht am"
)
geprueft_am = models.DateTimeField(
null=True,
blank=True,
verbose_name="Geprüft am"
)
geprueft_von = models.ForeignKey(
"auth.User",
on_delete=models.SET_NULL,
null=True,
blank=True,
verbose_name="Geprüft von"
)
# Deadline tracking
faelligkeitsdatum = models.DateField(
null=True,
blank=True,
verbose_name="Fälligkeitsdatum"
)
class Meta:
verbose_name = "Vierteljahresnachweis"
verbose_name_plural = "Vierteljahresnachweise"
ordering = ["-jahr", "-quartal", "destinataer__nachname"]
unique_together = ["destinataer", "jahr", "quartal"] # One entry per quarter per person
indexes = [
models.Index(fields=["jahr", "quartal", "status"]),
models.Index(fields=["destinataer", "status"]),
models.Index(fields=["faelligkeitsdatum"]),
]
def __str__(self):
return f"{self.destinataer.get_full_name()} - {self.jahr} Q{self.quartal} ({self.get_status_display()})"
def get_quarter_display(self):
"""Get a nice display name for the quarter"""
quarter_names = {
1: "Q1 (Januar - März)",
2: "Q2 (April - Juni)",
3: "Q3 (Juli - September)",
4: "Q4 (Oktober - Dezember)"
}
return quarter_names.get(self.quartal, f"Q{self.quartal}")
def is_complete(self):
"""Check if all required documents/confirmations are provided"""
complete = True
# Check study proof (always required now)
complete &= self.studiennachweis_eingereicht and (
bool(self.studiennachweis_datei) or bool(self.studiennachweis_bemerkung)
)
# Check income situation (either text or file)
complete &= self.einkommenssituation_bestaetigt and (
bool(self.einkommenssituation_text) or bool(self.einkommenssituation_datei)
)
# Check asset situation (either text or file)
complete &= self.vermogenssituation_bestaetigt and (
bool(self.vermogenssituation_text) or bool(self.vermogenssituation_datei)
)
return complete
def is_overdue(self):
"""Check if the deadline has passed"""
if not self.faelligkeitsdatum:
return False
return timezone.now().date() > self.faelligkeitsdatum and self.status in ["offen", "teilweise"]
def get_completion_percentage(self):
"""Calculate completion percentage"""
total_requirements = 2 # Income and assets always required
completed_requirements = 0
# Study proof (if required)
if self.studiennachweis_erforderlich:
total_requirements += 1
if self.studiennachweis_eingereicht and (
bool(self.studiennachweis_datei) or bool(self.studiennachweis_bemerkung)
):
completed_requirements += 1
# Income situation
if self.einkommenssituation_bestaetigt and (
bool(self.einkommenssituation_text) or bool(self.einkommenssituation_datei)
):
completed_requirements += 1
# Asset situation
if self.vermogenssituation_bestaetigt and (
bool(self.vermogenssituation_text) or bool(self.vermogenssituation_datei)
):
completed_requirements += 1
return int((completed_requirements / total_requirements) * 100) if total_requirements > 0 else 0
def save(self, *args, **kwargs):
"""Override save to auto-update status and timestamps"""
# Auto-set deadline if not provided (15th of the quarter's second month)
if not self.faelligkeitsdatum:
from datetime import date
quarter_deadlines = {
1: date(self.jahr, 2, 15), # Q1 deadline: Feb 15
2: date(self.jahr, 5, 15), # Q2 deadline: May 15
3: date(self.jahr, 8, 15), # Q3 deadline: Aug 15
4: date(self.jahr, 11, 15), # Q4 deadline: Nov 15
}
self.faelligkeitsdatum = quarter_deadlines.get(self.quartal)
# Auto-update status based on completion
if self.is_complete():
if self.status == "offen":
self.status = "eingereicht"
self.eingereicht_am = timezone.now()
else:
completion = self.get_completion_percentage()
if completion > 0 and completion < 100 and self.status == "offen":
self.status = "teilweise"
super().save(*args, **kwargs)
@classmethod
def get_or_create_for_period(cls, destinataer, jahr, quartal):
"""Get or create a quarterly confirmation for a specific period"""
nachweis, created = cls.objects.get_or_create(
destinataer=destinataer,
jahr=jahr,
quartal=quartal,
defaults={
'studiennachweis_erforderlich': destinataer.studiennachweis_erforderlich,
'status': 'offen'
}
)
return nachweis, created
@classmethod
def get_current_quarter(cls):
"""Get the current quarter based on today's date"""
from datetime import date
today = date.today()
month = today.month
if month <= 3:
return today.year, 1
elif month <= 6:
return today.year, 2
elif month <= 9:
return today.year, 3
else:
return today.year, 4
@classmethod
def get_overdue_confirmations(cls):
"""Get all overdue quarterly confirmations"""
from datetime import date
today = date.today()
return cls.objects.filter(
faelligkeitsdatum__lt=today,
status__in=["offen", "teilweise"]
).select_related("destinataer")