feat: Implement quarterly confirmation system with automatic support payments

- Add VierteljahresNachweis model for quarterly document tracking
- Remove studiennachweis_erforderlich field (now always required)
- Fix modal edit view to include studiennachweis section
- Implement automatic DestinataerUnterstuetzung creation when requirements met
- Set payment due dates to exact quarter end dates (Mar 31, Jun 30, Sep 30, Dec 31)
- Add quarterly confirmation CRUD views with modal and full-screen editing
- Update templates with comprehensive quarterly management interface
- Include proper validation, status tracking, and progress indicators
This commit is contained in:
2025-09-23 23:52:44 +02:00
parent 0184982f8c
commit 126f68ec68
9 changed files with 2177 additions and 56 deletions

View File

@@ -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"

View File

@@ -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

View 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')},
},
),
]

View File

@@ -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")

View File

@@ -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",
),
] ]

View File

@@ -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)

View File

@@ -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>

View File

@@ -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 %}

View 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 %}