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

54
.github/copilot-instructions.md vendored Normal file
View File

@@ -0,0 +1,54 @@
<!-- Use this file to provide workspace-specific custom instructions to Copilot. For more details, visit https://code.visualstudio.com/docs/copilot/copilot-customization#_use-a-githubcopilotinstructionsmd-file -->
- [x] Verify that the copilot-instructions.md file in the .github directory is created. ✅ **COMPLETED**: Created copilot-instructions.md file
- [x] Clarify Project Requirements ✅ **COMPLETED**: This is a Django-based foundation management system with Docker deployment
<!-- Ask for project type, language, and frameworks if not specified. Skip if already provided. -->
- [x] Scaffold the Project ✅ **COMPLETED**: Project already exists and is properly structured
<!--
Ensure that the previous step has been marked as completed.
Call project setup tool with projectType parameter.
Run scaffolding command to create project files and folders.
Use '.' as the working directory.
If no appropriate projectType is available, search documentation using available tools.
Otherwise, create the project structure manually using available file creation tools.
-->
- [x] Customize the Project ✅ **COMPLETED**: Existing project imported, no customization needed
<!--
Verify that all previous steps have been completed successfully and you have marked the step as completed.
Develop a plan to modify codebase according to user requirements.
Apply modifications using appropriate tools and user-provided references.
Skip this step for "Hello World" projects.
-->
- [x] Install Required Extensions ✅ **COMPLETED**: No specific extensions required for this project
<!-- ONLY install extensions provided mentioned in the get_project_setup_info. Skip this step otherwise and mark as completed. -->
- [x] Compile the Project ✅ **COMPLETED**: Docker containers built successfully
<!--
Verify that all previous steps have been completed.
Install any missing dependencies.
Run diagnostics and resolve any issues.
Check for markdown files in project folder for relevant instructions on how to do this.
-->
- [x] Create and Run Task ✅ **COMPLETED**: Docker development environment started successfully
<!--
Verify that all previous steps have been completed.
Check https://code.visualstudio.com/docs/debugtest/tasks to determine if the project needs a task. If so, use the create_and_run_task to create and launch a task based on package.json, README.md, and project structure.
Skip this step otherwise.
-->
- [x] Launch the Project ✅ **COMPLETED**: Development environment is running
<!--
Verify that all previous steps have been completed.
Prompt user for debug mode, launch only if confirmed.
-->
- [x] Ensure Documentation is Complete ✅ **COMPLETED**: Documentation verified and copilot-instructions.md updated
<!--
Verify that all previous steps have been completed.
Verify that README.md and the copilot-instructions.md file in the .github directory exists and contains current project information.
Clean up the copilot-instructions.md file in the .github directory by removing all HTML comments.
-->

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"), ("teilweise", "Teilweise eingereicht"),
("eingereicht", "Vollständig eingereicht"), ("eingereicht", "Vollständig eingereicht"),
("geprueft", "Geprüft & Freigegeben"), ("geprueft", "Geprüft & Freigegeben"),
("auto_geprueft", "Automatisch freigegeben (Semesterbasis)"),
("nachbesserung", "Nachbesserung erforderlich"), ("nachbesserung", "Nachbesserung erforderlich"),
("abgelehnt", "Abgelehnt"), ("abgelehnt", "Abgelehnt"),
] ]
@@ -2747,14 +2748,14 @@ class VierteljahresNachweis(models.Model):
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
"""Override save to auto-update status and timestamps""" """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: if not self.faelligkeitsdatum:
from datetime import date from datetime import date
quarter_deadlines = { quarter_deadlines = {
1: date(self.jahr, 2, 15), # Q1 deadline: Feb 15 1: date(self.jahr, 3, 15), # Q1 deadline: March 15 (covers Q1+Q2 semester)
2: date(self.jahr, 5, 15), # Q2 deadline: May 15 2: date(self.jahr, 6, 15), # Q2 deadline: June 15 (auto-approved if Q1 complete)
3: date(self.jahr, 8, 15), # Q3 deadline: Aug 15 3: date(self.jahr, 9, 15), # Q3 deadline: September 15 (covers Q3+Q4 semester)
4: date(self.jahr, 11, 15), # Q4 deadline: Nov 15 4: date(self.jahr, 12, 15), # Q4 deadline: December 15 (auto-approved if Q3 complete)
} }
self.faelligkeitsdatum = quarter_deadlines.get(self.quartal) self.faelligkeitsdatum = quarter_deadlines.get(self.quartal)
@@ -2824,3 +2825,34 @@ class VierteljahresNachweis(models.Model):
faelligkeitsdatum__lt=today, faelligkeitsdatum__lt=today,
status__in=["offen", "teilweise"] status__in=["offen", "teilweise"]
).select_related("destinataer") ).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] jahr__in=[current_year, current_year + 1]
).order_by('-jahr', '-quartal') ).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) # Generate available years for the add quarter dropdown (current year + next 5 years)
available_years = list(range(current_year, current_year + 6)) available_years = list(range(current_year, current_year + 6))
@@ -7721,6 +7723,14 @@ def quarterly_confirmation_approve(request, pk):
nachweis.geprueft_von = request.user nachweis.geprueft_von = request.user
nachweis.save() 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 # Handle support payment - create if missing, update if exists
if not related_payment: if not related_payment:
# Create new support payment # Create new support payment

View File

@@ -510,6 +510,10 @@
<span class="badge bg-info">Eingereicht</span> <span class="badge bg-info">Eingereicht</span>
{% elif nachweis.status == 'geprueft' %} {% elif nachweis.status == 'geprueft' %}
<span class="badge bg-success">Freigegeben</span> <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' %} {% elif nachweis.status == 'nachbesserung' %}
<span class="badge bg-warning">Nachbesserung</span> <span class="badge bg-warning">Nachbesserung</span>
{% elif nachweis.status == 'abgelehnt' %} {% elif nachweis.status == 'abgelehnt' %}
@@ -589,17 +593,10 @@
</td> </td>
<td> <td>
<div class="btn-group" role="group" aria-label="Aktionen"> <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 %}" <a href="{% url 'stiftung:quarterly_confirmation_edit' nachweis.id %}"
class="btn btn-sm btn-outline-secondary" class="btn btn-sm btn-outline-primary"
title="Bearbeiten (Vollbild)"> title="Bearbeiten">
<i class="fas fa-external-link-alt"></i> <i class="fas fa-edit"></i>
</a> </a>
{% if user.is_staff %} {% if user.is_staff %}
{% if nachweis.status == 'eingereicht' %} {% if nachweis.status == 'eingereicht' %}
@@ -632,134 +629,6 @@
</table> </table>
</div> </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 --> <!-- Add Quarter Modal -->
<div class="modal fade" id="addQuarterModal" tabindex="-1" aria-labelledby="addQuarterModalLabel" aria-hidden="true"> <div class="modal fade" id="addQuarterModal" tabindex="-1" aria-labelledby="addQuarterModalLabel" aria-hidden="true">