feat: Veranstaltungsmodul + Serienbrief mit editierbaren Feldern (STI-35, STI-39)
Implementierung des Veranstaltungsmoduls inkl. Serienbrief-PDF-Generator mit dynamischen, editierbaren Feldern für Betreff und Unterschriften. ### Veranstaltungsmodul (STI-35) - Neues Veranstaltungs-Modell: Titel, Datum, Uhrzeit, Ort, Gasthaus-Adresse, Briefvorlage, Gästeliste (VerstaltungsGast mit freien/Destinatär-Feldern) - Views: Veranstaltungsliste, -detail, Serienbrief-PDF-Generator - Templates: list.html, detail.html, serienbrief_pdf.html (A4, einseitig) - API: Serializer + Endpunkte für Veranstaltungen - Admin: Inline-Bearbeitung der Gästeliste - Migration: 0044_veranstaltungsmodul ### Serienbrief editierbare Felder + PDF-Fix (STI-39) - Neue Felder an Veranstaltung: betreff, unterschrift_1_name/titel, unterschrift_2_name/titel (mit Defaults: Katrin Kleinpaß / Jan Remmer Siebels) - PDF-CSS: Margins, Font-Sizes und Line-Heights reduziert für einseitigen Druck - Migration: 0045_add_serienbrief_editable_fields ### Infrastruktur - scripts/init-paperless-db.sh: Erstellt separate Paperless-DB beim DB-Init - compose.yml: init-paperless-db.sh eingebunden, PAPERLESS_DBNAME-Fix - .gitignore: .claude/ ausgeschlossen Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -31,7 +31,8 @@ from .models import (AppConfiguration, CSVImport, Destinataer,
|
||||
DestinataerEmailEingang, DestinataerUnterstuetzung,
|
||||
DokumentLink, Foerderung, Land,
|
||||
LandAbrechnung, LandVerpachtung, Paechter, Person,
|
||||
StiftungsKonto, UnterstuetzungWiederkehrend, VierteljahresNachweis)
|
||||
StiftungsKonto, UnterstuetzungWiederkehrend, Veranstaltung,
|
||||
Veranstaltungsteilnehmer, VierteljahresNachweis)
|
||||
|
||||
|
||||
def get_pdf_generator():
|
||||
@@ -279,7 +280,7 @@ def paperless_document_redirect(_request, doc_id: int):
|
||||
return Response({"error": "Paperless API not configured"}, status=400)
|
||||
|
||||
# Remove /api suffix if present, then construct the document URL
|
||||
base_url = url.rstrip("/api") if url.endswith("/api") else url
|
||||
base_url = url[:-4] if url.endswith("/api") else url
|
||||
|
||||
# For external Paperless (already includes /paperless/ in base URL)
|
||||
return redirect(f"{base_url}/documents/{doc_id}/details/")
|
||||
@@ -1930,7 +1931,6 @@ def verpachtung_list(request):
|
||||
return render(request, "stiftung/verpachtung_list.html", context)
|
||||
|
||||
|
||||
@login_required
|
||||
@login_required
|
||||
def land_verpachtung_detail(request, pk):
|
||||
"""Detail view for LandVerpachtung"""
|
||||
@@ -2197,7 +2197,7 @@ def dokument_list(request):
|
||||
available_dokumente = []
|
||||
if url and token:
|
||||
try:
|
||||
base_url = url.rstrip("/api") if url.endswith("/api") else url
|
||||
base_url = url[:-4] if url.endswith("/api") else url
|
||||
headers = {"Authorization": f"Token {token}"}
|
||||
|
||||
# Alle verfügbaren Dokumente abrufen (mit Paginierung)
|
||||
@@ -2553,7 +2553,7 @@ def paperless_ping(_request):
|
||||
)
|
||||
try:
|
||||
# Entferne /api vom Ende der URL falls vorhanden
|
||||
base_url = url.rstrip("/api") if url.endswith("/api") else url
|
||||
base_url = url[:-4] if url.endswith("/api") else url
|
||||
r = requests.get(
|
||||
f"{base_url}/api/tags/",
|
||||
headers={"Authorization": f"Token {token}"},
|
||||
@@ -2598,7 +2598,7 @@ def paperless_documents(request):
|
||||
|
||||
try:
|
||||
# Entferne /api vom Ende der URL falls vorhanden
|
||||
base_url = url.rstrip("/api") if url.endswith("/api") else url
|
||||
base_url = url[:-4] if url.endswith("/api") else url
|
||||
headers = {"Authorization": f"Token {token}"}
|
||||
|
||||
def fetch_tagged():
|
||||
@@ -2735,7 +2735,7 @@ def paperless_debug(request):
|
||||
|
||||
try:
|
||||
# Entferne /api vom Ende der URL falls vorhanden
|
||||
base_url = url.rstrip("/api") if url.endswith("/api") else url
|
||||
base_url = url[:-4] if url.endswith("/api") else url
|
||||
|
||||
headers = {"Authorization": f"Token {token}"}
|
||||
|
||||
@@ -2857,7 +2857,7 @@ def paperless_tags_only(request):
|
||||
|
||||
try:
|
||||
# Entferne /api vom Ende der URL falls vorhanden
|
||||
base_url = url.rstrip("/api") if url.endswith("/api") else url
|
||||
base_url = url[:-4] if url.endswith("/api") else url
|
||||
|
||||
# Alle Tags abrufen (mit großer page_size)
|
||||
headers = {"Authorization": f"Token {token}"}
|
||||
@@ -4147,7 +4147,6 @@ def verwaltungskosten_create(request):
|
||||
rentmeister = Rentmeister.objects.get(pk=rentmeister_id)
|
||||
initial_data["rentmeister"] = rentmeister
|
||||
redirect_url = "stiftung:rentmeister_detail"
|
||||
redirect_args = [rentmeister_id]
|
||||
except Rentmeister.DoesNotExist:
|
||||
pass
|
||||
|
||||
@@ -8440,13 +8439,6 @@ def kalender_admin(request):
|
||||
}
|
||||
|
||||
return render(request, 'stiftung/kalender/admin.html', context)
|
||||
|
||||
context = {
|
||||
'title': f'Löschen: {event.titel}',
|
||||
'event': event,
|
||||
}
|
||||
|
||||
return render(request, 'stiftung/kalender/delete.html', context)
|
||||
|
||||
|
||||
@login_required
|
||||
@@ -8642,3 +8634,54 @@ def email_eingang_poll_trigger(request):
|
||||
except Exception as exc:
|
||||
messages.error(request, f"Fehler beim Starten des Tasks: {exc}")
|
||||
return redirect("email_eingang_list")
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Veranstaltungsmodul
|
||||
# ============================================================
|
||||
|
||||
@login_required
|
||||
def veranstaltung_list(request):
|
||||
"""Liste aller Veranstaltungen"""
|
||||
veranstaltungen = Veranstaltung.objects.all()
|
||||
return render(request, "stiftung/veranstaltung/list.html", {"veranstaltungen": veranstaltungen})
|
||||
|
||||
|
||||
@login_required
|
||||
def veranstaltung_detail(request, pk):
|
||||
"""Detail-Ansicht einer Veranstaltung mit RSVP-Übersicht"""
|
||||
veranstaltung = get_object_or_404(Veranstaltung, pk=pk)
|
||||
teilnehmer = veranstaltung.teilnehmer.all()
|
||||
context = {
|
||||
"veranstaltung": veranstaltung,
|
||||
"teilnehmer": teilnehmer,
|
||||
"zugesagte": teilnehmer.filter(rsvp_status="zugesagt"),
|
||||
"abgesagte": teilnehmer.filter(rsvp_status="abgesagt"),
|
||||
"keine_rueckmeldung": teilnehmer.filter(rsvp_status="keine_rueckmeldung"),
|
||||
"eingeladen": teilnehmer.filter(rsvp_status="eingeladen"),
|
||||
}
|
||||
return render(request, "stiftung/veranstaltung/detail.html", context)
|
||||
|
||||
|
||||
@login_required
|
||||
def veranstaltung_serienbrief_pdf(request, pk):
|
||||
"""Generiert Serienbrief-PDF für alle Teilnehmer einer Veranstaltung"""
|
||||
from weasyprint import HTML
|
||||
from django.template.loader import render_to_string
|
||||
|
||||
veranstaltung = get_object_or_404(Veranstaltung, pk=pk)
|
||||
teilnehmer = veranstaltung.teilnehmer.all().order_by("nachname", "vorname")
|
||||
|
||||
# Render HTML for all letters
|
||||
html_string = render_to_string(
|
||||
"stiftung/veranstaltung/serienbrief_pdf.html",
|
||||
{
|
||||
"veranstaltung": veranstaltung,
|
||||
"teilnehmer": teilnehmer,
|
||||
},
|
||||
)
|
||||
pdf = HTML(string=html_string).write_pdf()
|
||||
filename = f"einladungen_{veranstaltung.datum}_{veranstaltung.titel[:30].replace(' ', '_')}.pdf"
|
||||
response = HttpResponse(pdf, content_type="application/pdf")
|
||||
response["Content-Disposition"] = f'attachment; filename="{filename}"'
|
||||
return response
|
||||
|
||||
Reference in New Issue
Block a user