Compare commits

...

2 Commits

Author SHA1 Message Date
SysAdmin Agent
28621d2774 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>
2026-03-10 22:36:58 +00:00
SysAdmin Agent
f8f9dc3319 feat: Memory-Konzept für Agents implementieren (STI-21)
- REST API: 9 Read-Only-Endpunkte unter /api/v1/ für alle Kernmodelle
  (Destinatäre, Ländereien, Pächter, Förderungen, Konten,
  Verpachtungen, Verwaltungskosten, Kalender, Transaktionen)
- Token-Authentifizierung via DRF TokenAuthentication
- Management-Command `create_agent_token` für Agent-Tokens
- Wissensbasis: knowledge/ mit Satzung, Richtlinien, Verfahren,
  Kontakte, Historie
- Agent-Instructions: Datenzugriff-Sektion in AGENTS.md dokumentiert

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-09 22:45:11 +00:00
36 changed files with 2296 additions and 69 deletions

5
.gitignore vendored
View File

@@ -138,4 +138,7 @@ dev-debug.log
# Task files # Task files
# tasks.json # tasks.json
# tasks/ # tasks/
# Claude Code local config
.claude/

30
agents/dog/AGENTS.md Normal file
View 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

View File

@@ -162,7 +162,32 @@ Nutze für die Prüfung möglichst dieses Raster:
- Protokollentwurf - Protokollentwurf
- Wenn eine Frage unklar ist, nenne zuerst die Annahmen, auf denen deine Antwort beruht. - Wenn eine Frage unklar ist, nenne zuerst die Annahmen, auf denen deine Antwort beruht.
### 11. KLARE GRENZEN ### 11. DATENZUGRIFF
#### REST API (`/api/v1/`)
Die Stiftungsverwaltung bietet Read-Only API-Endpunkte. Authentifizierung über Token (`Authorization: Token <token>`).
| Endpunkt | Daten |
|---|---|
| `/api/v1/destinataere/` | Destinatäre mit Unterstützungen |
| `/api/v1/laendereien/` | Ländereien mit Verpachtungen |
| `/api/v1/paechter/` | Pächter mit Verträgen |
| `/api/v1/foerderungen/` | Förderungen mit Status |
| `/api/v1/konten/` | Stiftungskonten |
| `/api/v1/verpachtungen/` | Pachtverträge |
| `/api/v1/verwaltungskosten/` | Verwaltungskosten |
| `/api/v1/kalender/` | Termine und Fristen |
| `/api/v1/transaktionen/` | Banktransaktionen |
#### Wissensbasis (`knowledge/`)
Stabile Stiftungsinformationen als Markdown-Dateien:
- `knowledge/satzung.md` — Stiftungssatzung und Zwecke
- `knowledge/richtlinien.md` — Förderrichtlinien
- `knowledge/verfahren.md` — Verwaltungsverfahren und Abläufe
- `knowledge/kontakte.md` — Wichtige Kontakte
- `knowledge/historie.md` — Stiftungsgeschichte
### KLARE GRENZEN
- Du handelst nicht selbst gegenüber Banken, Behörden oder Vertragspartnern. - Du handelst nicht selbst gegenüber Banken, Behörden oder Vertragspartnern.
- Du gibst keine finalen rechtlichen Freigaben. - Du gibst keine finalen rechtlichen Freigaben.
- Du bestätigst keine Gemeinnützigkeitskonformität mit Verbindlichkeit. - Du bestätigst keine Gemeinnützigkeitskonformität mit Verbindlichkeit.

View File

@@ -53,6 +53,33 @@ Du bist Systemadministrator einer gemeinnützigen deutschen Familienstiftung. Du
- Erfinde keine Fakten. Benenne Unsicherheiten und offene Punkte klar. - Erfinde keine Fakten. Benenne Unsicherheiten und offene Punkte klar.
- Eskaliere bei Unklarheiten oder potenziellen Risiken. - Eskaliere bei Unklarheiten oder potenziellen Risiken.
## Datenzugriff
### REST API (`/api/v1/`)
Die Stiftungsverwaltung bietet Read-Only API-Endpunkte für alle Kernmodelle. Authentifizierung über Token (`Authorization: Token <token>`).
| Endpunkt | Daten |
|---|---|
| `/api/v1/destinataere/` | Destinatäre mit Unterstützungen |
| `/api/v1/laendereien/` | Ländereien mit Verpachtungen |
| `/api/v1/paechter/` | Pächter mit Verträgen |
| `/api/v1/foerderungen/` | Förderungen mit Status |
| `/api/v1/konten/` | Stiftungskonten |
| `/api/v1/verpachtungen/` | Pachtverträge |
| `/api/v1/verwaltungskosten/` | Verwaltungskosten |
| `/api/v1/kalender/` | Termine und Fristen |
| `/api/v1/transaktionen/` | Banktransaktionen |
Token erstellen: `python manage.py create_agent_token <username>`
### Wissensbasis (`knowledge/`)
Stabile Stiftungsinformationen als Markdown-Dateien:
- `knowledge/satzung.md` — Stiftungssatzung und Zwecke
- `knowledge/richtlinien.md` — Förderrichtlinien
- `knowledge/verfahren.md` — Verwaltungsverfahren und Abläufe
- `knowledge/kontakte.md` — Wichtige Kontakte
- `knowledge/historie.md` — Stiftungsgeschichte
## Grenzen ## Grenzen
- Du triffst keine eigenständigen Entscheidungen über Architektur oder Technologieauswahl ohne Rücksprache. - Du triffst keine eigenständigen Entscheidungen über Architektur oder Technologieauswahl ohne Rücksprache.

View File

@@ -34,6 +34,7 @@ INSTALLED_APPS = [
"django.contrib.staticfiles", "django.contrib.staticfiles",
"django.contrib.humanize", "django.contrib.humanize",
"rest_framework", "rest_framework",
"rest_framework.authtoken",
"django_otp", "django_otp",
"django_otp.plugins.otp_totp", "django_otp.plugins.otp_totp",
"django_otp.plugins.otp_static", "django_otp.plugins.otp_static",
@@ -99,6 +100,16 @@ STATICFILES_DIRS = [
] ]
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
REST_FRAMEWORK = {
"DEFAULT_AUTHENTICATION_CLASSES": [
"rest_framework.authentication.SessionAuthentication",
"rest_framework.authentication.TokenAuthentication",
],
"DEFAULT_PERMISSION_CLASSES": [
"rest_framework.permissions.IsAuthenticated",
],
}
MEDIA_URL = "/media/" MEDIA_URL = "/media/"
MEDIA_ROOT = BASE_DIR / "media" MEDIA_ROOT = BASE_DIR / "media"

View File

@@ -7,6 +7,7 @@ from django.urls import include, path
from stiftung.views import home from stiftung.views import home
urlpatterns = [ urlpatterns = [
path("api/v1/", include("stiftung.api_urls")),
path("", include("stiftung.urls")), path("", include("stiftung.urls")),
path("admin/", admin.site.urls), path("admin/", admin.site.urls),
# Authentication URLs # Authentication URLs

View File

@@ -4,6 +4,7 @@ celery==5.3.6
redis==5.0.7 redis==5.0.7
djangorestframework==3.15.2 djangorestframework==3.15.2
weasyprint==62.3 weasyprint==62.3
pydyf==0.11.0
python-dotenv==1.0.1 python-dotenv==1.0.1
requests==2.32.3 requests==2.32.3
gunicorn==22.0.0 gunicorn==22.0.0

View File

@@ -11,6 +11,7 @@ from .models import (AppConfiguration, AuditLog, BackupJob, BankTransaction,
DestinataerUnterstuetzung, DestinataerUnterstuetzung,
DokumentLink, Foerderung, Land, LandVerpachtung, Paechter, Person, DokumentLink, Foerderung, Land, LandVerpachtung, Paechter, Person,
Rentmeister, StiftungsKonto, UnterstuetzungWiederkehrend, Rentmeister, StiftungsKonto, UnterstuetzungWiederkehrend,
Veranstaltung, Veranstaltungsteilnehmer,
Verwaltungskosten, VierteljahresNachweis) Verwaltungskosten, VierteljahresNachweis)
@@ -1228,6 +1229,109 @@ class DestinataerEmailEingangAdmin(admin.ModelAdmin):
mark_verarbeitet.short_description = "Als verarbeitet markieren" 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 # Customize admin site
admin.site.site_header = "Stiftungsverwaltung Administration" admin.site.site_header = "Stiftungsverwaltung Administration"
admin.site.site_title = "Stiftungsverwaltung Admin" admin.site.site_title = "Stiftungsverwaltung Admin"

View File

@@ -0,0 +1,110 @@
from rest_framework import serializers
from .models import (
BankTransaction,
Destinataer,
DestinataerUnterstuetzung,
Foerderung,
Land,
LandVerpachtung,
Paechter,
StiftungsKalenderEintrag,
StiftungsKonto,
Veranstaltung,
Veranstaltungsteilnehmer,
Verwaltungskosten,
)
class DestinataerUnterstuetzungSerializer(serializers.ModelSerializer):
class Meta:
model = DestinataerUnterstuetzung
fields = "__all__"
class DestinataerSerializer(serializers.ModelSerializer):
unterstuetzungen = DestinataerUnterstuetzungSerializer(many=True, read_only=True)
class Meta:
model = Destinataer
fields = "__all__"
class LandVerpachtungSerializer(serializers.ModelSerializer):
class Meta:
model = LandVerpachtung
fields = "__all__"
class LandSerializer(serializers.ModelSerializer):
aktive_verpachtungen = serializers.SerializerMethodField()
class Meta:
model = Land
fields = "__all__"
def get_aktive_verpachtungen(self, obj):
qs = obj.neue_verpachtungen.filter(status="aktiv")
return LandVerpachtungSerializer(qs, many=True).data
class PaechterSerializer(serializers.ModelSerializer):
aktive_verpachtungen = serializers.SerializerMethodField()
class Meta:
model = Paechter
fields = "__all__"
def get_aktive_verpachtungen(self, obj):
qs = obj.neue_verpachtungen.filter(status="aktiv")
return LandVerpachtungSerializer(qs, many=True).data
class FoerderungSerializer(serializers.ModelSerializer):
class Meta:
model = Foerderung
fields = "__all__"
class StiftungsKontoSerializer(serializers.ModelSerializer):
class Meta:
model = StiftungsKonto
fields = "__all__"
class VerwaltungskostenSerializer(serializers.ModelSerializer):
class Meta:
model = Verwaltungskosten
fields = "__all__"
class StiftungsKalenderEintragSerializer(serializers.ModelSerializer):
class Meta:
model = StiftungsKalenderEintrag
fields = "__all__"
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__"

28
app/stiftung/api_urls.py Normal file
View File

@@ -0,0 +1,28 @@
from rest_framework.routers import DefaultRouter
from .api_views import (
BankTransactionViewSet,
DestinataerViewSet,
FoerderungViewSet,
LandVerpachtungViewSet,
LandViewSet,
PaechterViewSet,
StiftungsKalenderEintragViewSet,
StiftungsKontoViewSet,
VeranstaltungViewSet,
VerwaltungskostenViewSet,
)
router = DefaultRouter()
router.register(r"destinataere", DestinataerViewSet)
router.register(r"laendereien", LandViewSet)
router.register(r"paechter", PaechterViewSet)
router.register(r"foerderungen", FoerderungViewSet)
router.register(r"konten", StiftungsKontoViewSet)
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

76
app/stiftung/api_views.py Normal file
View File

@@ -0,0 +1,76 @@
from rest_framework.viewsets import ReadOnlyModelViewSet
from .api_serializers import (
BankTransactionSerializer,
DestinataerSerializer,
FoerderungSerializer,
LandSerializer,
LandVerpachtungSerializer,
PaechterSerializer,
StiftungsKalenderEintragSerializer,
StiftungsKontoSerializer,
VeranstaltungSerializer,
VerwaltungskostenSerializer,
)
from .models import (
BankTransaction,
Destinataer,
Foerderung,
Land,
LandVerpachtung,
Paechter,
StiftungsKalenderEintrag,
StiftungsKonto,
Veranstaltung,
Verwaltungskosten,
)
class DestinataerViewSet(ReadOnlyModelViewSet):
queryset = Destinataer.objects.all()
serializer_class = DestinataerSerializer
class LandViewSet(ReadOnlyModelViewSet):
queryset = Land.objects.all()
serializer_class = LandSerializer
class PaechterViewSet(ReadOnlyModelViewSet):
queryset = Paechter.objects.all()
serializer_class = PaechterSerializer
class FoerderungViewSet(ReadOnlyModelViewSet):
queryset = Foerderung.objects.all()
serializer_class = FoerderungSerializer
class StiftungsKontoViewSet(ReadOnlyModelViewSet):
queryset = StiftungsKonto.objects.all()
serializer_class = StiftungsKontoSerializer
class LandVerpachtungViewSet(ReadOnlyModelViewSet):
queryset = LandVerpachtung.objects.all()
serializer_class = LandVerpachtungSerializer
class VerwaltungskostenViewSet(ReadOnlyModelViewSet):
queryset = Verwaltungskosten.objects.all()
serializer_class = VerwaltungskostenSerializer
class StiftungsKalenderEintragViewSet(ReadOnlyModelViewSet):
queryset = StiftungsKalenderEintrag.objects.all()
serializer_class = StiftungsKalenderEintragSerializer
class BankTransactionViewSet(ReadOnlyModelViewSet):
queryset = BankTransaction.objects.all()
serializer_class = BankTransactionSerializer
class VeranstaltungViewSet(ReadOnlyModelViewSet):
queryset = Veranstaltung.objects.all()
serializer_class = VeranstaltungSerializer

View File

@@ -0,0 +1,28 @@
from django.contrib.auth import get_user_model
from django.core.management.base import BaseCommand, CommandError
from rest_framework.authtoken.models import Token
class Command(BaseCommand):
help = "Erstellt oder gibt ein API-Token für einen Django-User aus (für Agent-Zugriff)"
def add_arguments(self, parser):
parser.add_argument("username", type=str, help="Django-Username")
def handle(self, *args, **options):
User = get_user_model()
username = options["username"]
try:
user = User.objects.get(username=username)
except User.DoesNotExist:
raise CommandError(f'User "{username}" nicht gefunden.')
token, created = Token.objects.get_or_create(user=user)
if created:
self.stdout.write(self.style.SUCCESS(f"Neues Token erstellt für {username}:"))
else:
self.stdout.write(self.style.WARNING(f"Bestehendes Token für {username}:"))
self.stdout.write(token.key)

View 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'],
},
),
]

View File

@@ -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'),
),
]

View File

@@ -671,7 +671,7 @@ class Land(models.Model):
def get_verpachtungsgrad_neu(self): def get_verpachtungsgrad_neu(self):
"""Berechnet den Verpachtungsgrad basierend auf neuen Verpachtungen""" """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 (self.get_verpachtete_flaeche_aktuell() / self.groesse_qm) * 100
return 0 return 0
@@ -3279,3 +3279,178 @@ class DestinataerEmailEingang(models.Model):
f"{base}/documents/{doc_id}/" f"{base}/documents/{doc_id}/"
for doc_id in (self.paperless_dokument_ids or []) 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)

View File

@@ -359,6 +359,14 @@ urlpatterns = [
views.paperless_document_redirect, views.paperless_document_redirect,
name="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) # Gramps integration (probe)
path("api/gramps/search/", views.gramps_search_api, name="gramps_search_api"), path("api/gramps/search/", views.gramps_search_api, name="gramps_search_api"),
path("api/gramps/debug/", views.gramps_debug_api, name="gramps_debug_api"), path("api/gramps/debug/", views.gramps_debug_api, name="gramps_debug_api"),

View File

@@ -31,7 +31,8 @@ from .models import (AppConfiguration, CSVImport, Destinataer,
DestinataerEmailEingang, DestinataerUnterstuetzung, DestinataerEmailEingang, DestinataerUnterstuetzung,
DokumentLink, Foerderung, Land, DokumentLink, Foerderung, Land,
LandAbrechnung, LandVerpachtung, Paechter, Person, LandAbrechnung, LandVerpachtung, Paechter, Person,
StiftungsKonto, UnterstuetzungWiederkehrend, VierteljahresNachweis) StiftungsKonto, UnterstuetzungWiederkehrend, Veranstaltung,
Veranstaltungsteilnehmer, VierteljahresNachweis)
def get_pdf_generator(): 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) return Response({"error": "Paperless API not configured"}, status=400)
# Remove /api suffix if present, then construct the document URL # 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) # For external Paperless (already includes /paperless/ in base URL)
return redirect(f"{base_url}/documents/{doc_id}/details/") 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) return render(request, "stiftung/verpachtung_list.html", context)
@login_required
@login_required @login_required
def land_verpachtung_detail(request, pk): def land_verpachtung_detail(request, pk):
"""Detail view for LandVerpachtung""" """Detail view for LandVerpachtung"""
@@ -2197,7 +2197,7 @@ def dokument_list(request):
available_dokumente = [] available_dokumente = []
if url and token: if url and token:
try: 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}"} headers = {"Authorization": f"Token {token}"}
# Alle verfügbaren Dokumente abrufen (mit Paginierung) # Alle verfügbaren Dokumente abrufen (mit Paginierung)
@@ -2553,7 +2553,7 @@ def paperless_ping(_request):
) )
try: try:
# Entferne /api vom Ende der URL falls vorhanden # 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( r = requests.get(
f"{base_url}/api/tags/", f"{base_url}/api/tags/",
headers={"Authorization": f"Token {token}"}, headers={"Authorization": f"Token {token}"},
@@ -2598,7 +2598,7 @@ def paperless_documents(request):
try: try:
# Entferne /api vom Ende der URL falls vorhanden # 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}"} headers = {"Authorization": f"Token {token}"}
def fetch_tagged(): def fetch_tagged():
@@ -2735,7 +2735,7 @@ def paperless_debug(request):
try: try:
# Entferne /api vom Ende der URL falls vorhanden # 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}"} headers = {"Authorization": f"Token {token}"}
@@ -2857,7 +2857,7 @@ def paperless_tags_only(request):
try: try:
# Entferne /api vom Ende der URL falls vorhanden # 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) # Alle Tags abrufen (mit großer page_size)
headers = {"Authorization": f"Token {token}"} headers = {"Authorization": f"Token {token}"}
@@ -4147,7 +4147,6 @@ def verwaltungskosten_create(request):
rentmeister = Rentmeister.objects.get(pk=rentmeister_id) rentmeister = Rentmeister.objects.get(pk=rentmeister_id)
initial_data["rentmeister"] = rentmeister initial_data["rentmeister"] = rentmeister
redirect_url = "stiftung:rentmeister_detail" redirect_url = "stiftung:rentmeister_detail"
redirect_args = [rentmeister_id]
except Rentmeister.DoesNotExist: except Rentmeister.DoesNotExist:
pass pass
@@ -8440,13 +8439,6 @@ def kalender_admin(request):
} }
return render(request, 'stiftung/kalender/admin.html', context) 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 @login_required
@@ -8642,3 +8634,54 @@ def email_eingang_poll_trigger(request):
except Exception as exc: except Exception as exc:
messages.error(request, f"Fehler beim Starten des Tasks: {exc}") messages.error(request, f"Fehler beim Starten des Tasks: {exc}")
return redirect("email_eingang_list") 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

View File

@@ -25,9 +25,9 @@
--orange-dark: #e8590c; --orange-dark: #e8590c;
} }
/* Global Typography - More Compact */ /* Global Typography */
html { html {
font-size: 14px; /* Reduced from default 16px */ font-size: 15px;
} }
body { body {
@@ -69,26 +69,6 @@
padding-right: 0.5rem; 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-primary {
border-left: 0.25rem solid var(--racing-green) !important; border-left: 0.25rem solid var(--racing-green) !important;
@@ -209,19 +189,19 @@
margin-bottom: 0; margin-bottom: 0;
} }
/* Tables - More Compact */ /* Tables */
.table { .table {
font-size: 0.8rem; font-size: 0.85rem;
margin-bottom: 0.75rem; margin-bottom: 0.75rem;
} }
.table th { .table th {
border-top: none; border-top: none;
font-weight: 600; font-weight: 600;
color: var(--racing-green-dark); color: var(--racing-green-dark);
background-color: var(--grey-light); background-color: var(--grey-light);
padding: 0.5rem; padding: 0.5rem;
font-size: 0.75rem; font-size: 0.8rem;
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 0.025em; letter-spacing: 0.025em;
} }
@@ -375,47 +355,61 @@
color: var(--orange-dark); color: var(--orange-dark);
} }
/* Responsive adjustments for very compact design */ /* Responsive adjustments */
@media (max-width: 768px) { @media (max-width: 768px) {
html { html {
font-size: 13px; font-size: 13px;
} }
body { body {
font-size: 0.8rem; font-size: 0.8rem;
} }
.navbar-brand { .navbar-brand {
font-size: 1rem; font-size: 1rem;
} }
.navbar-nav .nav-link { .navbar-nav .nav-link {
font-size: 0.75rem; font-size: 0.75rem;
padding: 0.25rem 0.375rem !important; padding: 0.25rem 0.375rem !important;
} }
.card-body { .card-body {
padding: 0.5rem; padding: 0.5rem;
} }
.btn { .btn {
font-size: 0.75rem; font-size: 0.75rem;
padding: 0.25rem 0.5rem; padding: 0.25rem 0.5rem;
} }
.table { .table {
font-size: 0.75rem; font-size: 0.75rem;
} }
.table th, .table th,
.table td { .table td {
padding: 0.375rem; 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) { @media (min-width: 1400px) {
.container-lg { .container-lg {
max-width: 1400px; max-width: 1600px;
} }
} }
@@ -638,6 +632,23 @@
</ul> </ul>
</li> </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 --> <!-- Geschichte -->
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="{% url 'stiftung:geschichte_list' %}"> <a class="nav-link" href="{% url 'stiftung:geschichte_list' %}">

View File

@@ -25,6 +25,10 @@
</div> </div>
<div class="col-md-4 text-end"> <div class="col-md-4 text-end">
<div class="btn-group" role="group"> <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"> <a href="{% url 'stiftung:land_update' land.pk %}" class="btn btn-warning">
<i class="fas fa-edit me-2"></i>Bearbeiten <i class="fas fa-edit me-2"></i>Bearbeiten
</a> </a>

View File

@@ -90,7 +90,7 @@
<div class="small text-muted">Grünland</div> <div class="small text-muted">Grünland</div>
</div> </div>
</div> </div>
<div style="height: 200px;"> <div style="min-height: 200px;">
<canvas id="usageChart"></canvas> <canvas id="usageChart"></canvas>
</div> </div>
</div> </div>
@@ -110,7 +110,7 @@
<div class="h6 mb-0">{{ stats.total_plots }}</div> <div class="h6 mb-0">{{ stats.total_plots }}</div>
<div class="small text-muted">Grundstücke gesamt</div> <div class="small text-muted">Grundstücke gesamt</div>
</div> </div>
<div style="height: 200px;"> <div style="min-height: 200px;">
<canvas id="sizesChart"></canvas> <canvas id="sizesChart"></canvas>
</div> </div>
</div> </div>
@@ -136,7 +136,7 @@
<div class="small text-muted">Verfügbar</div> <div class="small text-muted">Verfügbar</div>
</div> </div>
</div> </div>
<div style="height: 200px;"> <div style="min-height: 200px;">
<canvas id="verpachtungChart"></canvas> <canvas id="verpachtungChart"></canvas>
</div> </div>
</div> </div>
@@ -245,15 +245,19 @@
</td> </td>
<td> <td>
<div class="btn-group" role="group"> <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"> class="btn btn-sm btn-outline-primary" title="Anzeigen">
<i class="fas fa-eye"></i> <i class="fas fa-eye"></i>
</a> </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"> class="btn btn-sm btn-outline-warning" title="Bearbeiten">
<i class="fas fa-edit"></i> <i class="fas fa-edit"></i>
</a> </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"> class="btn btn-sm btn-outline-danger" title="Löschen">
<i class="fas fa-trash"></i> <i class="fas fa-trash"></i>
</a> </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>

View File

@@ -7,6 +7,7 @@ services:
POSTGRES_PASSWORD: postgres_dev POSTGRES_PASSWORD: postgres_dev
volumes: volumes:
- dbdata_dev:/var/lib/postgresql/data - dbdata_dev:/var/lib/postgresql/data
- ./scripts/init-paperless-db.sh:/docker-entrypoint-initdb.d/init-paperless-db.sh
ports: ports:
- "5433:5432" - "5433:5432"
healthcheck: healthcheck:
@@ -40,7 +41,7 @@ services:
- TIME_ZONE=Europe/Berlin - TIME_ZONE=Europe/Berlin
- REDIS_URL=redis://redis:6379/0 - REDIS_URL=redis://redis:6379/0
- PAPERLESS_API_URL=http://paperless:8000 - PAPERLESS_API_URL=http://paperless:8000
- PAPERLESS_API_TOKEN=d477152aca264ea00620910ac09a06f0a4faaecc - PAPERLESS_API_TOKEN=1972509e25810d9ae7497c1c79ecfea9e942f18d
- PAPERLESS_REQUIRED_TAG=Stiftung_Destinatäre - PAPERLESS_REQUIRED_TAG=Stiftung_Destinatäre
- PAPERLESS_LAND_TAG=Stiftung_Land_und_Pächter - PAPERLESS_LAND_TAG=Stiftung_Land_und_Pächter
- PAPERLESS_ADMIN_TAG=Stiftung_Administration - PAPERLESS_ADMIN_TAG=Stiftung_Administration
@@ -64,12 +65,12 @@ services:
- PAPERLESS_REDIS=redis://redis:6379 - PAPERLESS_REDIS=redis://redis:6379
- PAPERLESS_DBHOST=db - PAPERLESS_DBHOST=db
- PAPERLESS_DBPORT=5432 - PAPERLESS_DBPORT=5432
- PAPERLESS_DBNAME=stiftung_dev - PAPERLESS_DBNAME=paperless_dev
- PAPERLESS_DBUSER=postgres - PAPERLESS_DBUSER=postgres
- PAPERLESS_DBPASS=postgres_dev - PAPERLESS_DBPASS=postgres_dev
- PAPERLESS_SECRET_KEY=dev-paperless-secret-key - PAPERLESS_SECRET_KEY=dev-paperless-secret-key
- PAPERLESS_URL=http://localhost:8082 - 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_CORS_ALLOWED_HOSTS=http://localhost:8082,http://localhost:8081
- PAPERLESS_ADMIN_USER=admin - PAPERLESS_ADMIN_USER=admin
- PAPERLESS_ADMIN_PASSWORD=admin123 - PAPERLESS_ADMIN_PASSWORD=admin123

View File

@@ -15,6 +15,7 @@ services:
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
volumes: volumes:
- dbdata:/var/lib/postgresql/data - dbdata:/var/lib/postgresql/data
- ./scripts/init-paperless-db.sh:/docker-entrypoint-initdb.d/init-paperless-db.sh
healthcheck: healthcheck:
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"] test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
interval: 10s interval: 10s
@@ -157,12 +158,12 @@ services:
- PAPERLESS_REDIS=redis://redis:6379 - PAPERLESS_REDIS=redis://redis:6379
- PAPERLESS_DBHOST=db - PAPERLESS_DBHOST=db
- PAPERLESS_DBPORT=5432 - PAPERLESS_DBPORT=5432
- PAPERLESS_DBNAME=${POSTGRES_DB} - PAPERLESS_DBNAME=${PAPERLESS_DBNAME:-paperless}
- PAPERLESS_DBUSER=${POSTGRES_USER} - PAPERLESS_DBUSER=${POSTGRES_USER}
- PAPERLESS_DBPASS=${POSTGRES_PASSWORD} - PAPERLESS_DBPASS=${POSTGRES_PASSWORD}
- PAPERLESS_SECRET_KEY=${PAPERLESS_SECRET_KEY} - PAPERLESS_SECRET_KEY=${PAPERLESS_SECRET_KEY}
- PAPERLESS_URL=https://vhtv-stiftung.de - 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_CORS_ALLOWED_HOSTS=https://vhtv-stiftung.de
- PAPERLESS_FORCE_SCRIPT_NAME=/paperless - PAPERLESS_FORCE_SCRIPT_NAME=/paperless
- PAPERLESS_STATIC_URL=/paperless/static/ - PAPERLESS_STATIC_URL=/paperless/static/

View File

@@ -16,6 +16,7 @@ services:
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
volumes: volumes:
- dbdata:/var/lib/postgresql/data - dbdata:/var/lib/postgresql/data
- ./scripts/init-paperless-db.sh:/docker-entrypoint-initdb.d/init-paperless-db.sh
healthcheck: healthcheck:
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"] test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
interval: 10s interval: 10s
@@ -66,6 +67,15 @@ services:
- DJANGO_SECRET_KEY=${DJANGO_SECRET_KEY} - DJANGO_SECRET_KEY=${DJANGO_SECRET_KEY}
- DJANGO_DEBUG=${DJANGO_DEBUG} - DJANGO_DEBUG=${DJANGO_DEBUG}
- REDIS_URL=${REDIS_URL} - REDIS_URL=${REDIS_URL}
- IMAP_HOST=${IMAP_HOST}
- IMAP_PORT=${IMAP_PORT}
- IMAP_USER=${IMAP_USER}
- IMAP_PASSWORD=${IMAP_PASSWORD}
- IMAP_FOLDER=${IMAP_FOLDER}
- IMAP_USE_SSL=${IMAP_USE_SSL}
- PAPERLESS_API_URL=${PAPERLESS_API_URL}
- PAPERLESS_API_TOKEN=${PAPERLESS_API_TOKEN}
- PAPERLESS_DESTINATAERE_TAG_ID=${PAPERLESS_DESTINATAERE_TAG_ID}
depends_on: depends_on:
- redis - redis
- db - db
@@ -83,6 +93,15 @@ services:
- DJANGO_SECRET_KEY=${DJANGO_SECRET_KEY} - DJANGO_SECRET_KEY=${DJANGO_SECRET_KEY}
- DJANGO_DEBUG=${DJANGO_DEBUG} - DJANGO_DEBUG=${DJANGO_DEBUG}
- REDIS_URL=${REDIS_URL} - REDIS_URL=${REDIS_URL}
- IMAP_HOST=${IMAP_HOST}
- IMAP_PORT=${IMAP_PORT}
- IMAP_USER=${IMAP_USER}
- IMAP_PASSWORD=${IMAP_PASSWORD}
- IMAP_FOLDER=${IMAP_FOLDER}
- IMAP_USE_SSL=${IMAP_USE_SSL}
- PAPERLESS_API_URL=${PAPERLESS_API_URL}
- PAPERLESS_API_TOKEN=${PAPERLESS_API_TOKEN}
- PAPERLESS_DESTINATAERE_TAG_ID=${PAPERLESS_DESTINATAERE_TAG_ID}
depends_on: depends_on:
- redis - redis
- db - db
@@ -107,12 +126,12 @@ services:
- PAPERLESS_REDIS=redis://redis:6379 - PAPERLESS_REDIS=redis://redis:6379
- PAPERLESS_DBHOST=db - PAPERLESS_DBHOST=db
- PAPERLESS_DBPORT=5432 - PAPERLESS_DBPORT=5432
- PAPERLESS_DBNAME=${POSTGRES_DB} - PAPERLESS_DBNAME=${PAPERLESS_DBNAME:-paperless}
- PAPERLESS_DBUSER=${POSTGRES_USER} - PAPERLESS_DBUSER=${POSTGRES_USER}
- PAPERLESS_DBPASS=${POSTGRES_PASSWORD} - PAPERLESS_DBPASS=${POSTGRES_PASSWORD}
- PAPERLESS_SECRET_KEY=${PAPERLESS_SECRET_KEY} - PAPERLESS_SECRET_KEY=${PAPERLESS_SECRET_KEY}
- PAPERLESS_URL=https://vhtv-stiftung.de - 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_CORS_ALLOWED_HOSTS=https://vhtv-stiftung.de
- PAPERLESS_FORCE_SCRIPT_NAME=/paperless - PAPERLESS_FORCE_SCRIPT_NAME=/paperless
- PAPERLESS_STATIC_URL=/paperless/static/ - PAPERLESS_STATIC_URL=/paperless/static/

View File

@@ -38,7 +38,7 @@ CSRF_COOKIE_NAME=stiftung_csrftoken
REDIS_URL=redis://redis:6379/0 REDIS_URL=redis://redis:6379/0
# PAPERLESS CONFIGURATION # PAPERLESS CONFIGURATION
PAPERLESS_API_URL=http://paperless:8000/api PAPERLESS_API_URL=http://paperless:8000
PAPERLESS_API_TOKEN=your_paperless_api_token_here PAPERLESS_API_TOKEN=your_paperless_api_token_here
PAPERLESS_SECRET_KEY=your_paperless_secret_key_here PAPERLESS_SECRET_KEY=your_paperless_secret_key_here
PAPERLESS_ADMIN_USER=admin PAPERLESS_ADMIN_USER=admin

30
knowledge/README.md Normal file
View File

@@ -0,0 +1,30 @@
# Wissensbasis van Hees-Theyssen-Vogel'sche Stiftung
Dieses Verzeichnis enthält langfristig stabiles Stiftungswissen als Referenz für alle Agents und Mitarbeiter.
## Dateien
| Datei | Inhalt |
|---|---|
| [satzung.md](satzung.md) | Stiftungsname, Zweck, Förderberechtigte, Organe |
| [richtlinien.md](richtlinien.md) | Förderrichtlinien, Einkommensgrenzen, Nachweisfristen |
| [verfahren.md](verfahren.md) | Verwaltungsabläufe: Anträge, E-Mail, Pacht, Backup |
| [kontakte.md](kontakte.md) | Institutionelle Kontakte (Behörden, Berater) |
| [historie.md](historie.md) | Stiftungsgeschichte und Meilensteine |
## Wichtige Hinweise
- **Keine personenbezogenen Daten** von Destinatären in dieser Wissensbasis
- Angaben mit `[TODO]` müssen manuell aus Originalunterlagen ergänzt werden
- Operative Daten (Destinatäre, Zahlungen, Ländereien) befinden sich in der Datenbank
- Diese Dateien sind Strukturwissen, nicht Echtzeitdaten
## Pflege
Diese Dateien sollten aktualisiert werden bei:
- Satzungsänderungen
- Änderungen der Förderkriterien
- Wechsel externer Dienstleister/Behörden
- Wesentlichen Änderungen der Verwaltungsverfahren
*Erstellt: 2026-03 durch RentmeisterAI*

View File

@@ -0,0 +1,163 @@
# Bedienungsanleitung: E-Mail-Eingangsverarbeitung für Destinatäre
> VHTV-Stiftung | Stand: März 2026
---
## 1. Überblick
Das E-Mail-Eingangssystem verarbeitet automatisch eingehende E-Mails von Destinatären. Eingehende Nachrichten an **paperless@vhtv-stiftung.de** werden alle 15 Minuten abgerufen, dem richtigen Destinatär zugeordnet und Anhänge in Paperless-NGX archiviert.
**Typische Inhalte:** Studien- und Ausbildungsnachweise, Quartalsbelege, Anträge, allgemeine Korrespondenz.
---
## 2. So funktioniert der Workflow
```
E-Mail eingehend
Automatischer Abruf (alle 15 Min.)
Absender-Zuordnung ──► Bekannter Destinatär? ──► Ja: Status "zugewiesen"
│ Anhänge → Paperless
▼ Nein
Status "unbekannt" → Manuelle Zuordnung nötig
```
1. **E-Mail-Eingang:** Jan Siebels leitet Destinatär-Emails von jan.siebels@gmail.com an paperless@vhtv-stiftung.de weiter.
2. **Automatische Zuordnung:** Das System gleicht die Absender-E-Mail mit den hinterlegten E-Mail-Adressen der aktiven Destinatäre ab.
3. **Anhänge:** Werden automatisch in Paperless-NGX hochgeladen und mit dem Tag `Stiftung_Destinatäre` versehen.
4. **Unbekannte Absender:** Erhalten den Status `unbekannt` und müssen manuell zugeordnet werden.
---
## 3. E-Mail-Eingang aufrufen
Öffnen Sie im Hauptmenü der Stiftungsverwaltung:
**Pfad:** `/email-eingang/`
### 3.1 Listenansicht
Die Übersicht zeigt:
- **Statuskarten** oben: Gesamtzahl, Neue, Unbekannte, Fehler
- **Tabelle** mit allen eingegangenen E-Mails:
- Eingangsdatum
- Absender (Name und E-Mail)
- Zugeordneter Destinatär
- Betreff
- Anzahl Anhänge
- Status
**Filtern und Suchen:**
- **Suchfeld:** Durchsucht Absender, Betreff und Destinatär-Namen
- **Status-Filter:** Dropdown zur Einschränkung nach Status (Neu, Zugewiesen, Verarbeitet, Unbekannt, Fehler)
### 3.2 Detailansicht
Klicken Sie auf eine E-Mail, um die Details zu sehen (`/email-eingang/<id>/`):
- Vollständiger E-Mail-Text
- Anhänge mit Direktlinks zu Paperless-NGX
- Zuordnungsinformationen
- Fehlermeldungen (falls vorhanden)
---
## 4. Häufige Aufgaben
### 4.1 Unbekannten Absender zuordnen
1. Öffnen Sie die E-Mail mit Status `unbekannt`
2. Im Seitenbereich **"Destinatär zuordnen"**: Wählen Sie den richtigen Destinatär aus dem Dropdown
3. Klicken Sie **"Zuordnen"**
4. Der Status wechselt automatisch zu `zugewiesen`
### 4.2 E-Mail als verarbeitet markieren
1. Öffnen Sie die zugewiesene E-Mail
2. Im Seitenbereich **"Als verarbeitet markieren"**:
- Optional: Notizen zur Bearbeitung eintragen
3. Klicken Sie **"Verarbeitet"**
4. Der Status wechselt zu `verarbeitet`
### 4.3 Interne Notizen hinzufügen
1. Öffnen Sie eine E-Mail
2. Im Bereich **"Interne Notizen"**: Text eingeben
3. Klicken Sie **"Speichern"**
Notizen sind nur intern sichtbar und dienen der Dokumentation der Bearbeitung.
### 4.4 E-Mails manuell abrufen
Falls Sie nicht auf den nächsten automatischen Abruf warten möchten:
1. In der Listenansicht: Klicken Sie **"Jetzt abrufen"**
2. Der Abruf startet im Hintergrund
3. Neue E-Mails erscheinen nach dem Neuladen der Seite
---
## 5. Status-Übersicht
| Status | Bedeutung | Aktion erforderlich? |
|---|---|---|
| **Neu** | Gerade eingegangen, Destinatär zugeordnet | Inhalt prüfen, ggf. verarbeiten |
| **Zugewiesen** | Destinatär wurde zugeordnet | Inhalt prüfen, dann verarbeiten |
| **Verarbeitet** | Bearbeitung abgeschlossen | Keine |
| **Unbekannt** | Absender konnte nicht zugeordnet werden | Manuell zuordnen |
| **Fehler** | Technischer Fehler bei Verarbeitung | SysAdmin informieren |
---
## 6. Anhänge und Paperless-NGX
Alle E-Mail-Anhänge werden automatisch in Paperless-NGX gespeichert:
- **Tag:** `Stiftung_Destinatäre`
- **Korrespondent:** Name des zugeordneten Destinatärs
- **Zugriff:** Direktlinks in der Detailansicht der E-Mail
Um Anhänge in Paperless einzusehen, klicken Sie auf den jeweiligen Dokumentlink in der Detailansicht.
---
## 7. Empfohlener Bearbeitungsablauf
1. **Täglich** die Listenansicht öffnen (`/email-eingang/`)
2. **Statuskarten** prüfen: Gibt es neue oder unbekannte E-Mails?
3. **Unbekannte Absender** zuerst zuordnen
4. **Neue E-Mails** inhaltlich prüfen:
- Handelt es sich um einen Studiennachweis? → Zum Quartalsnachweis verknüpfen
- Allgemeine Korrespondenz? → Notizen ergänzen
5. **Als verarbeitet markieren** wenn erledigt
---
## 8. Fehlerbehebung
| Problem | Lösung |
|---|---|
| Keine neuen E-Mails trotz Weiterleitung | "Jetzt abrufen" klicken; ggf. SysAdmin kontaktieren |
| Anhänge fehlen in Paperless | Paperless-API-Verbindung prüfen (SysAdmin) |
| Status "Fehler" | Fehlerdetails in der Detailansicht lesen; SysAdmin informieren |
| Falscher Destinatär zugeordnet | In Detailansicht korrekten Destinatär neu zuordnen |
---
## 9. Technische Hinweise (für Administratoren)
- **E-Mail-Postfach:** paperless@vhtv-stiftung.de (IMAP, SSL, Port 993)
- **Polling-Intervall:** Alle 15 Minuten via Celery Beat
- **Duplikaterkennung:** Basierend auf Absender, Datum und Betreff
- **Konfiguration:** Umgebungsvariablen in der Docker-Compose-Datei
---
*VHTV-Stiftung | Bedienungsanleitung E-Mail-Eingang | Stand: März 2026*

View File

@@ -0,0 +1,124 @@
%PDF-1.4
%<25><><EFBFBD><EFBFBD> ReportLab Generated PDF document (opensource)
1 0 obj
<<
/F1 2 0 R /F2 3 0 R /F3 4 0 R /F4 6 0 R
>>
endobj
2 0 obj
<<
/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font
>>
endobj
3 0 obj
<<
/BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding /Name /F2 /Subtype /Type1 /Type /Font
>>
endobj
4 0 obj
<<
/BaseFont /Helvetica-BoldOblique /Encoding /WinAnsiEncoding /Name /F3 /Subtype /Type1 /Type /Font
>>
endobj
5 0 obj
<<
/Contents 12 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 11 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
6 0 obj
<<
/BaseFont /Symbol /Name /F4 /Subtype /Type1 /Type /Font
>>
endobj
7 0 obj
<<
/Contents 13 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 11 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
8 0 obj
<<
/Contents 14 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 11 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
9 0 obj
<<
/PageMode /UseNone /Pages 11 0 R /Type /Catalog
>>
endobj
10 0 obj
<<
/Author (\(anonymous\)) /CreationDate (D:20260309224153+00'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260309224153+00'00') /Producer (ReportLab PDF Library - \(opensource\))
/Subject (\(unspecified\)) /Title (\(anonymous\)) /Trapped /False
>>
endobj
11 0 obj
<<
/Count 3 /Kids [ 5 0 R 7 0 R 8 0 R ] /Type /Pages
>>
endobj
12 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1781
>>
stream
Gau0DD/Yn7&H88.1$U.\A.pV(Ce(C%;^HifF?TNTdj`^ZD$I=(/Em5X27alZZcc"W0/62T6jZV=jdG;"H"Ifr6i(EWpG)qt3JZWr%%%Lq!17Q40`M.9o:rKZXLuuf/T5B")2q'qMbUl6h++Auk-S;4d0cs]e:W$?$eiBY]qB6.jk.YZ]]!g%kN>&$?D9)Lk&Q`-)55.8drUn+Jb^rQBO=@k5190X/G?#n`NhZ')T1]W'A]TVkEp39)I;fZE5=t"3#?VS$.KD6Xqh0n#d0]&X5FE89Lta\3NfEe6Mgbq@?W0VjQJ8L4Dr#W$[<(,#+a_UpZ]s#dpSJu^u5#!Af6s7$!\;qG@1quY7$q1!tU#gqK[9q9hMuTNdn]*P%KCb>]4@O-IW;"&]&;@nt%f9@;7J(XrPJfTNX`m\NV"&B/a=94i+Dr,FHSePN>11TIP#VGK*S6:d=oEAf*n+"IK59ZR"\$8=(JB?%sLp+3PU%\4PaNIRjjllg(PNHC'!s.^9C<Pe\T4s-r3]W89DH7MT@#^:4KURmC4-DNua-_sbDn.UOhA77afi(II7rdDZGm*h_kP:Ec*<M%sp$X7>!,.nl0\.Df?dE%45'Sm[24n9]k$E;B>`i+a-e)hBKF%dPj<Gl.-['&W"`Ds9"VG-+n:GUl-&AU(KqqCKgtGmk&$H+FQLYFS&o\[Ntbm)-l6X,KR5![Csthkr5.3\pZ%-sL+R,2UN65#D[9l\)'sG?r'):Oct*NSO%7.j`r%=TUlAf9o1*QB)YG_<44gK.+:t&1_h58\&,1<5tCFhQF5&D;S2jbQO)H.7?Vh[5)rF*nLSiKhEpJY'mS's0e4b9P%5I]"ac6Ht[BI6#*kToll+.DhiggD^,O;kIU<V>r64nRM@H^+r[D9JPmOa(TFhV@_pCsZ-[5X#_gKEUara$3r5$);kHsWDDSS>b(?*W2aDDC/kidMa"lW?@%t)umH?d$hU_2O/NX<'A[oU5lO1*2'NE")kOftFHY,J7WlFk\NXjomVJSNI2Vj&gHS5Qj_[O'0%/_Oj\E8s=X**6qi`m?7PX]8"qcTLW)-1)"?NjnPd'JUl46A<=."[2m(HnNdS8@aPrQT&!LSsqeV,l>L-uB%7!=stMG^I'P!BI>'gC"(>`iMGDn:p,X=c?8-D^qk]3^$*lP(^Ej[o2-$[;?AW5^qJhZ"kF$^R]s(#8euRq3!*0*s=T]j>,8?dc"WC]:0l=0L'A9hMSa4h_fQ=;Oh?n-OZ.dn/>4HB6QIsqJ_9OFmJoSl,,"[,?,\6[jLRA".sZ"1"#\'VEIE[@'UUU[>(W$U7;QdaMtPKmapRVDu4Do?=Kcc#l9OsX'\p'i@+*XSsf>Xo&RtJ^rYE\NH1KddTh!iqbToO`c]ueqNAm)Wm;aT)(_AD?@u=2r,RRVSuLf):MqHrU=,esFVQ\Fb-g))l.N='oBu7u<G3%K).c>$.^U5i8<\A'pJ.PROX]Q@X`c8GSlQ@:/Ne"&lXEc*?]0R+MrS`piHURZX61iN,%U$?ST\LM>c_3$BbKB'&k/eA4mCF%7o^",NrO:\QVH9`4h\+5dXH-^'X"c5aE:+CefJ!UJ/KGT^%*kEHDF5g$kfD7VaQga;W43%Ra"`s!bLDaqSIbC5OO4+NE:&*$@+lC5^U@U7@)aU7!;-!I?dM#20tIfNKHUfdoM6Z:X/Md)1@!GS+e/P#t3Bb@o)U`T\/4'dA)iOT''r!1VLDpV^fB&WF3*%7%:&g;Eo`YqJ7!K@F9)@.h$I*e-u8?YAZf48,i\.<`30~>endstream
endobj
13 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1861
>>
stream
Gau0D>BALf'Roe[30T+-ai#sH&jgU[4Cf):cG`KbBfd2E3l7u:#&1?=<Sd63HWF>+&AecoN'X]%bBNC3&V2+>8G7<J!PG#?fd-Yc@HIZs$k$Yhi9fmlre0L'2Do<;\cg%Ha<7YQi]=`3$V@lFY)72+>+=n;<4`UG<uf#k1uNf'Dq@[uPV[RX*+n)9n29%?9>O$qlP[nq*Q+&Z0U85''6mooX+Z0>[V<!WRg/51Z_Uij?"P`(im7DRLY<q0e-2]rHrIjG]/5)#>Ra;eJRqZ&Hhu.M+a@Dss01.6(Q;!6+nFVMB<d22pj?g?HG5Z:?RS[A:CNV[eFn<aosVe_V#dI0ome_QO#.h6i&ZIh&m?h_?@KL99JJQ+&oF&Q3W+%Jq*oTPgh\7nU*P[*)1+Xkh:^)Y_"W32*u3YhraV#EU]&5dCEZi>VCI7+ENr7:\N.+p.A5Y4hkP_!kFg*.GCf,1ZV.@`Ci_YCopm*^K\&r9mtaq2mrr\3A,f.)/78iRq;'4ORd$B[L4K+>P`1Y]W25cHTY$,HKpW_nEU*9QjiDS*2!)-dZJl+\?kdo26$_R;_nEI8W*WX1h`E0oZ@Pj0WD4<Jn-8WYW&]r1034dj,P[NS.gL&DQ:;tLBMCAd&,:Tt@Q6:FNFDSXqpgPEG)3r>?,LYe#ChC\mG-P>f![N9hJVMfCVs"e6WAr2g!IiW3&5T$3LuY:KfL_`kMmaiVH([p>(hiOfY&I5,oWIj<-nnKG`<U90,e`*0Y&&HWf(^'[uH+[/*)=&Adqm[Q;^M)c'BoGW<i4DUp[1c[7\ER@NPt2Mp[)V(X5t&0b-HinpPCXg'LDJ\\_%PA9iD)*e&p3k\,h6_NT&iZJqsIAbBDR"4s#JMj9&Si%A&UdoT`WJT'&<O4_4$%<fJ%f-Y@eLKi)@@XL^D:VDkD>tC<R&:KXDq$+GGC*-EU0RQ%t9oh"MYiKsBDN[a4BsmNF.L06EBKkSef)%=6mtRM`:)8)Mb"k>-;D2VGPok,Uh:?l0*^ohj9=>11gkdAM1dfD6GbPNH**PVN("<2GaYMiW\A5&!G"LM<QH9D[_e]PQ5Gg)DKIO&0oI()iohD9e.^KSX3DuK5hefPB_fBj$NB5%O;iTJLQsG\S`L'YbPlT!+SG[&uIPTH+Zo7ck1DNfc9SFK#aNlCBVW0jC3Nrs[Lm<j@bY`o@%R,/^Ql9+jSDINLolsarm>eJI+/`d_4DpGPR2;C^enD-egfJLpG9]8U*p?)c8^Y6/`%apQ2AkpM[W1/K&U#TSWFZ*@-k4Np&("oYT6V\qbl0?krANFi4D&@HEj8@nr%jN/L.S1bRUftSI@Ddo()9'Ml)AG_RF@3+GT5K7)pF?E-*.iE`*7n$l9?mQ*SX)i=mTm-5-I'n:irLE,1Z6BLbH3T[VfpDK/4::D,(hh5`ThBJ)1;&kZHcM%6UP%CW2`2$[PAGcJaX6Ygl<1Y`usnGu\%4f:jii/p^naO2Z"FZ%&_a'ICC<_DXi3/GBnC/qi:fKN9&NNRF-D:(%udR$R<29$ui7;jjB'#iXUB*Z.[*?kS1pMmE>kT"MKJI@Q#$a,2ZfMf\ZE8R8b0'ED)E>4S=VCI/s4nB!Hs)3(7>o/MK%AIGqUMe:Gdp$aHk'$dFWj5p2(hk_e"Vg,NR=k(8,LZQ/Z.M3u/^?^mkWm8DdF<J^R$8UJZ/c;T<"&7)PZuj3+T3q$8NI9:?oCF,-kDojSjfMG&En;6R4_t4EB^WSETZ2n;AMZc;.sBDO9eW\peE#*O.J>V,/s`M=BnWP,eHj/XjBk+`c=P0^.0&-5UY[jZZ`9T3q.>0g<DWAsI]Nk.8'c7nn*K*3cCm_dGZmmuR_fTY,B<u!-iX;82Ve3~>endstream
endobj
14 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 802
>>
stream
Gau0A968fP%)2<`F<mF8G\uJ2:5(g!Zse\)2^^3s/Ah[=:"O/9c1K%X?Hf/km;PU5N#98+8Y9$;Yj#c^H^4aBA%9$[^`8//@%QIGquAZqk=::bT"\_1OV9GTqN8`@5GC+mgW$>WSL-^7&i`TF.<k:n5b(49Q=o;qJI8Qbe3\1=FZiO:CGj.!,nfkg`6T!EBSAPA2Sm)&:sDGKALV4!%Eh'e>\]M*PS,K=CQchmUif8$l$;]!+`'@"5'<qj]QBrp=\b#f6O"3%GlC04bFN[Wg2!3'0?;rEeZr&$2><S7Fa$Q.gVNIl]$:Rt?%j$iF9U$W<6u[tA;+G+_B-Ntl&#"[HHU$N[H*'S[[Wh:9ut;U"3`X0LGeJTaFY7A$Roh^iD.H1'ICW'RnhdiGSqd;+JVtB(CK9rm!tV@_"4<gG^kYUGVob=#1ea;>sm;*-$6%9#f+*h!&`?q9lEE%e/K4#:AJ^Gg"<d]?!-V3WXR)Aj*IEN4W:A#=)r1)FWL4O_>"usPD=2r(J7%/\L/+("RfO4\B$EKkIl5A;6>,0*LAM%1'H0`OcWRk5h:P)'kXCn_@X7A!OH^Z/*?/4Q&-e7^e9oT3dQa1a@h(@(CBR\Ku$\Z5>duh2:kh<($ULP'rL4%kWSZGd=3TU.cqHTXsi?co'cAoT;\W-c'@3_))YYi8EsJ^ba,2R=%0hQ9'fD(-r,<]f`,7b5'-be;WpX;E%1G8qLZ.l0*Q:#'GuN#^Okn6?2W?E1oCV.&VZ(WRfAPo%#*(eL[p13RN&/]jtaK#O]gkN7%3/FRKf>?fdimH~>endstream
endobj
xref
0 15
0000000000 65535 f
0000000061 00000 n
0000000122 00000 n
0000000229 00000 n
0000000341 00000 n
0000000460 00000 n
0000000665 00000 n
0000000742 00000 n
0000000947 00000 n
0000001152 00000 n
0000001221 00000 n
0000001502 00000 n
0000001574 00000 n
0000003447 00000 n
0000005400 00000 n
trailer
<<
/ID
[<bb45e303f875ee9fae4b75f06d3984e4><bb45e303f875ee9fae4b75f06d3984e4>]
% ReportLab generated PDF document -- digest (opensource)
/Info 10 0 R
/Root 9 0 R
/Size 15
>>
startxref
6293
%%EOF

90
knowledge/historie.md Normal file
View File

@@ -0,0 +1,90 @@
# Stiftungsgeschichte
> **Status:** Grundstruktur angelegt. Inhalte aus der Datenbank-Tabelle `GeschichteSeite` sind zu entnehmen.
> Die Verwaltungssoftware enthält ein Wiki-ähnliches Modul (`Geschichte`) unter `/geschichte/`,
> in dem die Stiftungsgeschichte gepflegt wird.
---
## Hinweis zur Datenpflege
Die Stiftungsgeschichte wird primär in der **Django-App** gepflegt:
- **URL:** `/geschichte/`
- **Modell:** `GeschichteSeite` (Markdown-Seiten) + `GeschichteBild` (Bilder)
- Seiten sind sortierbar und können mit Bildern versehen werden
- Bearbeitungsrecht erfordert Berechtigung `change_geschichteseite`
Diese Datei dient als **statischer Referenzpunkt** für Agents. Für aktuelle und detaillierte Inhalte ist die Datenbank maßgeblich.
---
## Stiftungsname und Familienhintergrund
Die **van Hees-Theyssen-Vogel'sche Stiftung** (VHTV) ist eine Familienstiftung, deren Name auf die Gründerfamilien(zweige) zurückgeht:
- van Hees
- Theyssen
- Vogel
[TODO: Herkunft und Bedeutung dieser Familiennamen aus Stiftungsunterlagen ergänzen]
---
## Gründungsgeschichte
[TODO: Folgende Angaben aus der Datenbank (GeschichteSeite) oder Stiftungsunterlagen entnehmen]
- **Gründungsjahr:** [TODO]
- **Gründer(in):** [TODO]
- **Gründungsanlass:** [TODO]
- **Ursprüngliches Stiftungsvermögen:** [TODO]
- **Ursprünglicher Stiftungszweck:** [TODO]
---
## Historische Entwicklung
[TODO: Wichtige Meilensteine in chronologischer Reihenfolge aus Stiftungsunterlagen eintragen]
| Jahr | Ereignis |
|---|---|
| [TODO] | Gründung der Stiftung |
| [TODO] | [Weitere Meilensteine] |
| [TODO] | Erste digitale Verwaltung |
| 2024/2025 | Einführung der modernen Django-Verwaltungssoftware |
| 2025 | Implementierung der automatischen E-Mail-Verarbeitung für Destinatäre |
| 2026 | Einführung KI-gestützter Stiftungsverwaltung (RentmeisterAI) |
---
## Ländereien und Vermögensgeschichte
[TODO: Geschichte des Landbesitzes beschreiben]
- Die Stiftung verwaltet landwirtschaftliche Ländereien in der Region Hamminkeln/Kreis Wesel (NRW)
- Nutzungsarten: Grünland, Acker, Wald
- Die Ländereien werden verpachtet; der Pachtzins bildet einen wesentlichen Teil der Erträge
---
## Digitalisierung der Verwaltung
Die Stiftungsverwaltung wurde sukzessive modernisiert:
- **Frühere Verwaltung:** [TODO: Wie wurde früher verwaltet?]
- **2024:** Entwicklung der Django-basierten Verwaltungssoftware
- **2025:** Integration von Paperless-NGX zur Dokumentenverwaltung
- **2025:** Automatische E-Mail-Verarbeitung für Destinatärs-Eingänge
- **2026:** KI-Unterstützung durch RentmeisterAI (Paperclip-Plattform)
---
## Zugriff auf ausführliche Geschichte
Die vollständige Stiftungsgeschichte mit Bildern und Artikeln ist in der Anwendung abrufbar:
1. In der Verwaltungsoberfläche → Menüpunkt **Geschichte**
2. URL: `https://vhtv-stiftung.de/geschichte/`
---
*Zuletzt aktualisiert: 2026-03 | Pflege der ausführlichen Geschichte: Verwaltungs-App unter /geschichte/*

105
knowledge/kontakte.md Normal file
View File

@@ -0,0 +1,105 @@
# Wichtige Kontakte der Stiftung
> **Datenschutzhinweis:** Diese Datei enthält ausschließlich institutionelle Kontakte (Behörden, externe Stellen).
> **Keine personenbezogenen Daten von Destinatären** werden hier gespeichert.
> Personenbezogene Daten von Rentmeistern und externen Dienstleistern nur in anonymisierter Form.
---
## 1. Stiftung (Eigene Kontaktdaten)
| Feld | Wert |
|---|---|
| Name | van Hees-Theyssen-Vogel'sche Stiftung |
| Adresse | Raesfelder Str. 3, 46499 Hamminkeln |
| E-Mail (Paperless/Eingang) | paperless@vhtv-stiftung.de |
| Website | https://vhtv-stiftung.de |
---
## 2. Rentmeister / Geschäftsführung
Die aktuell aktiven Rentmeister sind im System unter `/rentmeister/` (Geschäftsführung) einsehbar.
> **Hinweis:** Aus Datenschutzgründen werden hier keine Namen oder persönlichen Kontaktdaten gespeichert. Die aktuellen Rentmeister sind in der Datenbank (`Rentmeister`-Tabelle) hinterlegt.
---
## 3. Steuerberater
[TODO: Kanzleiname, Adresse, Telefon, E-Mail eintragen]
- **Kanzlei:** [TODO]
- **Ansprechpartner:** [TODO]
- **Adresse:** [TODO]
- **Telefon:** [TODO]
- **E-Mail:** [TODO]
- **Zuständig für:** Jahresabschluss, Gemeinnützigkeitsrecht, Steuererklärungen
---
## 4. Notar
[TODO: Notariat und Kontakt eintragen]
- **Notariat:** [TODO]
- **Ansprechpartner:** [TODO]
- **Adresse:** [TODO]
- **Telefon:** [TODO]
- **Zuständig für:** Satzungsänderungen, Grundstücksangelegenheiten
---
## 5. Bankverbindungen
Die Bankkonten der Stiftung sind im System unter `/konten/` (Geschäftsführung → Konten) einsehbar.
> **Sicherheitshinweis:** Kontodaten (IBAN, BIC) werden ausschließlich in der Datenbank gespeichert, nicht in dieser Datei.
[TODO: Hauptbank und Kontoart eintragen (ohne IBAN)]
- **Hauptbank:** [TODO z. B. Sparkasse, Volksbank, etc.]
- **Konto-Typ:** [TODO]
---
## 6. Stiftungsaufsicht
- **Behörde:** [TODO Bezirksregierung Düsseldorf? Hamminkeln liegt im Kreis Wesel, Regierungsbezirk Düsseldorf]
- **Referat:** [TODO]
- **Adresse:** [TODO]
- **Telefon:** [TODO]
- **Aktenzeichen:** [TODO]
---
## 7. Finanzamt
- **Finanzamt:** [TODO zuständiges Finanzamt für Hamminkeln/Kreis Wesel]
- **Steuernummer:** [TODO]
- **Freistellungsbescheid:** [TODO Datum des letzten Freistellungsbescheids]
---
## 8. Amtsgericht / Grundbuch
Für Grundstücksangelegenheiten zuständig:
- **Amtsgericht:** [TODO Amtsgericht Wesel? Hamminkeln liegt im Kreis Wesel]
- **Grundbuchamt:** [TODO]
- **Kontakt:** [TODO]
---
## 9. Sonstige externe Stellen
### Landwirtschaft / Ländereien
- **Landwirtschaftskammer NRW:** [TODO]
- **Katasteramt Kreis Wesel:** [TODO]
### IT / Hosting
- **Server-Hosting:** [TODO Hoster für vhtv-stiftung.de]
- **Paperless-NGX Installation:** https://vhtv-stiftung.de/paperless
---
*Zuletzt aktualisiert: 2026-03 | Hinweis: Alle mit [TODO] markierten Felder sind manuell zu ergänzen*

142
knowledge/richtlinien.md Normal file
View File

@@ -0,0 +1,142 @@
# Förderrichtlinien und Vergabekriterien
> **Status:** Abgeleitet aus der Systemlogik der Verwaltungssoftware (models.py).
> Angaben mit [TODO] sind aus der formalen Richtliniendokumentation zu ergänzen.
---
## 1. Förderberechtigung (Grundvoraussetzungen)
Für eine laufende Unterstützung durch die Stiftung müssen **alle drei** Voraussetzungen erfüllt sein:
### 1.1 Abstammungsnachweis
- Der Antragsteller muss **Abkömmling gemäß Satzung** sein (`ist_abkoemmling`)
- [TODO: Welche Abstammungsnachweise sind zu erbringen?]
### 1.2 Einkommensgrenzen
Die monatlichen Bezüge dürfen eine Einkommensgrenze nicht überschreiten:
| Haushaltsgröße | Max. monatliche Bezüge |
|---|---|
| 1 Person | 2.815,00 € |
| 2 Personen | 3.265,40 € |
| 3 Personen | 3.715,80 € |
| n Personen | 2.815,00 € + (n-1) × 450,40 € |
**Berechnungsgrundlage:** Regelsatz 563 € × 5 (Basis) + 0,8 × Regelsatz je weitere Haushaltsperson
### 1.3 Vermögensgrenze
- Eigenvermögen ≤ **15.500 €**
---
## 2. Förderkategorien
| Kategorie | Beschreibung |
|---|---|
| `bildung` | Bildung und Studium |
| `forschung` | Wissenschaftliche Forschung |
| `kultur` | Kulturelle Projekte |
| `soziales` | Soziale Unterstützung |
| `umwelt` | Umweltschutz |
| `anderes` | Sonstiges |
---
## 3. Laufende Unterstützungen (Destinatär-Unterstützungen)
### 3.1 Zahlungsmodalitäten
- Zahlungen erfolgen **quartalsweise im Voraus**
- Betrag: individuell festgelegter **vierteljährlicher Betrag** je Destinatär
- Zahlung auf das hinterlegte Bankkonto des Destinatärs (IBAN)
### 3.2 Zahlungsfristen (quartalsweise, im Voraus)
| Quartal | Zahlungsfälligkeit |
|---|---|
| Q1 (JanMär) | 15. Dezember (Vorjahr) |
| Q2 (AprJun) | 15. März |
| Q3 (JulSep) | 15. Juni |
| Q4 (OktDez) | 15. September |
### 3.3 Wiederkehrende Zahlungen
Folgende Zahlungsintervalle können eingerichtet werden:
- Monatlich
- Vierteljährlich (standard)
- Halbjährlich
- Jährlich
---
## 4. Nachweispflichten (Vierteljahresnachweise)
Destinatäre müssen quartalsweise folgende Nachweise einreichen:
### 4.1 Pflichtbestandteile
1. **Studiennachweis** (sofern erforderlich)
2. **Einkommenssituation** Bestätigung oder Beschreibung von Änderungen
3. **Vermögenssituation** Bestätigung oder Beschreibung von Änderungen
4. Ggf. weitere Dokumente
### 4.2 Fristen für Studiennachweise (semesterbasiert)
| Quartal | Studiennachweis-Frist |
|---|---|
| Q1 (JanMär) | 15. März |
| Q2 (AprJun) | 15. März |
| Q3 (JulSep) | 15. September |
| Q4 (OktDez) | 15. September |
**Hintergrund:** Die Studiennachweise sind semesterbasiert (Wintersemester / Sommersemester), nicht quartalsbasiert.
### 4.3 Nachweisstatus
| Status | Bedeutung |
|---|---|
| `offen` | Nachweis ausstehend |
| `teilweise` | Teilweise eingereicht |
| `eingereicht` | Vollständig eingereicht |
| `geprueft` | Geprüft & Freigegeben |
| `auto_geprueft` | Automatisch freigegeben (Semesterbasis) |
| `nachbesserung` | Nachbesserung erforderlich |
| `abgelehnt` | Abgelehnt |
---
## 5. Einzel-Förderungen (Projektförderungen)
### 5.1 Antragsprozess
[TODO: Formalen Antragsprozess beschreiben]
Bekannte Felder aus der Förderungsdatenbank:
- Antragsteller (Destinatär)
- Jahr
- Betrag
- Kategorie
- Antragsdatum / Entscheidungsdatum
- Verwendungsnachweis
### 5.2 Förderungsstatus
| Status | Bedeutung |
|---|---|
| `beantragt` | Antrag gestellt |
| `genehmigt` | Genehmigt, noch nicht ausgezahlt |
| `ausgezahlt` | Ausgezahlt |
| `abgelehnt` | Abgelehnt |
| `storniert` | Storniert |
---
## 6. Prüfkriterien bei Förderanfragen
Checkliste für die Prüfung von Förderanträgen:
- [ ] Antragsteller ist Abkömmling gem. Satzung
- [ ] Einkommensgrenzen eingehalten
- [ ] Vermögensgrenze eingehalten
- [ ] Kategorie entspricht Stiftungszweck
- [ ] Betrag ist angemessen und plausibel
- [ ] Verwendungsnachweis planbar
- [ ] Kein Interessenkonflikt
---
*Zuletzt aktualisiert: 2026-03 | Quelle: Systemanalyse models.py, Softwarelogik*

101
knowledge/satzung.md Normal file
View File

@@ -0,0 +1,101 @@
# Satzung und Stiftungszweck
> **Status:** Abgeleitet aus dem Systemcode und den Datenmodellen der Verwaltungsanwendung.
> Angaben ohne Quellennachweis aus der Originalsatzung sind als **[TODO: aus Satzungsurkunde prüfen]** markiert.
---
## 1. Stiftungsname und Sitz
- **Name:** van Hees-Theyssen-Vogel'sche Stiftung
- **Kurzbezeichnung:** VHTV-Stiftung
- **Sitz:** Raesfelder Str. 3, 46499 Hamminkeln (Nordrhein-Westfalen)
- **Website / E-Mail:** vhtv-stiftung.de / paperless@vhtv-stiftung.de
- **Rechtsform:** Gemeinnützige Stiftung des bürgerlichen Rechts
---
## 2. Stiftungszweck
[TODO: Stiftungszweck(e) aus der Satzungsurkunde entnehmen und eintragen]
Aus dem Systemcode ableitbar sind folgende Förderfelder, die die Stiftung in ihrer Verwaltungssoftware abbildet:
- **Bildung** Förderung von Ausbildung und Studium
- **Forschung** Unterstützung wissenschaftlicher Arbeiten
- **Kultur** Kulturelle Projekte und Aktivitäten
- **Soziales** Soziale Unterstützungsleistungen
- **Umwelt** Umweltschutz und Nachhaltigkeit
- **Anderes** Sonstige zweckentsprechende Maßnahmen
---
## 3. Förderberechtigter Personenkreis (Destinatäre)
Die Stiftung fördert **Destinatäre** natürliche Personen, die der Stifterfamilie angehören oder ihr nahestehen.
### Familienzweige
Die Software unterscheidet folgende Familienzweige:
- Hauptzweig
- Nebenzweig
- Verwandt
- Anderer
### Fördervoraussetzungen (gemäß Systemlogik)
Für eine Förderung/Unterstützung müssen kumulativ erfüllt sein:
1. **Abkömmling gemäß Satzung** (`ist_abkoemmling = True`)
2. **Einkommensgrenzen eingehalten:**
- Basierend auf Regelsatz (563 €/Monat)
- Einkommensgrenze = 5 × Regelsatz für erste Person + 0,8 × Regelsatz je weiterer Haushaltsperson
- Beispiel Einzelperson: max. 2.815 €/Monat
- Beispiel 2 Personen: max. 3.265,40 €/Monat
3. **Vermögen ≤ 15.500 €**
Diese Werte sind aus der Softwarelogik abgeleitet und sollten mit der Satzung abgeglichen werden.
---
## 4. Organe der Stiftung
[TODO: Organe und deren Zusammensetzung aus der Satzungsurkunde entnehmen]
Aus dem System ableitbar:
- **Rentmeister / Geschäftsführung:** Hauptverwaltungsorgan, verwaltet Ländereien, Finanzen und Destinatäre
- [TODO: Vorstand? Kuratorium? Beirat? aus Satzung prüfen]
---
## 5. Stiftungsvermögen
Das Stiftungsvermögen setzt sich zusammen aus:
- **Immobilien / Ländereien:** Landwirtschaftliche Nutzflächen (Grünland, Acker, Wald) in verschiedenen Gemeinden des Kreises Wesel/NRW, die verpachtet werden
- **Bankkonten:** Stiftungskonten bei [TODO: Bank(en) eintragen]
- **Sonstige Vermögenswerte:** [TODO: ergänzen]
Der Vermögenserhalt ist Grundprinzip der Stiftungsführung; Erträge werden für den Stiftungszweck eingesetzt.
---
## 6. Wichtige Regelungen
[TODO: Folgende Punkte aus der Satzungsurkunde entnehmen]
- Regelungen zur Mittelverwendung
- Rechnungslegung und Berichtspflichten
- Satzungsänderungen und Auflösung
- Stiftungsaufsicht (zuständige Behörde: [TODO])
- Steuerliche Anerkennung / Freistellungsbescheid [TODO: Finanzamt, AZ]
---
## 7. Stiftungsaufsicht
- **Zuständige Behörde:** [TODO: Bezirksregierung Düsseldorf? prüfen, Hamminkeln liegt im Regierungsbezirk Düsseldorf]
- **Aktenzeichen Stiftungsregister:** [TODO]
- **Steuer-Nr. / Finanzamt:** [TODO]
---
*Zuletzt aktualisiert: 2026-03 | Quelle: Systemanalyse der Verwaltungssoftware*

176
knowledge/verfahren.md Normal file
View File

@@ -0,0 +1,176 @@
# Verwaltungsverfahren und Abläufe
> **Status:** Aus Systemcode, Celery-Tasks und App-Struktur abgeleitet.
> Punkte mit [TODO] sind manuell zu ergänzen.
---
## 1. Antragsprozess für Förderungen
### 1.1 Erstaufnahme eines Destinatärs
1. Destinatär-Datensatz anlegen (Django Admin oder Weboberfläche `/destinataere/`)
2. Pflichtfelder ausfüllen:
- Name, Geburtsdatum, E-Mail, Telefon
- Familienzweig
- `ist_abkoemmling` setzen
- Haushaltsgröße, monatliche Bezüge, Vermögen
3. Prüfung Fördervoraussetzungen (automatisch via `erfuellt_voraussetzungen()`)
4. `unterstuetzung_bestaetigt` setzen wenn Voraussetzungen erfüllt
5. Vierteljährlichen Betrag festlegen
6. Standard-Auszahlungskonto zuordnen
### 1.2 Einzel-Förderungsantrag
1. Förderung anlegen unter `/foerderungen/`
2. Status beginnt mit `beantragt`
3. Prüfung durch Rentmeister
4. Entscheidung: `genehmigt` oder `abgelehnt`
5. Nach Auszahlung: Status auf `ausgezahlt` setzen
6. Verwendungsnachweis als Dokument in Paperless hochladen und verknüpfen
---
## 2. Vierteljährliches Nachweisverfahren
### 2.1 Ablauf
1. Zu Beginn jedes Quartals: VierteljahresNachweis-Datensätze für alle aktiven Destinatäre erstellen
2. Destinatäre werden benachrichtigt (per E-Mail, [TODO: Benachrichtigungsvorlage?])
3. Destinatäre reichen Unterlagen ein:
- Per E-Mail an paperless@vhtv-stiftung.de (automatisch erfasst)
- Oder direkt über das Self-Service-Portal (sofern eingerichtet)
4. Rentmeister prüft eingegangene Unterlagen
5. Status aktualisieren: `eingereicht``geprueft`
6. Zahlung freigeben wenn Nachweis genehmigt
### 2.2 Fristen (Überblick)
- **Studiennachweis:** 15. März (Q1/Q2) bzw. 15. September (Q3/Q4)
- **Zahlung:** 15. Dez. Vorjahr (Q1), 15. März (Q2), 15. Juni (Q3), 15. September (Q4)
### 2.3 Automatische Freigabe
Bei Destinatären mit Semesterbasis-Studiennachweis kann eine automatische Freigabe erfolgen (`auto_geprueft`).
---
## 3. E-Mail-Eingangsverarbeitung (Automatisiert)
### 3.1 Übersicht
Das System verarbeitet automatisch eingehende E-Mails an `paperless@vhtv-stiftung.de`.
**Technologie:** Celery Beat Task, läuft alle 15 Minuten
### 3.2 Konfiguration
| Env-Variable | Standard | Bedeutung |
|---|---|---|
| `IMAP_HOST` | | IMAP-Server (Pflicht) |
| `IMAP_PORT` | 993 | IMAP-Port (SSL) |
| `IMAP_USER` | paperless@vhtv-stiftung.de | Benutzername |
| `IMAP_PASSWORD` | | Passwort (Pflicht) |
| `IMAP_FOLDER` | INBOX | E-Mail-Ordner |
| `IMAP_USE_SSL` | true | SSL verwenden |
### 3.3 Workflow
1. System liest ungelesene E-Mails aus dem IMAP-Postfach
2. Absender-E-Mail wird mit Destinatär-Datenbank abgeglichen
3. `DestinataerEmailEingang`-Datensatz wird angelegt
4. Anhänge werden in **Paperless-NGX** hochgeladen mit Tag `Stiftung_Destinatäre`
5. Für jeden Anhang wird ein `DokumentLink` erstellt
6. Unbekannte Absender werden als `unbekannt` markiert (manuelle Nachbearbeitung nötig)
### 3.4 Paperless-Tags
| Tag | Verwendung |
|---|---|
| `Stiftung_Destinatäre` | Dokumente von/für Destinatäre |
| `Stiftung_Land_und_Pächter` | Dokumente zu Ländereien/Pächtern |
| `Stiftung_Administration` | Verwaltungsdokumente |
---
## 4. Pachtvertragsverwaltung
### 4.1 Datenstruktur
- **Land** (Flurstück): Grundeinheit, identifiziert durch `lfd_nr`
- **LandVerpachtung**: Pachtvertrag (neue Struktur)
- **LandAbrechnung**: Jährliche Abrechnung je Flurstück
### 4.2 Anlage eines Pachtvertrags
1. Land-Datensatz prüfen/anlegen (`/land/`)
2. Pächter anlegen (`/paechter/`) falls nicht vorhanden
3. LandVerpachtung anlegen (`/land/<id>/verpachtung/`)
- Vertragsnummer vergeben
- Pachtbeginn, Pachtende, Verlängerungsklausel
- Pachtzins (pro ha oder pauschal)
- Zahlungsweise
- USt-Option und Umlagen konfigurieren
4. System aktualisiert LandAbrechnung automatisch
### 4.3 Pachtzins-Zahlungsweisen
| Option | Beschreibung |
|---|---|
| `jaehrlich` | Einmal jährlich |
| `halbjaehrlich` | Zweimal jährlich |
| `vierteljaehrlich` | Quartalsweise |
| `monatlich` | Monatlich |
### 4.4 Umlagen (Durchreichungen an Pächter)
Folgende Kosten können als Umlage auf Pächter umgelegt werden:
- Grundsteuer
- Versicherungen
- Verbandsbeiträge
- Jagdpachtanteile (optional)
---
## 5. Abrechnungsverfahren für Ländereien (LandAbrechnung)
### 5.1 Jährliche Abrechnung
- Pro Flurstück wird automatisch eine `LandAbrechnung` für jedes Abrechnungsjahr erstellt
- Felder: Pacht vereinnahmt, Umlagen, sonstige Einnahmen
- Ausgaben: nach Kategorien (Grundsteuer, Versicherung, Verwaltung etc.)
- USt-Berechnung wenn USt-Option aktiv
### 5.2 Dokumentenablage
Relevante Dokumente (Pachtverträge, Grundsteuerbescheide, Versicherungsnachweise) werden in **Paperless-NGX** abgelegt und per `DokumentLink` verknüpft.
---
## 6. Backup-Verfahren
### 6.1 Backup-Typen
| Typ | Inhalt |
|---|---|
| `full` | Datenbank + Dateien |
| `database` | Nur PostgreSQL-Datenbank |
| `files` | Nur Mediendateien |
### 6.2 Ablauf
1. Backup-Job über Weboberfläche anlegen (`/backup/`)
2. System erstellt Backup asynchron im Hintergrund
3. Backup wird als `.tar.gz` unter `/app/backups/` gespeichert
4. Status: `pending``running``completed` / `failed`
### 6.3 Speicherort
- **Container:** `/app/backups/`
- **Dateiname:** `stiftung_backup_YYYYMMDD_HHMMSS.tar.gz`
---
## 7. Verwaltungskosten-Erfassung
Verwaltungskosten werden kategorisiert erfasst:
- Bezeichnung, Kategorie, Betrag, Datum
- Lieferant/Firma, Rechnungsnummer
- Zuordnung zu Rentmeister (für Fahrtkosten etc.)
- Kilometerpauschale: Standard 0,30 €/km
---
## 8. Audit Trail
Alle Änderungen in der Anwendung werden im `AuditLog` erfasst:
- Benutzer, Zeitstempel, Aktion
- Entitätstyp, ID, Name
- Änderungsdetails (JSON)
- IP-Adresse, Browser
---
*Zuletzt aktualisiert: 2026-03 | Quelle: Systemcode tasks.py, models.py, backup_utils.py*

11
scripts/init-paperless-db.sh Executable file
View 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