Add Vorlagen editor, upload portal, onboarding, and participant import command
- Dokument-Vorlagen-Editor: create/edit/reset document templates (admin) - Upload-Portal: public portal for Nachweis uploads via token - Onboarding: invite Destinatäre via email with multi-step wizard - Bestätigungsschreiben: preview and send confirmation letters - Email settings: SMTP configuration UI - Management command: import_veranstaltung_teilnehmer for bulk participant import Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -31,6 +31,7 @@ from django_otp.util import random_hex
|
||||
from rest_framework.decorators import api_view
|
||||
from rest_framework.response import Response
|
||||
|
||||
from stiftung.audit import log_action
|
||||
from stiftung.models import (AppConfiguration, AuditLog, BackupJob, BankTransaction,
|
||||
BriefVorlage, CSVImport, Destinataer,
|
||||
DestinataerEmailEingang, DestinataerNotiz,
|
||||
@@ -476,11 +477,13 @@ def destinataer_toggle_archiv(request, pk):
|
||||
destinataer.aktiv = not destinataer.aktiv
|
||||
destinataer.save(update_fields=["aktiv"])
|
||||
status_text = "aktiviert" if destinataer.aktiv else "archiviert (inaktiv gesetzt)"
|
||||
AuditLog.objects.create(
|
||||
user=request.user,
|
||||
action=f"Destinatär {destinataer.get_full_name()} wurde {status_text}.",
|
||||
model_name="Destinataer",
|
||||
object_id=str(destinataer.pk),
|
||||
log_action(
|
||||
request,
|
||||
action="update",
|
||||
entity_type="destinataer",
|
||||
entity_id=str(destinataer.pk),
|
||||
entity_name=destinataer.get_full_name(),
|
||||
description=f"Destinatär {destinataer.get_full_name()} wurde {status_text}.",
|
||||
)
|
||||
messages.success(request, f'Destinatär "{destinataer.get_full_name()}" wurde {status_text}.')
|
||||
return redirect("stiftung:destinataer_detail", pk=destinataer.pk)
|
||||
@@ -760,3 +763,101 @@ def destinataer_export(request, pk):
|
||||
pass
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Bestätigungsschreiben
|
||||
# =============================================================================
|
||||
|
||||
@login_required
|
||||
def bestaetigung_vorschau(request, pk):
|
||||
"""
|
||||
PDF-Vorschau eines Bestätigungsschreibens für einen Destinatär im Browser.
|
||||
Generiert das PDF on-the-fly via WeasyPrint.
|
||||
"""
|
||||
from decimal import Decimal
|
||||
from django.template.loader import render_to_string
|
||||
|
||||
destinataer = get_object_or_404(Destinataer, id=pk)
|
||||
|
||||
unterstuetzungen = DestinataerUnterstuetzung.objects.filter(
|
||||
destinataer=destinataer,
|
||||
status__in=["ausgezahlt", "abgeschlossen"],
|
||||
).order_by("faellig_am")
|
||||
|
||||
gesamtbetrag = unterstuetzungen.aggregate(total=Sum("betrag"))["total"] or Decimal("0")
|
||||
|
||||
zeitraum = None
|
||||
if unterstuetzungen.exists():
|
||||
erste = unterstuetzungen.first().faellig_am
|
||||
letzte = unterstuetzungen.last().faellig_am
|
||||
if erste == letzte:
|
||||
zeitraum = erste.strftime("%d.%m.%Y")
|
||||
else:
|
||||
zeitraum = f"{erste.strftime('%d.%m.%Y')} – {letzte.strftime('%d.%m.%Y')}"
|
||||
|
||||
betrag_quartal = destinataer.vierteljaehrlicher_betrag
|
||||
betrag_jaehrlich = (betrag_quartal * 4) if betrag_quartal else None
|
||||
zweck = destinataer.ausbildungsstand or (destinataer.get_berufsgruppe_display() if destinataer.berufsgruppe else None)
|
||||
|
||||
context = {
|
||||
"destinataer": destinataer,
|
||||
"unterstuetzungen": unterstuetzungen,
|
||||
"gesamtbetrag": gesamtbetrag,
|
||||
"datum": timezone.now().date(),
|
||||
"zeitraum": zeitraum,
|
||||
"betrag_quartal": betrag_quartal,
|
||||
"betrag_jaehrlich": betrag_jaehrlich,
|
||||
"zweck": zweck,
|
||||
}
|
||||
|
||||
try:
|
||||
from weasyprint import HTML
|
||||
from stiftung.utils.vorlagen import render_vorlage
|
||||
html_content = render_vorlage("pdf/bestaetigung.html", context)
|
||||
pdf_bytes = HTML(string=html_content).write_pdf()
|
||||
response = HttpResponse(pdf_bytes, content_type="application/pdf")
|
||||
filename = f"bestaetigung_{destinataer.nachname}_{destinataer.vorname}.pdf"
|
||||
response["Content-Disposition"] = f'inline; filename="{filename}"'
|
||||
return response
|
||||
except Exception as exc:
|
||||
messages.error(request, f"PDF-Generierung fehlgeschlagen: {exc}")
|
||||
return redirect("stiftung:destinataer_detail", pk=pk)
|
||||
|
||||
|
||||
@login_required
|
||||
def bestaetigung_versenden(request, pk):
|
||||
"""
|
||||
Sendet das Bestätigungsschreiben per E-Mail an den Destinatär.
|
||||
POST-only (CSRF-geschützt). Startet asynchronen Celery-Task.
|
||||
"""
|
||||
from stiftung.tasks import send_bestaetigung
|
||||
|
||||
if request.method != "POST":
|
||||
return redirect("stiftung:destinataer_detail", pk=pk)
|
||||
|
||||
destinataer = get_object_or_404(Destinataer, id=pk)
|
||||
|
||||
if not destinataer.email:
|
||||
messages.error(
|
||||
request,
|
||||
f"Destinatär {destinataer.get_full_name()} hat keine E-Mail-Adresse hinterlegt.",
|
||||
)
|
||||
return redirect("stiftung:destinataer_detail", pk=pk)
|
||||
|
||||
base_url = request.build_absolute_uri("/").rstrip("/")
|
||||
send_bestaetigung.delay(str(destinataer.id), base_url=base_url)
|
||||
|
||||
log_action(
|
||||
request,
|
||||
action="update",
|
||||
entity_type="destinataer",
|
||||
entity_id=str(destinataer.id),
|
||||
entity_name=destinataer.get_full_name(),
|
||||
description=f"Bestätigungsschreiben per E-Mail gesendet an {destinataer.get_full_name()} ({destinataer.email})",
|
||||
)
|
||||
|
||||
messages.success(
|
||||
request,
|
||||
f"Bestätigungsschreiben wird per E-Mail an {destinataer.email} gesendet.",
|
||||
)
|
||||
return redirect("stiftung:destinataer_detail", pk=pk)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user