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:
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user