Implement semester-based quarterly tracking system

- Update quarterly confirmation deadlines to semester-based schedule:
  - Q1: March 15 (covers Spring semester Q1+Q2)
  - Q2: June 15 (auto-approved when Q1 approved)
  - Q3: September 15 (covers Fall semester Q3+Q4)
  - Q4: December 15 (auto-approved when Q3 approved)

- Add auto-approval functionality:
  - Q1 approval automatically approves Q2 with same document status
  - Q3 approval automatically approves Q4 with same document status
  - New 'auto_geprueft' status with distinct badge UI

- Maintain quarterly payment cycle while simplifying document submissions
- Remove modal edit functionality, keep full-screen editor only
- Update copilot instructions documentation

Changes align with academic semester system where students submit
documents twice yearly instead of quarterly.
This commit is contained in:
2025-09-30 21:32:12 +02:00
parent ed6a02232e
commit 656af599bb
6 changed files with 140 additions and 144 deletions

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.0.6 on 2025-09-30 19:25
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('stiftung', '0033_alter_backupjob_status'),
]
operations = [
migrations.AlterField(
model_name='vierteljahresnachweis',
name='status',
field=models.CharField(choices=[('offen', 'Nachweis ausstehend'), ('teilweise', 'Teilweise eingereicht'), ('eingereicht', 'Vollständig eingereicht'), ('geprueft', 'Geprüft & Freigegeben'), ('auto_geprueft', 'Automatisch freigegeben (Semesterbasis)'), ('nachbesserung', 'Nachbesserung erforderlich'), ('abgelehnt', 'Abgelehnt')], default='offen', max_length=20, verbose_name='Status'),
),
]

View File

@@ -0,0 +1,13 @@
# Generated by Django 5.0.6 on 2025-09-30 19:29
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('stiftung', '0034_add_auto_geprueft_status'),
]
operations = [
]

View File

@@ -2534,6 +2534,7 @@ class VierteljahresNachweis(models.Model):
("teilweise", "Teilweise eingereicht"),
("eingereicht", "Vollständig eingereicht"),
("geprueft", "Geprüft & Freigegeben"),
("auto_geprueft", "Automatisch freigegeben (Semesterbasis)"),
("nachbesserung", "Nachbesserung erforderlich"),
("abgelehnt", "Abgelehnt"),
]
@@ -2747,14 +2748,14 @@ class VierteljahresNachweis(models.Model):
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)
# Auto-set deadline if not provided (semester-based deadlines)
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
1: date(self.jahr, 3, 15), # Q1 deadline: March 15 (covers Q1+Q2 semester)
2: date(self.jahr, 6, 15), # Q2 deadline: June 15 (auto-approved if Q1 complete)
3: date(self.jahr, 9, 15), # Q3 deadline: September 15 (covers Q3+Q4 semester)
4: date(self.jahr, 12, 15), # Q4 deadline: December 15 (auto-approved if Q3 complete)
}
self.faelligkeitsdatum = quarter_deadlines.get(self.quartal)
@@ -2824,3 +2825,34 @@ class VierteljahresNachweis(models.Model):
faelligkeitsdatum__lt=today,
status__in=["offen", "teilweise"]
).select_related("destinataer")
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 document confirmations from current quarter
next_nachweis.studiennachweis_eingereicht = self.studiennachweis_eingereicht
next_nachweis.einkommenssituation_bestaetigt = self.einkommenssituation_bestaetigt
next_nachweis.vermogenssituation_bestaetigt = self.vermogenssituation_bestaetigt
# 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', 'einkommenssituation_bestaetigt',
'vermogenssituation_bestaetigt', 'status', 'geprueft_am', 'geprueft_von'
])
return next_nachweis
except VierteljahresNachweis.DoesNotExist:
pass
return None

View File

@@ -1267,6 +1267,8 @@ def destinataer_detail(request, pk):
jahr__in=[current_year, current_year + 1]
).order_by('-jahr', '-quartal')
# Modal forms removed - only using full-screen editor now
# Generate available years for the add quarter dropdown (current year + next 5 years)
available_years = list(range(current_year, current_year + 6))
@@ -7720,6 +7722,14 @@ def quarterly_confirmation_approve(request, pk):
nachweis.geprueft_am = timezone.now()
nachweis.geprueft_von = request.user
nachweis.save()
# Auto-approve next quarter for semester-based tracking (Q1→Q2, Q3→Q4)
auto_approved_next = nachweis.auto_approve_next_quarter()
if auto_approved_next:
messages.info(
request,
f"Q{auto_approved_next.quartal} wurde automatisch auf Basis der Q{nachweis.quartal}-Nachweise freigegeben."
)
# Handle support payment - create if missing, update if exists
if not related_payment:

View File

@@ -510,6 +510,10 @@
<span class="badge bg-info">Eingereicht</span>
{% elif nachweis.status == 'geprueft' %}
<span class="badge bg-success">Freigegeben</span>
{% elif nachweis.status == 'auto_geprueft' %}
<span class="badge bg-success">
<i class="fas fa-magic me-1"></i>Auto-Freigabe
</span>
{% elif nachweis.status == 'nachbesserung' %}
<span class="badge bg-warning">Nachbesserung</span>
{% elif nachweis.status == 'abgelehnt' %}
@@ -589,17 +593,10 @@
</td>
<td>
<div class="btn-group" role="group" aria-label="Aktionen">
<button type="button"
class="btn btn-sm btn-outline-primary"
data-bs-toggle="modal"
data-bs-target="#quartalModal{{ nachweis.id }}"
title="Bearbeiten (Modal)">
<i class="fas fa-edit"></i>
</button>
<a href="{% url 'stiftung:quarterly_confirmation_edit' nachweis.id %}"
class="btn btn-sm btn-outline-secondary"
title="Bearbeiten (Vollbild)">
<i class="fas fa-external-link-alt"></i>
class="btn btn-sm btn-outline-primary"
title="Bearbeiten">
<i class="fas fa-edit"></i>
</a>
{% if user.is_staff %}
{% if nachweis.status == 'eingereicht' %}
@@ -631,135 +628,7 @@
</tbody>
</table>
</div>
<!-- Quarterly Confirmation Modals -->
{% for nachweis in quarterly_confirmations %}
<div class="modal fade" id="quartalModal{{ nachweis.id }}" tabindex="-1" aria-labelledby="quartalModalLabel{{ nachweis.id }}" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="quartalModalLabel{{ nachweis.id }}">
<i class="fas fa-calendar-check me-2"></i>
Nachweis {{ nachweis.jahr }} {{ nachweis.get_quarter_display }} - {{ destinataer.get_full_name }}
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Schließen"></button>
</div>
<form id="quarterlyForm{{ nachweis.id }}" method="post" action="{% url 'stiftung:quarterly_confirmation_update' nachweis.id %}" enctype="multipart/form-data">
{% csrf_token %}
<div class="modal-body">
<div class="row">
<!-- Study Proof Section -->
<div class="col-12 mb-4">
<h6 class="text-primary border-bottom pb-2">
<i class="fas fa-graduation-cap me-2"></i>Studiennachweis
</h6>
<div class="form-check mb-3">
<input type="checkbox" class="form-check-input" id="studiennachweis_eingereicht{{ nachweis.id }}" name="studiennachweis_eingereicht" {% if nachweis.studiennachweis_eingereicht %}checked{% endif %}>
<label class="form-check-label" for="studiennachweis_eingereicht{{ nachweis.id }}">
Studiennachweis eingereicht
</label>
</div>
<div class="mb-3">
<label for="studiennachweis_datei{{ nachweis.id }}" class="form-label">Studiennachweis (Datei)</label>
<input type="file" class="form-control" id="studiennachweis_datei{{ nachweis.id }}" name="studiennachweis_datei" accept=".pdf,.jpg,.jpeg,.png,.doc,.docx">
{% if nachweis.studiennachweis_datei %}
<small class="text-muted">Aktuelle Datei: <a href="{{ nachweis.studiennachweis_datei.url }}" target="_blank">{{ nachweis.studiennachweis_datei.name }}</a></small>
{% endif %}
</div>
<div class="mb-3">
<label for="studiennachweis_bemerkung{{ nachweis.id }}" class="form-label">Bemerkung zum Studiennachweis</label>
<textarea class="form-control" id="studiennachweis_bemerkung{{ nachweis.id }}" name="studiennachweis_bemerkung" rows="2">{{ nachweis.studiennachweis_bemerkung|default:"" }}</textarea>
</div>
</div>
<!-- Income Situation Section -->
<div class="col-12 mb-4">
<h6 class="text-success border-bottom pb-2">
<i class="fas fa-euro-sign me-2"></i>Einkommenssituation
</h6>
<div class="form-check mb-3">
<input type="checkbox" class="form-check-input" id="einkommenssituation_bestaetigt{{ nachweis.id }}" name="einkommenssituation_bestaetigt" {% if nachweis.einkommenssituation_bestaetigt %}checked{% endif %}>
<label class="form-check-label" for="einkommenssituation_bestaetigt{{ nachweis.id }}">
Einkommenssituation bestätigt
</label>
</div>
<div class="mb-3">
<label for="einkommenssituation_text{{ nachweis.id }}" class="form-label">Einkommenssituation (Text)</label>
<textarea class="form-control" id="einkommenssituation_text{{ nachweis.id }}" name="einkommenssituation_text" rows="3" placeholder='Z.B. "Keine Änderungen seit letzter Meldung"'>{{ nachweis.einkommenssituation_text|default:"" }}</textarea>
</div>
<div class="mb-3">
<label for="einkommenssituation_datei{{ nachweis.id }}" class="form-label">Einkommenssituation (Datei)</label>
<input type="file" class="form-control" id="einkommenssituation_datei{{ nachweis.id }}" name="einkommenssituation_datei" accept=".pdf,.jpg,.jpeg,.png,.doc,.docx">
{% if nachweis.einkommenssituation_datei %}
<small class="text-muted">Aktuelle Datei: <a href="{{ nachweis.einkommenssituation_datei.url }}" target="_blank">{{ nachweis.einkommenssituation_datei.name }}</a></small>
{% endif %}
</div>
</div>
<!-- Asset Situation Section -->
<div class="col-12 mb-4">
<h6 class="text-warning border-bottom pb-2">
<i class="fas fa-piggy-bank me-2"></i>Vermögenssituation
</h6>
<div class="form-check mb-3">
<input type="checkbox" class="form-check-input" id="vermogenssituation_bestaetigt{{ nachweis.id }}" name="vermogenssituation_bestaetigt" {% if nachweis.vermogenssituation_bestaetigt %}checked{% endif %}>
<label class="form-check-label" for="vermogenssituation_bestaetigt{{ nachweis.id }}">
Vermögenssituation bestätigt
</label>
</div>
<div class="mb-3">
<label for="vermogenssituation_text{{ nachweis.id }}" class="form-label">Vermögenssituation (Text)</label>
<textarea class="form-control" id="vermogenssituation_text{{ nachweis.id }}" name="vermogenssituation_text" rows="3" placeholder='Z.B. "Keine Änderungen seit letzter Meldung"'>{{ nachweis.vermogenssituation_text|default:"" }}</textarea>
</div>
<div class="mb-3">
<label for="vermogenssituation_datei{{ nachweis.id }}" class="form-label">Vermögenssituation (Datei)</label>
<input type="file" class="form-control" id="vermogenssituation_datei{{ nachweis.id }}" name="vermogenssituation_datei" accept=".pdf,.jpg,.jpeg,.png,.doc,.docx">
{% if nachweis.vermogenssituation_datei %}
<small class="text-muted">Aktuelle Datei: <a href="{{ nachweis.vermogenssituation_datei.url }}" target="_blank">{{ nachweis.vermogenssituation_datei.name }}</a></small>
{% endif %}
</div>
</div>
<!-- Additional Documents Section -->
<div class="col-12 mb-4">
<h6 class="text-info border-bottom pb-2">
<i class="fas fa-file-alt me-2"></i>Weitere Dokumente (optional)
</h6>
<div class="mb-3">
<label for="weitere_dokumente{{ nachweis.id }}" class="form-label">Weitere Dokumente</label>
<input type="file" class="form-control" id="weitere_dokumente{{ nachweis.id }}" name="weitere_dokumente" accept=".pdf,.jpg,.jpeg,.png,.doc,.docx">
{% if nachweis.weitere_dokumente %}
<small class="text-muted">Aktuelle Datei: <a href="{{ nachweis.weitere_dokumente.url }}" target="_blank">{{ nachweis.weitere_dokumente.name }}</a></small>
{% endif %}
</div>
<div class="mb-3">
<label for="weitere_dokumente_beschreibung{{ nachweis.id }}" class="form-label">Beschreibung weitere Dokumente</label>
<textarea class="form-control" id="weitere_dokumente_beschreibung{{ nachweis.id }}" name="weitere_dokumente_beschreibung" rows="2">{{ nachweis.weitere_dokumente_beschreibung|default:"" }}</textarea>
</div>
</div>
<!-- Internal Notes (Staff Only) -->
{% if user.is_staff %}
<div class="col-12 mb-3">
<h6 class="text-secondary border-bottom pb-2">
<i class="fas fa-user-shield me-2"></i>Interne Notizen (nur für Verwaltung)
</h6>
<textarea class="form-control" name="interne_notizen" rows="3" placeholder="Interne Notizen zur Bearbeitung">{{ nachweis.interne_notizen|default:"" }}</textarea>
</div>
{% endif %}
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
<button type="submit" class="btn btn-primary">
<i class="fas fa-save me-2"></i>Speichern
</button>
</div>
</form>
</div>
</div>
</div>
{% endfor %}
<!-- Add Quarter Modal -->
<div class="modal fade" id="addQuarterModal" tabindex="-1" aria-labelledby="addQuarterModalLabel" aria-hidden="true">