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:
@@ -1,6 +1,7 @@
|
|||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.db.models import Count, Sum
|
from django.db.models import Count, Sum
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
from django.utils import timezone
|
||||||
from django.utils.html import format_html
|
from django.utils.html import format_html
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
|
|
||||||
@@ -9,7 +10,7 @@ from .models import (AppConfiguration, AuditLog, BackupJob, BankTransaction,
|
|||||||
CSVImport, Destinataer, DestinataerUnterstuetzung,
|
CSVImport, Destinataer, DestinataerUnterstuetzung,
|
||||||
DokumentLink, Foerderung, Land, LandVerpachtung, Paechter, Person,
|
DokumentLink, Foerderung, Land, LandVerpachtung, Paechter, Person,
|
||||||
Rentmeister, StiftungsKonto, UnterstuetzungWiederkehrend,
|
Rentmeister, StiftungsKonto, UnterstuetzungWiederkehrend,
|
||||||
Verwaltungskosten)
|
Verwaltungskosten, VierteljahresNachweis)
|
||||||
|
|
||||||
|
|
||||||
@admin.register(CSVImport)
|
@admin.register(CSVImport)
|
||||||
@@ -968,6 +969,194 @@ class HelpBoxAdmin(admin.ModelAdmin):
|
|||||||
super().save_model(request, obj, form, change)
|
super().save_model(request, obj, form, change)
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(VierteljahresNachweis)
|
||||||
|
class VierteljahresNachweisAdmin(admin.ModelAdmin):
|
||||||
|
list_display = [
|
||||||
|
"destinataer",
|
||||||
|
"jahr",
|
||||||
|
"quartal",
|
||||||
|
"status",
|
||||||
|
"completion_percentage",
|
||||||
|
"faelligkeitsdatum",
|
||||||
|
"is_overdue_display",
|
||||||
|
"eingereicht_am",
|
||||||
|
"geprueft_von",
|
||||||
|
]
|
||||||
|
list_filter = [
|
||||||
|
"jahr",
|
||||||
|
"quartal",
|
||||||
|
"status",
|
||||||
|
"studiennachweis_erforderlich",
|
||||||
|
"studiennachweis_eingereicht",
|
||||||
|
"einkommenssituation_bestaetigt",
|
||||||
|
"vermogenssituation_bestaetigt",
|
||||||
|
"faelligkeitsdatum",
|
||||||
|
]
|
||||||
|
search_fields = [
|
||||||
|
"destinataer__vorname",
|
||||||
|
"destinataer__nachname",
|
||||||
|
"destinataer__email",
|
||||||
|
]
|
||||||
|
readonly_fields = [
|
||||||
|
"id",
|
||||||
|
"erstellt_am",
|
||||||
|
"aktualisiert_am",
|
||||||
|
"completion_percentage",
|
||||||
|
"is_overdue_display",
|
||||||
|
]
|
||||||
|
ordering = ["-jahr", "-quartal", "destinataer__nachname"]
|
||||||
|
|
||||||
|
fieldsets = (
|
||||||
|
(
|
||||||
|
"Grundinformationen",
|
||||||
|
{
|
||||||
|
"fields": (
|
||||||
|
"destinataer",
|
||||||
|
"jahr",
|
||||||
|
"quartal",
|
||||||
|
"status",
|
||||||
|
"faelligkeitsdatum",
|
||||||
|
)
|
||||||
|
},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Studiennachweis",
|
||||||
|
{
|
||||||
|
"fields": (
|
||||||
|
"studiennachweis_erforderlich",
|
||||||
|
"studiennachweis_eingereicht",
|
||||||
|
"studiennachweis_datei",
|
||||||
|
"studiennachweis_bemerkung",
|
||||||
|
),
|
||||||
|
"classes": ("collapse",),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Einkommenssituation",
|
||||||
|
{
|
||||||
|
"fields": (
|
||||||
|
"einkommenssituation_bestaetigt",
|
||||||
|
"einkommenssituation_text",
|
||||||
|
"einkommenssituation_datei",
|
||||||
|
),
|
||||||
|
"classes": ("collapse",),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Vermögenssituation",
|
||||||
|
{
|
||||||
|
"fields": (
|
||||||
|
"vermogenssituation_bestaetigt",
|
||||||
|
"vermogenssituation_text",
|
||||||
|
"vermogenssituation_datei",
|
||||||
|
),
|
||||||
|
"classes": ("collapse",),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Weitere Dokumente",
|
||||||
|
{
|
||||||
|
"fields": (
|
||||||
|
"weitere_dokumente",
|
||||||
|
"weitere_dokumente_beschreibung",
|
||||||
|
),
|
||||||
|
"classes": ("collapse",),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Verwaltung & Prüfung",
|
||||||
|
{
|
||||||
|
"fields": (
|
||||||
|
"interne_notizen",
|
||||||
|
"eingereicht_am",
|
||||||
|
"geprueft_am",
|
||||||
|
"geprueft_von",
|
||||||
|
),
|
||||||
|
"classes": ("collapse",),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Metadaten",
|
||||||
|
{
|
||||||
|
"fields": (
|
||||||
|
"id",
|
||||||
|
"erstellt_am",
|
||||||
|
"aktualisiert_am",
|
||||||
|
"completion_percentage",
|
||||||
|
"is_overdue_display",
|
||||||
|
)
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
def completion_percentage(self, obj):
|
||||||
|
"""Show completion percentage as colored badge"""
|
||||||
|
percentage = obj.get_completion_percentage()
|
||||||
|
if percentage == 100:
|
||||||
|
color = "success"
|
||||||
|
elif percentage >= 70:
|
||||||
|
color = "info"
|
||||||
|
elif percentage >= 30:
|
||||||
|
color = "warning"
|
||||||
|
else:
|
||||||
|
color = "danger"
|
||||||
|
|
||||||
|
return format_html(
|
||||||
|
'<span class="badge bg-{}">{} %</span>',
|
||||||
|
color,
|
||||||
|
percentage
|
||||||
|
)
|
||||||
|
completion_percentage.short_description = "Fortschritt"
|
||||||
|
|
||||||
|
def is_overdue_display(self, obj):
|
||||||
|
"""Display overdue status with icon"""
|
||||||
|
if obj.is_overdue():
|
||||||
|
return format_html(
|
||||||
|
'<span class="text-danger"><i class="fas fa-exclamation-triangle"></i> Ja</span>'
|
||||||
|
)
|
||||||
|
return format_html(
|
||||||
|
'<span class="text-success"><i class="fas fa-check"></i> Nein</span>'
|
||||||
|
)
|
||||||
|
is_overdue_display.short_description = "Überfällig"
|
||||||
|
|
||||||
|
actions = ["mark_as_approved", "mark_as_needs_revision"]
|
||||||
|
|
||||||
|
def mark_as_approved(self, request, queryset):
|
||||||
|
"""Bulk action to approve submitted confirmations"""
|
||||||
|
count = 0
|
||||||
|
for nachweis in queryset.filter(status="eingereicht"):
|
||||||
|
nachweis.status = "geprueft"
|
||||||
|
nachweis.geprueft_am = timezone.now()
|
||||||
|
nachweis.geprueft_von = request.user
|
||||||
|
nachweis.save()
|
||||||
|
count += 1
|
||||||
|
|
||||||
|
if count:
|
||||||
|
self.message_user(
|
||||||
|
request,
|
||||||
|
f"{count} Nachweise wurden als geprüft und freigegeben markiert."
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.message_user(
|
||||||
|
request,
|
||||||
|
"Keine eingereichten Nachweise gefunden.",
|
||||||
|
level="warning"
|
||||||
|
)
|
||||||
|
mark_as_approved.short_description = "Ausgewählte Nachweise freigeben"
|
||||||
|
|
||||||
|
def mark_as_needs_revision(self, request, queryset):
|
||||||
|
"""Bulk action to mark confirmations as needing revision"""
|
||||||
|
count = queryset.exclude(status__in=["offen", "nachbesserung"]).update(
|
||||||
|
status="nachbesserung"
|
||||||
|
)
|
||||||
|
if count:
|
||||||
|
self.message_user(
|
||||||
|
request,
|
||||||
|
f"{count} Nachweise wurden als nachbesserungsbedürftig markiert."
|
||||||
|
)
|
||||||
|
mark_as_needs_revision.short_description = "Nachbesserung erforderlich markieren"
|
||||||
|
|
||||||
|
|
||||||
# Customize admin site
|
# Customize admin site
|
||||||
admin.site.site_header = "Stiftungsverwaltung Administration"
|
admin.site.site_header = "Stiftungsverwaltung Administration"
|
||||||
admin.site.site_title = "Stiftungsverwaltung Admin"
|
admin.site.site_title = "Stiftungsverwaltung Admin"
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ from .models import (BankTransaction, Destinataer, DestinataerNotiz,
|
|||||||
DestinataerUnterstuetzung, DokumentLink, Foerderung, Land,
|
DestinataerUnterstuetzung, DokumentLink, Foerderung, Land,
|
||||||
LandAbrechnung, LandVerpachtung, Paechter, Person, Rentmeister,
|
LandAbrechnung, LandVerpachtung, Paechter, Person, Rentmeister,
|
||||||
StiftungsKonto, UnterstuetzungWiederkehrend,
|
StiftungsKonto, UnterstuetzungWiederkehrend,
|
||||||
Verwaltungskosten)
|
Verwaltungskosten, VierteljahresNachweis)
|
||||||
|
|
||||||
|
|
||||||
class RentmeisterForm(forms.ModelForm):
|
class RentmeisterForm(forms.ModelForm):
|
||||||
@@ -1381,3 +1381,98 @@ class UserPermissionForm(forms.Form):
|
|||||||
groups["system"]["permissions"].append((field_name, field, None))
|
groups["system"]["permissions"].append((field_name, field, None))
|
||||||
|
|
||||||
return groups
|
return groups
|
||||||
|
|
||||||
|
|
||||||
|
class VierteljahresNachweisForm(forms.ModelForm):
|
||||||
|
"""Form for quarterly confirmations (Vierteljahresnachweise)"""
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = VierteljahresNachweis
|
||||||
|
fields = [
|
||||||
|
'studiennachweis_eingereicht',
|
||||||
|
'studiennachweis_datei',
|
||||||
|
'studiennachweis_bemerkung',
|
||||||
|
'einkommenssituation_bestaetigt',
|
||||||
|
'einkommenssituation_text',
|
||||||
|
'einkommenssituation_datei',
|
||||||
|
'vermogenssituation_bestaetigt',
|
||||||
|
'vermogenssituation_text',
|
||||||
|
'vermogenssituation_datei',
|
||||||
|
'weitere_dokumente',
|
||||||
|
'weitere_dokumente_beschreibung',
|
||||||
|
'interne_notizen',
|
||||||
|
]
|
||||||
|
|
||||||
|
widgets = {
|
||||||
|
'studiennachweis_eingereicht': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
|
||||||
|
'studiennachweis_datei': forms.FileInput(attrs={'class': 'form-control', 'accept': '.pdf,.jpg,.jpeg,.png,.doc,.docx'}),
|
||||||
|
'studiennachweis_bemerkung': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}),
|
||||||
|
'einkommenssituation_bestaetigt': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
|
||||||
|
'einkommenssituation_text': forms.Textarea(attrs={'class': 'form-control', 'rows': 3, 'placeholder': 'Z.B. "Keine Änderungen seit letzter Meldung"'}),
|
||||||
|
'einkommenssituation_datei': forms.FileInput(attrs={'class': 'form-control', 'accept': '.pdf,.jpg,.jpeg,.png,.doc,.docx'}),
|
||||||
|
'vermogenssituation_bestaetigt': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
|
||||||
|
'vermogenssituation_text': forms.Textarea(attrs={'class': 'form-control', 'rows': 3, 'placeholder': 'Z.B. "Keine Änderungen seit letzter Meldung"'}),
|
||||||
|
'vermogenssituation_datei': forms.FileInput(attrs={'class': 'form-control', 'accept': '.pdf,.jpg,.jpeg,.png,.doc,.docx'}),
|
||||||
|
'weitere_dokumente': forms.FileInput(attrs={'class': 'form-control', 'accept': '.pdf,.jpg,.jpeg,.png,.doc,.docx'}),
|
||||||
|
'weitere_dokumente_beschreibung': forms.Textarea(attrs={'class': 'form-control', 'rows': 2}),
|
||||||
|
'interne_notizen': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}),
|
||||||
|
}
|
||||||
|
|
||||||
|
labels = {
|
||||||
|
'studiennachweis_erforderlich': 'Studiennachweis erforderlich',
|
||||||
|
'studiennachweis_eingereicht': 'Studiennachweis eingereicht',
|
||||||
|
'studiennachweis_datei': 'Studiennachweis (Datei)',
|
||||||
|
'studiennachweis_bemerkung': 'Bemerkung zum Studiennachweis',
|
||||||
|
'einkommenssituation_bestaetigt': 'Einkommenssituation bestätigt',
|
||||||
|
'einkommenssituation_text': 'Einkommenssituation (Text)',
|
||||||
|
'einkommenssituation_datei': 'Einkommenssituation (Datei)',
|
||||||
|
'vermogenssituation_bestaetigt': 'Vermögenssituation bestätigt',
|
||||||
|
'vermogenssituation_text': 'Vermögenssituation (Text)',
|
||||||
|
'vermogenssituation_datei': 'Vermögenssituation (Datei)',
|
||||||
|
'weitere_dokumente': 'Weitere Dokumente',
|
||||||
|
'weitere_dokumente_beschreibung': 'Beschreibung weitere Dokumente',
|
||||||
|
'interne_notizen': 'Interne Notizen (nur für Verwaltung)',
|
||||||
|
}
|
||||||
|
|
||||||
|
help_texts = {
|
||||||
|
'einkommenssituation_text': 'Z.B. "Keine Änderungen seit letzter Meldung" oder Details zu Änderungen',
|
||||||
|
'vermogenssituation_text': 'Z.B. "Keine Änderungen seit letzter Meldung" oder Details zu Änderungen',
|
||||||
|
'interne_notizen': 'Diese Notizen sind nur für die interne Verwaltung sichtbar',
|
||||||
|
}
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
cleaned_data = super().clean()
|
||||||
|
|
||||||
|
# Validate that at least one form of confirmation is provided for income situation
|
||||||
|
einkommenssituation_text = cleaned_data.get('einkommenssituation_text')
|
||||||
|
einkommenssituation_datei = cleaned_data.get('einkommenssituation_datei')
|
||||||
|
einkommenssituation_bestaetigt = cleaned_data.get('einkommenssituation_bestaetigt')
|
||||||
|
|
||||||
|
if einkommenssituation_bestaetigt and not einkommenssituation_text and not einkommenssituation_datei:
|
||||||
|
raise ValidationError(
|
||||||
|
'Wenn die Einkommenssituation bestätigt wird, muss entweder ein Text oder eine Datei angegeben werden.'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Validate that at least one form of confirmation is provided for asset situation
|
||||||
|
vermogenssituation_text = cleaned_data.get('vermogenssituation_text')
|
||||||
|
vermogenssituation_datei = cleaned_data.get('vermogenssituation_datei')
|
||||||
|
vermogenssituation_bestaetigt = cleaned_data.get('vermogenssituation_bestaetigt')
|
||||||
|
|
||||||
|
if vermogenssituation_bestaetigt and not vermogenssituation_text and not vermogenssituation_datei:
|
||||||
|
raise ValidationError(
|
||||||
|
'Wenn die Vermögenssituation bestätigt wird, muss entweder ein Text oder eine Datei angegeben werden.'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Validate study proof if required and marked as submitted
|
||||||
|
studiennachweis_erforderlich = cleaned_data.get('studiennachweis_erforderlich')
|
||||||
|
studiennachweis_eingereicht = cleaned_data.get('studiennachweis_eingereicht')
|
||||||
|
studiennachweis_datei = cleaned_data.get('studiennachweis_datei')
|
||||||
|
studiennachweis_bemerkung = cleaned_data.get('studiennachweis_bemerkung')
|
||||||
|
|
||||||
|
if studiennachweis_erforderlich and studiennachweis_eingereicht:
|
||||||
|
if not studiennachweis_datei and not studiennachweis_bemerkung:
|
||||||
|
raise ValidationError(
|
||||||
|
'Wenn der Studiennachweis als eingereicht markiert wird, muss entweder eine Datei oder eine Bemerkung angegeben werden.'
|
||||||
|
)
|
||||||
|
|
||||||
|
return cleaned_data
|
||||||
|
|||||||
54
app/stiftung/migrations/0030_vierteljahresnachweis.py
Normal file
54
app/stiftung/migrations/0030_vierteljahresnachweis.py
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
# Generated by Django 5.0.6 on 2025-09-23 19:33
|
||||||
|
|
||||||
|
import django.core.validators
|
||||||
|
import django.db.models.deletion
|
||||||
|
import uuid
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('stiftung', '0029_alter_destinataer_berufsgruppe_and_more'),
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='VierteljahresNachweis',
|
||||||
|
fields=[
|
||||||
|
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||||
|
('jahr', models.IntegerField(validators=[django.core.validators.MinValueValidator(2020), django.core.validators.MaxValueValidator(2050)], verbose_name='Jahr')),
|
||||||
|
('quartal', models.IntegerField(choices=[(1, 'Q1 (Jan-Mär)'), (2, 'Q2 (Apr-Jun)'), (3, 'Q3 (Jul-Sep)'), (4, 'Q4 (Okt-Dez)')], verbose_name='Quartal')),
|
||||||
|
('studiennachweis_erforderlich', models.BooleanField(default=True, verbose_name='Studiennachweis erforderlich')),
|
||||||
|
('studiennachweis_eingereicht', models.BooleanField(default=False, verbose_name='Studiennachweis eingereicht')),
|
||||||
|
('studiennachweis_datei', models.FileField(blank=True, null=True, upload_to='quarterly_proofs/studies/%Y/Q%m/', verbose_name='Studiennachweis (Datei)')),
|
||||||
|
('studiennachweis_bemerkung', models.TextField(blank=True, null=True, verbose_name='Bemerkung zum Studiennachweis')),
|
||||||
|
('einkommenssituation_bestaetigt', models.BooleanField(default=False, verbose_name='Einkommenssituation bestätigt')),
|
||||||
|
('einkommenssituation_text', models.TextField(blank=True, help_text="Z.B. 'Keine Änderungen seit letzter Meldung' oder Details zu Änderungen", null=True, verbose_name='Einkommenssituation (Text)')),
|
||||||
|
('einkommenssituation_datei', models.FileField(blank=True, null=True, upload_to='quarterly_proofs/income/%Y/Q%m/', verbose_name='Einkommenssituation (Datei)')),
|
||||||
|
('vermogenssituation_bestaetigt', models.BooleanField(default=False, verbose_name='Vermögenssituation bestätigt')),
|
||||||
|
('vermogenssituation_text', models.TextField(blank=True, help_text="Z.B. 'Keine Änderungen seit letzter Meldung' oder Details zu Änderungen", null=True, verbose_name='Vermögenssituation (Text)')),
|
||||||
|
('vermogenssituation_datei', models.FileField(blank=True, null=True, upload_to='quarterly_proofs/assets/%Y/Q%m/', verbose_name='Vermögenssituation (Datei)')),
|
||||||
|
('weitere_dokumente', models.FileField(blank=True, null=True, upload_to='quarterly_proofs/additional/%Y/Q%m/', verbose_name='Weitere Dokumente')),
|
||||||
|
('weitere_dokumente_beschreibung', models.TextField(blank=True, null=True, verbose_name='Beschreibung weitere Dokumente')),
|
||||||
|
('status', models.CharField(choices=[('offen', 'Nachweis ausstehend'), ('teilweise', 'Teilweise eingereicht'), ('eingereicht', 'Vollständig eingereicht'), ('geprueft', 'Geprüft & Freigegeben'), ('nachbesserung', 'Nachbesserung erforderlich'), ('abgelehnt', 'Abgelehnt')], default='offen', max_length=20, verbose_name='Status')),
|
||||||
|
('interne_notizen', models.TextField(blank=True, null=True, verbose_name='Interne Notizen (nur für Verwaltung)')),
|
||||||
|
('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(blank=True, null=True, verbose_name='Eingereicht am')),
|
||||||
|
('geprueft_am', models.DateTimeField(blank=True, null=True, verbose_name='Geprüft am')),
|
||||||
|
('faelligkeitsdatum', models.DateField(blank=True, null=True, verbose_name='Fälligkeitsdatum')),
|
||||||
|
('destinataer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='quartalseinreichungen', to='stiftung.destinataer', verbose_name='Destinatär')),
|
||||||
|
('geprueft_von', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='Geprüft von')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Vierteljahresnachweis',
|
||||||
|
'verbose_name_plural': 'Vierteljahresnachweise',
|
||||||
|
'ordering': ['-jahr', '-quartal', 'destinataer__nachname'],
|
||||||
|
'indexes': [models.Index(fields=['jahr', 'quartal', 'status'], name='stiftung_vi_jahr_d9607d_idx'), models.Index(fields=['destinataer', 'status'], name='stiftung_vi_destina_957fd8_idx'), models.Index(fields=['faelligkeitsdatum'], name='stiftung_vi_faellig_353e7c_idx')],
|
||||||
|
'unique_together': {('destinataer', 'jahr', 'quartal')},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -2472,3 +2472,296 @@ class HelpBox(models.Model):
|
|||||||
return cls.objects.get(page_key=page_key, is_active=True)
|
return cls.objects.get(page_key=page_key, is_active=True)
|
||||||
except cls.DoesNotExist:
|
except cls.DoesNotExist:
|
||||||
return None
|
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")
|
||||||
|
|||||||
@@ -348,4 +348,20 @@ urlpatterns = [
|
|||||||
# Gramps integration (probe)
|
# Gramps integration (probe)
|
||||||
path("api/gramps/search/", views.gramps_search_api, name="gramps_search_api"),
|
path("api/gramps/search/", views.gramps_search_api, name="gramps_search_api"),
|
||||||
path("api/gramps/debug/", views.gramps_debug_api, name="gramps_debug_api"),
|
path("api/gramps/debug/", views.gramps_debug_api, name="gramps_debug_api"),
|
||||||
|
# Quarterly Confirmations
|
||||||
|
path(
|
||||||
|
"quarterly-confirmations/create/<uuid:destinataer_id>/",
|
||||||
|
views.quarterly_confirmation_create,
|
||||||
|
name="quarterly_confirmation_create",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"quarterly-confirmations/<uuid:pk>/edit/",
|
||||||
|
views.quarterly_confirmation_edit,
|
||||||
|
name="quarterly_confirmation_edit",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"quarterly-confirmations/<uuid:pk>/update/",
|
||||||
|
views.quarterly_confirmation_update,
|
||||||
|
name="quarterly_confirmation_update",
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import io
|
|||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import time
|
import time
|
||||||
from datetime import datetime
|
from datetime import datetime, timedelta
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
@@ -24,7 +24,7 @@ from rest_framework.response import Response
|
|||||||
from .models import (AppConfiguration, CSVImport, Destinataer,
|
from .models import (AppConfiguration, CSVImport, Destinataer,
|
||||||
DestinataerUnterstuetzung, DokumentLink, Foerderung, Land,
|
DestinataerUnterstuetzung, DokumentLink, Foerderung, Land,
|
||||||
LandAbrechnung, LandVerpachtung, Paechter, Person,
|
LandAbrechnung, LandVerpachtung, Paechter, Person,
|
||||||
StiftungsKonto, UnterstuetzungWiederkehrend)
|
StiftungsKonto, UnterstuetzungWiederkehrend, VierteljahresNachweis)
|
||||||
|
|
||||||
|
|
||||||
def get_pdf_generator():
|
def get_pdf_generator():
|
||||||
@@ -214,7 +214,7 @@ from stiftung.models import DestinataerNotiz, DestinataerUnterstuetzung
|
|||||||
from .forms import (DestinataerForm, DestinataerNotizForm,
|
from .forms import (DestinataerForm, DestinataerNotizForm,
|
||||||
DestinataerUnterstuetzungForm, DokumentLinkForm,
|
DestinataerUnterstuetzungForm, DokumentLinkForm,
|
||||||
FoerderungForm, LandForm, PaechterForm, PersonForm,
|
FoerderungForm, LandForm, PaechterForm, PersonForm,
|
||||||
UnterstuetzungForm, UnterstuetzungMarkAsPaidForm)
|
UnterstuetzungForm, UnterstuetzungMarkAsPaidForm, VierteljahresNachweisForm)
|
||||||
|
|
||||||
|
|
||||||
def home(request):
|
def home(request):
|
||||||
@@ -1233,6 +1233,30 @@ def destinataer_detail(request, pk):
|
|||||||
destinataer=destinataer
|
destinataer=destinataer
|
||||||
).order_by("-erstellt_am")
|
).order_by("-erstellt_am")
|
||||||
|
|
||||||
|
# Quarterly confirmations - load for current and next year
|
||||||
|
from datetime import date
|
||||||
|
current_year = date.today().year
|
||||||
|
quarterly_confirmations = VierteljahresNachweis.objects.filter(
|
||||||
|
destinataer=destinataer,
|
||||||
|
jahr__in=[current_year, current_year + 1]
|
||||||
|
).order_by('-jahr', '-quartal')
|
||||||
|
|
||||||
|
# Create missing quarterly confirmations for current year if destinataer requires study proof
|
||||||
|
if destinataer.studiennachweis_erforderlich:
|
||||||
|
for quartal in range(1, 5): # Q1-Q4
|
||||||
|
nachweis, created = VierteljahresNachweis.get_or_create_for_period(
|
||||||
|
destinataer, current_year, quartal
|
||||||
|
)
|
||||||
|
|
||||||
|
# Reload to get any newly created confirmations
|
||||||
|
quarterly_confirmations = VierteljahresNachweis.objects.filter(
|
||||||
|
destinataer=destinataer,
|
||||||
|
jahr__in=[current_year, current_year + 1]
|
||||||
|
).order_by('-jahr', '-quartal')
|
||||||
|
|
||||||
|
# Generate available years for the add quarter dropdown (current year + next 5 years)
|
||||||
|
available_years = list(range(current_year, current_year + 6))
|
||||||
|
|
||||||
# Alle verfügbaren StiftungsKonten für das Select-Feld laden
|
# Alle verfügbaren StiftungsKonten für das Select-Feld laden
|
||||||
stiftungskonten = StiftungsKonto.objects.all().order_by("kontoname")
|
stiftungskonten = StiftungsKonto.objects.all().order_by("kontoname")
|
||||||
|
|
||||||
@@ -1243,6 +1267,9 @@ def destinataer_detail(request, pk):
|
|||||||
"unterstuetzungen": unterstuetzungen,
|
"unterstuetzungen": unterstuetzungen,
|
||||||
"notizen_eintraege": notizen_eintraege,
|
"notizen_eintraege": notizen_eintraege,
|
||||||
"stiftungskonten": stiftungskonten,
|
"stiftungskonten": stiftungskonten,
|
||||||
|
"quarterly_confirmations": quarterly_confirmations,
|
||||||
|
"available_years": available_years,
|
||||||
|
"current_year": current_year,
|
||||||
}
|
}
|
||||||
return render(request, "stiftung/destinataer_detail.html", context)
|
return render(request, "stiftung/destinataer_detail.html", context)
|
||||||
|
|
||||||
@@ -7195,3 +7222,318 @@ def verpachtung_delete(request, pk):
|
|||||||
'title': f'Verpachtung {verpachtung.vertragsnummer} löschen',
|
'title': f'Verpachtung {verpachtung.vertragsnummer} löschen',
|
||||||
}
|
}
|
||||||
return render(request, 'stiftung/verpachtung_confirm_delete.html', context)
|
return render(request, 'stiftung/verpachtung_confirm_delete.html', context)
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def quarterly_confirmation_update(request, pk):
|
||||||
|
"""Update quarterly confirmation for destinataer"""
|
||||||
|
nachweis = get_object_or_404(VierteljahresNachweis, pk=pk)
|
||||||
|
|
||||||
|
if request.method == "POST":
|
||||||
|
form = VierteljahresNachweisForm(request.POST, request.FILES, instance=nachweis)
|
||||||
|
if form.is_valid():
|
||||||
|
quarterly_proof = form.save(commit=False)
|
||||||
|
|
||||||
|
# Calculate current status before saving
|
||||||
|
old_status = nachweis.status
|
||||||
|
|
||||||
|
# Auto-update status based on completion
|
||||||
|
if quarterly_proof.is_complete():
|
||||||
|
if quarterly_proof.status in ['offen', 'teilweise']:
|
||||||
|
quarterly_proof.status = 'eingereicht'
|
||||||
|
quarterly_proof.eingereicht_am = timezone.now()
|
||||||
|
else:
|
||||||
|
# If not complete, set to teilweise if some fields are filled
|
||||||
|
has_partial_data = (
|
||||||
|
quarterly_proof.einkommenssituation_bestaetigt or
|
||||||
|
quarterly_proof.vermogenssituation_bestaetigt or
|
||||||
|
quarterly_proof.studiennachweis_eingereicht
|
||||||
|
)
|
||||||
|
if has_partial_data and quarterly_proof.status == 'offen':
|
||||||
|
quarterly_proof.status = 'teilweise'
|
||||||
|
|
||||||
|
quarterly_proof.save()
|
||||||
|
|
||||||
|
# Try to create automatic support payment if complete
|
||||||
|
if quarterly_proof.is_complete() and quarterly_proof.status == 'eingereicht':
|
||||||
|
support_payment = create_quarterly_support_payment(quarterly_proof)
|
||||||
|
if support_payment:
|
||||||
|
messages.success(
|
||||||
|
request,
|
||||||
|
f"Automatische Unterstützung über {support_payment.betrag}€ für {support_payment.destinataer.get_full_name()} wurde erstellt (fällig am {support_payment.faellig_am.strftime('%d.%m.%Y')})."
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# Log why payment wasn't created
|
||||||
|
reasons = []
|
||||||
|
if not quarterly_proof.destinataer.vierteljaehrlicher_betrag:
|
||||||
|
reasons.append("kein vierteljährlicher Betrag hinterlegt")
|
||||||
|
if not quarterly_proof.destinataer.iban:
|
||||||
|
reasons.append("keine IBAN hinterlegt")
|
||||||
|
if not quarterly_proof.destinataer.standard_konto and not StiftungsKonto.objects.exists():
|
||||||
|
reasons.append("kein Auszahlungskonto verfügbar")
|
||||||
|
|
||||||
|
if reasons:
|
||||||
|
messages.warning(
|
||||||
|
request,
|
||||||
|
f"Automatische Unterstützung konnte nicht erstellt werden: {', '.join(reasons)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Debug message to see what happened
|
||||||
|
status_changed = old_status != quarterly_proof.status
|
||||||
|
status_msg = f" (Status: {old_status} → {quarterly_proof.status})" if status_changed else f" (Status: {quarterly_proof.status})"
|
||||||
|
|
||||||
|
messages.success(
|
||||||
|
request,
|
||||||
|
f"Vierteljahresnachweis für {nachweis.destinataer.get_full_name()} "
|
||||||
|
f"({nachweis.jahr} Q{nachweis.quartal}) wurde erfolgreich aktualisiert{status_msg}."
|
||||||
|
)
|
||||||
|
return redirect("stiftung:destinataer_detail", pk=nachweis.destinataer.pk)
|
||||||
|
else:
|
||||||
|
# Add form errors to messages
|
||||||
|
for field, errors in form.errors.items():
|
||||||
|
for error in errors:
|
||||||
|
messages.error(request, f"Fehler in {field}: {error}")
|
||||||
|
|
||||||
|
# If GET request or form errors, redirect back to destinataer detail
|
||||||
|
return redirect("stiftung:destinataer_detail", pk=nachweis.destinataer.pk)
|
||||||
|
|
||||||
|
|
||||||
|
def create_quarterly_support_payment(nachweis):
|
||||||
|
"""
|
||||||
|
Create an automatic support payment when all quarterly requirements are met
|
||||||
|
"""
|
||||||
|
destinataer = nachweis.destinataer
|
||||||
|
|
||||||
|
# Check if all requirements are met
|
||||||
|
if not nachweis.is_complete():
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Check if destinataer has required payment info
|
||||||
|
if not destinataer.vierteljaehrlicher_betrag or destinataer.vierteljaehrlicher_betrag <= 0:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if not destinataer.iban:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Check if a payment for this quarter already exists
|
||||||
|
quarter_start = datetime(nachweis.jahr, (nachweis.quartal - 1) * 3 + 1, 1).date()
|
||||||
|
quarter_end = datetime(nachweis.jahr, nachweis.quartal * 3, 1).date()
|
||||||
|
|
||||||
|
existing_payment = DestinataerUnterstuetzung.objects.filter(
|
||||||
|
destinataer=destinataer,
|
||||||
|
faellig_am__gte=quarter_start,
|
||||||
|
faellig_am__lt=quarter_end,
|
||||||
|
beschreibung__contains=f"Q{nachweis.quartal}/{nachweis.jahr}"
|
||||||
|
).first()
|
||||||
|
|
||||||
|
if existing_payment:
|
||||||
|
return existing_payment
|
||||||
|
|
||||||
|
# Get default payment account
|
||||||
|
default_konto = destinataer.standard_konto
|
||||||
|
if not default_konto:
|
||||||
|
# Try to get any StiftungsKonto
|
||||||
|
default_konto = StiftungsKonto.objects.first()
|
||||||
|
if not default_konto:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Calculate payment due date (last day of quarter)
|
||||||
|
# Quarter end months and their last days:
|
||||||
|
# Q1: March (31), Q2: June (30), Q3: September (30), Q4: December (31)
|
||||||
|
quarter_end_month = nachweis.quartal * 3
|
||||||
|
|
||||||
|
if nachweis.quartal == 1: # Q1: January-March (ends March 31)
|
||||||
|
quarter_end_day = 31
|
||||||
|
elif nachweis.quartal == 2: # Q2: April-June (ends June 30)
|
||||||
|
quarter_end_day = 30
|
||||||
|
elif nachweis.quartal == 3: # Q3: July-September (ends September 30)
|
||||||
|
quarter_end_day = 30
|
||||||
|
else: # Q4: October-December (ends December 31)
|
||||||
|
quarter_end_day = 31
|
||||||
|
|
||||||
|
payment_due_date = datetime(nachweis.jahr, quarter_end_month, quarter_end_day).date()
|
||||||
|
|
||||||
|
# Create the support payment
|
||||||
|
payment = DestinataerUnterstuetzung.objects.create(
|
||||||
|
destinataer=destinataer,
|
||||||
|
konto=default_konto,
|
||||||
|
betrag=destinataer.vierteljaehrlicher_betrag,
|
||||||
|
faellig_am=payment_due_date,
|
||||||
|
status='geplant',
|
||||||
|
beschreibung=f"Vierteljährliche Unterstützung Q{nachweis.quartal}/{nachweis.jahr} (automatisch erstellt)",
|
||||||
|
empfaenger_iban=destinataer.iban,
|
||||||
|
empfaenger_name=destinataer.get_full_name(),
|
||||||
|
verwendungszweck=f"Vierteljährliche Unterstützung Q{nachweis.quartal}/{nachweis.jahr} - {destinataer.get_full_name()}",
|
||||||
|
erstellt_am=timezone.now(),
|
||||||
|
aktualisiert_am=timezone.now()
|
||||||
|
)
|
||||||
|
|
||||||
|
return payment
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def quarterly_confirmation_create(request, destinataer_id):
|
||||||
|
"""Create a new quarterly confirmation for a destinataer"""
|
||||||
|
destinataer = get_object_or_404(Destinataer, pk=destinataer_id)
|
||||||
|
|
||||||
|
if request.method == "POST":
|
||||||
|
jahr = request.POST.get('jahr')
|
||||||
|
quartal = request.POST.get('quartal')
|
||||||
|
|
||||||
|
if jahr and quartal:
|
||||||
|
try:
|
||||||
|
jahr = int(jahr)
|
||||||
|
quartal = int(quartal)
|
||||||
|
|
||||||
|
# Check if this quarter already exists
|
||||||
|
existing = VierteljahresNachweis.objects.filter(
|
||||||
|
destinataer=destinataer,
|
||||||
|
jahr=jahr,
|
||||||
|
quartal=quartal
|
||||||
|
).exists()
|
||||||
|
|
||||||
|
if existing:
|
||||||
|
messages.warning(
|
||||||
|
request,
|
||||||
|
f"Quartal {jahr} Q{quartal} existiert bereits für {destinataer.get_full_name()}."
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# Create new quarterly confirmation
|
||||||
|
nachweis = VierteljahresNachweis.objects.create(
|
||||||
|
destinataer=destinataer,
|
||||||
|
jahr=jahr,
|
||||||
|
quartal=quartal,
|
||||||
|
studiennachweis_erforderlich=True, # Always required now
|
||||||
|
)
|
||||||
|
|
||||||
|
# Set deadline (15th of second month of quarter)
|
||||||
|
deadline_months = {1: 5, 2: 8, 3: 11, 4: 2} # Q1->May, Q2->Aug, Q3->Nov, Q4->Feb(next year)
|
||||||
|
deadline_month = deadline_months[quartal]
|
||||||
|
deadline_year = jahr if quartal != 4 else jahr + 1
|
||||||
|
|
||||||
|
from datetime import date
|
||||||
|
nachweis.faelligkeitsdatum = date(deadline_year, deadline_month, 15)
|
||||||
|
nachweis.save()
|
||||||
|
|
||||||
|
messages.success(
|
||||||
|
request,
|
||||||
|
f"Quartal {jahr} Q{quartal} wurde erfolgreich für {destinataer.get_full_name()} erstellt."
|
||||||
|
)
|
||||||
|
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
messages.error(request, "Ungültige Jahr- oder Quartalswerte.")
|
||||||
|
else:
|
||||||
|
messages.error(request, "Jahr und Quartal müssen angegeben werden.")
|
||||||
|
|
||||||
|
return redirect("stiftung:destinataer_detail", pk=destinataer.pk)
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def quarterly_confirmation_edit(request, pk):
|
||||||
|
"""Standalone edit view for quarterly confirmation"""
|
||||||
|
nachweis = get_object_or_404(VierteljahresNachweis, pk=pk)
|
||||||
|
|
||||||
|
if request.method == "POST":
|
||||||
|
form = VierteljahresNachweisForm(request.POST, request.FILES, instance=nachweis)
|
||||||
|
if form.is_valid():
|
||||||
|
quarterly_proof = form.save(commit=False)
|
||||||
|
|
||||||
|
# Calculate current status before saving
|
||||||
|
old_status = nachweis.status
|
||||||
|
|
||||||
|
# Auto-update status based on completion
|
||||||
|
if quarterly_proof.is_complete():
|
||||||
|
if quarterly_proof.status in ['offen', 'teilweise']:
|
||||||
|
quarterly_proof.status = 'eingereicht'
|
||||||
|
quarterly_proof.eingereicht_am = timezone.now()
|
||||||
|
else:
|
||||||
|
# If not complete, set to teilweise if some fields are filled
|
||||||
|
has_partial_data = (
|
||||||
|
quarterly_proof.einkommenssituation_bestaetigt or
|
||||||
|
quarterly_proof.vermogenssituation_bestaetigt or
|
||||||
|
quarterly_proof.studiennachweis_eingereicht
|
||||||
|
)
|
||||||
|
if has_partial_data and quarterly_proof.status == 'offen':
|
||||||
|
quarterly_proof.status = 'teilweise'
|
||||||
|
|
||||||
|
quarterly_proof.save()
|
||||||
|
|
||||||
|
# Try to create automatic support payment if complete
|
||||||
|
if quarterly_proof.is_complete() and quarterly_proof.status == 'eingereicht':
|
||||||
|
support_payment = create_quarterly_support_payment(quarterly_proof)
|
||||||
|
if support_payment:
|
||||||
|
messages.success(
|
||||||
|
request,
|
||||||
|
f"Automatische Unterstützung über {support_payment.betrag}€ für {support_payment.destinataer.get_full_name()} wurde erstellt (fällig am {support_payment.faellig_am.strftime('%d.%m.%Y')})."
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# Log why payment wasn't created
|
||||||
|
reasons = []
|
||||||
|
if not quarterly_proof.destinataer.vierteljaehrlicher_betrag:
|
||||||
|
reasons.append("kein vierteljährlicher Betrag hinterlegt")
|
||||||
|
if not quarterly_proof.destinataer.iban:
|
||||||
|
reasons.append("keine IBAN hinterlegt")
|
||||||
|
if not quarterly_proof.destinataer.standard_konto and not StiftungsKonto.objects.exists():
|
||||||
|
reasons.append("kein Auszahlungskonto verfügbar")
|
||||||
|
|
||||||
|
if reasons:
|
||||||
|
messages.warning(
|
||||||
|
request,
|
||||||
|
f"Automatische Unterstützung konnte nicht erstellt werden: {', '.join(reasons)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Debug message to see what happened
|
||||||
|
status_changed = old_status != quarterly_proof.status
|
||||||
|
status_msg = f" (Status: {old_status} → {quarterly_proof.status})" if status_changed else f" (Status: {quarterly_proof.status})"
|
||||||
|
|
||||||
|
messages.success(
|
||||||
|
request,
|
||||||
|
f"Vierteljahresnachweis für {nachweis.destinataer.get_full_name()} "
|
||||||
|
f"({nachweis.jahr} Q{nachweis.quartal}) wurde erfolgreich aktualisiert{status_msg}."
|
||||||
|
)
|
||||||
|
return redirect("stiftung:destinataer_detail", pk=nachweis.destinataer.pk)
|
||||||
|
else:
|
||||||
|
# Add form errors to messages
|
||||||
|
for field, errors in form.errors.items():
|
||||||
|
for error in errors:
|
||||||
|
messages.error(request, f"Fehler in {field}: {error}")
|
||||||
|
else:
|
||||||
|
form = VierteljahresNachweisForm(instance=nachweis)
|
||||||
|
|
||||||
|
context = {
|
||||||
|
'form': form,
|
||||||
|
'nachweis': nachweis,
|
||||||
|
'destinataer': nachweis.destinataer,
|
||||||
|
'title': f'Vierteljahresnachweis bearbeiten - {nachweis.destinataer.get_full_name()} {nachweis.jahr} Q{nachweis.quartal}',
|
||||||
|
}
|
||||||
|
return render(request, 'stiftung/quarterly_confirmation_edit.html', context)
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def quarterly_confirmation_approve(request, pk):
|
||||||
|
"""Approve quarterly confirmation (staff only)"""
|
||||||
|
if not request.user.is_staff:
|
||||||
|
messages.error(request, "Sie haben keine Berechtigung für diese Aktion.")
|
||||||
|
return redirect("stiftung:destinataer_list")
|
||||||
|
|
||||||
|
nachweis = get_object_or_404(VierteljahresNachweis, pk=pk)
|
||||||
|
|
||||||
|
if request.method == "POST":
|
||||||
|
if nachweis.status == 'eingereicht':
|
||||||
|
nachweis.status = 'geprueft'
|
||||||
|
nachweis.geprueft_am = timezone.now()
|
||||||
|
nachweis.geprueft_von = request.user
|
||||||
|
nachweis.save()
|
||||||
|
|
||||||
|
messages.success(
|
||||||
|
request,
|
||||||
|
f"Vierteljahresnachweis für {nachweis.destinataer.get_full_name()} "
|
||||||
|
f"({nachweis.jahr} Q{nachweis.quartal}) wurde freigegeben."
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
messages.error(
|
||||||
|
request,
|
||||||
|
"Nur eingereichte Nachweise können freigegeben werden."
|
||||||
|
)
|
||||||
|
|
||||||
|
return redirect("stiftung:destinataer_detail", pk=nachweis.destinataer.pk)
|
||||||
|
|||||||
@@ -25,6 +25,71 @@
|
|||||||
--orange-dark: #e8590c;
|
--orange-dark: #e8590c;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Global Typography - More Compact */
|
||||||
|
html {
|
||||||
|
font-size: 14px; /* Reduced from default 16px */
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-size: 0.875rem; /* 12.25px */
|
||||||
|
line-height: 1.4;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
.h1, .h2, .h3, .h4, .h5, .h6,
|
||||||
|
h1, h2, h3, h4, h5, h6 {
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1.3;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.h1, h1 { font-size: 2rem; }
|
||||||
|
.h2, h2 { font-size: 1.65rem; }
|
||||||
|
.h3, h3 { font-size: 1.4rem; }
|
||||||
|
.h4, h4 { font-size: 1.15rem; }
|
||||||
|
.h5, h5 { font-size: 1rem; }
|
||||||
|
.h6, h6 { font-size: 0.875rem; }
|
||||||
|
|
||||||
|
/* Compact spacing */
|
||||||
|
.container, .container-fluid, .container-lg {
|
||||||
|
padding-left: 0.75rem;
|
||||||
|
padding-right: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row {
|
||||||
|
margin-left: -0.5rem;
|
||||||
|
margin-right: -0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.col, .col-1, .col-2, .col-3, .col-4, .col-5, .col-6,
|
||||||
|
.col-7, .col-8, .col-9, .col-10, .col-11, .col-12,
|
||||||
|
.col-auto, .col-sm, .col-sm-auto, .col-md, .col-md-auto,
|
||||||
|
.col-lg, .col-lg-auto, .col-xl, .col-xl-auto {
|
||||||
|
padding-left: 0.5rem;
|
||||||
|
padding-right: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Compact margins */
|
||||||
|
.mb-1 { margin-bottom: 0.25rem !important; }
|
||||||
|
.mb-2 { margin-bottom: 0.4rem !important; }
|
||||||
|
.mb-3 { margin-bottom: 0.75rem !important; }
|
||||||
|
.mb-4 { margin-bottom: 1rem !important; }
|
||||||
|
.mb-5 { margin-bottom: 1.5rem !important; }
|
||||||
|
|
||||||
|
.mt-1 { margin-top: 0.25rem !important; }
|
||||||
|
.mt-2 { margin-top: 0.4rem !important; }
|
||||||
|
.mt-3 { margin-top: 0.75rem !important; }
|
||||||
|
.mt-4 { margin-top: 1rem !important; }
|
||||||
|
.mt-5 { margin-top: 1.5rem !important; }
|
||||||
|
|
||||||
|
.py-1 { padding-top: 0.25rem !important; padding-bottom: 0.25rem !important; }
|
||||||
|
.py-2 { padding-top: 0.4rem !important; padding-bottom: 0.4rem !important; }
|
||||||
|
.py-3 { padding-top: 0.75rem !important; padding-bottom: 0.75rem !important; }
|
||||||
|
|
||||||
|
.px-1 { padding-left: 0.25rem !important; padding-right: 0.25rem !important; }
|
||||||
|
.px-2 { padding-left: 0.4rem !important; padding-right: 0.4rem !important; }
|
||||||
|
.px-3 { padding-left: 0.75rem !important; padding-right: 0.75rem !important; }
|
||||||
|
|
||||||
.border-left-primary {
|
.border-left-primary {
|
||||||
border-left: 0.25rem solid var(--racing-green) !important;
|
border-left: 0.25rem solid var(--racing-green) !important;
|
||||||
}
|
}
|
||||||
@@ -47,7 +112,27 @@
|
|||||||
background-image: linear-gradient(180deg, var(--racing-green-light) 10%, var(--racing-green-dark) 100%);
|
background-image: linear-gradient(180deg, var(--racing-green-light) 10%, var(--racing-green-dark) 100%);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Navigation styling */
|
/* Navigation styling - More Compact */
|
||||||
|
.navbar {
|
||||||
|
padding: 0.375rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-brand {
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
color: white !important;
|
||||||
|
padding: 0.375rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-nav .nav-link,
|
||||||
|
.navbar-nav .nav-link.dropdown-toggle {
|
||||||
|
padding: 0.375rem 0.5rem !important;
|
||||||
|
display: inline-flex !important;
|
||||||
|
align-items: center !important;
|
||||||
|
white-space: nowrap !important;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
.navbar-dark .navbar-nav .nav-link {
|
.navbar-dark .navbar-nav .nav-link {
|
||||||
color: rgba(255, 255, 255, 0.9);
|
color: rgba(255, 255, 255, 0.9);
|
||||||
}
|
}
|
||||||
@@ -56,31 +141,46 @@
|
|||||||
color: var(--orange-light);
|
color: var(--orange-light);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Ensure all nav links have consistent alignment */
|
/* Dropdown menus - More Compact */
|
||||||
.navbar-nav .nav-link,
|
.dropdown-menu {
|
||||||
.navbar-nav .nav-link.dropdown-toggle {
|
border: 1px solid #dee2e6;
|
||||||
padding: 0.5rem 0.75rem !important;
|
box-shadow: 0 0.25rem 0.5rem rgba(0, 66, 37, 0.15);
|
||||||
display: inline-flex !important;
|
font-size: 0.8rem;
|
||||||
align-items: center !important;
|
|
||||||
white-space: nowrap !important;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar-brand {
|
.dropdown-item {
|
||||||
font-weight: 700;
|
padding: 0.375rem 0.75rem;
|
||||||
font-size: 1.5rem;
|
font-size: 0.8rem;
|
||||||
color: white !important;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Cards and content */
|
.dropdown-item:hover {
|
||||||
|
background-color: rgba(0, 66, 37, 0.1);
|
||||||
|
color: var(--racing-green-dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-header {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
padding: 0.25rem 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Cards and content - More Compact */
|
||||||
.card {
|
.card {
|
||||||
border: 1px solid #e9ecef;
|
border: 1px solid #e9ecef;
|
||||||
box-shadow: 0 0.15rem 1.75rem 0 rgba(0, 66, 37, 0.1);
|
box-shadow: 0 0.125rem 1rem 0 rgba(0, 66, 37, 0.08);
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-body {
|
||||||
|
padding: 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-header {
|
.card-header {
|
||||||
background-color: var(--grey-light);
|
background-color: var(--grey-light);
|
||||||
border-bottom: 1px solid #dee2e6;
|
border-bottom: 1px solid #dee2e6;
|
||||||
color: var(--racing-green-dark);
|
color: var(--racing-green-dark);
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-header.bg-primary {
|
.card-header.bg-primary {
|
||||||
@@ -103,7 +203,107 @@
|
|||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Buttons */
|
.card-title {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tables - More Compact */
|
||||||
|
.table {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table th {
|
||||||
|
border-top: none;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--racing-green-dark);
|
||||||
|
background-color: var(--grey-light);
|
||||||
|
padding: 0.5rem;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.025em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table td {
|
||||||
|
padding: 0.5rem;
|
||||||
|
border-top: 1px solid #dee2e6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-sm th,
|
||||||
|
.table-sm td {
|
||||||
|
padding: 0.375rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-light {
|
||||||
|
background-color: var(--grey-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Buttons - More Compact */
|
||||||
|
.btn {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
padding: 0.375rem 0.75rem;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-sm {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
border-radius: 0.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-lg {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
border-radius: 0.3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Form Controls - More Compact */
|
||||||
|
.form-control, .form-select {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
padding: 0.375rem 0.5rem;
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control-sm, .form-select-sm {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
padding: 0.25rem 0.375rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-label {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: 500;
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-text {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
margin-top: 0.15rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-check-label {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Badges - More Compact */
|
||||||
|
.badge {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
padding: 0.25em 0.5em;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Progress bars */
|
||||||
|
.progress {
|
||||||
|
height: 1rem;
|
||||||
|
font-size: 0.65rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar {
|
||||||
|
line-height: 1rem;
|
||||||
|
}
|
||||||
.btn-primary {
|
.btn-primary {
|
||||||
background-color: var(--racing-green);
|
background-color: var(--racing-green);
|
||||||
border-color: var(--racing-green);
|
border-color: var(--racing-green);
|
||||||
@@ -156,14 +356,70 @@
|
|||||||
border-color: var(--grey-medium);
|
border-color: var(--grey-medium);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Table styling */
|
/* Alerts - More Compact */
|
||||||
.table th {
|
.alert {
|
||||||
border-top: none;
|
padding: 0.5rem 0.75rem;
|
||||||
font-weight: 600;
|
margin-bottom: 0.75rem;
|
||||||
color: var(--racing-green-dark);
|
font-size: 0.8rem;
|
||||||
background-color: var(--grey-light);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.alert-success {
|
||||||
|
background-color: rgba(0, 104, 55, 0.1);
|
||||||
|
border-color: var(--racing-green-light);
|
||||||
|
color: var(--racing-green-dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-warning {
|
||||||
|
background-color: rgba(253, 126, 20, 0.1);
|
||||||
|
border-color: var(--orange-accent);
|
||||||
|
color: var(--orange-dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive adjustments for very compact design */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
html {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-brand {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-nav .nav-link {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
padding: 0.25rem 0.375rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-body {
|
||||||
|
padding: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table th,
|
||||||
|
.table td {
|
||||||
|
padding: 0.375rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1400px) {
|
||||||
|
.container-lg {
|
||||||
|
max-width: 1400px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Table styling */
|
||||||
.table-light {
|
.table-light {
|
||||||
background-color: var(--grey-light);
|
background-color: var(--grey-light);
|
||||||
}
|
}
|
||||||
@@ -190,6 +446,8 @@
|
|||||||
.pagination .page-link {
|
.pagination .page-link {
|
||||||
color: var(--racing-green);
|
color: var(--racing-green);
|
||||||
border-color: #dee2e6;
|
border-color: #dee2e6;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
padding: 0.375rem 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pagination .page-item.active .page-link {
|
.pagination .page-item.active .page-link {
|
||||||
@@ -214,42 +472,20 @@
|
|||||||
/* Form controls */
|
/* Form controls */
|
||||||
.form-control:focus {
|
.form-control:focus {
|
||||||
border-color: var(--racing-green-light);
|
border-color: var(--racing-green-light);
|
||||||
box-shadow: 0 0 0 0.2rem rgba(0, 66, 37, 0.25);
|
box-shadow: 0 0 0 0.15rem rgba(0, 66, 37, 0.25);
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-select:focus {
|
.form-select:focus {
|
||||||
border-color: var(--racing-green-light);
|
border-color: var(--racing-green-light);
|
||||||
box-shadow: 0 0 0 0.2rem rgba(0, 66, 37, 0.25);
|
box-shadow: 0 0 0 0.15rem rgba(0, 66, 37, 0.25);
|
||||||
}
|
|
||||||
|
|
||||||
/* Alerts */
|
|
||||||
.alert-success {
|
|
||||||
background-color: rgba(0, 104, 55, 0.1);
|
|
||||||
border-color: var(--racing-green-light);
|
|
||||||
color: var(--racing-green-dark);
|
|
||||||
}
|
|
||||||
|
|
||||||
.alert-warning {
|
|
||||||
background-color: rgba(253, 126, 20, 0.1);
|
|
||||||
border-color: var(--orange-accent);
|
|
||||||
color: var(--orange-dark);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Footer */
|
/* Footer */
|
||||||
.sticky-footer {
|
.sticky-footer {
|
||||||
background-color: var(--grey-light) !important;
|
background-color: var(--grey-light) !important;
|
||||||
border-top: 1px solid #dee2e6;
|
border-top: 1px solid #dee2e6;
|
||||||
}
|
padding: 0.75rem 0;
|
||||||
|
font-size: 0.8rem;
|
||||||
/* Dropdown menus */
|
|
||||||
.dropdown-menu {
|
|
||||||
border: 1px solid #dee2e6;
|
|
||||||
box-shadow: 0 0.5rem 1rem rgba(0, 66, 37, 0.15);
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdown-item:hover {
|
|
||||||
background-color: rgba(0, 66, 37, 0.1);
|
|
||||||
color: var(--racing-green-dark);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Custom accent colors for specific elements */
|
/* Custom accent colors for specific elements */
|
||||||
@@ -416,12 +652,12 @@
|
|||||||
<!-- Content Wrapper -->
|
<!-- Content Wrapper -->
|
||||||
<div id="content-wrapper" class="d-flex flex-column">
|
<div id="content-wrapper" class="d-flex flex-column">
|
||||||
<!-- Main Content -->
|
<!-- Main Content -->
|
||||||
<div id="content" style="padding-top: 80px;">
|
<div id="content" style="padding-top: 60px;">
|
||||||
<!-- Messages -->
|
<!-- Messages -->
|
||||||
{% if messages %}
|
{% if messages %}
|
||||||
<div class="container-lg mx-auto mt-3" style="max-width: 1200px;">
|
<div class="container-lg mx-auto mt-2" style="max-width: 1400px;">
|
||||||
{% for message in messages %}
|
{% for message in messages %}
|
||||||
<div class="alert alert-{% if message.tags == 'error' %}danger{% else %}{{ message.tags }}{% endif %} alert-dismissible fade show" role="alert">
|
<div class="alert alert-{% if message.tags == 'error' %}danger{% else %}{{ message.tags }}{% endif %} alert-dismissible fade show" role="alert" style="padding: 0.5rem 0.75rem; font-size: 0.8rem;">
|
||||||
{{ message }}
|
{{ message }}
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||||
</div>
|
</div>
|
||||||
@@ -430,7 +666,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<!-- Page Content -->
|
<!-- Page Content -->
|
||||||
<div class="container-lg mx-auto" style="max-width: 1200px; padding: 20px;">
|
<div class="container-lg mx-auto" style="max-width: 1400px; padding: 15px;">
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -448,6 +448,359 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Quarterly Confirmations -->
|
||||||
|
{% if destinataer.studiennachweis_erforderlich %}
|
||||||
|
<div class="card shadow mb-4">
|
||||||
|
<div class="card-header bg-primary text-white">
|
||||||
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
|
<h5 class="card-title mb-0">
|
||||||
|
<i class="fas fa-calendar-check me-2"></i>Vierteljährliche Nachweise
|
||||||
|
</h5>
|
||||||
|
<div>
|
||||||
|
<small class="text-light me-3">Frist: jeweils 15. des zweiten Quartalsmonats</small>
|
||||||
|
<button type="button" class="btn btn-sm btn-light"
|
||||||
|
data-bs-toggle="modal"
|
||||||
|
data-bs-target="#addQuarterModal"
|
||||||
|
title="Neues Quartal hinzufügen">
|
||||||
|
<i class="fas fa-plus me-1"></i>Quartal hinzufügen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
{% if quarterly_confirmations %}
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-hover">
|
||||||
|
<thead class="table-light">
|
||||||
|
<tr>
|
||||||
|
<th>Zeitraum</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>Fortschritt</th>
|
||||||
|
<th>Fälligkeit</th>
|
||||||
|
<th>Studiennachweis</th>
|
||||||
|
<th>Einkommen</th>
|
||||||
|
<th>Vermögen</th>
|
||||||
|
<th>Aktionen</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for nachweis in quarterly_confirmations %}
|
||||||
|
<tr {% if nachweis.is_overdue %}class="table-warning"{% endif %}>
|
||||||
|
<td>
|
||||||
|
<strong>{{ nachweis.jahr }} {{ nachweis.get_quarter_display|truncatechars:3 }}</strong>
|
||||||
|
{% if nachweis.is_overdue %}
|
||||||
|
<br><small class="text-danger"><i class="fas fa-exclamation-triangle"></i> Überfällig</small>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% if nachweis.status == 'offen' %}
|
||||||
|
<span class="badge bg-secondary">Ausstehend</span>
|
||||||
|
{% elif nachweis.status == 'teilweise' %}
|
||||||
|
<span class="badge bg-warning">Teilweise</span>
|
||||||
|
{% elif nachweis.status == 'eingereicht' %}
|
||||||
|
<span class="badge bg-info">Eingereicht</span>
|
||||||
|
{% elif nachweis.status == 'geprueft' %}
|
||||||
|
<span class="badge bg-success">Freigegeben</span>
|
||||||
|
{% elif nachweis.status == 'nachbesserung' %}
|
||||||
|
<span class="badge bg-warning">Nachbesserung</span>
|
||||||
|
{% elif nachweis.status == 'abgelehnt' %}
|
||||||
|
<span class="badge bg-danger">Abgelehnt</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% with completion=nachweis.get_completion_percentage %}
|
||||||
|
<div class="progress" style="height: 20px;">
|
||||||
|
<div class="progress-bar
|
||||||
|
{% if completion == 100 %}bg-success
|
||||||
|
{% elif completion >= 70 %}bg-info
|
||||||
|
{% elif completion >= 30 %}bg-warning
|
||||||
|
{% else %}bg-danger
|
||||||
|
{% endif %}"
|
||||||
|
role="progressbar"
|
||||||
|
style="width: {{ completion }}%"
|
||||||
|
aria-valuenow="{{ completion }}"
|
||||||
|
aria-valuemin="0"
|
||||||
|
aria-valuemax="100">
|
||||||
|
{{ completion }}%
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endwith %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% if nachweis.faelligkeitsdatum %}
|
||||||
|
<small>{{ nachweis.faelligkeitsdatum|date:"d.m.Y" }}</small>
|
||||||
|
{% else %}
|
||||||
|
<small class="text-muted">Nicht festgelegt</small>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td class="text-center">
|
||||||
|
{% if nachweis.studiennachweis_eingereicht %}
|
||||||
|
{% if nachweis.studiennachweis_datei %}
|
||||||
|
<a href="{{ nachweis.studiennachweis_datei.url }}" target="_blank" class="text-success" title="Datei ansehen">
|
||||||
|
<i class="fas fa-file-pdf"></i>
|
||||||
|
</a>
|
||||||
|
{% elif nachweis.studiennachweis_bemerkung %}
|
||||||
|
<i class="fas fa-comment text-info" title="Bemerkung vorhanden"></i>
|
||||||
|
{% else %}
|
||||||
|
<i class="fas fa-check text-success"></i>
|
||||||
|
{% endif %}
|
||||||
|
{% else %}
|
||||||
|
<i class="fas fa-times text-muted"></i>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td class="text-center">
|
||||||
|
{% if nachweis.einkommenssituation_bestaetigt %}
|
||||||
|
{% if nachweis.einkommenssituation_datei %}
|
||||||
|
<a href="{{ nachweis.einkommenssituation_datei.url }}" target="_blank" class="text-success" title="Datei ansehen">
|
||||||
|
<i class="fas fa-file-pdf"></i>
|
||||||
|
</a>
|
||||||
|
{% elif nachweis.einkommenssituation_text %}
|
||||||
|
<i class="fas fa-comment text-info" title="{{ nachweis.einkommenssituation_text|truncatechars:50 }}"></i>
|
||||||
|
{% else %}
|
||||||
|
<i class="fas fa-check text-success"></i>
|
||||||
|
{% endif %}
|
||||||
|
{% else %}
|
||||||
|
<i class="fas fa-times text-muted"></i>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td class="text-center">
|
||||||
|
{% if nachweis.vermogenssituation_bestaetigt %}
|
||||||
|
{% if nachweis.vermogenssituation_datei %}
|
||||||
|
<a href="{{ nachweis.vermogenssituation_datei.url }}" target="_blank" class="text-success" title="Datei ansehen">
|
||||||
|
<i class="fas fa-file-pdf"></i>
|
||||||
|
</a>
|
||||||
|
{% elif nachweis.vermogenssituation_text %}
|
||||||
|
<i class="fas fa-comment text-info" title="{{ nachweis.vermogenssituation_text|truncatechars:50 }}"></i>
|
||||||
|
{% else %}
|
||||||
|
<i class="fas fa-check text-success"></i>
|
||||||
|
{% endif %}
|
||||||
|
{% else %}
|
||||||
|
<i class="fas fa-times text-muted"></i>
|
||||||
|
{% endif %}
|
||||||
|
</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>
|
||||||
|
</a>
|
||||||
|
{% if nachweis.status == 'eingereicht' and user.is_staff %}
|
||||||
|
<button type="button"
|
||||||
|
class="btn btn-sm btn-outline-success"
|
||||||
|
onclick="approveQuarterly('{{ nachweis.id }}')"
|
||||||
|
title="Freigeben">
|
||||||
|
<i class="fas fa-check"></i>
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</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">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title" id="addQuarterModalLabel">
|
||||||
|
<i class="fas fa-plus me-2"></i>Neues Quartal hinzufügen
|
||||||
|
</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Schließen"></button>
|
||||||
|
</div>
|
||||||
|
<form method="post" action="{% url 'stiftung:quarterly_confirmation_create' destinataer.pk %}">
|
||||||
|
{% csrf_token %}
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="jahr" class="form-label">Jahr</label>
|
||||||
|
<select class="form-select" id="jahr" name="jahr" required>
|
||||||
|
{% for year in available_years %}
|
||||||
|
<option value="{{ year }}" {% if year == current_year %}selected{% endif %}>{{ year }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="quartal" class="form-label">Quartal</label>
|
||||||
|
<select class="form-select" id="quartal" name="quartal" required>
|
||||||
|
<option value="1">Q1 (Januar - März)</option>
|
||||||
|
<option value="2">Q2 (April - Juni)</option>
|
||||||
|
<option value="3">Q3 (Juli - September)</option>
|
||||||
|
<option value="4">Q4 (Oktober - Dezember)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="alert alert-info">
|
||||||
|
<i class="fas fa-info-circle me-2"></i>
|
||||||
|
Das Quartal wird mit dem aktuellen Datum als Stichtag erstellt.
|
||||||
|
Die Fälligkeit wird automatisch auf den 15. des zweiten Quartalsmonats gesetzt.
|
||||||
|
</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-plus me-2"></i>Quartal erstellen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% else %}
|
||||||
|
<div class="text-center py-4">
|
||||||
|
<i class="fas fa-calendar-check fa-3x text-muted mb-3"></i>
|
||||||
|
<h5 class="text-muted">Keine vierteljährlichen Nachweise</h5>
|
||||||
|
<p class="text-muted">Nachweise werden automatisch erstellt, wenn Studiennachweise erforderlich sind.</p>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<!-- Notes Section -->
|
<!-- Notes Section -->
|
||||||
<div class="card shadow mb-4">
|
<div class="card shadow mb-4">
|
||||||
<div class="card-header bg-secondary text-white">
|
<div class="card-header bg-secondary text-white">
|
||||||
@@ -976,5 +1329,29 @@ document.addEventListener('keydown', function(e) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Quarterly confirmation functions
|
||||||
|
function approveQuarterly(nachweisId) {
|
||||||
|
if (confirm('Möchten Sie diesen Vierteljahresnachweis wirklich freigeben?')) {
|
||||||
|
fetch(`/quarterly-confirmations/${nachweisId}/approve/`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
if (response.ok) {
|
||||||
|
location.reload(); // Reload to show updated status
|
||||||
|
} else {
|
||||||
|
alert('Fehler beim Freigeben des Nachweises.');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error:', error);
|
||||||
|
alert('Fehler beim Freigeben des Nachweises.');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
519
app/templates/stiftung/quarterly_confirmation_edit.html
Normal file
519
app/templates/stiftung/quarterly_confirmation_edit.html
Normal file
@@ -0,0 +1,519 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
{% block title %}{{ title }} - Stiftungsverwaltung{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_css %}
|
||||||
|
<style>
|
||||||
|
/* Override font sizes for better readability */
|
||||||
|
body {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.h1, .h2, .h3, .h4, .h5, .h6,
|
||||||
|
h1, h2, h3, h4, h5, h6 {
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.h3, h3 {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.h5, h5 {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.h6, h6 {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Better card spacing */
|
||||||
|
.card {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-body {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Form improvements */
|
||||||
|
.form-label {
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
margin-bottom: 0.375rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control, .form-select {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-text {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Button improvements */
|
||||||
|
.btn {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-sm {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
padding: 0.375rem 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Progress bar styling */
|
||||||
|
.progress {
|
||||||
|
height: 1.25rem;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Badge sizing */
|
||||||
|
.badge {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
padding: 0.35em 0.65em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Table improvements */
|
||||||
|
.table {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table th {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table td {
|
||||||
|
padding: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive adjustments */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
body {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.h3, h3 {
|
||||||
|
font-size: 1.3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
padding: 0.375rem 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-body {
|
||||||
|
padding: 0.75rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container-fluid">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||||
|
<h1 class="h3 mb-0">
|
||||||
|
<i class="fas fa-calendar-check text-primary me-2"></i>
|
||||||
|
Vierteljahresnachweis bearbeiten
|
||||||
|
</h1>
|
||||||
|
<a href="{% url 'stiftung:destinataer_detail' pk=destinataer.pk %}" class="btn btn-outline-secondary btn-sm">
|
||||||
|
<i class="fas fa-arrow-left me-1"></i>Zurück
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Quarter Info Card -->
|
||||||
|
<div class="card shadow mb-3">
|
||||||
|
<div class="card-header bg-primary text-white">
|
||||||
|
<div class="row align-items-center">
|
||||||
|
<div class="col-md-8">
|
||||||
|
<h5 class="card-title mb-0">
|
||||||
|
<i class="fas fa-user me-2"></i>{{ destinataer.get_full_name }}
|
||||||
|
</h5>
|
||||||
|
<small class="opacity-75">{{ nachweis.jahr }} {{ nachweis.get_quarter_display }}</small>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4 text-end">
|
||||||
|
<span class="badge
|
||||||
|
{% if nachweis.status == 'offen' %}bg-secondary
|
||||||
|
{% elif nachweis.status == 'teilweise' %}bg-warning
|
||||||
|
{% elif nachweis.status == 'eingereicht' %}bg-info
|
||||||
|
{% elif nachweis.status == 'geprueft' %}bg-success
|
||||||
|
{% elif nachweis.status == 'nachbesserung' %}bg-warning
|
||||||
|
{% elif nachweis.status == 'abgelehnt' %}bg-danger
|
||||||
|
{% endif %}">
|
||||||
|
{{ nachweis.get_status_display }}
|
||||||
|
</span>
|
||||||
|
{% if nachweis.faelligkeitsdatum %}
|
||||||
|
<br><small class="opacity-75">Fällig: {{ nachweis.faelligkeitsdatum|date:"d.m.Y" }}</small>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<p class="mb-2"><strong>Destinatär:</strong> {{ destinataer.get_full_name }}</p>
|
||||||
|
<p class="mb-2"><strong>E-Mail:</strong>
|
||||||
|
{% if destinataer.email %}
|
||||||
|
<a href="mailto:{{ destinataer.email }}">{{ destinataer.email }}</a>
|
||||||
|
{% else %}
|
||||||
|
<em class="text-muted">Nicht angegeben</em>
|
||||||
|
{% endif %}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<p class="mb-2"><strong>Zeitraum:</strong> {{ nachweis.jahr }} {{ nachweis.get_quarter_display }}</p>
|
||||||
|
{% if nachweis.faelligkeitsdatum %}
|
||||||
|
<p class="mb-0"><strong>Fälligkeitsdatum:</strong> {{ nachweis.faelligkeitsdatum|date:"d.m.Y" }}</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Edit Form -->
|
||||||
|
<form method="post" enctype="multipart/form-data">
|
||||||
|
{% csrf_token %}
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg-8">
|
||||||
|
<!-- Study Proof Section -->
|
||||||
|
<div class="card shadow mb-3">
|
||||||
|
<div class="card-header bg-primary text-white">
|
||||||
|
<h6 class="card-title mb-0">
|
||||||
|
<i class="fas fa-graduation-cap me-2"></i>Studiennachweis
|
||||||
|
</h6>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="form-check mb-3">
|
||||||
|
{{ form.studiennachweis_eingereicht }}
|
||||||
|
<label class="form-check-label" for="{{ form.studiennachweis_eingereicht.id_for_label }}">
|
||||||
|
{{ form.studiennachweis_eingereicht.label }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="{{ form.studiennachweis_datei.id_for_label }}" class="form-label">{{ form.studiennachweis_datei.label }}</label>
|
||||||
|
{{ form.studiennachweis_datei }}
|
||||||
|
{% if nachweis.studiennachweis_datei %}
|
||||||
|
<div class="mt-2">
|
||||||
|
<small class="text-muted">Aktuelle Datei:
|
||||||
|
<a href="{{ nachweis.studiennachweis_datei.url }}" target="_blank" class="text-decoration-none">
|
||||||
|
<i class="fas fa-file-pdf text-danger"></i> {{ nachweis.studiennachweis_datei.name }}
|
||||||
|
</a>
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% if form.studiennachweis_datei.help_text %}
|
||||||
|
<small class="form-text text-muted">{{ form.studiennachweis_datei.help_text }}</small>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-0">
|
||||||
|
<label for="{{ form.studiennachweis_bemerkung.id_for_label }}" class="form-label">{{ form.studiennachweis_bemerkung.label }}</label>
|
||||||
|
{{ form.studiennachweis_bemerkung }}
|
||||||
|
{% if form.studiennachweis_bemerkung.help_text %}
|
||||||
|
<small class="form-text text-muted">{{ form.studiennachweis_bemerkung.help_text }}</small>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Income Situation Section -->
|
||||||
|
<div class="card shadow mb-3">
|
||||||
|
<div class="card-header bg-success text-white">
|
||||||
|
<h6 class="card-title mb-0">
|
||||||
|
<i class="fas fa-euro-sign me-2"></i>Einkommenssituation
|
||||||
|
</h6>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="form-check mb-3">
|
||||||
|
{{ form.einkommenssituation_bestaetigt }}
|
||||||
|
<label class="form-check-label" for="{{ form.einkommenssituation_bestaetigt.id_for_label }}">
|
||||||
|
{{ form.einkommenssituation_bestaetigt.label }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="{{ form.einkommenssituation_text.id_for_label }}" class="form-label">{{ form.einkommenssituation_text.label }}</label>
|
||||||
|
{{ form.einkommenssituation_text }}
|
||||||
|
{% if form.einkommenssituation_text.help_text %}
|
||||||
|
<small class="form-text text-muted">{{ form.einkommenssituation_text.help_text }}</small>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-0">
|
||||||
|
<label for="{{ form.einkommenssituation_datei.id_for_label }}" class="form-label">{{ form.einkommenssituation_datei.label }}</label>
|
||||||
|
{{ form.einkommenssituation_datei }}
|
||||||
|
{% if nachweis.einkommenssituation_datei %}
|
||||||
|
<div class="mt-2">
|
||||||
|
<small class="text-muted">Aktuelle Datei:
|
||||||
|
<a href="{{ nachweis.einkommenssituation_datei.url }}" target="_blank" class="text-decoration-none">
|
||||||
|
<i class="fas fa-file-pdf text-danger"></i> {{ nachweis.einkommenssituation_datei.name }}
|
||||||
|
</a>
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% if form.einkommenssituation_datei.help_text %}
|
||||||
|
<small class="form-text text-muted">{{ form.einkommenssituation_datei.help_text }}</small>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Asset Situation Section -->
|
||||||
|
<div class="card shadow mb-4">
|
||||||
|
<div class="card-header bg-warning text-dark">
|
||||||
|
<h5 class="card-title mb-0">
|
||||||
|
<i class="fas fa-piggy-bank me-2"></i>Vermögenssituation
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="form-check mb-3">
|
||||||
|
{{ form.vermogenssituation_bestaetigt }}
|
||||||
|
<label class="form-check-label" for="{{ form.vermogenssituation_bestaetigt.id_for_label }}">
|
||||||
|
{{ form.vermogenssituation_bestaetigt.label }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="{{ form.vermogenssituation_text.id_for_label }}" class="form-label">{{ form.vermogenssituation_text.label }}</label>
|
||||||
|
{{ form.vermogenssituation_text }}
|
||||||
|
{% if form.vermogenssituation_text.help_text %}
|
||||||
|
<small class="form-text text-muted">{{ form.vermogenssituation_text.help_text }}</small>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="{{ form.vermogenssituation_datei.id_for_label }}" class="form-label">{{ form.vermogenssituation_datei.label }}</label>
|
||||||
|
{{ form.vermogenssituation_datei }}
|
||||||
|
{% if nachweis.vermogenssituation_datei %}
|
||||||
|
<div class="mt-2">
|
||||||
|
<small class="text-muted">Aktuelle Datei:
|
||||||
|
<a href="{{ nachweis.vermogenssituation_datei.url }}" target="_blank" class="text-decoration-none">
|
||||||
|
<i class="fas fa-file-pdf text-danger"></i> {{ nachweis.vermogenssituation_datei.name }}
|
||||||
|
</a>
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% if form.vermogenssituation_datei.help_text %}
|
||||||
|
<small class="form-text text-muted">{{ form.vermogenssituation_datei.help_text }}</small>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Additional Documents Section -->
|
||||||
|
<div class="card shadow mb-4">
|
||||||
|
<div class="card-header bg-info text-white">
|
||||||
|
<h5 class="card-title mb-0">
|
||||||
|
<i class="fas fa-file-alt me-2"></i>Weitere Dokumente (optional)
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="{{ form.weitere_dokumente.id_for_label }}" class="form-label">{{ form.weitere_dokumente.label }}</label>
|
||||||
|
{{ form.weitere_dokumente }}
|
||||||
|
{% if nachweis.weitere_dokumente %}
|
||||||
|
<div class="mt-2">
|
||||||
|
<small class="text-muted">Aktuelle Datei:
|
||||||
|
<a href="{{ nachweis.weitere_dokumente.url }}" target="_blank" class="text-decoration-none">
|
||||||
|
<i class="fas fa-file-pdf text-danger"></i> {{ nachweis.weitere_dokumente.name }}
|
||||||
|
</a>
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% if form.weitere_dokumente.help_text %}
|
||||||
|
<small class="form-text text-muted">{{ form.weitere_dokumente.help_text }}</small>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="{{ form.weitere_dokumente_beschreibung.id_for_label }}" class="form-label">{{ form.weitere_dokumente_beschreibung.label }}</label>
|
||||||
|
{{ form.weitere_dokumente_beschreibung }}
|
||||||
|
{% if form.weitere_dokumente_beschreibung.help_text %}
|
||||||
|
<small class="form-text text-muted">{{ form.weitere_dokumente_beschreibung.help_text }}</small>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Internal Notes (Staff Only) -->
|
||||||
|
{% if user.is_staff %}
|
||||||
|
<div class="card shadow mb-4">
|
||||||
|
<div class="card-header bg-secondary text-white">
|
||||||
|
<h5 class="card-title mb-0">
|
||||||
|
<i class="fas fa-user-shield me-2"></i>Interne Notizen (nur für Verwaltung)
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="{{ form.interne_notizen.id_for_label }}" class="form-label">{{ form.interne_notizen.label }}</label>
|
||||||
|
{{ form.interne_notizen }}
|
||||||
|
{% if form.interne_notizen.help_text %}
|
||||||
|
<small class="form-text text-muted">{{ form.interne_notizen.help_text }}</small>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<!-- Form Actions -->
|
||||||
|
<div class="card shadow">
|
||||||
|
<div class="card-body bg-light">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-6">
|
||||||
|
<a href="{% url 'stiftung:destinataer_detail' pk=destinataer.pk %}" class="btn btn-secondary w-100">
|
||||||
|
<i class="fas fa-times me-2"></i>Abbrechen
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="col-6">
|
||||||
|
<button type="submit" class="btn btn-primary w-100">
|
||||||
|
<i class="fas fa-save me-2"></i>Speichern
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row mt-3">
|
||||||
|
<div class="col-12">
|
||||||
|
<small class="text-muted">
|
||||||
|
<i class="fas fa-info-circle me-1"></i>
|
||||||
|
Ihre Änderungen werden gespeichert und der Status wird automatisch aktualisiert.
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Sidebar -->
|
||||||
|
<div class="col-lg-4">
|
||||||
|
<!-- Status Card -->
|
||||||
|
<div class="card shadow mb-3">
|
||||||
|
<div class="card-header bg-light">
|
||||||
|
<h6 class="card-title mb-0">
|
||||||
|
<i class="fas fa-info-circle me-2"></i>Status & Informationen
|
||||||
|
</h6>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<p class="mb-2"><strong>Status:</strong>
|
||||||
|
<span class="badge
|
||||||
|
{% if nachweis.status == 'offen' %}bg-secondary
|
||||||
|
{% elif nachweis.status == 'teilweise' %}bg-warning
|
||||||
|
{% elif nachweis.status == 'eingereicht' %}bg-info
|
||||||
|
{% elif nachweis.status == 'geprueft' %}bg-success
|
||||||
|
{% elif nachweis.status == 'nachbesserung' %}bg-warning
|
||||||
|
{% elif nachweis.status == 'abgelehnt' %}bg-danger
|
||||||
|
{% endif %}">
|
||||||
|
{{ nachweis.get_status_display }}
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{% if nachweis.faelligkeitsdatum %}
|
||||||
|
<p class="mb-2"><strong>Fälligkeit:</strong> {{ nachweis.faelligkeitsdatum|date:"d.m.Y" }}</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if nachweis.eingereicht_am %}
|
||||||
|
<p class="mb-2"><strong>Eingereicht:</strong> {{ nachweis.eingereicht_am|date:"d.m.Y H:i" }}</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if nachweis.geprueft_am %}
|
||||||
|
<p class="mb-2"><strong>Geprüft:</strong> {{ nachweis.geprueft_am|date:"d.m.Y H:i" }}</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if nachweis.geprueft_von %}
|
||||||
|
<p class="mb-2"><strong>Geprüft von:</strong> {{ nachweis.geprueft_von.get_full_name|default:nachweis.geprueft_von.username }}</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<p class="mb-1"><strong>Erstellt:</strong> {{ nachweis.erstellt_am|date:"d.m.Y H:i" }}</p>
|
||||||
|
<p class="mb-0"><strong>Aktualisiert:</strong> {{ nachweis.aktualisiert_am|date:"d.m.Y H:i" }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Requirements Checklist -->
|
||||||
|
<div class="card shadow mb-3">
|
||||||
|
<div class="card-header bg-primary text-white">
|
||||||
|
<h6 class="card-title mb-0">
|
||||||
|
<i class="fas fa-list-check me-2"></i>Anforderungen
|
||||||
|
</h6>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<ul class="list-unstyled mb-0">
|
||||||
|
<li class="mb-2">
|
||||||
|
{% if nachweis.studiennachweis_eingereicht %}
|
||||||
|
<i class="fas fa-check-circle text-success me-2"></i>
|
||||||
|
{% else %}
|
||||||
|
<i class="fas fa-circle text-muted me-2"></i>
|
||||||
|
{% endif %}
|
||||||
|
Studiennachweis
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="mb-2">
|
||||||
|
{% if nachweis.einkommenssituation_bestaetigt %}
|
||||||
|
<i class="fas fa-check-circle text-success me-2"></i>
|
||||||
|
{% else %}
|
||||||
|
<i class="fas fa-circle text-muted me-2"></i>
|
||||||
|
{% endif %}
|
||||||
|
Einkommenssituation
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="mb-0">
|
||||||
|
{% if nachweis.vermogenssituation_bestaetigt %}
|
||||||
|
<i class="fas fa-check-circle text-success me-2"></i>
|
||||||
|
{% else %}
|
||||||
|
<i class="fas fa-circle text-muted me-2"></i>
|
||||||
|
{% endif %}
|
||||||
|
Vermögenssituation
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Help Card -->
|
||||||
|
<div class="card shadow">
|
||||||
|
<div class="card-header bg-info text-white">
|
||||||
|
<h6 class="card-title mb-0">
|
||||||
|
<i class="fas fa-question-circle me-2"></i>Hilfe
|
||||||
|
</h6>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<small>
|
||||||
|
<strong>Einkommenssituation:</strong><br>
|
||||||
|
Sie können entweder einen kurzen Text eingeben oder eine Datei hochladen.<br><br>
|
||||||
|
|
||||||
|
<strong>Unterstützte Dateiformate:</strong><br>
|
||||||
|
PDF, DOC, DOCX, JPG, PNG<br><br>
|
||||||
|
|
||||||
|
<strong>Fragen?</strong><br>
|
||||||
|
Wenden Sie sich an die Stiftungsverwaltung.
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block javascript %}
|
||||||
|
<script>
|
||||||
|
// Auto-save functionality could be added here
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// Add any JavaScript functionality here
|
||||||
|
console.log('Quarterly confirmation edit page loaded');
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
Reference in New Issue
Block a user