feat: Veranstaltungsmodul + Serienbrief mit editierbaren Feldern (STI-35, STI-39)
Some checks failed
CI/CD Pipeline / test (push) Has been cancelled
CI/CD Pipeline / deploy (push) Has been cancelled
Code Quality / quality (push) Has been cancelled

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:
SysAdmin Agent
2026-03-10 22:36:58 +00:00
parent f8f9dc3319
commit 28621d2774
24 changed files with 1072 additions and 68 deletions

View File

@@ -25,6 +25,10 @@
</div>
<div class="col-md-4 text-end">
<div class="btn-group" role="group">
<a href="https://www.tim-online.nrw.de/tim-online2/?WFS_gemarkung={{ land.gemarkung|urlencode }}&WFS_flur={{ land.flur|urlencode }}&WFS_flurstueck={{ land.flurstueck|urlencode }}"
class="btn btn-outline-success" title="TIM-Online NRW öffnen" target="_blank" rel="noopener">
<i class="fas fa-map-marked-alt me-2"></i>TIM-Online
</a>
<a href="{% url 'stiftung:land_update' land.pk %}" class="btn btn-warning">
<i class="fas fa-edit me-2"></i>Bearbeiten
</a>

View File

@@ -90,7 +90,7 @@
<div class="small text-muted">Grünland</div>
</div>
</div>
<div style="height: 200px;">
<div style="min-height: 200px;">
<canvas id="usageChart"></canvas>
</div>
</div>
@@ -110,7 +110,7 @@
<div class="h6 mb-0">{{ stats.total_plots }}</div>
<div class="small text-muted">Grundstücke gesamt</div>
</div>
<div style="height: 200px;">
<div style="min-height: 200px;">
<canvas id="sizesChart"></canvas>
</div>
</div>
@@ -136,7 +136,7 @@
<div class="small text-muted">Verfügbar</div>
</div>
</div>
<div style="height: 200px;">
<div style="min-height: 200px;">
<canvas id="verpachtungChart"></canvas>
</div>
</div>
@@ -245,15 +245,19 @@
</td>
<td>
<div class="btn-group" role="group">
<a href="{% url 'stiftung:land_detail' land.pk %}"
<a href="{% url 'stiftung:land_detail' land.pk %}"
class="btn btn-sm btn-outline-primary" title="Anzeigen">
<i class="fas fa-eye"></i>
</a>
<a href="{% url 'stiftung:land_update' land.pk %}"
<a href="https://www.tim-online.nrw.de/tim-online2/?WFS_gemarkung={{ land.gemarkung|urlencode }}&WFS_flur={{ land.flur|urlencode }}&WFS_flurstueck={{ land.flurstueck|urlencode }}"
class="btn btn-sm btn-outline-success" title="TIM-Online NRW" target="_blank" rel="noopener">
<i class="fas fa-map-marked-alt"></i>
</a>
<a href="{% url 'stiftung:land_update' land.pk %}"
class="btn btn-sm btn-outline-warning" title="Bearbeiten">
<i class="fas fa-edit"></i>
</a>
<a href="{% url 'stiftung:land_delete' land.pk %}"
<a href="{% url 'stiftung:land_delete' land.pk %}"
class="btn btn-sm btn-outline-danger" title="Löschen">
<i class="fas fa-trash"></i>
</a>

View File

@@ -0,0 +1,199 @@
{% extends "base.html" %}
{% block title %}{{ veranstaltung.titel }} Stiftungsverwaltung{% endblock %}
{% block content %}
<div class="container-fluid py-4">
<!-- Header -->
<div class="d-flex justify-content-between align-items-start mb-4">
<div>
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{% url 'stiftung:veranstaltung_list' %}">Veranstaltungen</a></li>
<li class="breadcrumb-item active">{{ veranstaltung.titel }}</li>
</ol>
</nav>
<h1 class="h3 mb-1">{{ veranstaltung.titel }}</h1>
<p class="text-muted mb-0">
{{ veranstaltung.datum|date:"l, d. F Y" }}{% if veranstaltung.uhrzeit %}, {{ veranstaltung.uhrzeit|time:"H:i" }} Uhr{% endif %}
&nbsp;·&nbsp; {{ veranstaltung.ort }}
</p>
</div>
<div class="d-flex gap-2">
<a href="{% url 'admin:stiftung_veranstaltung_change' veranstaltung.pk %}" class="btn btn-outline-secondary">
<i class="fas fa-edit me-1"></i>Bearbeiten
</a>
<a href="{% url 'stiftung:veranstaltung_serienbrief_pdf' veranstaltung.pk %}" class="btn btn-success">
<i class="fas fa-file-pdf me-1"></i>Serienbrief-PDF
</a>
</div>
</div>
<div class="row g-4">
<!-- Veranstaltungsdetails -->
<div class="col-lg-4">
<div class="card shadow-sm h-100">
<div class="card-header bg-dark text-white">
<i class="fas fa-info-circle me-2"></i>Details
</div>
<div class="card-body">
<dl class="row mb-0">
<dt class="col-sm-5">Status</dt>
<dd class="col-sm-7">
{% if veranstaltung.status == "geplant" %}
<span class="badge bg-secondary">Geplant</span>
{% elif veranstaltung.status == "einladungen_versendet" %}
<span class="badge bg-primary">Einladungen versendet</span>
{% elif veranstaltung.status == "abgeschlossen" %}
<span class="badge bg-success">Abgeschlossen</span>
{% elif veranstaltung.status == "abgesagt" %}
<span class="badge bg-danger">Abgesagt</span>
{% endif %}
</dd>
<dt class="col-sm-5">Gasthaus</dt>
<dd class="col-sm-7">{{ veranstaltung.ort }}</dd>
{% if veranstaltung.adresse %}
<dt class="col-sm-5">Adresse</dt>
<dd class="col-sm-7">{{ veranstaltung.adresse }}</dd>
{% endif %}
{% if veranstaltung.budget_pro_person %}
<dt class="col-sm-5">Budget/Person</dt>
<dd class="col-sm-7">{{ veranstaltung.budget_pro_person }} €</dd>
{% endif %}
{% if veranstaltung.beschreibung %}
<dt class="col-sm-5">Beschreibung</dt>
<dd class="col-sm-7">{{ veranstaltung.beschreibung }}</dd>
{% endif %}
</dl>
</div>
</div>
</div>
<!-- RSVP-Statistik -->
<div class="col-lg-4">
<div class="card shadow-sm h-100">
<div class="card-header bg-dark text-white">
<i class="fas fa-chart-pie me-2"></i>RSVP-Übersicht
</div>
<div class="card-body">
<div class="row text-center g-3">
<div class="col-6">
<div class="border rounded p-3">
<div class="fs-2 fw-bold text-primary">{{ teilnehmer.count }}</div>
<div class="small text-muted">Eingeladen gesamt</div>
</div>
</div>
<div class="col-6">
<div class="border rounded p-3">
<div class="fs-2 fw-bold text-success">{{ zugesagte.count }}</div>
<div class="small text-muted">Zugesagt</div>
</div>
</div>
<div class="col-6">
<div class="border rounded p-3">
<div class="fs-2 fw-bold text-danger">{{ abgesagte.count }}</div>
<div class="small text-muted">Abgesagt</div>
</div>
</div>
<div class="col-6">
<div class="border rounded p-3">
<div class="fs-2 fw-bold text-warning">{{ keine_rueckmeldung.count }}</div>
<div class="small text-muted">Keine Rückmeldung</div>
</div>
</div>
{% if eingeladen.count %}
<div class="col-12">
<div class="border rounded p-3">
<div class="fs-2 fw-bold text-secondary">{{ eingeladen.count }}</div>
<div class="small text-muted">Nur eingeladen (noch kein RSVP)</div>
</div>
</div>
{% endif %}
</div>
</div>
</div>
</div>
<!-- Schnellaktionen -->
<div class="col-lg-4">
<div class="card shadow-sm h-100">
<div class="card-header bg-dark text-white">
<i class="fas fa-tools me-2"></i>Aktionen
</div>
<div class="card-body d-flex flex-column gap-2">
<a href="{% url 'stiftung:veranstaltung_serienbrief_pdf' veranstaltung.pk %}"
class="btn btn-success w-100">
<i class="fas fa-file-pdf me-2"></i>Serienbrief-PDF (alle Teilnehmer)
</a>
<a href="{% url 'admin:stiftung_veranstaltungsteilnehmer_add' %}?veranstaltung={{ veranstaltung.pk }}"
class="btn btn-outline-primary w-100">
<i class="fas fa-user-plus me-2"></i>Teilnehmer hinzufügen
</a>
<a href="{% url 'admin:stiftung_veranstaltung_change' veranstaltung.pk %}"
class="btn btn-outline-secondary w-100">
<i class="fas fa-edit me-2"></i>Veranstaltung bearbeiten
</a>
</div>
</div>
</div>
</div>
<!-- Teilnehmerliste -->
<div class="card shadow-sm mt-4">
<div class="card-header bg-dark text-white d-flex justify-content-between align-items-center">
<span><i class="fas fa-users me-2"></i>Teilnehmerliste ({{ teilnehmer.count }})</span>
</div>
<div class="card-body p-0">
{% if teilnehmer %}
<table class="table table-hover mb-0">
<thead class="table-light">
<tr>
<th>Name</th>
<th>Adresse</th>
<th>E-Mail</th>
<th>RSVP</th>
<th>Bemerkungen</th>
</tr>
</thead>
<tbody>
{% for t in teilnehmer %}
<tr>
<td>
{{ t.anrede }} {{ t.vorname }} {{ t.nachname }}
</td>
<td>
{% if t.strasse %}{{ t.strasse }},{% endif %}
{% if t.plz %}{{ t.plz }}{% endif %}
{% if t.ort %}{{ t.ort }}{% endif %}
</td>
<td>{% if t.email %}<a href="mailto:{{ t.email }}">{{ t.email }}</a>{% else %}{% endif %}</td>
<td>
{% if t.rsvp_status == "eingeladen" %}
<span class="badge bg-secondary">Eingeladen</span>
{% elif t.rsvp_status == "zugesagt" %}
<span class="badge bg-success">Zugesagt</span>
{% elif t.rsvp_status == "abgesagt" %}
<span class="badge bg-danger">Abgesagt</span>
{% elif t.rsvp_status == "keine_rueckmeldung" %}
<span class="badge bg-warning text-dark">Keine Rückmeldung</span>
{% endif %}
</td>
<td>{{ t.bemerkungen|default:"" }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<div class="p-4 text-center text-muted">
<i class="fas fa-users fa-2x mb-2"></i>
<p>Noch keine Teilnehmer eingetragen.</p>
<a href="{% url 'admin:stiftung_veranstaltungsteilnehmer_add' %}?veranstaltung={{ veranstaltung.pk }}"
class="btn btn-primary">
<i class="fas fa-user-plus me-1"></i>Ersten Teilnehmer hinzufügen
</a>
</div>
{% endif %}
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,75 @@
{% extends "base.html" %}
{% block title %}Veranstaltungen Stiftungsverwaltung{% endblock %}
{% block content %}
<div class="container-fluid py-4">
<div class="d-flex justify-content-between align-items-center mb-4">
<h1 class="h3 mb-0">
<i class="fas fa-calendar-alt me-2"></i>Veranstaltungen
</h1>
<a href="{% url 'admin:stiftung_veranstaltung_add' %}" class="btn btn-primary">
<i class="fas fa-plus me-1"></i>Neue Veranstaltung
</a>
</div>
{% if veranstaltungen %}
<div class="card shadow-sm">
<div class="card-body p-0">
<table class="table table-hover mb-0">
<thead class="table-dark">
<tr>
<th>Titel</th>
<th>Datum</th>
<th>Ort / Gasthaus</th>
<th>Status</th>
<th>Teilnehmer</th>
<th>Zugesagt</th>
<th>Aktionen</th>
</tr>
</thead>
<tbody>
{% for v in veranstaltungen %}
<tr>
<td>
<a href="{% url 'stiftung:veranstaltung_detail' v.pk %}">
<strong>{{ v.titel }}</strong>
</a>
</td>
<td>{{ v.datum|date:"d.m.Y" }}{% if v.uhrzeit %}, {{ v.uhrzeit|time:"H:i" }} Uhr{% endif %}</td>
<td>{{ v.ort }}</td>
<td>
{% if v.status == "geplant" %}
<span class="badge bg-secondary">Geplant</span>
{% elif v.status == "einladungen_versendet" %}
<span class="badge bg-primary">Einladungen versendet</span>
{% elif v.status == "abgeschlossen" %}
<span class="badge bg-success">Abgeschlossen</span>
{% elif v.status == "abgesagt" %}
<span class="badge bg-danger">Abgesagt</span>
{% endif %}
</td>
<td>{{ v.get_teilnehmer_count }}</td>
<td>{{ v.get_zugesagte_count }}</td>
<td>
<a href="{% url 'stiftung:veranstaltung_detail' v.pk %}" class="btn btn-sm btn-outline-secondary me-1">
<i class="fas fa-eye"></i>
</a>
<a href="{% url 'stiftung:veranstaltung_serienbrief_pdf' v.pk %}" class="btn btn-sm btn-outline-success">
<i class="fas fa-file-pdf"></i> Serienbrief
</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% else %}
<div class="alert alert-info">
<i class="fas fa-info-circle me-2"></i>Noch keine Veranstaltungen angelegt.
<a href="{% url 'admin:stiftung_veranstaltung_add' %}">Jetzt erste Veranstaltung erstellen.</a>
</div>
{% endif %}
</div>
{% endblock %}

View File

@@ -0,0 +1,197 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<title>Einladungen {{ veranstaltung.titel }}</title>
<style>
@page {
size: A4;
margin: 2cm 2.5cm 2cm 2.5cm;
}
body {
font-family: "Times New Roman", Times, serif;
font-size: 10pt;
line-height: 1.35;
color: #000;
}
.letter {
page-break-after: always;
}
.letter:last-child {
page-break-after: avoid;
}
/* Absenderzeile (klein, über Adressfeld) */
.absender-zeile {
font-size: 7.5pt;
border-bottom: 1px solid #000;
margin-bottom: 3pt;
padding-bottom: 1pt;
color: #444;
}
/* Empfängeradresse */
.empfaenger {
min-height: 35mm;
margin-bottom: 5mm;
}
.empfaenger p {
margin: 0;
line-height: 1.3;
}
/* Datum und Ort */
.datum-zeile {
text-align: right;
margin-bottom: 4mm;
}
/* Betreff */
.betreff {
font-weight: bold;
margin-bottom: 4mm;
}
/* Brieftext */
.brieftext p {
margin: 0 0 3mm 0;
}
/* Veranstaltungsblock (eingerückt) */
.veranstaltungs-block {
margin: 4mm 0 4mm 10mm;
font-weight: bold;
}
/* Unterschrift */
.unterschrift {
margin-top: 10mm;
display: table;
width: 100%;
}
.unterschrift-person {
display: inline-block;
width: 45%;
vertical-align: top;
}
.unterschrift-linie {
border-top: 1px solid #000;
margin-bottom: 2mm;
width: 80%;
}
.stiftungsname-header {
font-size: 12pt;
font-weight: bold;
margin-bottom: 1mm;
}
.stiftungsadresse {
font-size: 8.5pt;
color: #444;
margin-bottom: 5mm;
}
</style>
</head>
<body>
{% for t in teilnehmer %}
<div class="letter">
<!-- Stiftungskopf -->
<div class="stiftungsname-header">van Hees-Theyssen-Vogel'sche Stiftung</div>
<div class="stiftungsadresse">
Raesfelder Str. 3 &nbsp;·&nbsp; 46499 Hamminkeln
</div>
<!-- Empfänger -->
<div class="empfaenger">
<div class="absender-zeile">van Hees-Theyssen-Vogel'sche Stiftung · Raesfelder Str. 3 · 46499 Hamminkeln</div>
<p>{{ t.anrede }} {{ t.vorname }} {{ t.nachname }}</p>
{% if t.strasse %}<p>{{ t.strasse }}</p>{% endif %}
{% if t.plz or t.ort %}<p>{{ t.plz }} {{ t.ort }}</p>{% endif %}
</div>
<!-- Datum -->
<div class="datum-zeile">
Hamminkeln, den {{ veranstaltung.datum|date:"j. F Y" }}
</div>
<!-- Betreff -->
<div class="betreff">
{% if veranstaltung.betreff %}{{ veranstaltung.betreff }}{% else %}Einladung zum {{ veranstaltung.titel }}{% endif %}
</div>
<!-- Anrede -->
<div class="brieftext">
<p>
Sehr geehrte{% if t.anrede == "Herr" %}r Herr{% elif t.anrede == "Frau" %} Frau{% else %}
{{ t.anrede }}{% endif %} {{ t.nachname }},
</p>
<!-- Entweder freie Briefvorlage oder Standardtext -->
{% if veranstaltung.briefvorlage %}
{{ veranstaltung.briefvorlage|safe }}
{% else %}
<p>
wir laden Sie herzlich ein, an der jährlichen Vorstellung der Rechnungslegung
der van Hees-Theyssen-Vogel'schen Stiftung teilzunehmen.
</p>
<p>Die Veranstaltung findet statt am:</p>
<div class="veranstaltungs-block">
{{ veranstaltung.datum|date:"l, j. F Y" }}{% if veranstaltung.uhrzeit %}, {{ veranstaltung.uhrzeit|time:"H:i" }} Uhr{% endif %}<br>
{{ veranstaltung.ort }}<br>
{% if veranstaltung.adresse %}{{ veranstaltung.adresse }}{% endif %}
</div>
<p>
Am Abend werden wir Ihnen einen Überblick über das abgelaufene Wirtschaftsjahr 2025
der Stiftung geben und gemeinsam das Abendessen genießen. Es bietet sich die
Gelegenheit zum persönlichen Austausch.
</p>
<p>
Bitte teilen Sie uns bis zum <strong>4. April 2026</strong> mit, ob Sie an der
Veranstaltung teilnehmen werden. Eine Rückmeldung per Post an die oben genannte
Adresse ist erbeten.
</p>
<p>Wir freuen uns auf Ihr Kommen.</p>
{% endif %}
<p>Mit freundlichen Grüßen</p>
</div>
<!-- Unterschriften -->
<div class="unterschrift">
{% if veranstaltung.unterschrift_1_name %}
<div class="unterschrift-person">
<div class="unterschrift-linie"></div>
{{ veranstaltung.unterschrift_1_name }}<br>
{{ veranstaltung.unterschrift_1_titel }}<br>
van Hees-Theyssen-Vogel'sche Stiftung
</div>
{% endif %}
{% if veranstaltung.unterschrift_2_name %}
<div class="unterschrift-person">
<div class="unterschrift-linie"></div>
{{ veranstaltung.unterschrift_2_name }}<br>
{{ veranstaltung.unterschrift_2_titel }}<br>
van Hees-Theyssen-Vogel'sche Stiftung
</div>
{% endif %}
</div>
</div>
{% endfor %}
</body>
</html>