Phase 2: Destinatär-Timeline, Nachweis-Board, Zahlungs-Pipeline & Pächter-Workflow
2a. Destinatär-Timeline (/destinataere/<pk>/timeline/)
- Chronologische Ansicht aller Events (Zahlungen, Nachweise, E-Mails, Notizen)
- Filter nach Typ via GET-Parameter
2b. Nachweis-Board (/nachweis-board/)
- Quartals-Übersicht aller aktiver Destinatäre (Q1–Q4) in einer Tabellenansicht
- Batch-Erinnerung: erzeugt Audit-Log-Einträge für säumige Destinatäre
- Semester-Logik erhalten (15.03 / 15.09 Fristen)
2c. Zahlungs-Pipeline (/zahlungs-pipeline/)
- 5-Stufen-Kanban: Offen → Nachweis eingereicht → Freigegeben → Überwiesen → Abgeschlossen
- Vier-Augen-Prinzip: can_be_freigegeben() prüft anderen Nutzer als Ersteller
- SEPA pain.001 XML-Export (/sepa-export/) für freigegebene Zahlungen
- Neue Status-Werte: nachweis_eingereicht, freigegeben, abgeschlossen
- Neue Felder: freigegeben_von, freigegeben_am, erstellt_von
2d. Pächter-Workflow (/paechter/workflow/)
- Pipeline nach Restlaufzeit: abgelaufen / <6M / 6–24M / >24M / unbefristet
- Ausstehende Jahresabrechnungen (Vorjahr ohne Abrechnung)
- Pachtanpassungen fällig (Verträge > 5 Jahre laufend)
- Top-Pächter nach Gesamtfläche
Sidebar-Navigation um Pipeline, Nachweis-Board und Pacht-Workflow erweitert.
Migration 0047 erzeugt und angewendet.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
45
app/stiftung/migrations/0047_phase2_zahlungs_pipeline.py
Normal file
45
app/stiftung/migrations/0047_phase2_zahlungs_pipeline.py
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
# Generated by Django 5.0.6 on 2026-03-11 10:39
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('stiftung', '0046_briefvorlage_model'),
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='applicationpermission',
|
||||||
|
options={'default_permissions': (), 'managed': False, 'permissions': [('manage_destinataere', 'Kann Destinatäre verwalten'), ('view_destinataere', 'Kann Destinatäre anzeigen'), ('manage_land', 'Kann Ländereien verwalten'), ('view_land', 'Kann Ländereien anzeigen'), ('manage_paechter', 'Kann Pächter verwalten'), ('view_paechter', 'Kann Pächter anzeigen'), ('manage_verpachtungen', 'Kann Verpachtungen verwalten'), ('view_verpachtungen', 'Kann Verpachtungen anzeigen'), ('manage_foerderungen', 'Kann Förderungen verwalten'), ('view_foerderungen', 'Kann Förderungen anzeigen'), ('manage_documents', 'Kann Dokumente verwalten'), ('view_documents', 'Kann Dokumente anzeigen'), ('link_documents', 'Kann Dokumente verknüpfen'), ('manage_verwaltungskosten', 'Kann Verwaltungskosten verwalten'), ('view_verwaltungskosten', 'Kann Verwaltungskosten anzeigen'), ('approve_payments', 'Kann Zahlungen genehmigen'), ('manage_konten', 'Kann Stiftungskonten verwalten'), ('view_konten', 'Kann Stiftungskonten anzeigen'), ('manage_rentmeister', 'Kann Rentmeister verwalten'), ('view_rentmeister', 'Kann Rentmeister anzeigen'), ('access_administration', 'Kann Administration aufrufen'), ('view_audit_logs', 'Kann Audit-Logs anzeigen'), ('manage_backups', 'Kann Backups erstellen und verwalten'), ('manage_users', 'Kann Benutzer verwalten'), ('manage_permissions', 'Kann Berechtigungen verwalten'), ('manage_veranstaltungen', 'Kann Veranstaltungen verwalten'), ('view_veranstaltungen', 'Kann Veranstaltungen anzeigen'), ('import_data', 'Kann Daten importieren'), ('export_data', 'Kann Daten exportieren'), ('access_django_admin', 'Kann Django Admin aufrufen'), ('view_system_stats', 'Kann Systemstatistiken anzeigen')]},
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='destinataerunterstuetzung',
|
||||||
|
name='erstellt_von',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='erstellte_unterstuetzungen', to=settings.AUTH_USER_MODEL, verbose_name='Erstellt von'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='destinataerunterstuetzung',
|
||||||
|
name='freigegeben_am',
|
||||||
|
field=models.DateField(blank=True, null=True, verbose_name='Freigegeben am'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='destinataerunterstuetzung',
|
||||||
|
name='freigegeben_von',
|
||||||
|
field=models.ForeignKey(blank=True, help_text='Muss ein anderer Nutzer als der Ersteller sein (Vier-Augen-Prinzip)', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='freigegebene_unterstuetzungen', to=settings.AUTH_USER_MODEL, verbose_name='Freigegeben von (4-Augen)'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='destinataerunterstuetzung',
|
||||||
|
name='ausgezahlt_von',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='ausgezahlte_unterstuetzungen', to=settings.AUTH_USER_MODEL, verbose_name='Ausgezahlt von'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='destinataerunterstuetzung',
|
||||||
|
name='status',
|
||||||
|
field=models.CharField(choices=[('geplant', 'Offen'), ('faellig', 'Fällig'), ('nachweis_eingereicht', 'Nachweis eingereicht'), ('freigegeben', 'Freigegeben (4-Augen)'), ('in_bearbeitung', 'In Bearbeitung'), ('ausgezahlt', 'Überwiesen'), ('abgeschlossen', 'Abgeschlossen'), ('storniert', 'Storniert')], default='geplant', max_length=20, verbose_name='Status'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -346,13 +346,25 @@ class DestinataerUnterstuetzung(models.Model):
|
|||||||
"""Geplante/ausgeführte Unterstützungszahlungen an Destinatäre"""
|
"""Geplante/ausgeführte Unterstützungszahlungen an Destinatäre"""
|
||||||
|
|
||||||
STATUS_CHOICES = [
|
STATUS_CHOICES = [
|
||||||
("geplant", "Geplant"),
|
("geplant", "Offen"),
|
||||||
("faellig", "Fällig"),
|
("faellig", "Fällig"),
|
||||||
|
("nachweis_eingereicht", "Nachweis eingereicht"),
|
||||||
|
("freigegeben", "Freigegeben (4-Augen)"),
|
||||||
("in_bearbeitung", "In Bearbeitung"),
|
("in_bearbeitung", "In Bearbeitung"),
|
||||||
("ausgezahlt", "Ausgezahlt"),
|
("ausgezahlt", "Überwiesen"),
|
||||||
|
("abgeschlossen", "Abgeschlossen"),
|
||||||
("storniert", "Storniert"),
|
("storniert", "Storniert"),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# Pipeline-stage für Zahlungsstatus-Anzeige
|
||||||
|
PIPELINE_STAGES = [
|
||||||
|
("geplant", "Offen"),
|
||||||
|
("nachweis_eingereicht", "Nachweis eingereicht"),
|
||||||
|
("freigegeben", "Freigegeben"),
|
||||||
|
("ausgezahlt", "Überwiesen"),
|
||||||
|
("abgeschlossen", "Abgeschlossen"),
|
||||||
|
]
|
||||||
|
|
||||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||||
destinataer = models.ForeignKey(
|
destinataer = models.ForeignKey(
|
||||||
"Destinataer",
|
"Destinataer",
|
||||||
@@ -392,9 +404,32 @@ class DestinataerUnterstuetzung(models.Model):
|
|||||||
on_delete=models.SET_NULL,
|
on_delete=models.SET_NULL,
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
|
related_name="ausgezahlte_unterstuetzungen",
|
||||||
verbose_name="Ausgezahlt von",
|
verbose_name="Ausgezahlt von",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# 4-Augen-Prinzip: Freigabe durch zweiten Nutzer
|
||||||
|
freigegeben_am = models.DateField(
|
||||||
|
null=True, blank=True, verbose_name="Freigegeben am"
|
||||||
|
)
|
||||||
|
freigegeben_von = models.ForeignKey(
|
||||||
|
"auth.User",
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
related_name="freigegebene_unterstuetzungen",
|
||||||
|
verbose_name="Freigegeben von (4-Augen)",
|
||||||
|
help_text="Muss ein anderer Nutzer als der Ersteller sein (Vier-Augen-Prinzip)",
|
||||||
|
)
|
||||||
|
erstellt_von = models.ForeignKey(
|
||||||
|
"auth.User",
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
related_name="erstellte_unterstuetzungen",
|
||||||
|
verbose_name="Erstellt von",
|
||||||
|
)
|
||||||
|
|
||||||
# Link to recurrent payment template if this was auto-generated
|
# Link to recurrent payment template if this was auto-generated
|
||||||
wiederkehrend_von = models.ForeignKey(
|
wiederkehrend_von = models.ForeignKey(
|
||||||
"UnterstuetzungWiederkehrend",
|
"UnterstuetzungWiederkehrend",
|
||||||
@@ -431,7 +466,29 @@ class DestinataerUnterstuetzung(models.Model):
|
|||||||
|
|
||||||
def can_be_marked_paid(self):
|
def can_be_marked_paid(self):
|
||||||
"""Check if payment can be marked as paid"""
|
"""Check if payment can be marked as paid"""
|
||||||
return self.status in ["geplant", "faellig", "in_bearbeitung"]
|
return self.status in ["geplant", "faellig", "nachweis_eingereicht", "freigegeben", "in_bearbeitung"]
|
||||||
|
|
||||||
|
def can_be_freigegeben(self, requesting_user):
|
||||||
|
"""4-Augen: Freigabe nur durch anderen Nutzer als Ersteller"""
|
||||||
|
if self.status not in ["nachweis_eingereicht", "faellig", "in_bearbeitung"]:
|
||||||
|
return False
|
||||||
|
if self.erstellt_von and self.erstellt_von == requesting_user:
|
||||||
|
return False # Selber Nutzer darf nicht freigeben
|
||||||
|
return True
|
||||||
|
|
||||||
|
def get_pipeline_stage(self):
|
||||||
|
"""Gibt die Pipeline-Stufe als Integer zurück (1-5)"""
|
||||||
|
stage_map = {
|
||||||
|
"geplant": 1,
|
||||||
|
"faellig": 2,
|
||||||
|
"nachweis_eingereicht": 2,
|
||||||
|
"in_bearbeitung": 3,
|
||||||
|
"freigegeben": 3,
|
||||||
|
"ausgezahlt": 4,
|
||||||
|
"abgeschlossen": 5,
|
||||||
|
"storniert": 0,
|
||||||
|
}
|
||||||
|
return stage_map.get(self.status, 1)
|
||||||
|
|
||||||
|
|
||||||
class UnterstuetzungWiederkehrend(models.Model):
|
class UnterstuetzungWiederkehrend(models.Model):
|
||||||
|
|||||||
@@ -440,4 +440,41 @@ urlpatterns = [
|
|||||||
path("kalender/<uuid:pk>/bearbeiten/", views.kalender_edit, name="kalender_edit"),
|
path("kalender/<uuid:pk>/bearbeiten/", views.kalender_edit, name="kalender_edit"),
|
||||||
path("kalender/<uuid:pk>/loeschen/", views.kalender_delete, name="kalender_delete"),
|
path("kalender/<uuid:pk>/loeschen/", views.kalender_delete, name="kalender_delete"),
|
||||||
path("kalender/api/events/", views.kalender_api_events, name="kalender_api_events"),
|
path("kalender/api/events/", views.kalender_api_events, name="kalender_api_events"),
|
||||||
|
|
||||||
|
# Phase 2: Destinatär-Timeline (2a)
|
||||||
|
path(
|
||||||
|
"destinataere/<uuid:pk>/timeline/",
|
||||||
|
views.destinataer_timeline,
|
||||||
|
name="destinataer_timeline",
|
||||||
|
),
|
||||||
|
|
||||||
|
# Phase 2: Nachweis-Board (2b)
|
||||||
|
path("nachweis-board/", views.nachweis_board, name="nachweis_board"),
|
||||||
|
path(
|
||||||
|
"nachweis-board/erinnerung/",
|
||||||
|
views.batch_erinnerung_senden,
|
||||||
|
name="batch_erinnerung_senden",
|
||||||
|
),
|
||||||
|
|
||||||
|
# Phase 2: Zahlungs-Pipeline (2c)
|
||||||
|
path("zahlungs-pipeline/", views.zahlungs_pipeline, name="zahlungs_pipeline"),
|
||||||
|
path(
|
||||||
|
"unterstuetzungen/<uuid:pk>/freigeben/",
|
||||||
|
views.unterstuetzung_freigeben,
|
||||||
|
name="unterstuetzung_freigeben",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"unterstuetzungen/<uuid:pk>/nachweis-eingereicht/",
|
||||||
|
views.unterstuetzung_nachweis_eingereicht,
|
||||||
|
name="unterstuetzung_nachweis_eingereicht",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"unterstuetzungen/<uuid:pk>/abschliessen/",
|
||||||
|
views.unterstuetzung_abschliessen,
|
||||||
|
name="unterstuetzung_abschliessen",
|
||||||
|
),
|
||||||
|
path("sepa-export/", views.sepa_xml_export, name="sepa_xml_export"),
|
||||||
|
|
||||||
|
# Phase 2: Pächter-Workflow (2d)
|
||||||
|
path("paechter/workflow/", views.paechter_workflow, name="paechter_workflow"),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -122,6 +122,8 @@ from .land import ( # noqa: F401
|
|||||||
verpachtung_create,
|
verpachtung_create,
|
||||||
verpachtung_update,
|
verpachtung_update,
|
||||||
verpachtung_delete,
|
verpachtung_delete,
|
||||||
|
# Phase 2d
|
||||||
|
paechter_workflow,
|
||||||
)
|
)
|
||||||
|
|
||||||
from .system import ( # noqa: F401
|
from .system import ( # noqa: F401
|
||||||
@@ -180,6 +182,15 @@ from .unterstuetzungen import ( # noqa: F401
|
|||||||
quarterly_confirmation_edit,
|
quarterly_confirmation_edit,
|
||||||
quarterly_confirmation_approve,
|
quarterly_confirmation_approve,
|
||||||
quarterly_confirmation_reset,
|
quarterly_confirmation_reset,
|
||||||
|
# Phase 2
|
||||||
|
destinataer_timeline,
|
||||||
|
nachweis_board,
|
||||||
|
batch_erinnerung_senden,
|
||||||
|
zahlungs_pipeline,
|
||||||
|
unterstuetzung_freigeben,
|
||||||
|
unterstuetzung_nachweis_eingereicht,
|
||||||
|
unterstuetzung_abschliessen,
|
||||||
|
sepa_xml_export,
|
||||||
)
|
)
|
||||||
|
|
||||||
from .veranstaltung import ( # noqa: F401
|
from .veranstaltung import ( # noqa: F401
|
||||||
|
|||||||
@@ -1551,3 +1551,114 @@ def verpachtung_delete(request, pk):
|
|||||||
return render(request, 'stiftung/verpachtung_confirm_delete.html', context)
|
return render(request, 'stiftung/verpachtung_confirm_delete.html', context)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Phase 2d: Pächter-Workflow-Verbesserung
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def paechter_workflow(request):
|
||||||
|
"""2d: Pipeline-Ansicht für Pächter – Vertragsfristen, Pachtanpassungen, Abrechnungen."""
|
||||||
|
heute = date.today()
|
||||||
|
|
||||||
|
# Aktive Verpachtungen abrufen mit Restlaufzeit
|
||||||
|
aktive_verpachtungen = LandVerpachtung.objects.filter(
|
||||||
|
status="aktiv"
|
||||||
|
).select_related("land", "paechter").order_by("pachtende")
|
||||||
|
|
||||||
|
# Kategorisieren
|
||||||
|
abgelaufen = []
|
||||||
|
demnachst = [] # < 6 Monate
|
||||||
|
mittelfrisitig = [] # 6–24 Monate
|
||||||
|
langfristig = [] # > 24 Monate
|
||||||
|
kein_enddatum = []
|
||||||
|
|
||||||
|
for v in aktive_verpachtungen:
|
||||||
|
if not v.pachtende:
|
||||||
|
kein_enddatum.append(v)
|
||||||
|
continue
|
||||||
|
restlaufzeit = (v.pachtende - heute).days
|
||||||
|
if restlaufzeit < 0:
|
||||||
|
abgelaufen.append(v)
|
||||||
|
elif restlaufzeit <= 180:
|
||||||
|
demnachst.append(v)
|
||||||
|
elif restlaufzeit <= 730:
|
||||||
|
mittelfrisitig.append(v)
|
||||||
|
else:
|
||||||
|
langfristig.append(v)
|
||||||
|
|
||||||
|
# Ausstehende Jahresabrechnungen (letztes Jahr ohne Abrechnung)
|
||||||
|
letztes_jahr = heute.year - 1
|
||||||
|
laender_ohne_abrechnung = Land.objects.filter(
|
||||||
|
aktiv=True
|
||||||
|
).exclude(
|
||||||
|
landabrechnung__abrechnungsjahr=letztes_jahr
|
||||||
|
).order_by("lfd_nr")[:20]
|
||||||
|
|
||||||
|
# Pächter mit hoher Gesamtfläche (Top-Pächter)
|
||||||
|
top_paechter = Paechter.objects.annotate(
|
||||||
|
flaeche=Sum("landverpachtung__verpachtete_flaeche"),
|
||||||
|
anzahl_vertraege=Count("landverpachtung")
|
||||||
|
).filter(flaeche__gt=0).order_by("-flaeche")[:10]
|
||||||
|
|
||||||
|
# Anstehende Pachtanpassungen (> 5 Jahre laufend, keine Erhöhung dokumentiert)
|
||||||
|
fuenf_jahre_ago = date(heute.year - 5, heute.month, heute.day)
|
||||||
|
lang_laufend = LandVerpachtung.objects.filter(
|
||||||
|
status="aktiv",
|
||||||
|
pachtbeginn__lte=fuenf_jahre_ago,
|
||||||
|
).select_related("land", "paechter").order_by("pachtbeginn")[:20]
|
||||||
|
|
||||||
|
pipeline_stages = [
|
||||||
|
{
|
||||||
|
"key": "abgelaufen",
|
||||||
|
"label": "Abgelaufen / Handlungsbedarf",
|
||||||
|
"farbe": "danger",
|
||||||
|
"icon": "fa-exclamation-triangle",
|
||||||
|
"verpachtungen": abgelaufen,
|
||||||
|
"count": len(abgelaufen),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "demnachst",
|
||||||
|
"label": "Bald fällig (< 6 Monate)",
|
||||||
|
"farbe": "warning",
|
||||||
|
"icon": "fa-clock",
|
||||||
|
"verpachtungen": demnachst,
|
||||||
|
"count": len(demnachst),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "mittelfristig",
|
||||||
|
"label": "Mittelfristig (6–24 Monate)",
|
||||||
|
"farbe": "info",
|
||||||
|
"icon": "fa-calendar",
|
||||||
|
"verpachtungen": mittelfrisitig,
|
||||||
|
"count": len(mittelfrisitig),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "langfristig",
|
||||||
|
"label": "Langfristig (> 24 Monate)",
|
||||||
|
"farbe": "success",
|
||||||
|
"icon": "fa-check",
|
||||||
|
"verpachtungen": langfristig,
|
||||||
|
"count": len(langfristig),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "unbefristet",
|
||||||
|
"label": "Unbefristet / Kein Enddatum",
|
||||||
|
"farbe": "secondary",
|
||||||
|
"icon": "fa-infinity",
|
||||||
|
"verpachtungen": kein_enddatum,
|
||||||
|
"count": len(kein_enddatum),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
context = {
|
||||||
|
"pipeline_stages": pipeline_stages,
|
||||||
|
"laender_ohne_abrechnung": laender_ohne_abrechnung,
|
||||||
|
"top_paechter": top_paechter,
|
||||||
|
"lang_laufend": lang_laufend,
|
||||||
|
"letztes_jahr": letztes_jahr,
|
||||||
|
"heute": heute,
|
||||||
|
}
|
||||||
|
return render(request, "stiftung/paechter_workflow.html", context)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1491,5 +1491,346 @@ def quarterly_confirmation_reset(request, pk):
|
|||||||
return redirect("stiftung:destinataer_detail", pk=nachweis.destinataer.pk)
|
return redirect("stiftung:destinataer_detail", pk=nachweis.destinataer.pk)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Phase 2: Destinatär-Timeline, Nachweis-Board, Zahlungs-Pipeline
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def destinataer_timeline(request, pk):
|
||||||
|
"""2a: Chronologische Timeline eines Destinatärs – alle Events in einer Ansicht."""
|
||||||
|
destinataer = get_object_or_404(Destinataer, pk=pk)
|
||||||
|
|
||||||
|
typ_filter = request.GET.get("typ", "")
|
||||||
|
|
||||||
|
events = []
|
||||||
|
|
||||||
|
if not typ_filter or typ_filter == "zahlung":
|
||||||
|
for u in destinataer.unterstuetzungen.select_related("konto", "ausgezahlt_von", "freigegeben_von").order_by("-faellig_am"):
|
||||||
|
events.append({
|
||||||
|
"datum": u.faellig_am,
|
||||||
|
"typ": "zahlung",
|
||||||
|
"icon": "fa-money-bill-wave",
|
||||||
|
"farbe": "success" if u.status == "ausgezahlt" else ("danger" if u.is_overdue() else "primary"),
|
||||||
|
"titel": f"Zahlung €{u.betrag}",
|
||||||
|
"beschreibung": u.beschreibung or u.get_status_display(),
|
||||||
|
"status": u.get_status_display(),
|
||||||
|
"objekt": u,
|
||||||
|
})
|
||||||
|
|
||||||
|
if not typ_filter or typ_filter == "nachweis":
|
||||||
|
for n in destinataer.quartalseinreichungen.order_by("-jahr", "-quartal"):
|
||||||
|
datum = n.zahlung_faelligkeitsdatum or n.faelligkeitsdatum
|
||||||
|
if datum:
|
||||||
|
events.append({
|
||||||
|
"datum": datum,
|
||||||
|
"typ": "nachweis",
|
||||||
|
"icon": "fa-file-alt",
|
||||||
|
"farbe": "success" if n.status in ("geprueft", "auto_geprueft") else ("danger" if n.is_overdue() else "warning"),
|
||||||
|
"titel": f"Nachweis {n.jahr} Q{n.quartal}",
|
||||||
|
"beschreibung": n.get_status_display(),
|
||||||
|
"status": n.get_status_display(),
|
||||||
|
"objekt": n,
|
||||||
|
})
|
||||||
|
|
||||||
|
if not typ_filter or typ_filter == "email":
|
||||||
|
for e in destinataer.email_eingaenge.order_by("-eingangsdatum"):
|
||||||
|
events.append({
|
||||||
|
"datum": e.eingangsdatum.date() if hasattr(e.eingangsdatum, "date") else e.eingangsdatum,
|
||||||
|
"typ": "email",
|
||||||
|
"icon": "fa-envelope",
|
||||||
|
"farbe": "info",
|
||||||
|
"titel": e.betreff or "(kein Betreff)",
|
||||||
|
"beschreibung": e.absender_email,
|
||||||
|
"status": e.get_status_display(),
|
||||||
|
"objekt": e,
|
||||||
|
})
|
||||||
|
|
||||||
|
if not typ_filter or typ_filter == "notiz":
|
||||||
|
for n in destinataer.notizen_eintraege.order_by("-erstellt_am"):
|
||||||
|
events.append({
|
||||||
|
"datum": n.erstellt_am.date() if hasattr(n.erstellt_am, "date") else n.erstellt_am,
|
||||||
|
"typ": "notiz",
|
||||||
|
"icon": "fa-sticky-note",
|
||||||
|
"farbe": "secondary",
|
||||||
|
"titel": n.titel or "Notiz",
|
||||||
|
"beschreibung": (n.text[:100] + "…") if n.text and len(n.text) > 100 else n.text,
|
||||||
|
"status": f"von {n.erstellt_von.get_full_name() or n.erstellt_von.username}" if n.erstellt_von else "",
|
||||||
|
"objekt": n,
|
||||||
|
})
|
||||||
|
|
||||||
|
events.sort(key=lambda e: e["datum"] if e["datum"] else date.min, reverse=True)
|
||||||
|
|
||||||
|
context = {
|
||||||
|
"destinataer": destinataer,
|
||||||
|
"events": events,
|
||||||
|
"typ_filter": typ_filter,
|
||||||
|
}
|
||||||
|
return render(request, "stiftung/destinataer_timeline.html", context)
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def nachweis_board(request):
|
||||||
|
"""2b: Nachweis-Board – Quartals-Übersicht aller Destinatäre."""
|
||||||
|
heute = date.today()
|
||||||
|
jahr_filter = int(request.GET.get("jahr", heute.year))
|
||||||
|
status_filter = request.GET.get("status", "")
|
||||||
|
|
||||||
|
destinataere = Destinataer.objects.filter(aktiv=True).order_by("nachname", "vorname")
|
||||||
|
|
||||||
|
board = []
|
||||||
|
for d in destinataere:
|
||||||
|
quartale = {}
|
||||||
|
for q in range(1, 5):
|
||||||
|
nachweis = VierteljahresNachweis.objects.filter(
|
||||||
|
destinataer=d, jahr=jahr_filter, quartal=q
|
||||||
|
).first()
|
||||||
|
quartale[q] = nachweis
|
||||||
|
board.append({"destinataer": d, "quartale": quartale})
|
||||||
|
|
||||||
|
if status_filter:
|
||||||
|
board = [
|
||||||
|
row for row in board
|
||||||
|
if any(
|
||||||
|
(q is not None and q.status == status_filter)
|
||||||
|
for q in row["quartale"].values()
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
overdue_count = VierteljahresNachweis.objects.filter(
|
||||||
|
jahr=jahr_filter,
|
||||||
|
status__in=["offen", "teilweise"],
|
||||||
|
studiennachweis_faelligkeitsdatum__lt=heute,
|
||||||
|
).count()
|
||||||
|
|
||||||
|
verfuegbare_jahre = list(range(heute.year - 2, heute.year + 2))
|
||||||
|
|
||||||
|
context = {
|
||||||
|
"board": board,
|
||||||
|
"jahr_filter": jahr_filter,
|
||||||
|
"status_filter": status_filter,
|
||||||
|
"overdue_count": overdue_count,
|
||||||
|
"verfuegbare_jahre": verfuegbare_jahre,
|
||||||
|
"status_choices": VierteljahresNachweis.STATUS_CHOICES,
|
||||||
|
"heute": heute,
|
||||||
|
}
|
||||||
|
return render(request, "stiftung/nachweis_board.html", context)
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def batch_erinnerung_senden(request):
|
||||||
|
"""2b: Batch-Erinnerungen an säumige Destinatäre – Audit-Log-Einträge."""
|
||||||
|
if request.method != "POST":
|
||||||
|
return redirect("stiftung:nachweis_board")
|
||||||
|
|
||||||
|
heute = date.today()
|
||||||
|
jahr = int(request.POST.get("jahr", heute.year))
|
||||||
|
|
||||||
|
overdue = VierteljahresNachweis.objects.filter(
|
||||||
|
jahr=jahr,
|
||||||
|
status__in=["offen", "teilweise"],
|
||||||
|
studiennachweis_faelligkeitsdatum__lt=heute,
|
||||||
|
destinataer__aktiv=True,
|
||||||
|
).select_related("destinataer")
|
||||||
|
|
||||||
|
count = 0
|
||||||
|
for nachweis in overdue:
|
||||||
|
try:
|
||||||
|
AuditLog.objects.create(
|
||||||
|
user=request.user,
|
||||||
|
action=f"ERINNERUNG: Nachweis {nachweis.jahr} Q{nachweis.quartal} – {nachweis.destinataer.get_full_name()} ({nachweis.destinataer.email})",
|
||||||
|
model_name="VierteljahresNachweis",
|
||||||
|
object_id=str(nachweis.id),
|
||||||
|
)
|
||||||
|
count += 1
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
messages.success(
|
||||||
|
request,
|
||||||
|
f"{count} Erinnerung(en) im Audit-Log vermerkt.",
|
||||||
|
)
|
||||||
|
return redirect("stiftung:nachweis_board")
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def zahlungs_pipeline(request):
|
||||||
|
"""2c: Zahlungs-Pipeline – 5-Stufen-Kanban-Ansicht."""
|
||||||
|
heute = date.today()
|
||||||
|
destinataer_id = request.GET.get("destinataer", "")
|
||||||
|
konto_id = request.GET.get("konto", "")
|
||||||
|
|
||||||
|
qs = DestinataerUnterstuetzung.objects.select_related(
|
||||||
|
"destinataer", "konto", "ausgezahlt_von", "freigegeben_von", "erstellt_von"
|
||||||
|
).exclude(status="storniert")
|
||||||
|
|
||||||
|
if destinataer_id:
|
||||||
|
qs = qs.filter(destinataer_id=destinataer_id)
|
||||||
|
if konto_id:
|
||||||
|
qs = qs.filter(konto_id=konto_id)
|
||||||
|
|
||||||
|
pipeline = {
|
||||||
|
"offen": qs.filter(status__in=["geplant", "faellig"]).order_by("faellig_am"),
|
||||||
|
"nachweis_eingereicht": qs.filter(status="nachweis_eingereicht").order_by("faellig_am"),
|
||||||
|
"freigegeben": qs.filter(status__in=["freigegeben", "in_bearbeitung"]).order_by("faellig_am"),
|
||||||
|
"ueberwiesen": qs.filter(status="ausgezahlt").order_by("-ausgezahlt_am"),
|
||||||
|
"abgeschlossen": qs.filter(status="abgeschlossen").order_by("-ausgezahlt_am"),
|
||||||
|
}
|
||||||
|
|
||||||
|
stage_meta = {
|
||||||
|
"offen": ("Offen", "secondary", "fa-clock"),
|
||||||
|
"nachweis_eingereicht": ("Nachweis eingereicht", "info", "fa-file-alt"),
|
||||||
|
"freigegeben": ("Freigegeben (4-Augen)", "warning", "fa-shield-alt"),
|
||||||
|
"ueberwiesen": ("Überwiesen", "success", "fa-university"),
|
||||||
|
"abgeschlossen": ("Abgeschlossen", "dark", "fa-check-double"),
|
||||||
|
}
|
||||||
|
|
||||||
|
pipeline_stages = [
|
||||||
|
{
|
||||||
|
"key": key,
|
||||||
|
"label": stage_meta[key][0],
|
||||||
|
"farbe": stage_meta[key][1],
|
||||||
|
"icon": stage_meta[key][2],
|
||||||
|
"zahlungen": list(pipeline[key]),
|
||||||
|
"gesamt": pipeline[key].aggregate(s=Sum("betrag"))["s"] or Decimal("0"),
|
||||||
|
}
|
||||||
|
for key in ["offen", "nachweis_eingereicht", "freigegeben", "ueberwiesen", "abgeschlossen"]
|
||||||
|
]
|
||||||
|
|
||||||
|
destinataere = Destinataer.objects.filter(aktiv=True).order_by("nachname", "vorname")
|
||||||
|
konten = StiftungsKonto.objects.filter(aktiv=True).order_by("kontoname")
|
||||||
|
|
||||||
|
context = {
|
||||||
|
"pipeline_stages": pipeline_stages,
|
||||||
|
"destinataere": destinataere,
|
||||||
|
"konten": konten,
|
||||||
|
"destinataer_filter": destinataer_id,
|
||||||
|
"konto_filter": konto_id,
|
||||||
|
"heute": heute,
|
||||||
|
}
|
||||||
|
return render(request, "stiftung/zahlungs_pipeline.html", context)
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def unterstuetzung_freigeben(request, pk):
|
||||||
|
"""2c: 4-Augen-Prinzip – Freigabe durch zweiten Nutzer."""
|
||||||
|
unterstuetzung = get_object_or_404(DestinataerUnterstuetzung, pk=pk)
|
||||||
|
|
||||||
|
if request.method == "POST":
|
||||||
|
if not unterstuetzung.can_be_freigegeben(request.user):
|
||||||
|
messages.error(
|
||||||
|
request,
|
||||||
|
"Freigabe nicht möglich: Status nicht korrekt oder Sie sind der Ersteller (Vier-Augen-Prinzip).",
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
unterstuetzung.status = "freigegeben"
|
||||||
|
unterstuetzung.freigegeben_von = request.user
|
||||||
|
unterstuetzung.freigegeben_am = date.today()
|
||||||
|
unterstuetzung.save()
|
||||||
|
messages.success(
|
||||||
|
request,
|
||||||
|
f"Zahlung €{unterstuetzung.betrag} für {unterstuetzung.destinataer.get_full_name()} freigegeben.",
|
||||||
|
)
|
||||||
|
|
||||||
|
next_url = request.POST.get("next") or request.META.get("HTTP_REFERER") or reverse("stiftung:zahlungs_pipeline")
|
||||||
|
return redirect(next_url)
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def unterstuetzung_nachweis_eingereicht(request, pk):
|
||||||
|
"""2c: Status auf 'Nachweis eingereicht' setzen."""
|
||||||
|
unterstuetzung = get_object_or_404(DestinataerUnterstuetzung, pk=pk)
|
||||||
|
if request.method == "POST":
|
||||||
|
if unterstuetzung.status in ["geplant", "faellig"]:
|
||||||
|
unterstuetzung.status = "nachweis_eingereicht"
|
||||||
|
unterstuetzung.save()
|
||||||
|
messages.success(request, "Status auf 'Nachweis eingereicht' gesetzt.")
|
||||||
|
else:
|
||||||
|
messages.error(request, "Status-Übergang nicht möglich.")
|
||||||
|
next_url = request.POST.get("next") or reverse("stiftung:zahlungs_pipeline")
|
||||||
|
return redirect(next_url)
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def unterstuetzung_abschliessen(request, pk):
|
||||||
|
"""2c: Abschließen einer überwiesenen Zahlung."""
|
||||||
|
unterstuetzung = get_object_or_404(DestinataerUnterstuetzung, pk=pk)
|
||||||
|
if request.method == "POST":
|
||||||
|
if unterstuetzung.status == "ausgezahlt":
|
||||||
|
unterstuetzung.status = "abgeschlossen"
|
||||||
|
unterstuetzung.save()
|
||||||
|
messages.success(request, "Zahlung als abgeschlossen markiert.")
|
||||||
|
else:
|
||||||
|
messages.error(request, "Nur überwiesene Zahlungen können abgeschlossen werden.")
|
||||||
|
next_url = request.POST.get("next") or reverse("stiftung:zahlungs_pipeline")
|
||||||
|
return redirect(next_url)
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def sepa_xml_export(request):
|
||||||
|
"""2c: SEPA pain.001 XML-Export für freigegebene Zahlungen."""
|
||||||
|
from xml.etree.ElementTree import Element, SubElement, tostring
|
||||||
|
import xml.dom.minidom
|
||||||
|
|
||||||
|
zahlungen = DestinataerUnterstuetzung.objects.filter(
|
||||||
|
status="freigegeben"
|
||||||
|
).select_related("destinataer", "konto")
|
||||||
|
|
||||||
|
if not zahlungen.exists():
|
||||||
|
messages.warning(request, "Keine freigegebenen Zahlungen für SEPA-Export vorhanden.")
|
||||||
|
return redirect("stiftung:zahlungs_pipeline")
|
||||||
|
|
||||||
|
heute = date.today()
|
||||||
|
msg_id = f"STIFTUNG-{heute.strftime('%Y%m%d%H%M%S')}"
|
||||||
|
nb_of_txs = zahlungen.count()
|
||||||
|
ctrl_sum = str(sum(z.betrag for z in zahlungen))
|
||||||
|
|
||||||
|
root = Element("Document", {
|
||||||
|
"xmlns": "urn:iso:std:iso:20022:tech:xsd:pain.001.001.03",
|
||||||
|
"xmlns:xsi": "http://www.w3.org/2001/XMLSchema-instance",
|
||||||
|
})
|
||||||
|
cstmr_cdt = SubElement(root, "CstmrCdtTrfInitn")
|
||||||
|
grp_hdr = SubElement(cstmr_cdt, "GrpHdr")
|
||||||
|
SubElement(grp_hdr, "MsgId").text = msg_id
|
||||||
|
SubElement(grp_hdr, "CreDtTm").text = timezone.now().strftime("%Y-%m-%dT%H:%M:%S")
|
||||||
|
SubElement(grp_hdr, "NbOfTxs").text = str(nb_of_txs)
|
||||||
|
SubElement(grp_hdr, "CtrlSum").text = ctrl_sum
|
||||||
|
initg_pty = SubElement(grp_hdr, "InitgPty")
|
||||||
|
SubElement(initg_pty, "Nm").text = "van Hees-Theyssen-Vogel'sche Stiftung"
|
||||||
|
|
||||||
|
pmt_inf = SubElement(cstmr_cdt, "PmtInf")
|
||||||
|
SubElement(pmt_inf, "PmtInfId").text = f"PMT-{msg_id}"
|
||||||
|
SubElement(pmt_inf, "PmtMtd").text = "TRF"
|
||||||
|
SubElement(pmt_inf, "NbOfTxs").text = str(nb_of_txs)
|
||||||
|
SubElement(pmt_inf, "CtrlSum").text = ctrl_sum
|
||||||
|
pmt_tp_inf = SubElement(pmt_inf, "PmtTpInf")
|
||||||
|
svc_lvl = SubElement(pmt_tp_inf, "SvcLvl")
|
||||||
|
SubElement(svc_lvl, "Cd").text = "SEPA"
|
||||||
|
SubElement(pmt_inf, "ReqdExctnDt").text = heute.strftime("%Y-%m-%d")
|
||||||
|
dbtr = SubElement(pmt_inf, "Dbtr")
|
||||||
|
SubElement(dbtr, "Nm").text = "van Hees-Theyssen-Vogel'sche Stiftung"
|
||||||
|
|
||||||
|
for zahlung in zahlungen:
|
||||||
|
cdt_trf = SubElement(pmt_inf, "CdtTrfTxInf")
|
||||||
|
pmt_id_el = SubElement(cdt_trf, "PmtId")
|
||||||
|
SubElement(pmt_id_el, "EndToEndId").text = str(zahlung.id)[:35]
|
||||||
|
amt = SubElement(cdt_trf, "Amt")
|
||||||
|
instd_amt = SubElement(amt, "InstdAmt", {"Ccy": "EUR"})
|
||||||
|
instd_amt.text = str(zahlung.betrag)
|
||||||
|
cdtr = SubElement(cdt_trf, "Cdtr")
|
||||||
|
SubElement(cdtr, "Nm").text = (zahlung.empfaenger_name or zahlung.destinataer.get_full_name())[:70]
|
||||||
|
cdtr_acct = SubElement(cdt_trf, "CdtrAcct")
|
||||||
|
cdtr_id = SubElement(cdtr_acct, "Id")
|
||||||
|
SubElement(cdtr_id, "IBAN").text = (zahlung.empfaenger_iban or zahlung.destinataer.iban or "").replace(" ", "")
|
||||||
|
rmt_inf = SubElement(cdt_trf, "RmtInf")
|
||||||
|
SubElement(rmt_inf, "Ustrd").text = (zahlung.verwendungszweck or zahlung.beschreibung or "Stiftungsunterstützung")[:140]
|
||||||
|
|
||||||
|
xml_str = xml.dom.minidom.parseString(tostring(root, encoding="unicode")).toprettyxml(indent=" ")
|
||||||
|
|
||||||
|
response = HttpResponse(xml_str, content_type="application/xml; charset=utf-8")
|
||||||
|
response["Content-Disposition"] = f'attachment; filename="sepa_export_{heute.strftime("%Y%m%d")}.xml"'
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
# Two-Factor Authentication Views
|
# Two-Factor Authentication Views
|
||||||
|
|
||||||
|
|||||||
@@ -617,6 +617,10 @@
|
|||||||
<i class="fas fa-calculator"></i>
|
<i class="fas fa-calculator"></i>
|
||||||
<span>Abrechnungen</span>
|
<span>Abrechnungen</span>
|
||||||
</a>
|
</a>
|
||||||
|
<a class="sidebar-link" href="{% url 'stiftung:paechter_workflow' %}">
|
||||||
|
<i class="fas fa-tractor"></i>
|
||||||
|
<span>Pacht-Workflow</span>
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Finanzen -->
|
<!-- Finanzen -->
|
||||||
@@ -626,6 +630,14 @@
|
|||||||
<i class="fas fa-hand-holding-usd"></i>
|
<i class="fas fa-hand-holding-usd"></i>
|
||||||
<span>Unterstuetzungen</span>
|
<span>Unterstuetzungen</span>
|
||||||
</a>
|
</a>
|
||||||
|
<a class="sidebar-link" href="{% url 'stiftung:zahlungs_pipeline' %}">
|
||||||
|
<i class="fas fa-tasks"></i>
|
||||||
|
<span>Zahlungs-Pipeline</span>
|
||||||
|
</a>
|
||||||
|
<a class="sidebar-link" href="{% url 'stiftung:nachweis_board' %}">
|
||||||
|
<i class="fas fa-th"></i>
|
||||||
|
<span>Nachweis-Board</span>
|
||||||
|
</a>
|
||||||
<a class="sidebar-link" href="{% url 'stiftung:geschaeftsfuehrung' %}">
|
<a class="sidebar-link" href="{% url 'stiftung:geschaeftsfuehrung' %}">
|
||||||
<i class="fas fa-briefcase"></i>
|
<i class="fas fa-briefcase"></i>
|
||||||
<span>Geschaeftsfuehrung</span>
|
<span>Geschaeftsfuehrung</span>
|
||||||
|
|||||||
151
app/templates/stiftung/destinataer_timeline.html
Normal file
151
app/templates/stiftung/destinataer_timeline.html
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
{% block title %}Timeline – {{ destinataer.get_full_name }} – Stiftungsverwaltung{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
|
<h1 class="h3">
|
||||||
|
<i class="fas fa-stream text-primary me-2"></i>
|
||||||
|
Timeline: {{ destinataer.get_full_name }}
|
||||||
|
</h1>
|
||||||
|
<div>
|
||||||
|
<a href="{% url 'stiftung:destinataer_detail' pk=destinataer.pk %}" class="btn btn-outline-secondary">
|
||||||
|
<i class="fas fa-arrow-left me-2"></i>Zum Destinatär
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Filter Bar -->
|
||||||
|
<div class="card shadow mb-4">
|
||||||
|
<div class="card-body py-2">
|
||||||
|
<div class="d-flex gap-2 flex-wrap align-items-center">
|
||||||
|
<span class="text-muted small fw-bold me-2">Filter:</span>
|
||||||
|
<a href="?typ=" class="btn btn-sm {% if not typ_filter %}btn-primary{% else %}btn-outline-primary{% endif %}">
|
||||||
|
<i class="fas fa-list me-1"></i>Alle
|
||||||
|
</a>
|
||||||
|
<a href="?typ=zahlung" class="btn btn-sm {% if typ_filter == 'zahlung' %}btn-success{% else %}btn-outline-success{% endif %}">
|
||||||
|
<i class="fas fa-money-bill-wave me-1"></i>Zahlungen
|
||||||
|
</a>
|
||||||
|
<a href="?typ=nachweis" class="btn btn-sm {% if typ_filter == 'nachweis' %}btn-warning{% else %}btn-outline-warning{% endif %}">
|
||||||
|
<i class="fas fa-file-alt me-1"></i>Nachweise
|
||||||
|
</a>
|
||||||
|
<a href="?typ=email" class="btn btn-sm {% if typ_filter == 'email' %}btn-info{% else %}btn-outline-info{% endif %}">
|
||||||
|
<i class="fas fa-envelope me-1"></i>E-Mails
|
||||||
|
</a>
|
||||||
|
<a href="?typ=notiz" class="btn btn-sm {% if typ_filter == 'notiz' %}btn-secondary{% else %}btn-outline-secondary{% endif %}">
|
||||||
|
<i class="fas fa-sticky-note me-1"></i>Notizen
|
||||||
|
</a>
|
||||||
|
<span class="ms-auto text-muted small">{{ events|length }} Einträge</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Timeline -->
|
||||||
|
{% if events %}
|
||||||
|
<div class="timeline-wrapper">
|
||||||
|
{% for event in events %}
|
||||||
|
<div class="timeline-item mb-3">
|
||||||
|
<div class="card shadow-sm border-start border-4 border-{{ event.farbe }}">
|
||||||
|
<div class="card-body py-2 px-3">
|
||||||
|
<div class="d-flex align-items-start gap-3">
|
||||||
|
<!-- Icon -->
|
||||||
|
<div class="timeline-icon text-{{ event.farbe }} pt-1">
|
||||||
|
<i class="fas {{ event.icon }} fa-lg"></i>
|
||||||
|
</div>
|
||||||
|
<!-- Content -->
|
||||||
|
<div class="flex-grow-1">
|
||||||
|
<div class="d-flex justify-content-between align-items-start">
|
||||||
|
<div>
|
||||||
|
<span class="fw-semibold">{{ event.titel }}</span>
|
||||||
|
{% if event.beschreibung %}
|
||||||
|
<span class="text-muted ms-2 small">{{ event.beschreibung }}</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="text-end ms-3 text-nowrap">
|
||||||
|
<div class="small text-muted">{{ event.datum|date:"d.m.Y" }}</div>
|
||||||
|
{% if event.status %}
|
||||||
|
<span class="badge bg-{{ event.farbe }} text-white small">{{ event.status }}</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Extra info per type -->
|
||||||
|
{% if event.typ == 'zahlung' %}
|
||||||
|
<div class="small mt-1">
|
||||||
|
{% with u=event.objekt %}
|
||||||
|
Konto: {{ u.konto.kontoname }}
|
||||||
|
{% if u.empfaenger_iban %} · IBAN: {{ u.empfaenger_iban|slice:":4" }}…{% endif %}
|
||||||
|
{% if u.ausgezahlt_von %} · Ausgezahlt von: {{ u.ausgezahlt_von.get_full_name|default:u.ausgezahlt_von.username }}{% endif %}
|
||||||
|
{% if u.freigegeben_von %} · Freigegeben: {{ u.freigegeben_von.get_full_name|default:u.freigegeben_von.username }} ({{ u.freigegeben_am|date:"d.m.Y" }}){% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
</div>
|
||||||
|
{% elif event.typ == 'nachweis' %}
|
||||||
|
<div class="small mt-1">
|
||||||
|
{% with n=event.objekt %}
|
||||||
|
Fortschritt: {{ n.get_completion_percentage }}%
|
||||||
|
{% if n.geprueft_von %} · Geprüft von: {{ n.geprueft_von.get_full_name|default:n.geprueft_von.username }}{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
</div>
|
||||||
|
{% elif event.typ == 'email' %}
|
||||||
|
<div class="small mt-1">
|
||||||
|
{% with e=event.objekt %}
|
||||||
|
Von: {{ e.absender_name|default:e.absender_email }}
|
||||||
|
{% if e.quartalsnachweis %} · Zugeordnet: Q{{ e.quartalsnachweis.quartal }}/{{ e.quartalsnachweis.jahr }}{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="alert alert-info">
|
||||||
|
<i class="fas fa-info-circle me-2"></i>
|
||||||
|
Keine Timeline-Einträge vorhanden{% if typ_filter %} für den gewählten Filter{% endif %}.
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.timeline-wrapper {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.timeline-wrapper::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: 20px;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
width: 2px;
|
||||||
|
background: #dee2e6;
|
||||||
|
z-index: 0;
|
||||||
|
}
|
||||||
|
.timeline-item {
|
||||||
|
position: relative;
|
||||||
|
padding-left: 48px;
|
||||||
|
}
|
||||||
|
.timeline-icon {
|
||||||
|
position: absolute;
|
||||||
|
left: -28px;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: white;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 2px solid #dee2e6;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
170
app/templates/stiftung/nachweis_board.html
Normal file
170
app/templates/stiftung/nachweis_board.html
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
{% block title %}Nachweis-Board – Stiftungsverwaltung{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
|
<h1 class="h3">
|
||||||
|
<i class="fas fa-th text-primary me-2"></i>
|
||||||
|
Nachweis-Board {{ jahr_filter }}
|
||||||
|
</h1>
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
{% if overdue_count > 0 %}
|
||||||
|
<form method="post" action="{% url 'stiftung:batch_erinnerung_senden' %}">
|
||||||
|
{% csrf_token %}
|
||||||
|
<input type="hidden" name="jahr" value="{{ jahr_filter }}">
|
||||||
|
<button type="submit" class="btn btn-danger" onclick="return confirm('{{ overdue_count }} säumige Destinatäre markieren?')">
|
||||||
|
<i class="fas fa-bell me-2"></i>{{ overdue_count }} Erinnerung(en)
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Filter Bar -->
|
||||||
|
<div class="card shadow mb-4">
|
||||||
|
<div class="card-body py-2">
|
||||||
|
<form method="get" class="d-flex gap-3 flex-wrap align-items-center">
|
||||||
|
<div class="d-flex align-items-center gap-2">
|
||||||
|
<label class="text-muted small fw-bold mb-0">Jahr:</label>
|
||||||
|
<select name="jahr" class="form-select form-select-sm" style="width: auto;" onchange="this.form.submit()">
|
||||||
|
{% for j in verfuegbare_jahre %}
|
||||||
|
<option value="{{ j }}" {% if j == jahr_filter %}selected{% endif %}>{{ j }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex align-items-center gap-2">
|
||||||
|
<label class="text-muted small fw-bold mb-0">Status:</label>
|
||||||
|
<select name="status" class="form-select form-select-sm" style="width: auto;" onchange="this.form.submit()">
|
||||||
|
<option value="">Alle</option>
|
||||||
|
{% for code, label in status_choices %}
|
||||||
|
<option value="{{ code }}" {% if code == status_filter %}selected{% endif %}>{{ label }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<span class="ms-auto text-muted small">{{ board|length }} Destinatäre</span>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Semester-Hinweis -->
|
||||||
|
<div class="alert alert-info py-2 mb-4 small">
|
||||||
|
<i class="fas fa-info-circle me-2"></i>
|
||||||
|
<strong>Semester-Logik:</strong>
|
||||||
|
Studiennachweis-Frist Q1/Q2 → 15. März · Q3/Q4 → 15. September.
|
||||||
|
Zahlungsfrist Q1 → 15. Dez (Vorjahr) · Q2 → 15. Mär · Q3 → 15. Jun · Q4 → 15. Sep.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Board Table -->
|
||||||
|
{% if board %}
|
||||||
|
<div class="card shadow">
|
||||||
|
<div class="card-body p-0">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-hover table-sm mb-0">
|
||||||
|
<thead class="table-dark">
|
||||||
|
<tr>
|
||||||
|
<th style="min-width:180px">Destinatär</th>
|
||||||
|
<th class="text-center">Q1 (Jan–Mär)</th>
|
||||||
|
<th class="text-center">Q2 (Apr–Jun)</th>
|
||||||
|
<th class="text-center">Q3 (Jul–Sep)</th>
|
||||||
|
<th class="text-center">Q4 (Okt–Dez)</th>
|
||||||
|
<th class="text-center">Gesamt</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for row in board %}
|
||||||
|
<tr>
|
||||||
|
<td class="align-middle">
|
||||||
|
<a href="{% url 'stiftung:destinataer_detail' pk=row.destinataer.pk %}" class="text-decoration-none fw-semibold">
|
||||||
|
{{ row.destinataer.get_full_name }}
|
||||||
|
</a>
|
||||||
|
<div class="small text-muted">
|
||||||
|
<a href="{% url 'stiftung:destinataer_timeline' pk=row.destinataer.pk %}" class="text-muted">
|
||||||
|
<i class="fas fa-stream"></i> Timeline
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
{% for q in "1234" %}
|
||||||
|
{% with nachweis=row.quartale|get_item:q|default:None %}
|
||||||
|
<td class="text-center align-middle">
|
||||||
|
{% if nachweis %}
|
||||||
|
{% if nachweis.status == 'geprueft' or nachweis.status == 'auto_geprueft' %}
|
||||||
|
<span class="badge bg-success" title="{{ nachweis.get_status_display }}">
|
||||||
|
<i class="fas fa-check"></i>
|
||||||
|
</span>
|
||||||
|
{% elif nachweis.status == 'eingereicht' %}
|
||||||
|
<span class="badge bg-info" title="{{ nachweis.get_status_display }}">
|
||||||
|
<i class="fas fa-paper-plane"></i>
|
||||||
|
</span>
|
||||||
|
{% elif nachweis.status == 'teilweise' %}
|
||||||
|
<span class="badge bg-warning text-dark" title="{{ nachweis.get_status_display }}">
|
||||||
|
<i class="fas fa-circle-half-stroke"></i>
|
||||||
|
</span>
|
||||||
|
{% elif nachweis.status == 'nachbesserung' %}
|
||||||
|
<span class="badge bg-orange" title="{{ nachweis.get_status_display }}" style="background:#fd7e14">
|
||||||
|
<i class="fas fa-redo"></i>
|
||||||
|
</span>
|
||||||
|
{% elif nachweis.status == 'abgelehnt' %}
|
||||||
|
<span class="badge bg-danger" title="{{ nachweis.get_status_display }}">
|
||||||
|
<i class="fas fa-times"></i>
|
||||||
|
</span>
|
||||||
|
{% elif nachweis.is_overdue %}
|
||||||
|
<span class="badge bg-danger" title="Überfällig">
|
||||||
|
<i class="fas fa-exclamation-triangle"></i>
|
||||||
|
</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge bg-secondary" title="{{ nachweis.get_status_display }}">
|
||||||
|
<i class="fas fa-clock"></i>
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
<div class="small mt-1">
|
||||||
|
<a href="{% url 'stiftung:quarterly_confirmation_edit' pk=nachweis.pk %}" class="text-muted">
|
||||||
|
{{ nachweis.get_completion_percentage }}%
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<span class="text-muted small">–</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
{% endwith %}
|
||||||
|
{% endfor %}
|
||||||
|
<td class="text-center align-middle">
|
||||||
|
{% with total=0 done=0 %}
|
||||||
|
{% for q, nachweis in row.quartale.items %}
|
||||||
|
{% if nachweis %}
|
||||||
|
{% if nachweis.status == 'geprueft' or nachweis.status == 'auto_geprueft' %}
|
||||||
|
<i class="fas fa-check-circle text-success"></i>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
{% endwith %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Legend -->
|
||||||
|
<div class="mt-3 d-flex flex-wrap gap-3 small text-muted">
|
||||||
|
<span><span class="badge bg-success"><i class="fas fa-check"></i></span> Geprüft</span>
|
||||||
|
<span><span class="badge bg-info"><i class="fas fa-paper-plane"></i></span> Eingereicht</span>
|
||||||
|
<span><span class="badge bg-warning text-dark"><i class="fas fa-circle-half-stroke"></i></span> Teilweise</span>
|
||||||
|
<span><span class="badge bg-danger"><i class="fas fa-exclamation-triangle"></i></span> Überfällig</span>
|
||||||
|
<span><span class="badge bg-secondary"><i class="fas fa-clock"></i></span> Offen</span>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="alert alert-info">
|
||||||
|
<i class="fas fa-info-circle me-2"></i>
|
||||||
|
Keine aktiven Destinatäre gefunden.
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
227
app/templates/stiftung/paechter_workflow.html
Normal file
227
app/templates/stiftung/paechter_workflow.html
Normal file
@@ -0,0 +1,227 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
{% block title %}Pächter-Workflow – Stiftungsverwaltung{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
|
<h1 class="h3">
|
||||||
|
<i class="fas fa-tractor text-primary me-2"></i>
|
||||||
|
Pächter-Workflow
|
||||||
|
</h1>
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
<a href="{% url 'stiftung:land_abrechnung_create' %}" class="btn btn-outline-primary">
|
||||||
|
<i class="fas fa-plus me-2"></i>Neue Abrechnung
|
||||||
|
</a>
|
||||||
|
<a href="{% url 'stiftung:paechter_list' %}" class="btn btn-outline-secondary">
|
||||||
|
<i class="fas fa-list me-2"></i>Alle Pächter
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Pipeline: Vertragsfristen -->
|
||||||
|
<div class="card shadow mb-4">
|
||||||
|
<div class="card-header bg-dark text-white py-2">
|
||||||
|
<h5 class="mb-0 small fw-bold">
|
||||||
|
<i class="fas fa-calendar-alt me-2"></i>Pipeline: Vertragsfristen
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body p-0">
|
||||||
|
<div class="row g-0">
|
||||||
|
{% for stage in pipeline_stages %}
|
||||||
|
<div class="col-xl col-lg-4 col-md-6 border-end">
|
||||||
|
<div class="p-3">
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||||
|
<h6 class="mb-0 text-{{ stage.farbe }} fw-bold small">
|
||||||
|
<i class="fas {{ stage.icon }} me-1"></i>{{ stage.label }}
|
||||||
|
</h6>
|
||||||
|
<span class="badge bg-{{ stage.farbe }} {% if stage.farbe == 'warning' %}text-dark{% else %}text-white{% endif %}">
|
||||||
|
{{ stage.count }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{% if stage.verpachtungen %}
|
||||||
|
{% for v in stage.verpachtungen %}
|
||||||
|
<div class="card mb-2 border-start border-3 border-{{ stage.farbe }}" style="font-size:0.78rem;">
|
||||||
|
<div class="card-body py-2 px-2">
|
||||||
|
<div class="fw-semibold">
|
||||||
|
<a href="{% url 'stiftung:land_verpachtung_detail' pk=v.pk %}" class="text-decoration-none text-dark">
|
||||||
|
{{ v.land.lfd_nr }} – {{ v.paechter.get_full_name }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% if v.pachtende %}
|
||||||
|
<div class="text-muted mt-1">
|
||||||
|
Ende: {{ v.pachtende|date:"d.m.Y" }}
|
||||||
|
{% if v.pachtende >= heute %}
|
||||||
|
<span class="ms-1">({{ v.pachtende|timeuntil:heute }})</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge bg-danger ms-1">abgelaufen</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<div class="text-muted">
|
||||||
|
{% if v.verpachtete_flaeche %}{{ v.verpachtete_flaeche|floatformat:0 }} m²{% endif %}
|
||||||
|
{% if v.pachtzins_pauschal %} · €{{ v.pachtzins_pauschal|floatformat:2 }}/J{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% else %}
|
||||||
|
<div class="text-muted text-center small py-3">
|
||||||
|
<i class="fas fa-check-circle fa-lg d-block mb-1 opacity-25"></i>
|
||||||
|
Keine
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row g-4">
|
||||||
|
<!-- Ausstehende Jahresabrechnungen -->
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<div class="card shadow h-100">
|
||||||
|
<div class="card-header bg-warning text-dark py-2">
|
||||||
|
<h5 class="mb-0 small fw-bold">
|
||||||
|
<i class="fas fa-file-invoice me-2"></i>
|
||||||
|
Ausstehende Jahresabrechnungen {{ letztes_jahr }}
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body p-0">
|
||||||
|
{% if laender_ohne_abrechnung %}
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-sm table-hover mb-0">
|
||||||
|
<thead class="table-light">
|
||||||
|
<tr>
|
||||||
|
<th>Lfd. Nr.</th>
|
||||||
|
<th>Gemeinde</th>
|
||||||
|
<th>Pächter</th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for land in laender_ohne_abrechnung %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ land.lfd_nr }}</td>
|
||||||
|
<td>{{ land.gemeinde|default:"–" }}</td>
|
||||||
|
<td>{{ land.paechter_name|default:"–" }}</td>
|
||||||
|
<td>
|
||||||
|
<a href="{% url 'stiftung:land_abrechnung_create' %}?land={{ land.pk }}&jahr={{ letztes_jahr }}" class="btn btn-xs btn-outline-primary" style="font-size:0.7rem;padding:2px 6px;">
|
||||||
|
<i class="fas fa-plus"></i>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="text-muted text-center py-4">
|
||||||
|
<i class="fas fa-check-circle fa-2x mb-2 d-block text-success opacity-50"></i>
|
||||||
|
Alle Jahresabrechnungen {{ letztes_jahr }} vorhanden.
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Pachtanpassungen fällig (> 5 Jahre) -->
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<div class="card shadow h-100">
|
||||||
|
<div class="card-header bg-info text-white py-2">
|
||||||
|
<h5 class="mb-0 small fw-bold">
|
||||||
|
<i class="fas fa-chart-line me-2"></i>
|
||||||
|
Pachtanpassung fällig (> 5 Jahre)
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body p-0">
|
||||||
|
{% if lang_laufend %}
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-sm table-hover mb-0">
|
||||||
|
<thead class="table-light">
|
||||||
|
<tr>
|
||||||
|
<th>Land</th>
|
||||||
|
<th>Pächter</th>
|
||||||
|
<th>Beginn</th>
|
||||||
|
<th>Dauer</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for v in lang_laufend %}
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<a href="{% url 'stiftung:land_verpachtung_detail' pk=v.pk %}">
|
||||||
|
{{ v.land.lfd_nr }}
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td>{{ v.paechter.get_full_name }}</td>
|
||||||
|
<td>{{ v.pachtbeginn|date:"d.m.Y" }}</td>
|
||||||
|
<td>
|
||||||
|
<span class="badge bg-warning text-dark">
|
||||||
|
{{ v.pachtbeginn|timesince:heute }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="text-muted text-center py-4">
|
||||||
|
<i class="fas fa-check-circle fa-2x mb-2 d-block text-success opacity-50"></i>
|
||||||
|
Keine Pachtanpassungen fällig.
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Top-Pächter -->
|
||||||
|
<div class="card shadow mt-4">
|
||||||
|
<div class="card-header bg-primary text-white py-2">
|
||||||
|
<h5 class="mb-0 small fw-bold">
|
||||||
|
<i class="fas fa-crown me-2"></i>Top-Pächter nach Fläche
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body p-0">
|
||||||
|
{% if top_paechter %}
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-sm table-hover mb-0">
|
||||||
|
<thead class="table-light">
|
||||||
|
<tr>
|
||||||
|
<th>#</th>
|
||||||
|
<th>Pächter</th>
|
||||||
|
<th>Verträge</th>
|
||||||
|
<th>Gesamtfläche (m²)</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for p in top_paechter %}
|
||||||
|
<tr>
|
||||||
|
<td class="text-muted">{{ forloop.counter }}</td>
|
||||||
|
<td>
|
||||||
|
<a href="{% url 'stiftung:paechter_detail' pk=p.pk %}">
|
||||||
|
{{ p.get_full_name }}
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td>{{ p.anzahl_vertraege }}</td>
|
||||||
|
<td>{{ p.flaeche|floatformat:0|default:"–" }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="text-muted text-center py-4">Keine Pächter gefunden.</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
162
app/templates/stiftung/zahlungs_pipeline.html
Normal file
162
app/templates/stiftung/zahlungs_pipeline.html
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
{% block title %}Zahlungs-Pipeline – Stiftungsverwaltung{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
|
<h1 class="h3">
|
||||||
|
<i class="fas fa-tasks text-primary me-2"></i>
|
||||||
|
Zahlungs-Pipeline
|
||||||
|
</h1>
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
<a href="{% url 'stiftung:sepa_xml_export' %}" class="btn btn-outline-success">
|
||||||
|
<i class="fas fa-file-code me-2"></i>SEPA XML exportieren
|
||||||
|
</a>
|
||||||
|
<a href="{% url 'stiftung:unterstuetzung_create' %}" class="btn btn-primary">
|
||||||
|
<i class="fas fa-plus me-2"></i>Neue Zahlung
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Filter Bar -->
|
||||||
|
<div class="card shadow mb-4">
|
||||||
|
<div class="card-body py-2">
|
||||||
|
<form method="get" class="d-flex gap-3 flex-wrap align-items-center">
|
||||||
|
<div class="d-flex align-items-center gap-2">
|
||||||
|
<label class="text-muted small fw-bold mb-0">Destinatär:</label>
|
||||||
|
<select name="destinataer" class="form-select form-select-sm" style="width:200px" onchange="this.form.submit()">
|
||||||
|
<option value="">Alle</option>
|
||||||
|
{% for d in destinataere %}
|
||||||
|
<option value="{{ d.pk }}" {% if d.pk|stringformat:'s' == destinataer_filter %}selected{% endif %}>{{ d.get_full_name }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex align-items-center gap-2">
|
||||||
|
<label class="text-muted small fw-bold mb-0">Konto:</label>
|
||||||
|
<select name="konto" class="form-select form-select-sm" style="width:180px" onchange="this.form.submit()">
|
||||||
|
<option value="">Alle</option>
|
||||||
|
{% for k in konten %}
|
||||||
|
<option value="{{ k.pk }}" {% if k.pk|stringformat:'s' == konto_filter %}selected{% endif %}>{{ k.kontoname }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
{% if destinataer_filter or konto_filter %}
|
||||||
|
<a href="{% url 'stiftung:zahlungs_pipeline' %}" class="btn btn-sm btn-outline-secondary">
|
||||||
|
<i class="fas fa-times"></i> Filter zurücksetzen
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 4-Augen Hinweis -->
|
||||||
|
<div class="alert alert-warning py-2 mb-4 small">
|
||||||
|
<i class="fas fa-shield-alt me-2"></i>
|
||||||
|
<strong>Vier-Augen-Prinzip:</strong>
|
||||||
|
Zahlungen können nur von einem <em>anderen</em> Nutzer als dem Ersteller freigegeben werden.
|
||||||
|
SEPA-Export ist nur für Zahlungen im Status „Freigegeben" verfügbar.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Pipeline Kanban -->
|
||||||
|
<div class="row g-3">
|
||||||
|
{% for stage in pipeline_stages %}
|
||||||
|
<div class="col-xl col-lg-4 col-md-6">
|
||||||
|
<div class="card shadow h-100">
|
||||||
|
<div class="card-header bg-{{ stage.farbe }} {% if stage.farbe == 'secondary' or stage.farbe == 'dark' %}text-white{% elif stage.farbe == 'warning' %}text-dark{% else %}text-white{% endif %} py-2">
|
||||||
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
|
<span class="fw-semibold small">
|
||||||
|
<i class="fas {{ stage.icon }} me-1"></i>{{ stage.label }}
|
||||||
|
</span>
|
||||||
|
<span class="badge bg-white text-dark">{{ stage.zahlungen|length }}</span>
|
||||||
|
</div>
|
||||||
|
{% if stage.gesamt > 0 %}
|
||||||
|
<div class="small opacity-75 mt-1">€{{ stage.gesamt|floatformat:2 }}</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="card-body p-2" style="max-height: 600px; overflow-y: auto;">
|
||||||
|
{% if stage.zahlungen %}
|
||||||
|
{% for z in stage.zahlungen %}
|
||||||
|
<div class="card mb-2 border shadow-sm" style="font-size:0.8rem;">
|
||||||
|
<div class="card-body py-2 px-2">
|
||||||
|
<div class="d-flex justify-content-between align-items-start">
|
||||||
|
<div class="fw-semibold">
|
||||||
|
<a href="{% url 'stiftung:destinataer_detail' pk=z.destinataer.pk %}" class="text-decoration-none text-dark">
|
||||||
|
{{ z.destinataer.get_full_name }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<span class="badge bg-{{ stage.farbe }} {% if stage.farbe == 'warning' %}text-dark{% else %}text-white{% endif %}">
|
||||||
|
€{{ z.betrag|floatformat:2 }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="text-muted mt-1">
|
||||||
|
Fällig: {{ z.faellig_am|date:"d.m.Y" }}
|
||||||
|
{% if z.is_overdue %}
|
||||||
|
<span class="badge bg-danger ms-1">überfällig</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% if z.beschreibung %}
|
||||||
|
<div class="text-muted small mt-1">{{ z.beschreibung }}</div>
|
||||||
|
{% endif %}
|
||||||
|
{% if z.freigegeben_von %}
|
||||||
|
<div class="small text-success mt-1">
|
||||||
|
<i class="fas fa-shield-alt"></i>
|
||||||
|
{{ z.freigegeben_von.get_full_name|default:z.freigegeben_von.username }}
|
||||||
|
({{ z.freigegeben_am|date:"d.m.Y" }})
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<!-- Actions -->
|
||||||
|
<div class="mt-2 d-flex gap-1 flex-wrap">
|
||||||
|
{% if stage.key == 'offen' %}
|
||||||
|
<form method="post" action="{% url 'stiftung:unterstuetzung_nachweis_eingereicht' pk=z.pk %}">
|
||||||
|
{% csrf_token %}
|
||||||
|
<input type="hidden" name="next" value="{% url 'stiftung:zahlungs_pipeline' %}">
|
||||||
|
<button type="submit" class="btn btn-xs btn-outline-info" style="font-size:0.7rem;padding:2px 6px;" title="Nachweis eingereicht">
|
||||||
|
<i class="fas fa-file-alt"></i>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
{% elif stage.key == 'nachweis_eingereicht' %}
|
||||||
|
<form method="post" action="{% url 'stiftung:unterstuetzung_freigeben' pk=z.pk %}">
|
||||||
|
{% csrf_token %}
|
||||||
|
<input type="hidden" name="next" value="{% url 'stiftung:zahlungs_pipeline' %}">
|
||||||
|
<button type="submit" class="btn btn-xs btn-outline-warning" style="font-size:0.7rem;padding:2px 6px;" title="4-Augen-Freigabe">
|
||||||
|
<i class="fas fa-shield-alt"></i>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
{% elif stage.key == 'freigegeben' %}
|
||||||
|
<a href="{% url 'stiftung:unterstuetzung_mark_paid' pk=z.pk %}" class="btn btn-xs btn-outline-success" style="font-size:0.7rem;padding:2px 6px;" title="Als überwiesen markieren">
|
||||||
|
<i class="fas fa-university"></i>
|
||||||
|
</a>
|
||||||
|
{% elif stage.key == 'ueberwiesen' %}
|
||||||
|
<form method="post" action="{% url 'stiftung:unterstuetzung_abschliessen' pk=z.pk %}">
|
||||||
|
{% csrf_token %}
|
||||||
|
<input type="hidden" name="next" value="{% url 'stiftung:zahlungs_pipeline' %}">
|
||||||
|
<button type="submit" class="btn btn-xs btn-outline-dark" style="font-size:0.7rem;padding:2px 6px;" title="Abschließen">
|
||||||
|
<i class="fas fa-check-double"></i>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
{% endif %}
|
||||||
|
<a href="{% url 'stiftung:unterstuetzung_detail' pk=z.pk %}" class="btn btn-xs btn-outline-secondary" style="font-size:0.7rem;padding:2px 6px;" title="Details">
|
||||||
|
<i class="fas fa-eye"></i>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% else %}
|
||||||
|
<div class="text-muted text-center small py-4">
|
||||||
|
<i class="fas fa-inbox fa-2x mb-2 d-block opacity-25"></i>
|
||||||
|
Keine Zahlungen
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
Reference in New Issue
Block a user