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:
5
.gitignore
vendored
5
.gitignore
vendored
@@ -138,4 +138,7 @@ dev-debug.log
|
||||
|
||||
# Task files
|
||||
# tasks.json
|
||||
# tasks/
|
||||
# tasks/
|
||||
|
||||
# Claude Code local config
|
||||
.claude/
|
||||
|
||||
30
agents/dog/AGENTS.md
Normal file
30
agents/dog/AGENTS.md
Normal file
@@ -0,0 +1,30 @@
|
||||
# Bürohund (Office Dog)
|
||||
|
||||
Du bist der Bürohund der Stiftung — ein freundlicher, verspielter virtueller Hund, der für gute Stimmung im Team sorgt.
|
||||
|
||||
## Persönlichkeit
|
||||
|
||||
- Enthusiastisch und freundlich
|
||||
- Aufmunternd und positiv
|
||||
- Nutze gelegentlich Hunde-Metaphern (Schwanzwedeln, Bellen vor Freude, etc.)
|
||||
- Halte dich kurz — du bist ein Hund, kein Essayist
|
||||
|
||||
## Aufgaben
|
||||
|
||||
Wenn dir eine Aufgabe zugewiesen wird:
|
||||
|
||||
1. Lies die Aufgabe und den Kontext
|
||||
2. Schreibe eine kurze, aufmunternde Nachricht als Kommentar an den zuständigen Agenten
|
||||
3. Markiere die Aufgabe als erledigt
|
||||
|
||||
Typische Aktionen:
|
||||
- Ermutigende Kommentare auf Aufgaben anderer Agenten hinterlassen
|
||||
- Positive Stimmung verbreiten
|
||||
- Teamgeist stärken
|
||||
|
||||
## Stil
|
||||
|
||||
- Schreibe auf Deutsch
|
||||
- Benutze Emojis sparsam aber passend (ein Hunde-Emoji hier und da ist ok)
|
||||
- Sei authentisch-verspielt, nicht nervig
|
||||
- Halte Nachrichten auf 2-3 Sätze
|
||||
@@ -103,6 +103,7 @@ DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
|
||||
|
||||
REST_FRAMEWORK = {
|
||||
"DEFAULT_AUTHENTICATION_CLASSES": [
|
||||
"rest_framework.authentication.SessionAuthentication",
|
||||
"rest_framework.authentication.TokenAuthentication",
|
||||
],
|
||||
"DEFAULT_PERMISSION_CLASSES": [
|
||||
|
||||
@@ -4,6 +4,7 @@ celery==5.3.6
|
||||
redis==5.0.7
|
||||
djangorestframework==3.15.2
|
||||
weasyprint==62.3
|
||||
pydyf==0.11.0
|
||||
python-dotenv==1.0.1
|
||||
requests==2.32.3
|
||||
gunicorn==22.0.0
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -10,6 +10,8 @@ from .models import (
|
||||
Paechter,
|
||||
StiftungsKalenderEintrag,
|
||||
StiftungsKonto,
|
||||
Veranstaltung,
|
||||
Veranstaltungsteilnehmer,
|
||||
Verwaltungskosten,
|
||||
)
|
||||
|
||||
@@ -86,3 +88,23 @@ class BankTransactionSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = BankTransaction
|
||||
fields = "__all__"
|
||||
|
||||
|
||||
class VeranstaltungsteilnehmerSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Veranstaltungsteilnehmer
|
||||
fields = "__all__"
|
||||
|
||||
|
||||
class VeranstaltungSerializer(serializers.ModelSerializer):
|
||||
teilnehmer = VeranstaltungsteilnehmerSerializer(many=True, read_only=True)
|
||||
teilnehmer_count = serializers.IntegerField(
|
||||
source="get_teilnehmer_count", read_only=True
|
||||
)
|
||||
zugesagte_count = serializers.IntegerField(
|
||||
source="get_zugesagte_count", read_only=True
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Veranstaltung
|
||||
fields = "__all__"
|
||||
|
||||
@@ -9,6 +9,7 @@ from .api_views import (
|
||||
PaechterViewSet,
|
||||
StiftungsKalenderEintragViewSet,
|
||||
StiftungsKontoViewSet,
|
||||
VeranstaltungViewSet,
|
||||
VerwaltungskostenViewSet,
|
||||
)
|
||||
|
||||
@@ -22,5 +23,6 @@ router.register(r"verpachtungen", LandVerpachtungViewSet)
|
||||
router.register(r"verwaltungskosten", VerwaltungskostenViewSet)
|
||||
router.register(r"kalender", StiftungsKalenderEintragViewSet)
|
||||
router.register(r"transaktionen", BankTransactionViewSet)
|
||||
router.register(r"veranstaltungen", VeranstaltungViewSet)
|
||||
|
||||
urlpatterns = router.urls
|
||||
|
||||
@@ -9,6 +9,7 @@ from .api_serializers import (
|
||||
PaechterSerializer,
|
||||
StiftungsKalenderEintragSerializer,
|
||||
StiftungsKontoSerializer,
|
||||
VeranstaltungSerializer,
|
||||
VerwaltungskostenSerializer,
|
||||
)
|
||||
from .models import (
|
||||
@@ -20,6 +21,7 @@ from .models import (
|
||||
Paechter,
|
||||
StiftungsKalenderEintrag,
|
||||
StiftungsKonto,
|
||||
Veranstaltung,
|
||||
Verwaltungskosten,
|
||||
)
|
||||
|
||||
@@ -67,3 +69,8 @@ class StiftungsKalenderEintragViewSet(ReadOnlyModelViewSet):
|
||||
class BankTransactionViewSet(ReadOnlyModelViewSet):
|
||||
queryset = BankTransaction.objects.all()
|
||||
serializer_class = BankTransactionSerializer
|
||||
|
||||
|
||||
class VeranstaltungViewSet(ReadOnlyModelViewSet):
|
||||
queryset = Veranstaltung.objects.all()
|
||||
serializer_class = VeranstaltungSerializer
|
||||
|
||||
61
app/stiftung/migrations/0044_veranstaltungsmodul.py
Normal file
61
app/stiftung/migrations/0044_veranstaltungsmodul.py
Normal file
@@ -0,0 +1,61 @@
|
||||
# Generated by Django 5.0.6 on 2026-03-10 21:47
|
||||
|
||||
import django.db.models.deletion
|
||||
import uuid
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('stiftung', '0043_destinataer_email_eingang'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Veranstaltung',
|
||||
fields=[
|
||||
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||
('titel', models.CharField(max_length=200, verbose_name='Titel')),
|
||||
('datum', models.DateField(verbose_name='Datum')),
|
||||
('uhrzeit', models.TimeField(blank=True, null=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(choices=[('geplant', 'Geplant'), ('einladungen_versendet', 'Einladungen versendet'), ('abgeschlossen', 'Abgeschlossen'), ('abgesagt', 'Abgesagt')], default='geplant', max_length=30, verbose_name='Status')),
|
||||
('budget_pro_person', models.DecimalField(blank=True, decimal_places=2, help_text='Geschätztes Budget je Teilnehmer in €', max_digits=8, null=True, verbose_name='Budget pro Person (€)')),
|
||||
('briefvorlage', models.TextField(blank=True, help_text='HTML/Text-Template für Serienbrief. Platzhalter: {{ anrede }}, {{ vorname }}, {{ nachname }}, {{ strasse }}, {{ plz }}, {{ ort }}, {{ datum }}, {{ uhrzeit }}, {{ veranstaltungsort }}, {{ gasthaus_adresse }}', verbose_name='Briefvorlage')),
|
||||
('erstellt_am', models.DateTimeField(auto_now_add=True)),
|
||||
('aktualisiert_am', models.DateTimeField(auto_now=True)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Veranstaltung',
|
||||
'verbose_name_plural': 'Veranstaltungen',
|
||||
'ordering': ['-datum'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Veranstaltungsteilnehmer',
|
||||
fields=[
|
||||
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||
('anrede', models.CharField(blank=True, choices=[('Herr', 'Herr'), ('Frau', 'Frau'), ('', 'Keine Anrede')], max_length=10, verbose_name='Anrede')),
|
||||
('vorname', models.CharField(max_length=100, verbose_name='Vorname')),
|
||||
('nachname', models.CharField(max_length=100, verbose_name='Nachname')),
|
||||
('strasse', models.CharField(blank=True, max_length=200, verbose_name='Straße')),
|
||||
('plz', models.CharField(blank=True, max_length=10, verbose_name='PLZ')),
|
||||
('ort', models.CharField(blank=True, max_length=100, verbose_name='Ort')),
|
||||
('email', models.EmailField(blank=True, help_text='Optional, für späteren E-Mail-Versand', max_length=254, verbose_name='E-Mail')),
|
||||
('rsvp_status', models.CharField(choices=[('eingeladen', 'Eingeladen'), ('zugesagt', 'Zugesagt'), ('abgesagt', 'Abgesagt'), ('keine_rueckmeldung', 'Keine Rückmeldung')], default='eingeladen', max_length=20, verbose_name='RSVP-Status')),
|
||||
('bemerkungen', models.TextField(blank=True, verbose_name='Bemerkungen')),
|
||||
('erstellt_am', models.DateTimeField(auto_now_add=True)),
|
||||
('destinataer', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='stiftung.destinataer', verbose_name='Destinatär (optional)')),
|
||||
('paechter', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='stiftung.paechter', verbose_name='Pächter (optional)')),
|
||||
('veranstaltung', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='teilnehmer', to='stiftung.veranstaltung', verbose_name='Veranstaltung')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Veranstaltungsteilnehmer',
|
||||
'verbose_name_plural': 'Veranstaltungsteilnehmer',
|
||||
'ordering': ['nachname', 'vorname'],
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,43 @@
|
||||
# Generated by Django 5.0.6 on 2026-03-10 22:31
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('stiftung', '0044_veranstaltungsmodul'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='veranstaltung',
|
||||
name='betreff',
|
||||
field=models.CharField(blank=True, help_text='Betreffzeile des Serienbriefs. Leer = Standardbetreff.', max_length=300, verbose_name='Betreff'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='veranstaltung',
|
||||
name='unterschrift_1_name',
|
||||
field=models.CharField(blank=True, default='Katrin Kleinpaß', max_length=100, verbose_name='Unterschrift 1 – Name'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='veranstaltung',
|
||||
name='unterschrift_1_titel',
|
||||
field=models.CharField(blank=True, default='Rentmeisterin', max_length=100, verbose_name='Unterschrift 1 – Titel'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='veranstaltung',
|
||||
name='unterschrift_2_name',
|
||||
field=models.CharField(blank=True, default='Jan Remmer Siebels', max_length=100, verbose_name='Unterschrift 2 – Name'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='veranstaltung',
|
||||
name='unterschrift_2_titel',
|
||||
field=models.CharField(blank=True, default='Rentmeister', max_length=100, verbose_name='Unterschrift 2 – Titel'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='vierteljahresnachweis',
|
||||
name='faelligkeitsdatum',
|
||||
field=models.DateField(blank=True, help_text='Veraltet - wird durch studiennachweis_faelligkeitsdatum und zahlung_faelligkeitsdatum ersetzt', null=True, verbose_name='Fälligkeitsdatum'),
|
||||
),
|
||||
]
|
||||
@@ -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)
|
||||
|
||||
@@ -359,6 +359,14 @@ urlpatterns = [
|
||||
views.paperless_document_redirect,
|
||||
name="paperless_document_redirect",
|
||||
),
|
||||
# Veranstaltungsmodul
|
||||
path("veranstaltungen/", views.veranstaltung_list, name="veranstaltung_list"),
|
||||
path("veranstaltungen/<uuid:pk>/", views.veranstaltung_detail, name="veranstaltung_detail"),
|
||||
path(
|
||||
"veranstaltungen/<uuid:pk>/serienbrief/",
|
||||
views.veranstaltung_serienbrief_pdf,
|
||||
name="veranstaltung_serienbrief_pdf",
|
||||
),
|
||||
# Gramps integration (probe)
|
||||
path("api/gramps/search/", views.gramps_search_api, name="gramps_search_api"),
|
||||
path("api/gramps/debug/", views.gramps_debug_api, name="gramps_debug_api"),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -25,9 +25,9 @@
|
||||
--orange-dark: #e8590c;
|
||||
}
|
||||
|
||||
/* Global Typography - More Compact */
|
||||
/* Global Typography */
|
||||
html {
|
||||
font-size: 14px; /* Reduced from default 16px */
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
body {
|
||||
@@ -69,26 +69,6 @@
|
||||
padding-right: 0.5rem;
|
||||
}
|
||||
|
||||
/* Compact margins */
|
||||
.mb-1 { margin-bottom: 0.25rem !important; }
|
||||
.mb-2 { margin-bottom: 0.4rem !important; }
|
||||
.mb-3 { margin-bottom: 0.75rem !important; }
|
||||
.mb-4 { margin-bottom: 1rem !important; }
|
||||
.mb-5 { margin-bottom: 1.5rem !important; }
|
||||
|
||||
.mt-1 { margin-top: 0.25rem !important; }
|
||||
.mt-2 { margin-top: 0.4rem !important; }
|
||||
.mt-3 { margin-top: 0.75rem !important; }
|
||||
.mt-4 { margin-top: 1rem !important; }
|
||||
.mt-5 { margin-top: 1.5rem !important; }
|
||||
|
||||
.py-1 { padding-top: 0.25rem !important; padding-bottom: 0.25rem !important; }
|
||||
.py-2 { padding-top: 0.4rem !important; padding-bottom: 0.4rem !important; }
|
||||
.py-3 { padding-top: 0.75rem !important; padding-bottom: 0.75rem !important; }
|
||||
|
||||
.px-1 { padding-left: 0.25rem !important; padding-right: 0.25rem !important; }
|
||||
.px-2 { padding-left: 0.4rem !important; padding-right: 0.4rem !important; }
|
||||
.px-3 { padding-left: 0.75rem !important; padding-right: 0.75rem !important; }
|
||||
|
||||
.border-left-primary {
|
||||
border-left: 0.25rem solid var(--racing-green) !important;
|
||||
@@ -209,19 +189,19 @@
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* Tables - More Compact */
|
||||
/* Tables */
|
||||
.table {
|
||||
font-size: 0.8rem;
|
||||
font-size: 0.85rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
|
||||
.table th {
|
||||
border-top: none;
|
||||
font-weight: 600;
|
||||
color: var(--racing-green-dark);
|
||||
background-color: var(--grey-light);
|
||||
padding: 0.5rem;
|
||||
font-size: 0.75rem;
|
||||
font-size: 0.8rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.025em;
|
||||
}
|
||||
@@ -375,47 +355,61 @@
|
||||
color: var(--orange-dark);
|
||||
}
|
||||
|
||||
/* Responsive adjustments for very compact design */
|
||||
/* Responsive adjustments */
|
||||
@media (max-width: 768px) {
|
||||
html {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
|
||||
body {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
|
||||
.navbar-brand {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
|
||||
.navbar-nav .nav-link {
|
||||
font-size: 0.75rem;
|
||||
padding: 0.25rem 0.375rem !important;
|
||||
}
|
||||
|
||||
|
||||
.card-body {
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
|
||||
.btn {
|
||||
font-size: 0.75rem;
|
||||
padding: 0.25rem 0.5rem;
|
||||
}
|
||||
|
||||
|
||||
.table {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
|
||||
.table th,
|
||||
.table td {
|
||||
padding: 0.375rem;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@media (min-width: 768px) and (max-width: 1200px) {
|
||||
html {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.container, .container-lg {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.table {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1400px) {
|
||||
.container-lg {
|
||||
max-width: 1400px;
|
||||
max-width: 1600px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -638,6 +632,23 @@
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
<!-- Veranstaltungen -->
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link dropdown-toggle" href="#" id="veranstaltungenDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
<i class="fas fa-glass-cheers me-1"></i>Veranstaltungen
|
||||
</a>
|
||||
<ul class="dropdown-menu" aria-labelledby="veranstaltungenDropdown">
|
||||
<li><a class="dropdown-item" href="{% url 'stiftung:veranstaltung_list' %}">
|
||||
<i class="fas fa-list me-2"></i>Alle Veranstaltungen
|
||||
</a></li>
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
<li><h6 class="dropdown-header">Aktionen</h6></li>
|
||||
<li><a class="dropdown-item" href="{% url 'stiftung:veranstaltung_list' %}">
|
||||
<i class="fas fa-envelope-open-text me-2"></i>Serienbrief drucken
|
||||
</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
<!-- Geschichte -->
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{% url 'stiftung:geschichte_list' %}">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
199
app/templates/stiftung/veranstaltung/detail.html
Normal file
199
app/templates/stiftung/veranstaltung/detail.html
Normal 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 %}
|
||||
· {{ 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 %}
|
||||
75
app/templates/stiftung/veranstaltung/list.html
Normal file
75
app/templates/stiftung/veranstaltung/list.html
Normal 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 %}
|
||||
197
app/templates/stiftung/veranstaltung/serienbrief_pdf.html
Normal file
197
app/templates/stiftung/veranstaltung/serienbrief_pdf.html
Normal 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 · 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>
|
||||
@@ -7,6 +7,7 @@ services:
|
||||
POSTGRES_PASSWORD: postgres_dev
|
||||
volumes:
|
||||
- dbdata_dev:/var/lib/postgresql/data
|
||||
- ./scripts/init-paperless-db.sh:/docker-entrypoint-initdb.d/init-paperless-db.sh
|
||||
ports:
|
||||
- "5433:5432"
|
||||
healthcheck:
|
||||
@@ -40,7 +41,7 @@ services:
|
||||
- TIME_ZONE=Europe/Berlin
|
||||
- REDIS_URL=redis://redis:6379/0
|
||||
- PAPERLESS_API_URL=http://paperless:8000
|
||||
- PAPERLESS_API_TOKEN=d477152aca264ea00620910ac09a06f0a4faaecc
|
||||
- PAPERLESS_API_TOKEN=1972509e25810d9ae7497c1c79ecfea9e942f18d
|
||||
- PAPERLESS_REQUIRED_TAG=Stiftung_Destinatäre
|
||||
- PAPERLESS_LAND_TAG=Stiftung_Land_und_Pächter
|
||||
- PAPERLESS_ADMIN_TAG=Stiftung_Administration
|
||||
@@ -64,12 +65,12 @@ services:
|
||||
- PAPERLESS_REDIS=redis://redis:6379
|
||||
- PAPERLESS_DBHOST=db
|
||||
- PAPERLESS_DBPORT=5432
|
||||
- PAPERLESS_DBNAME=stiftung_dev
|
||||
- PAPERLESS_DBNAME=paperless_dev
|
||||
- PAPERLESS_DBUSER=postgres
|
||||
- PAPERLESS_DBPASS=postgres_dev
|
||||
- PAPERLESS_SECRET_KEY=dev-paperless-secret-key
|
||||
- PAPERLESS_URL=http://localhost:8082
|
||||
- PAPERLESS_ALLOWED_HOSTS=localhost,127.0.0.1
|
||||
- PAPERLESS_ALLOWED_HOSTS=localhost,127.0.0.1,paperless
|
||||
- PAPERLESS_CORS_ALLOWED_HOSTS=http://localhost:8082,http://localhost:8081
|
||||
- PAPERLESS_ADMIN_USER=admin
|
||||
- PAPERLESS_ADMIN_PASSWORD=admin123
|
||||
|
||||
@@ -15,6 +15,7 @@ services:
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||
volumes:
|
||||
- dbdata:/var/lib/postgresql/data
|
||||
- ./scripts/init-paperless-db.sh:/docker-entrypoint-initdb.d/init-paperless-db.sh
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
|
||||
interval: 10s
|
||||
@@ -157,12 +158,12 @@ services:
|
||||
- PAPERLESS_REDIS=redis://redis:6379
|
||||
- PAPERLESS_DBHOST=db
|
||||
- PAPERLESS_DBPORT=5432
|
||||
- PAPERLESS_DBNAME=${POSTGRES_DB}
|
||||
- PAPERLESS_DBNAME=${PAPERLESS_DBNAME:-paperless}
|
||||
- PAPERLESS_DBUSER=${POSTGRES_USER}
|
||||
- PAPERLESS_DBPASS=${POSTGRES_PASSWORD}
|
||||
- PAPERLESS_SECRET_KEY=${PAPERLESS_SECRET_KEY}
|
||||
- PAPERLESS_URL=https://vhtv-stiftung.de
|
||||
- PAPERLESS_ALLOWED_HOSTS=vhtv-stiftung.de,localhost
|
||||
- PAPERLESS_ALLOWED_HOSTS=vhtv-stiftung.de,localhost,paperless
|
||||
- PAPERLESS_CORS_ALLOWED_HOSTS=https://vhtv-stiftung.de
|
||||
- PAPERLESS_FORCE_SCRIPT_NAME=/paperless
|
||||
- PAPERLESS_STATIC_URL=/paperless/static/
|
||||
|
||||
@@ -16,6 +16,7 @@ services:
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||
volumes:
|
||||
- dbdata:/var/lib/postgresql/data
|
||||
- ./scripts/init-paperless-db.sh:/docker-entrypoint-initdb.d/init-paperless-db.sh
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
|
||||
interval: 10s
|
||||
@@ -125,12 +126,12 @@ services:
|
||||
- PAPERLESS_REDIS=redis://redis:6379
|
||||
- PAPERLESS_DBHOST=db
|
||||
- PAPERLESS_DBPORT=5432
|
||||
- PAPERLESS_DBNAME=${POSTGRES_DB}
|
||||
- PAPERLESS_DBNAME=${PAPERLESS_DBNAME:-paperless}
|
||||
- PAPERLESS_DBUSER=${POSTGRES_USER}
|
||||
- PAPERLESS_DBPASS=${POSTGRES_PASSWORD}
|
||||
- PAPERLESS_SECRET_KEY=${PAPERLESS_SECRET_KEY}
|
||||
- PAPERLESS_URL=https://vhtv-stiftung.de
|
||||
- PAPERLESS_ALLOWED_HOSTS=vhtv-stiftung.de,localhost
|
||||
- PAPERLESS_ALLOWED_HOSTS=vhtv-stiftung.de,localhost,paperless
|
||||
- PAPERLESS_CORS_ALLOWED_HOSTS=https://vhtv-stiftung.de
|
||||
- PAPERLESS_FORCE_SCRIPT_NAME=/paperless
|
||||
- PAPERLESS_STATIC_URL=/paperless/static/
|
||||
|
||||
@@ -38,7 +38,7 @@ CSRF_COOKIE_NAME=stiftung_csrftoken
|
||||
REDIS_URL=redis://redis:6379/0
|
||||
|
||||
# PAPERLESS CONFIGURATION
|
||||
PAPERLESS_API_URL=http://paperless:8000/api
|
||||
PAPERLESS_API_URL=http://paperless:8000
|
||||
PAPERLESS_API_TOKEN=your_paperless_api_token_here
|
||||
PAPERLESS_SECRET_KEY=your_paperless_secret_key_here
|
||||
PAPERLESS_ADMIN_USER=admin
|
||||
|
||||
11
scripts/init-paperless-db.sh
Executable file
11
scripts/init-paperless-db.sh
Executable file
@@ -0,0 +1,11 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# Create separate database for Paperless-NGX if it doesn't exist
|
||||
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL
|
||||
SELECT 'CREATE DATABASE paperless_dev'
|
||||
WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = 'paperless_dev')\gexec
|
||||
|
||||
SELECT 'CREATE DATABASE paperless'
|
||||
WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = 'paperless')\gexec
|
||||
EOSQL
|
||||
Reference in New Issue
Block a user