Baseline für Vision 2026: Veranstaltungsmodul + ausstehende Änderungen

Alle bestehenden, nicht commiteten Änderungen als Ausgangsbasis für den
vision-2026 Branch übernommen (Veranstaltungsmodul, Serienbrief, etc.).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
SysAdmin Agent
2026-03-11 08:51:48 +00:00
parent 28621d2774
commit 709903e627
15 changed files with 1210 additions and 28 deletions

View File

@@ -1,3 +1,4 @@
from django import forms
from django.contrib import admin
from django.db.models import Count, Sum
from django.urls import reverse
@@ -7,7 +8,7 @@ from django.utils.safestring import mark_safe
from . import models
from .models import (AppConfiguration, AuditLog, BackupJob, BankTransaction,
CSVImport, Destinataer, DestinataerEmailEingang,
BriefVorlage, CSVImport, Destinataer, DestinataerEmailEingang,
DestinataerUnterstuetzung,
DokumentLink, Foerderung, Land, LandVerpachtung, Paechter, Person,
Rentmeister, StiftungsKonto, UnterstuetzungWiederkehrend,
@@ -1238,8 +1239,31 @@ class VeranstaltungsteilnehmerInline(admin.TabularInline):
]
class BriefVorlageWidget(forms.Textarea):
"""Erweitertes Textarea-Widget für HTML-Briefvorlagen mit Editor-Panel und Platzhalter-Hilfe."""
class Media:
js = ["stiftung/js/briefvorlage_editor.js"]
def __init__(self, attrs=None):
default_attrs = {"rows": 18, "cols": 80, "class": "briefvorlage-textarea", "style": "font-family: monospace; font-size: 13px;"}
if attrs:
default_attrs.update(attrs)
super().__init__(attrs=default_attrs)
class VeranstaltungAdminForm(forms.ModelForm):
class Meta:
model = Veranstaltung
fields = "__all__"
widgets = {
"briefvorlage": BriefVorlageWidget(),
}
@admin.register(Veranstaltung)
class VeranstaltungAdmin(admin.ModelAdmin):
form = VeranstaltungAdminForm
list_display = [
"titel", "datum", "uhrzeit", "ort", "status",
"get_teilnehmer_count", "get_zugesagte_count", "budget_pro_person",
@@ -1247,7 +1271,7 @@ class VeranstaltungAdmin(admin.ModelAdmin):
list_filter = ["status", "datum"]
search_fields = ["titel", "ort", "beschreibung"]
ordering = ["-datum"]
readonly_fields = ["id", "erstellt_am", "aktualisiert_am", "serienbrief_link"]
readonly_fields = ["id", "erstellt_am", "aktualisiert_am", "serienbrief_aktionen", "platzhalter_dokumentation"]
inlines = [VeranstaltungsteilnehmerInline]
fieldsets = (
@@ -1255,19 +1279,22 @@ class VeranstaltungAdmin(admin.ModelAdmin):
("Veranstaltungsort", {"fields": ("ort", "adresse")}),
("Details", {"fields": ("beschreibung", "budget_pro_person")}),
(
"Serienbrief",
"Serienbrief Vorlage",
{
"fields": (
"platzhalter_dokumentation",
"betreff",
"briefvorlage",
),
},
),
(
"Serienbrief Unterschriften & Aktionen",
{
"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 }}"
"serienbrief_aktionen",
),
},
),
@@ -1282,15 +1309,43 @@ class VeranstaltungAdmin(admin.ModelAdmin):
return obj.get_zugesagte_count()
get_zugesagte_count.short_description = "Zugesagt"
def serienbrief_link(self, obj):
def platzhalter_dokumentation(self, obj):
return format_html(
"""<div class="help" style="background:#f8f9fa;border:1px solid #dee2e6;border-radius:4px;padding:10px 14px;margin-bottom:4px;">
<strong>Verfügbare Platzhalter im Brieftext:</strong><br>
<table style="margin-top:6px;border-collapse:collapse;font-size:13px;">
<tr><td style="padding:2px 12px 2px 0;font-family:monospace;color:#d63384;">{{{{ anrede }}}}</td><td>Anredetitel (Herr / Frau)</td></tr>
<tr><td style="padding:2px 12px 2px 0;font-family:monospace;color:#d63384;">{{{{ vorname }}}}</td><td>Vorname des Empfängers</td></tr>
<tr><td style="padding:2px 12px 2px 0;font-family:monospace;color:#d63384;">{{{{ nachname }}}}</td><td>Nachname des Empfängers</td></tr>
<tr><td style="padding:2px 12px 2px 0;font-family:monospace;color:#d63384;">{{{{ strasse }}}}</td><td>Straße und Hausnummer</td></tr>
<tr><td style="padding:2px 12px 2px 0;font-family:monospace;color:#d63384;">{{{{ plz }}}}</td><td>Postleitzahl</td></tr>
<tr><td style="padding:2px 12px 2px 0;font-family:monospace;color:#d63384;">{{{{ ort }}}}</td><td>Wohnort des Empfängers</td></tr>
<tr><td style="padding:2px 12px 2px 0;font-family:monospace;color:#d63384;">{{{{ datum }}}}</td><td>Datum der Veranstaltung (z.B. Freitag, 17. April 2026)</td></tr>
<tr><td style="padding:2px 12px 2px 0;font-family:monospace;color:#d63384;">{{{{ uhrzeit }}}}</td><td>Uhrzeit der Veranstaltung (z.B. 19:00 Uhr)</td></tr>
<tr><td style="padding:2px 12px 2px 0;font-family:monospace;color:#d63384;">{{{{ veranstaltungsort }}}}</td><td>Name des Veranstaltungsorts / Gasthaus</td></tr>
<tr><td style="padding:2px 12px 2px 0;font-family:monospace;color:#d63384;">{{{{ gasthaus_adresse }}}}</td><td>Adresse des Gasthauses</td></tr>
</table>
<div style="margin-top:8px;font-size:12px;color:#6c757d;">
Platzhalter werden beim PDF-Export automatisch mit den Empfänger- und Veranstaltungsdaten befüllt.
Tipp: Vorlagen unter <a href="/admin/stiftung/briefvorlage/" target="_blank">Verwaltung → Briefvorlagen</a> speichern und wiederverwenden.
</div>
</div>"""
)
platzhalter_dokumentation.short_description = "Platzhalter-Dokumentation"
platzhalter_dokumentation.allow_tags = True
def serienbrief_aktionen(self, obj):
if obj.pk:
from django.urls import reverse as url_reverse
url = url_reverse("stiftung:veranstaltung_serienbrief_pdf", args=[obj.pk])
pdf_url = url_reverse("stiftung:veranstaltung_serienbrief_pdf", args=[obj.pk])
vorschau_url = url_reverse("stiftung:veranstaltung_serienbrief_vorschau", args=[obj.pk])
return format_html(
'<a href="{}" target="_blank" class="button">Serienbrief-PDF generieren</a>', url
'<a href="{}" target="_blank" class="button" style="margin-right:8px;">Serienbrief-PDF generieren</a>'
'<a href="{}" target="_blank" class="button default">Vorschau im Browser</a>',
pdf_url, vorschau_url,
)
return ""
serienbrief_link.short_description = "Serienbrief"
serienbrief_aktionen.short_description = "Aktionen"
actions = ["generate_serienbrief"]
@@ -1310,6 +1365,34 @@ class VeranstaltungAdmin(admin.ModelAdmin):
generate_serienbrief.short_description = "Serienbrief-PDF generieren"
@admin.register(BriefVorlage)
class BriefVorlageAdmin(admin.ModelAdmin):
list_display = ["name", "beschreibung_kurz", "erstellt_am", "aktualisiert_am"]
search_fields = ["name", "beschreibung"]
ordering = ["name"]
readonly_fields = ["erstellt_am", "aktualisiert_am"]
fieldsets = (
(None, {"fields": ("name", "beschreibung")}),
(
"Briefinhalt",
{
"fields": ("betreff", "briefvorlage"),
"description": (
"Verfügbare Platzhalter: {{ anrede }}, {{ vorname }}, {{ nachname }}, "
"{{ strasse }}, {{ plz }}, {{ ort }}, {{ datum }}, {{ uhrzeit }}, "
"{{ veranstaltungsort }}, {{ gasthaus_adresse }}"
),
},
),
("System", {"fields": ("erstellt_am", "aktualisiert_am"), "classes": ("collapse",)}),
)
def beschreibung_kurz(self, obj):
return obj.beschreibung[:80] + "" if len(obj.beschreibung) > 80 else obj.beschreibung
beschreibung_kurz.short_description = "Beschreibung"
@admin.register(Veranstaltungsteilnehmer)
class VeranstaltungsteilnehmerAdmin(admin.ModelAdmin):
list_display = [