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:
@@ -11,6 +11,7 @@ from .models import (AppConfiguration, AuditLog, BackupJob, BankTransaction,
|
||||
DestinataerUnterstuetzung,
|
||||
DokumentLink, Foerderung, Land, LandVerpachtung, Paechter, Person,
|
||||
Rentmeister, StiftungsKonto, UnterstuetzungWiederkehrend,
|
||||
Veranstaltung, Veranstaltungsteilnehmer,
|
||||
Verwaltungskosten, VierteljahresNachweis)
|
||||
|
||||
|
||||
@@ -1228,6 +1229,109 @@ class DestinataerEmailEingangAdmin(admin.ModelAdmin):
|
||||
mark_verarbeitet.short_description = "Als verarbeitet markieren"
|
||||
|
||||
|
||||
class VeranstaltungsteilnehmerInline(admin.TabularInline):
|
||||
model = Veranstaltungsteilnehmer
|
||||
extra = 1
|
||||
fields = [
|
||||
"anrede", "vorname", "nachname", "strasse", "plz", "ort",
|
||||
"email", "rsvp_status", "bemerkungen",
|
||||
]
|
||||
|
||||
|
||||
@admin.register(Veranstaltung)
|
||||
class VeranstaltungAdmin(admin.ModelAdmin):
|
||||
list_display = [
|
||||
"titel", "datum", "uhrzeit", "ort", "status",
|
||||
"get_teilnehmer_count", "get_zugesagte_count", "budget_pro_person",
|
||||
]
|
||||
list_filter = ["status", "datum"]
|
||||
search_fields = ["titel", "ort", "beschreibung"]
|
||||
ordering = ["-datum"]
|
||||
readonly_fields = ["id", "erstellt_am", "aktualisiert_am", "serienbrief_link"]
|
||||
inlines = [VeranstaltungsteilnehmerInline]
|
||||
|
||||
fieldsets = (
|
||||
("Grunddaten", {"fields": ("titel", "datum", "uhrzeit", "status")}),
|
||||
("Veranstaltungsort", {"fields": ("ort", "adresse")}),
|
||||
("Details", {"fields": ("beschreibung", "budget_pro_person")}),
|
||||
(
|
||||
"Serienbrief",
|
||||
{
|
||||
"fields": (
|
||||
"betreff", "briefvorlage",
|
||||
"unterschrift_1_name", "unterschrift_1_titel",
|
||||
"unterschrift_2_name", "unterschrift_2_titel",
|
||||
"serienbrief_link",
|
||||
),
|
||||
"description": (
|
||||
"Betreff leer = 'Einladung zum [Titel]'. "
|
||||
"Platzhalter in der Vorlage: {{ anrede }}, {{ vorname }}, "
|
||||
"{{ nachname }}, {{ strasse }}, {{ plz }}, {{ ort_teilnehmer }}, "
|
||||
"{{ datum }}, {{ uhrzeit }}, {{ veranstaltungsort }}, {{ gasthaus_adresse }}"
|
||||
),
|
||||
},
|
||||
),
|
||||
("System", {"fields": ("id", "erstellt_am", "aktualisiert_am"), "classes": ("collapse",)}),
|
||||
)
|
||||
|
||||
def get_teilnehmer_count(self, obj):
|
||||
return obj.get_teilnehmer_count()
|
||||
get_teilnehmer_count.short_description = "Teilnehmer gesamt"
|
||||
|
||||
def get_zugesagte_count(self, obj):
|
||||
return obj.get_zugesagte_count()
|
||||
get_zugesagte_count.short_description = "Zugesagt"
|
||||
|
||||
def serienbrief_link(self, obj):
|
||||
if obj.pk:
|
||||
from django.urls import reverse as url_reverse
|
||||
url = url_reverse("stiftung:veranstaltung_serienbrief_pdf", args=[obj.pk])
|
||||
return format_html(
|
||||
'<a href="{}" target="_blank" class="button">Serienbrief-PDF generieren</a>', url
|
||||
)
|
||||
return "–"
|
||||
serienbrief_link.short_description = "Serienbrief"
|
||||
|
||||
actions = ["generate_serienbrief"]
|
||||
|
||||
def generate_serienbrief(self, request, queryset):
|
||||
if queryset.count() != 1:
|
||||
self.message_user(
|
||||
request,
|
||||
"Bitte genau eine Veranstaltung auswählen.",
|
||||
level="error",
|
||||
)
|
||||
return
|
||||
from django.urls import reverse as url_reverse
|
||||
from django.shortcuts import redirect
|
||||
veranstaltung = queryset.first()
|
||||
url = url_reverse("stiftung:veranstaltung_serienbrief_pdf", args=[veranstaltung.pk])
|
||||
return redirect(url)
|
||||
generate_serienbrief.short_description = "Serienbrief-PDF generieren"
|
||||
|
||||
|
||||
@admin.register(Veranstaltungsteilnehmer)
|
||||
class VeranstaltungsteilnehmerAdmin(admin.ModelAdmin):
|
||||
list_display = [
|
||||
"nachname", "vorname", "anrede", "veranstaltung", "rsvp_status", "ort",
|
||||
]
|
||||
list_filter = ["rsvp_status", "veranstaltung", "anrede"]
|
||||
search_fields = ["vorname", "nachname", "ort", "email"]
|
||||
ordering = ["veranstaltung", "nachname", "vorname"]
|
||||
readonly_fields = ["id", "erstellt_am"]
|
||||
|
||||
fieldsets = (
|
||||
("Zuordnung", {"fields": ("veranstaltung", "paechter", "destinataer")}),
|
||||
(
|
||||
"Persönliche Daten",
|
||||
{"fields": ("anrede", "vorname", "nachname", "email")},
|
||||
),
|
||||
("Adresse", {"fields": ("strasse", "plz", "ort")}),
|
||||
("RSVP", {"fields": ("rsvp_status", "bemerkungen")}),
|
||||
("System", {"fields": ("id", "erstellt_am"), "classes": ("collapse",)}),
|
||||
)
|
||||
|
||||
|
||||
# Customize admin site
|
||||
admin.site.site_header = "Stiftungsverwaltung Administration"
|
||||
admin.site.site_title = "Stiftungsverwaltung Admin"
|
||||
|
||||
Reference in New Issue
Block a user