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:
@@ -671,7 +671,7 @@ class Land(models.Model):
|
||||
|
||||
def get_verpachtungsgrad_neu(self):
|
||||
"""Berechnet den Verpachtungsgrad basierend auf neuen Verpachtungen"""
|
||||
if self.groesse_qm > 0:
|
||||
if self.groesse_qm and self.groesse_qm > 0:
|
||||
return (self.get_verpachtete_flaeche_aktuell() / self.groesse_qm) * 100
|
||||
return 0
|
||||
|
||||
@@ -3279,3 +3279,178 @@ class DestinataerEmailEingang(models.Model):
|
||||
f"{base}/documents/{doc_id}/"
|
||||
for doc_id in (self.paperless_dokument_ids or [])
|
||||
]
|
||||
|
||||
|
||||
class Veranstaltung(models.Model):
|
||||
"""Veranstaltungen der Stiftung, z.B. Stiftungsessen mit Rechnungslegung"""
|
||||
|
||||
STATUS_CHOICES = [
|
||||
("geplant", "Geplant"),
|
||||
("einladungen_versendet", "Einladungen versendet"),
|
||||
("abgeschlossen", "Abgeschlossen"),
|
||||
("abgesagt", "Abgesagt"),
|
||||
]
|
||||
|
||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||
titel = models.CharField(max_length=200, verbose_name="Titel")
|
||||
datum = models.DateField(verbose_name="Datum")
|
||||
uhrzeit = models.TimeField(null=True, blank=True, verbose_name="Uhrzeit")
|
||||
ort = models.CharField(max_length=200, verbose_name="Ort / Gasthaus")
|
||||
adresse = models.TextField(blank=True, verbose_name="Adresse Gasthaus")
|
||||
beschreibung = models.TextField(blank=True, verbose_name="Beschreibung / Zweck")
|
||||
status = models.CharField(
|
||||
max_length=30,
|
||||
choices=STATUS_CHOICES,
|
||||
default="geplant",
|
||||
verbose_name="Status",
|
||||
)
|
||||
budget_pro_person = models.DecimalField(
|
||||
max_digits=8,
|
||||
decimal_places=2,
|
||||
null=True,
|
||||
blank=True,
|
||||
verbose_name="Budget pro Person (€)",
|
||||
help_text="Geschätztes Budget je Teilnehmer in €",
|
||||
)
|
||||
briefvorlage = models.TextField(
|
||||
blank=True,
|
||||
verbose_name="Briefvorlage",
|
||||
help_text=(
|
||||
"HTML/Text-Template für Serienbrief. Platzhalter: "
|
||||
"{{ anrede }}, {{ vorname }}, {{ nachname }}, {{ strasse }}, "
|
||||
"{{ plz }}, {{ ort }}, {{ datum }}, {{ uhrzeit }}, "
|
||||
"{{ veranstaltungsort }}, {{ gasthaus_adresse }}"
|
||||
),
|
||||
)
|
||||
betreff = models.CharField(
|
||||
max_length=300,
|
||||
blank=True,
|
||||
verbose_name="Betreff",
|
||||
help_text="Betreffzeile des Serienbriefs. Leer = Standardbetreff.",
|
||||
)
|
||||
unterschrift_1_name = models.CharField(
|
||||
max_length=100,
|
||||
blank=True,
|
||||
default="Katrin Kleinpaß",
|
||||
verbose_name="Unterschrift 1 – Name",
|
||||
)
|
||||
unterschrift_1_titel = models.CharField(
|
||||
max_length=100,
|
||||
blank=True,
|
||||
default="Rentmeisterin",
|
||||
verbose_name="Unterschrift 1 – Titel",
|
||||
)
|
||||
unterschrift_2_name = models.CharField(
|
||||
max_length=100,
|
||||
blank=True,
|
||||
default="Jan Remmer Siebels",
|
||||
verbose_name="Unterschrift 2 – Name",
|
||||
)
|
||||
unterschrift_2_titel = models.CharField(
|
||||
max_length=100,
|
||||
blank=True,
|
||||
default="Rentmeister",
|
||||
verbose_name="Unterschrift 2 – Titel",
|
||||
)
|
||||
|
||||
erstellt_am = models.DateTimeField(auto_now_add=True)
|
||||
aktualisiert_am = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Veranstaltung"
|
||||
verbose_name_plural = "Veranstaltungen"
|
||||
ordering = ["-datum"]
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.titel} ({self.datum})"
|
||||
|
||||
def get_teilnehmer_count(self):
|
||||
return self.teilnehmer.count()
|
||||
|
||||
def get_zugesagte_count(self):
|
||||
return self.teilnehmer.filter(rsvp_status="zugesagt").count()
|
||||
|
||||
def get_abgesagte_count(self):
|
||||
return self.teilnehmer.filter(rsvp_status="abgesagt").count()
|
||||
|
||||
def get_keine_rueckmeldung_count(self):
|
||||
return self.teilnehmer.filter(rsvp_status="keine_rueckmeldung").count()
|
||||
|
||||
|
||||
class Veranstaltungsteilnehmer(models.Model):
|
||||
"""Teilnehmer einer Veranstaltung – primär freie Eingabe für Familienmitglieder"""
|
||||
|
||||
ANREDE_CHOICES = [
|
||||
("Herr", "Herr"),
|
||||
("Frau", "Frau"),
|
||||
("", "Keine Anrede"),
|
||||
]
|
||||
|
||||
RSVP_CHOICES = [
|
||||
("eingeladen", "Eingeladen"),
|
||||
("zugesagt", "Zugesagt"),
|
||||
("abgesagt", "Abgesagt"),
|
||||
("keine_rueckmeldung", "Keine Rückmeldung"),
|
||||
]
|
||||
|
||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||
veranstaltung = models.ForeignKey(
|
||||
Veranstaltung,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="teilnehmer",
|
||||
verbose_name="Veranstaltung",
|
||||
)
|
||||
|
||||
# Optionale Verknüpfung zu bestehenden Datensätzen
|
||||
paechter = models.ForeignKey(
|
||||
"Paechter",
|
||||
null=True,
|
||||
blank=True,
|
||||
on_delete=models.SET_NULL,
|
||||
verbose_name="Pächter (optional)",
|
||||
)
|
||||
destinataer = models.ForeignKey(
|
||||
"Destinataer",
|
||||
null=True,
|
||||
blank=True,
|
||||
on_delete=models.SET_NULL,
|
||||
verbose_name="Destinatär (optional)",
|
||||
)
|
||||
|
||||
# Freie Felder (Pflichtfelder für Serienbrief)
|
||||
anrede = models.CharField(
|
||||
max_length=10, choices=ANREDE_CHOICES, blank=True, verbose_name="Anrede"
|
||||
)
|
||||
vorname = models.CharField(max_length=100, verbose_name="Vorname")
|
||||
nachname = models.CharField(max_length=100, verbose_name="Nachname")
|
||||
strasse = models.CharField(max_length=200, blank=True, verbose_name="Straße")
|
||||
plz = models.CharField(max_length=10, blank=True, verbose_name="PLZ")
|
||||
ort = models.CharField(max_length=100, blank=True, verbose_name="Ort")
|
||||
email = models.EmailField(
|
||||
blank=True, verbose_name="E-Mail", help_text="Optional, für späteren E-Mail-Versand"
|
||||
)
|
||||
|
||||
rsvp_status = models.CharField(
|
||||
max_length=20,
|
||||
choices=RSVP_CHOICES,
|
||||
default="eingeladen",
|
||||
verbose_name="RSVP-Status",
|
||||
)
|
||||
bemerkungen = models.TextField(blank=True, verbose_name="Bemerkungen")
|
||||
|
||||
erstellt_am = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Veranstaltungsteilnehmer"
|
||||
verbose_name_plural = "Veranstaltungsteilnehmer"
|
||||
ordering = ["nachname", "vorname"]
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.anrede} {self.vorname} {self.nachname}".strip()
|
||||
|
||||
def get_full_name(self):
|
||||
return f"{self.vorname} {self.nachname}".strip()
|
||||
|
||||
def get_full_address(self):
|
||||
parts = [self.strasse, f"{self.plz} {self.ort}".strip()]
|
||||
return ", ".join(p for p in parts if p)
|
||||
|
||||
Reference in New Issue
Block a user