Phase 0: forms.py, admin.py und views.py in Domain-Packages aufteilen
- forms.py → forms/ Package (8 Domänen: destinataere, land, finanzen, foerderung, dokumente, veranstaltung, system, geschichte) - admin.py → admin/ Package (7 Domänen, alle 22 @admin.register dekoriert) - views.py (8845 Zeilen) → views/ Package (10 Domänen: dashboard, destinataere, land, paechter, finanzen, foerderung, dokumente, unterstuetzungen, veranstaltung, geschichte, system) - __init__.py in jedem Package re-exportiert alle Symbole für Rückwärtskompatibilität - urls.py bleibt unverändert (funktioniert durch Re-Exports) - Django system check: 0 Fehler, alle URL-Auflösungen funktionieren Keine funktionalen Änderungen – reine Strukturverbesserung für Vision 2026. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
199
app/stiftung/views/__init__.py
Normal file
199
app/stiftung/views/__init__.py
Normal file
@@ -0,0 +1,199 @@
|
||||
# views/__init__.py
|
||||
# Phase 0: Vision 2026 – Re-exportiert alle View-Funktionen für Rückwärtskompatibilität
|
||||
|
||||
from .dashboard import ( # noqa: F401
|
||||
home,
|
||||
health_check,
|
||||
health,
|
||||
)
|
||||
|
||||
from .destinataere import ( # noqa: F401
|
||||
person_list,
|
||||
person_detail,
|
||||
person_create,
|
||||
person_update,
|
||||
person_delete,
|
||||
destinataer_list,
|
||||
destinataer_detail,
|
||||
destinataer_create,
|
||||
destinataer_update,
|
||||
destinataer_delete,
|
||||
destinataer_notiz_create,
|
||||
destinataer_export,
|
||||
)
|
||||
|
||||
from .dokumente import ( # noqa: F401
|
||||
dokument_management,
|
||||
paperless_document_redirect,
|
||||
dokument_list,
|
||||
dokument_detail,
|
||||
dokument_create,
|
||||
dokument_update,
|
||||
dokument_delete,
|
||||
paperless_ping,
|
||||
paperless_documents,
|
||||
paperless_debug,
|
||||
paperless_tags_only,
|
||||
link_document_search,
|
||||
create_paechter_link_for_verpachtung,
|
||||
link_document_create,
|
||||
link_document_list,
|
||||
link_document_update,
|
||||
link_document_delete,
|
||||
)
|
||||
|
||||
from .finanzen import ( # noqa: F401
|
||||
bericht_list,
|
||||
jahresbericht_generate,
|
||||
jahresbericht_generate_redirect,
|
||||
jahresbericht_pdf,
|
||||
geschaeftsfuehrung,
|
||||
konto_list,
|
||||
verwaltungskosten_list,
|
||||
rentmeister_list,
|
||||
rentmeister_detail,
|
||||
rentmeister_ausgaben,
|
||||
rentmeister_create,
|
||||
rentmeister_edit,
|
||||
konto_create,
|
||||
konto_edit,
|
||||
konto_detail,
|
||||
verwaltungskosten_create,
|
||||
verwaltungskosten_edit,
|
||||
verwaltungskosten_delete,
|
||||
mark_expense_paid,
|
||||
)
|
||||
|
||||
from .foerderung import ( # noqa: F401
|
||||
foerderung_list,
|
||||
foerderung_detail,
|
||||
foerderung_create,
|
||||
foerderung_update,
|
||||
foerderung_delete,
|
||||
)
|
||||
|
||||
from .geschichte import ( # noqa: F401
|
||||
geschichte_list,
|
||||
geschichte_detail,
|
||||
geschichte_create,
|
||||
geschichte_edit,
|
||||
geschichte_bild_upload,
|
||||
geschichte_bild_delete,
|
||||
kalender_view,
|
||||
kalender_create,
|
||||
kalender_detail,
|
||||
kalender_edit,
|
||||
kalender_delete,
|
||||
kalender_admin,
|
||||
kalender_api_events,
|
||||
email_eingang_list,
|
||||
email_eingang_detail,
|
||||
email_eingang_poll_trigger,
|
||||
)
|
||||
|
||||
from .land import ( # noqa: F401
|
||||
paechter_list,
|
||||
paechter_detail,
|
||||
paechter_create,
|
||||
paechter_update,
|
||||
paechter_delete,
|
||||
land_list,
|
||||
land_detail,
|
||||
land_create,
|
||||
land_update,
|
||||
land_delete,
|
||||
verpachtung_list,
|
||||
land_verpachtung_detail,
|
||||
land_verpachtung_update,
|
||||
land_verpachtung_end_direct,
|
||||
land_stats_api,
|
||||
paechter_export,
|
||||
land_export,
|
||||
verpachtung_export,
|
||||
land_abrechnung_list,
|
||||
land_abrechnung_detail,
|
||||
land_abrechnung_create,
|
||||
land_abrechnung_update,
|
||||
land_abrechnung_delete,
|
||||
land_verpachtung_create,
|
||||
land_verpachtung_end,
|
||||
land_verpachtung_edit,
|
||||
verpachtung_detail,
|
||||
verpachtung_create,
|
||||
verpachtung_update,
|
||||
verpachtung_delete,
|
||||
)
|
||||
|
||||
from .system import ( # noqa: F401
|
||||
get_pdf_generator,
|
||||
GrampsClient,
|
||||
get_gramps_client,
|
||||
gramps_debug_api,
|
||||
csv_import_list,
|
||||
csv_import_create,
|
||||
process_personen_csv,
|
||||
process_destinataere_csv,
|
||||
process_paechter_csv,
|
||||
process_laendereien_csv,
|
||||
gramps_search_api,
|
||||
administration,
|
||||
audit_log_list,
|
||||
backup_management,
|
||||
backup_download,
|
||||
backup_restore,
|
||||
backup_cancel,
|
||||
user_management,
|
||||
user_create,
|
||||
user_detail,
|
||||
user_edit,
|
||||
user_change_password,
|
||||
user_permissions,
|
||||
user_delete,
|
||||
user_login,
|
||||
user_logout,
|
||||
app_settings,
|
||||
edit_help_box,
|
||||
two_factor_setup,
|
||||
two_factor_qr,
|
||||
two_factor_verify,
|
||||
two_factor_disable,
|
||||
backup_tokens,
|
||||
)
|
||||
|
||||
from .unterstuetzungen import ( # noqa: F401
|
||||
unterstuetzungen_list,
|
||||
export_unterstuetzungen_csv,
|
||||
export_unterstuetzungen_pdf,
|
||||
export_foerderungen_csv,
|
||||
export_foerderungen_pdf,
|
||||
unterstuetzung_edit,
|
||||
unterstuetzung_delete,
|
||||
unterstuetzungen_all,
|
||||
unterstuetzung_create,
|
||||
get_destinataer_info,
|
||||
unterstuetzung_detail,
|
||||
unterstuetzung_mark_paid,
|
||||
wiederkehrende_unterstuetzungen,
|
||||
quarterly_confirmation_update,
|
||||
create_quarterly_support_payment,
|
||||
quarterly_confirmation_create,
|
||||
quarterly_confirmation_edit,
|
||||
quarterly_confirmation_approve,
|
||||
quarterly_confirmation_reset,
|
||||
)
|
||||
|
||||
from .veranstaltung import ( # noqa: F401
|
||||
veranstaltung_list,
|
||||
veranstaltung_detail,
|
||||
veranstaltung_serienbrief_pdf,
|
||||
veranstaltung_serienbrief_vorschau,
|
||||
veranstaltung_create,
|
||||
veranstaltung_update,
|
||||
veranstaltung_delete,
|
||||
teilnehmer_create,
|
||||
teilnehmer_update,
|
||||
teilnehmer_delete,
|
||||
)
|
||||
|
||||
# Non-view exports (helpers used elsewhere)
|
||||
from .system import GrampsClient, get_gramps_client, get_pdf_generator # noqa: F401
|
||||
117
app/stiftung/views/dashboard.py
Normal file
117
app/stiftung/views/dashboard.py
Normal file
@@ -0,0 +1,117 @@
|
||||
# views/dashboard.py
|
||||
# Phase 0: Vision 2026 – Code-Refactoring
|
||||
|
||||
import csv
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
import time
|
||||
from datetime import datetime, timedelta, date
|
||||
from decimal import Decimal
|
||||
|
||||
import qrcode
|
||||
import qrcode.image.svg
|
||||
import requests
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.core.paginator import Paginator
|
||||
from django.db.models import (Avg, Count, DecimalField, F, IntegerField, Q,
|
||||
Sum, Value)
|
||||
from django.db.models.functions import Cast, Coalesce, NullIf, Replace
|
||||
from django.http import HttpResponse, JsonResponse
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from django_otp.decorators import otp_required
|
||||
from django_otp.plugins.otp_totp.models import TOTPDevice
|
||||
from django_otp.plugins.otp_static.models import StaticDevice, StaticToken
|
||||
from django_otp.util import random_hex
|
||||
from rest_framework.decorators import api_view
|
||||
from rest_framework.response import Response
|
||||
|
||||
from stiftung.models import (AppConfiguration, AuditLog, BackupJob, BankTransaction,
|
||||
BriefVorlage, CSVImport, Destinataer,
|
||||
DestinataerEmailEingang, DestinataerNotiz,
|
||||
DestinataerUnterstuetzung,
|
||||
DokumentLink, Foerderung, GeschichteBild, GeschichteSeite,
|
||||
Land, LandAbrechnung, LandVerpachtung, Paechter, Person,
|
||||
Rentmeister, StiftungsKalenderEintrag, StiftungsKonto,
|
||||
UnterstuetzungWiederkehrend, Veranstaltung,
|
||||
Veranstaltungsteilnehmer, Verwaltungskosten,
|
||||
VierteljahresNachweis)
|
||||
from stiftung.forms import (
|
||||
DestinataerForm, DestinataerUnterstuetzungForm, DestinataerNotizForm,
|
||||
FoerderungForm, GeschichteBildForm, GeschichteSeiteForm,
|
||||
LandForm, LandVerpachtungForm, LandAbrechnungForm,
|
||||
PaechterForm, DokumentLinkForm,
|
||||
RentmeisterForm, StiftungsKontoForm, VerwaltungskostenForm,
|
||||
BankTransactionForm, BankImportForm,
|
||||
UnterstuetzungForm, UnterstuetzungWiederkehrendForm,
|
||||
UnterstuetzungMarkAsPaidForm, VierteljahresNachweisForm,
|
||||
UserCreationForm, UserUpdateForm, PasswordChangeForm, UserPermissionForm,
|
||||
TwoFactorSetupForm, TwoFactorVerifyForm, TwoFactorDisableForm,
|
||||
BackupTokenRegenerateForm, PersonForm,
|
||||
VeranstaltungForm, VeranstaltungsteilnehmerForm,
|
||||
)
|
||||
|
||||
|
||||
@login_required
|
||||
def home(request):
|
||||
"""Home page for the Stiftungsverwaltung application"""
|
||||
from stiftung.services.calendar_service import StiftungsKalenderService
|
||||
|
||||
# Get upcoming events for the calendar widget
|
||||
calendar_service = StiftungsKalenderService()
|
||||
|
||||
# Get all events for the next 14 days
|
||||
from datetime import timedelta
|
||||
today = timezone.now().date()
|
||||
end_date = today + timedelta(days=14)
|
||||
all_events = calendar_service.get_all_events(today, end_date)
|
||||
|
||||
# Filter for upcoming and overdue
|
||||
upcoming_events = [e for e in all_events if not getattr(e, 'overdue', False)]
|
||||
overdue_events = [e for e in all_events if getattr(e, 'overdue', False)]
|
||||
|
||||
# Get current month events for mini calendar
|
||||
from calendar import monthrange
|
||||
_, last_day = monthrange(today.year, today.month)
|
||||
month_start = today.replace(day=1)
|
||||
month_end = today.replace(day=last_day)
|
||||
current_month_events = calendar_service.get_all_events(month_start, month_end)
|
||||
|
||||
context = {
|
||||
"title": "Stiftungsverwaltung",
|
||||
"description": "Foundation Management System",
|
||||
"upcoming_events": upcoming_events[:5], # Show only 5 upcoming events
|
||||
"overdue_events": overdue_events[:3], # Show only 3 overdue events
|
||||
"current_month_events": current_month_events,
|
||||
"today": today,
|
||||
}
|
||||
|
||||
return render(request, "stiftung/home.html", context)
|
||||
|
||||
|
||||
@api_view(["GET"])
|
||||
def health_check(request):
|
||||
"""Simple health check endpoint for deployment monitoring"""
|
||||
return JsonResponse(
|
||||
{
|
||||
"status": "healthy",
|
||||
"timestamp": timezone.now().isoformat(),
|
||||
"service": "stiftung-web",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
## Removed duplicate paperless_ping referencing non-existent PAPERLESS_URL
|
||||
|
||||
|
||||
# CSV Import Views
|
||||
@api_view(["GET"])
|
||||
def health(_request):
|
||||
return Response({"status": "ok"})
|
||||
|
||||
|
||||
697
app/stiftung/views/destinataere.py
Normal file
697
app/stiftung/views/destinataere.py
Normal file
@@ -0,0 +1,697 @@
|
||||
# views/destinataere.py
|
||||
# Phase 0: Vision 2026 – Code-Refactoring
|
||||
|
||||
import csv
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
import time
|
||||
from datetime import datetime, timedelta, date
|
||||
from decimal import Decimal
|
||||
|
||||
import qrcode
|
||||
import qrcode.image.svg
|
||||
import requests
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.core.paginator import Paginator
|
||||
from django.db.models import (Avg, Count, DecimalField, F, IntegerField, Q,
|
||||
Sum, Value)
|
||||
from django.db.models.functions import Cast, Coalesce, NullIf, Replace
|
||||
from django.http import HttpResponse, JsonResponse
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from django_otp.decorators import otp_required
|
||||
from django_otp.plugins.otp_totp.models import TOTPDevice
|
||||
from django_otp.plugins.otp_static.models import StaticDevice, StaticToken
|
||||
from django_otp.util import random_hex
|
||||
from rest_framework.decorators import api_view
|
||||
from rest_framework.response import Response
|
||||
|
||||
from stiftung.models import (AppConfiguration, AuditLog, BackupJob, BankTransaction,
|
||||
BriefVorlage, CSVImport, Destinataer,
|
||||
DestinataerEmailEingang, DestinataerNotiz,
|
||||
DestinataerUnterstuetzung,
|
||||
DokumentLink, Foerderung, GeschichteBild, GeschichteSeite,
|
||||
Land, LandAbrechnung, LandVerpachtung, Paechter, Person,
|
||||
Rentmeister, StiftungsKalenderEintrag, StiftungsKonto,
|
||||
UnterstuetzungWiederkehrend, Veranstaltung,
|
||||
Veranstaltungsteilnehmer, Verwaltungskosten,
|
||||
VierteljahresNachweis)
|
||||
from stiftung.forms import (
|
||||
DestinataerForm, DestinataerUnterstuetzungForm, DestinataerNotizForm,
|
||||
FoerderungForm, GeschichteBildForm, GeschichteSeiteForm,
|
||||
LandForm, LandVerpachtungForm, LandAbrechnungForm,
|
||||
PaechterForm, DokumentLinkForm,
|
||||
RentmeisterForm, StiftungsKontoForm, VerwaltungskostenForm,
|
||||
BankTransactionForm, BankImportForm,
|
||||
UnterstuetzungForm, UnterstuetzungWiederkehrendForm,
|
||||
UnterstuetzungMarkAsPaidForm, VierteljahresNachweisForm,
|
||||
UserCreationForm, UserUpdateForm, PasswordChangeForm, UserPermissionForm,
|
||||
TwoFactorSetupForm, TwoFactorVerifyForm, TwoFactorDisableForm,
|
||||
BackupTokenRegenerateForm, PersonForm,
|
||||
VeranstaltungForm, VeranstaltungsteilnehmerForm,
|
||||
)
|
||||
|
||||
|
||||
@login_required
|
||||
def person_list(request):
|
||||
search_query = request.GET.get("search", "")
|
||||
familienzweig_filter = request.GET.get("familienzweig", "")
|
||||
aktiv_filter = request.GET.get("aktiv", "")
|
||||
|
||||
persons = Person.objects.all()
|
||||
|
||||
if search_query:
|
||||
persons = persons.filter(
|
||||
Q(nachname__icontains=search_query)
|
||||
| Q(vorname__icontains=search_query)
|
||||
| Q(email__icontains=search_query)
|
||||
| Q(familienzweig__icontains=search_query)
|
||||
)
|
||||
|
||||
if familienzweig_filter:
|
||||
persons = persons.filter(familienzweig=familienzweig_filter)
|
||||
|
||||
if aktiv_filter == "true":
|
||||
persons = persons.filter(aktiv=True)
|
||||
elif aktiv_filter == "false":
|
||||
persons = persons.filter(aktiv=False)
|
||||
|
||||
# Annotate with total funding
|
||||
persons = persons.annotate(total_foerderungen=Sum("foerderung__betrag"))
|
||||
|
||||
paginator = Paginator(persons, 20)
|
||||
page_number = request.GET.get("page")
|
||||
page_obj = paginator.get_page(page_number)
|
||||
|
||||
context = {
|
||||
"page_obj": page_obj,
|
||||
"search_query": search_query,
|
||||
"familienzweig_filter": familienzweig_filter,
|
||||
"aktiv_filter": aktiv_filter,
|
||||
"familienzweig_choices": Person.FAMILIENZWIG_CHOICES,
|
||||
}
|
||||
return render(request, "stiftung/person_list.html", context)
|
||||
|
||||
|
||||
@login_required
|
||||
def person_detail(request, pk):
|
||||
person = get_object_or_404(Person, pk=pk)
|
||||
foerderungen = person.foerderung_set.all().order_by("-jahr", "-betrag")
|
||||
# Get new LandVerpachtungen for this person's Paechter instances
|
||||
verpachtungen = LandVerpachtung.objects.filter(paechter__person=person).order_by(
|
||||
"-pachtbeginn"
|
||||
)
|
||||
|
||||
context = {
|
||||
"person": person,
|
||||
"foerderungen": foerderungen,
|
||||
"verpachtungen": verpachtungen,
|
||||
}
|
||||
return render(request, "stiftung/person_detail.html", context)
|
||||
|
||||
|
||||
@login_required
|
||||
def person_create(request):
|
||||
if request.method == "POST":
|
||||
form = PersonForm(request.POST)
|
||||
if form.is_valid():
|
||||
person = form.save()
|
||||
messages.success(
|
||||
request,
|
||||
f'Person "{person.get_full_name()}" wurde erfolgreich erstellt.',
|
||||
)
|
||||
return redirect("stiftung:person_detail", pk=person.pk)
|
||||
else:
|
||||
form = PersonForm()
|
||||
|
||||
context = {"form": form, "title": "Neue Person erstellen"}
|
||||
return render(request, "stiftung/person_form.html", context)
|
||||
|
||||
|
||||
@login_required
|
||||
def person_update(request, pk):
|
||||
person = get_object_or_404(Person, pk=pk)
|
||||
if request.method == "POST":
|
||||
form = PersonForm(request.POST, instance=person)
|
||||
if form.is_valid():
|
||||
person = form.save()
|
||||
messages.success(
|
||||
request,
|
||||
f'Person "{person.get_full_name()}" wurde erfolgreich aktualisiert.',
|
||||
)
|
||||
return redirect("stiftung:person_detail", pk=person.pk)
|
||||
else:
|
||||
form = PersonForm(instance=person)
|
||||
|
||||
context = {
|
||||
"form": form,
|
||||
"person": person,
|
||||
"title": f"Person bearbeiten: {person.get_full_name()}",
|
||||
}
|
||||
return render(request, "stiftung/person_form.html", context)
|
||||
|
||||
|
||||
@login_required
|
||||
def person_delete(request, pk):
|
||||
person = get_object_or_404(Person, pk=pk)
|
||||
if request.method == "POST":
|
||||
person.delete()
|
||||
messages.success(
|
||||
request, f'Person "{person.get_full_name()}" wurde erfolgreich gelöscht.'
|
||||
)
|
||||
return redirect("stiftung:person_list")
|
||||
|
||||
context = {"person": person}
|
||||
return render(request, "stiftung/person_confirm_delete.html", context)
|
||||
|
||||
|
||||
# Destinatär Views (Förderungsempfänger)
|
||||
@login_required
|
||||
def destinataer_list(request):
|
||||
search_query = request.GET.get("search", "")
|
||||
familienzweig_filter = request.GET.get("familienzweig", "")
|
||||
berufsgruppe_filter = request.GET.get("berufsgruppe", "")
|
||||
aktiv_filter = request.GET.get("aktiv", "")
|
||||
sort = request.GET.get("sort", "")
|
||||
direction = request.GET.get("dir", "asc")
|
||||
|
||||
destinataere = Destinataer.objects.all()
|
||||
|
||||
if search_query:
|
||||
destinataere = destinataere.filter(
|
||||
Q(nachname__icontains=search_query)
|
||||
| Q(vorname__icontains=search_query)
|
||||
| Q(email__icontains=search_query)
|
||||
| Q(institution__icontains=search_query)
|
||||
| Q(familienzweig__icontains=search_query)
|
||||
)
|
||||
|
||||
if familienzweig_filter:
|
||||
destinataere = destinataere.filter(familienzweig=familienzweig_filter)
|
||||
|
||||
if berufsgruppe_filter:
|
||||
destinataere = destinataere.filter(berufsgruppe=berufsgruppe_filter)
|
||||
|
||||
if aktiv_filter == "true":
|
||||
destinataere = destinataere.filter(aktiv=True)
|
||||
elif aktiv_filter == "false":
|
||||
destinataere = destinataere.filter(aktiv=False)
|
||||
|
||||
# Annotate with total funding (coalesce nulls to Decimal for stable sorting)
|
||||
destinataere = destinataere.annotate(
|
||||
total_foerderungen=Coalesce(
|
||||
Sum("foerderung__betrag"),
|
||||
Value(
|
||||
Decimal("0.00"),
|
||||
output_field=DecimalField(max_digits=12, decimal_places=2),
|
||||
),
|
||||
output_field=DecimalField(max_digits=12, decimal_places=2),
|
||||
)
|
||||
)
|
||||
|
||||
# Sorting
|
||||
sort_map = {
|
||||
"vorname": ["vorname"],
|
||||
"nachname": ["nachname"],
|
||||
"email": ["email"],
|
||||
"vierteljaehrlicher_betrag": ["vierteljaehrlicher_betrag"],
|
||||
"letzter_studiennachweis": ["letzter_studiennachweis"],
|
||||
"unterstuetzung_bestaetigt": ["unterstuetzung_bestaetigt"],
|
||||
# Keep old mappings for backward compatibility
|
||||
"name": ["nachname", "vorname"],
|
||||
"familienzweig": ["familienzweig"],
|
||||
"berufsgruppe": ["berufsgruppe"],
|
||||
"institution": ["institution"],
|
||||
"foerderungen": ["total_foerderungen"],
|
||||
"status": ["aktiv"],
|
||||
}
|
||||
if sort in sort_map:
|
||||
fields = sort_map[sort]
|
||||
if direction == "desc":
|
||||
order_fields = [f"-{f}" for f in fields]
|
||||
else:
|
||||
order_fields = fields
|
||||
destinataere = destinataere.order_by(*order_fields)
|
||||
else:
|
||||
# Default sorting by last name (nachname) ascending
|
||||
destinataere = destinataere.order_by("nachname", "vorname")
|
||||
|
||||
paginator = Paginator(destinataere, 50) # Increased from 20 to 50 entries per page
|
||||
page_number = request.GET.get("page")
|
||||
page_obj = paginator.get_page(page_number)
|
||||
|
||||
# Set default sort to nachname if no sort is specified
|
||||
effective_sort = sort if sort else "nachname"
|
||||
effective_direction = direction if sort else "asc"
|
||||
|
||||
context = {
|
||||
"page_obj": page_obj,
|
||||
"search_query": search_query,
|
||||
"familienzweig_filter": familienzweig_filter,
|
||||
"berufsgruppe_filter": berufsgruppe_filter,
|
||||
"aktiv_filter": aktiv_filter,
|
||||
"familienzweig_choices": Destinataer.FAMILIENZWIG_CHOICES,
|
||||
"berufsgruppe_choices": Destinataer.BERUFSGRUPPE_CHOICES,
|
||||
"sort": effective_sort,
|
||||
"dir": effective_direction,
|
||||
}
|
||||
return render(request, "stiftung/destinataer_list.html", context)
|
||||
|
||||
|
||||
@login_required
|
||||
def destinataer_detail(request, pk):
|
||||
destinataer = get_object_or_404(Destinataer, pk=pk)
|
||||
|
||||
# Alle mit diesem Destinatär verknüpften Dokumente laden
|
||||
verknuepfte_dokumente = DokumentLink.objects.filter(
|
||||
destinataer_id=destinataer.pk
|
||||
).order_by("kontext", "titel")
|
||||
|
||||
# Förderungen für diesen Destinatär laden
|
||||
foerderungen = Foerderung.objects.filter(destinataer=destinataer).order_by(
|
||||
"-jahr", "-betrag"
|
||||
)
|
||||
|
||||
# Unterstützungen für diesen Destinatär laden
|
||||
unterstuetzungen = DestinataerUnterstuetzung.objects.filter(
|
||||
destinataer=destinataer
|
||||
).order_by("-faellig_am")
|
||||
|
||||
# Notizen laden
|
||||
notizen_eintraege = DestinataerNotiz.objects.filter(
|
||||
destinataer=destinataer
|
||||
).order_by("-erstellt_am")
|
||||
|
||||
# Quarterly confirmations - load for current and next year
|
||||
from datetime import date
|
||||
current_year = date.today().year
|
||||
quarterly_confirmations = VierteljahresNachweis.objects.filter(
|
||||
destinataer=destinataer,
|
||||
jahr__in=[current_year, current_year + 1]
|
||||
).order_by('-jahr', '-quartal')
|
||||
|
||||
# Create missing quarterly confirmations for current year
|
||||
# Quarterly tracking is now always available regardless of study proof requirements
|
||||
for quartal in range(1, 5): # Q1-Q4
|
||||
nachweis, created = VierteljahresNachweis.get_or_create_for_period(
|
||||
destinataer, current_year, quartal
|
||||
)
|
||||
|
||||
# Reload to get any newly created confirmations
|
||||
quarterly_confirmations = VierteljahresNachweis.objects.filter(
|
||||
destinataer=destinataer,
|
||||
jahr__in=[current_year, current_year + 1]
|
||||
).order_by('-jahr', '-quartal')
|
||||
|
||||
# Modal forms removed - only using full-screen editor now
|
||||
|
||||
# Generate available years for the add quarter dropdown (current year + next 5 years)
|
||||
available_years = list(range(current_year, current_year + 6))
|
||||
|
||||
# Alle verfügbaren StiftungsKonten für das Select-Feld laden
|
||||
stiftungskonten = StiftungsKonto.objects.all().order_by("kontoname")
|
||||
|
||||
context = {
|
||||
"destinataer": destinataer,
|
||||
"verknuepfte_dokumente": verknuepfte_dokumente,
|
||||
"foerderungen": foerderungen,
|
||||
"unterstuetzungen": unterstuetzungen,
|
||||
"notizen_eintraege": notizen_eintraege,
|
||||
"stiftungskonten": stiftungskonten,
|
||||
"quarterly_confirmations": quarterly_confirmations,
|
||||
"available_years": available_years,
|
||||
"current_year": current_year,
|
||||
}
|
||||
return render(request, "stiftung/destinataer_detail.html", context)
|
||||
|
||||
|
||||
@login_required
|
||||
def destinataer_create(request):
|
||||
if request.method == "POST":
|
||||
form = DestinataerForm(request.POST)
|
||||
if form.is_valid():
|
||||
destinataer = form.save()
|
||||
messages.success(
|
||||
request,
|
||||
f'Destinatär "{destinataer.get_full_name()}" wurde erfolgreich erstellt.',
|
||||
)
|
||||
return redirect("stiftung:destinataer_detail", pk=destinataer.pk)
|
||||
else:
|
||||
form = DestinataerForm()
|
||||
|
||||
context = {"form": form, "title": "Neuen Destinatär erstellen"}
|
||||
return render(request, "stiftung/destinataer_form.html", context)
|
||||
|
||||
|
||||
@login_required
|
||||
def destinataer_update(request, pk):
|
||||
destinataer = get_object_or_404(Destinataer, pk=pk)
|
||||
if request.method == "POST":
|
||||
form = DestinataerForm(request.POST, instance=destinataer)
|
||||
|
||||
# Handle AJAX requests
|
||||
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
|
||||
if form.is_valid():
|
||||
try:
|
||||
destinataer = form.save()
|
||||
|
||||
# Note: Support payments are now only created through quarterly confirmations
|
||||
# No automatic creation when unterstuetzung_bestaetigt is checked
|
||||
|
||||
return JsonResponse({
|
||||
'success': True,
|
||||
'message': f'Destinatär "{destinataer.get_full_name()}" wurde erfolgreich aktualisiert.'
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
return JsonResponse({
|
||||
'success': False,
|
||||
'error': f'Fehler beim Speichern: {str(e)}'
|
||||
})
|
||||
else:
|
||||
# Return form errors for AJAX requests
|
||||
errors = []
|
||||
for field, field_errors in form.errors.items():
|
||||
for error in field_errors:
|
||||
errors.append(f'{form[field].label}: {error}')
|
||||
|
||||
return JsonResponse({
|
||||
'success': False,
|
||||
'error': 'Formular enthält Fehler: ' + '; '.join(errors)
|
||||
})
|
||||
|
||||
# Handle regular form submission
|
||||
if form.is_valid():
|
||||
destinataer = form.save()
|
||||
# Note: Support payments are now only created through quarterly confirmations
|
||||
# No automatic creation when unterstuetzung_bestaetigt is checked
|
||||
messages.success(
|
||||
request,
|
||||
f'Destinatär "{destinataer.get_full_name()}" wurde erfolgreich aktualisiert.',
|
||||
)
|
||||
return redirect("stiftung:destinataer_detail", pk=destinataer.pk)
|
||||
else:
|
||||
form = DestinataerForm(instance=destinataer)
|
||||
|
||||
context = {
|
||||
"form": form,
|
||||
"destinataer": destinataer,
|
||||
"title": f"Destinatär bearbeiten: {destinataer.get_full_name()}",
|
||||
}
|
||||
return render(request, "stiftung/destinataer_form.html", context)
|
||||
|
||||
|
||||
@login_required
|
||||
def destinataer_delete(request, pk):
|
||||
destinataer = get_object_or_404(Destinataer, pk=pk)
|
||||
if request.method == "POST":
|
||||
destinataer.delete()
|
||||
messages.success(
|
||||
request,
|
||||
f'Destinatär "{destinataer.get_full_name()}" wurde erfolgreich gelöscht.',
|
||||
)
|
||||
return redirect("stiftung:destinataer_list")
|
||||
|
||||
context = {"destinataer": destinataer}
|
||||
return render(request, "stiftung/destinataer_confirm_delete.html", context)
|
||||
|
||||
|
||||
# Paechter Views (Landpächter)
|
||||
@login_required
|
||||
def destinataer_notiz_create(request, pk):
|
||||
destinataer = get_object_or_404(Destinataer, pk=pk)
|
||||
if request.method == "POST":
|
||||
form = DestinataerNotizForm(request.POST, request.FILES)
|
||||
if form.is_valid():
|
||||
note = form.save(commit=False)
|
||||
note.destinataer = destinataer
|
||||
note.erstellt_von = request.user
|
||||
note.save()
|
||||
messages.success(request, "Notiz wurde gespeichert.")
|
||||
return redirect("stiftung:destinataer_detail", pk=destinataer.pk)
|
||||
else:
|
||||
# Debug: show what validation failed
|
||||
for field, errors in form.errors.items():
|
||||
messages.error(request, f'Fehler in {field}: {", ".join(errors)}')
|
||||
else:
|
||||
form = DestinataerNotizForm()
|
||||
return render(
|
||||
request,
|
||||
"stiftung/destinataer_notiz_form.html",
|
||||
{"form": form, "destinataer": destinataer, "title": "Notiz hinzufügen"},
|
||||
)
|
||||
|
||||
|
||||
@login_required
|
||||
def destinataer_export(request, pk):
|
||||
"""Export complete Destinatär data as ZIP with documents"""
|
||||
import json
|
||||
import os
|
||||
import tempfile
|
||||
import zipfile
|
||||
|
||||
from django.http import HttpResponse
|
||||
|
||||
destinataer = get_object_or_404(Destinataer, pk=pk)
|
||||
|
||||
# Create a temporary file for the ZIP
|
||||
temp_file = tempfile.NamedTemporaryFile(delete=False, suffix=".zip")
|
||||
|
||||
try:
|
||||
with zipfile.ZipFile(temp_file.name, "w", zipfile.ZIP_DEFLATED) as zipf:
|
||||
# 1. Entity data as JSON
|
||||
entity_data = {
|
||||
"id": str(destinataer.id),
|
||||
"anrede": destinataer.get_anrede_display() if hasattr(destinataer, 'anrede') and destinataer.anrede else None,
|
||||
"titel": destinataer.titel if hasattr(destinataer, 'titel') else None,
|
||||
"vorname": destinataer.vorname,
|
||||
"nachname": destinataer.nachname,
|
||||
"geburtsdatum": (
|
||||
destinataer.geburtsdatum.isoformat()
|
||||
if destinataer.geburtsdatum
|
||||
else None
|
||||
),
|
||||
"email": destinataer.email,
|
||||
"telefon": destinataer.telefon,
|
||||
"mobil": destinataer.mobil if hasattr(destinataer, 'mobil') else None,
|
||||
"iban": destinataer.iban,
|
||||
"strasse": destinataer.strasse,
|
||||
"plz": destinataer.plz,
|
||||
"ort": destinataer.ort,
|
||||
"familienzweig": destinataer.get_familienzweig_display(),
|
||||
"berufsgruppe": destinataer.get_berufsgruppe_display(),
|
||||
"ausbildungsstand": destinataer.ausbildungsstand,
|
||||
"institution": destinataer.institution,
|
||||
"projekt_beschreibung": destinataer.projekt_beschreibung,
|
||||
"jaehrliches_einkommen": (
|
||||
str(destinataer.jaehrliches_einkommen)
|
||||
if destinataer.jaehrliches_einkommen
|
||||
else None
|
||||
),
|
||||
"finanzielle_notlage": destinataer.finanzielle_notlage,
|
||||
"ist_abkoemmling": destinataer.ist_abkoemmling,
|
||||
"haushaltsgroesse": destinataer.haushaltsgroesse,
|
||||
"monatliche_bezuege": (
|
||||
str(destinataer.monatliche_bezuege)
|
||||
if destinataer.monatliche_bezuege
|
||||
else None
|
||||
),
|
||||
"vermoegen": (
|
||||
str(destinataer.vermoegen) if destinataer.vermoegen else None
|
||||
),
|
||||
"unterstuetzung_bestaetigt": destinataer.unterstuetzung_bestaetigt,
|
||||
"vierteljaehrlicher_betrag": (
|
||||
str(destinataer.vierteljaehrlicher_betrag)
|
||||
if destinataer.vierteljaehrlicher_betrag
|
||||
else None
|
||||
),
|
||||
"standard_konto": (
|
||||
str(destinataer.standard_konto)
|
||||
if destinataer.standard_konto
|
||||
else None
|
||||
),
|
||||
"studiennachweis_erforderlich": destinataer.studiennachweis_erforderlich,
|
||||
"letzter_studiennachweis": (
|
||||
destinataer.letzter_studiennachweis.isoformat()
|
||||
if destinataer.letzter_studiennachweis
|
||||
else None
|
||||
),
|
||||
"notizen": destinataer.notizen,
|
||||
"aktiv": destinataer.aktiv,
|
||||
"export_datum": timezone.now().isoformat(),
|
||||
"export_user": request.user.username,
|
||||
}
|
||||
zipf.writestr(
|
||||
"destinataer_data.json",
|
||||
json.dumps(entity_data, indent=2, ensure_ascii=False),
|
||||
)
|
||||
|
||||
# 2. Notes with attachments
|
||||
notizen = DestinataerNotiz.objects.filter(destinataer=destinataer).order_by(
|
||||
"-erstellt_am"
|
||||
)
|
||||
notes_data = []
|
||||
for note in notizen:
|
||||
note_data = {
|
||||
"titel": note.titel,
|
||||
"text": note.text,
|
||||
"erstellt_am": note.erstellt_am.isoformat(),
|
||||
"erstellt_von": (
|
||||
note.erstellt_von.username if note.erstellt_von else None
|
||||
),
|
||||
"datei_name": note.datei.name if note.datei else None,
|
||||
}
|
||||
notes_data.append(note_data)
|
||||
|
||||
# Add attachment file if exists
|
||||
if note.datei and os.path.exists(note.datei.path):
|
||||
zipf.write(
|
||||
note.datei.path,
|
||||
f"notizen_anhaenge/{os.path.basename(note.datei.name)}",
|
||||
)
|
||||
|
||||
if notes_data:
|
||||
zipf.writestr(
|
||||
"notizen.json", json.dumps(notes_data, indent=2, ensure_ascii=False)
|
||||
)
|
||||
|
||||
# 3. Linked documents from Paperless
|
||||
dokumente = DokumentLink.objects.filter(destinataer_id=destinataer.pk)
|
||||
docs_data = []
|
||||
for doc in dokumente:
|
||||
doc_data = {
|
||||
"paperless_id": doc.paperless_document_id,
|
||||
"titel": doc.titel,
|
||||
"kontext": doc.get_kontext_display(),
|
||||
"beschreibung": doc.beschreibung,
|
||||
}
|
||||
docs_data.append(doc_data)
|
||||
|
||||
# Try to download document from Paperless
|
||||
try:
|
||||
if (
|
||||
hasattr(settings, "PAPERLESS_API_URL")
|
||||
and settings.PAPERLESS_API_URL
|
||||
):
|
||||
doc_url = f"{settings.PAPERLESS_API_URL}/api/documents/{doc.paperless_document_id}/download/"
|
||||
headers = {}
|
||||
if (
|
||||
hasattr(settings, "PAPERLESS_API_TOKEN")
|
||||
and settings.PAPERLESS_API_TOKEN
|
||||
):
|
||||
headers["Authorization"] = (
|
||||
f"Token {settings.PAPERLESS_API_TOKEN}"
|
||||
)
|
||||
|
||||
response = requests.get(doc_url, headers=headers, timeout=30)
|
||||
if response.status_code == 200:
|
||||
# Determine file extension from Content-Type or use .pdf as fallback
|
||||
content_type = response.headers.get("content-type", "")
|
||||
if "pdf" in content_type:
|
||||
ext = ".pdf"
|
||||
elif "jpeg" in content_type or "jpg" in content_type:
|
||||
ext = ".jpg"
|
||||
elif "png" in content_type:
|
||||
ext = ".png"
|
||||
else:
|
||||
ext = ".pdf" # fallback
|
||||
|
||||
safe_filename = f"dokument_{doc.paperless_document_id}_{doc.titel.replace('/', '_')[:50]}{ext}"
|
||||
zipf.writestr(
|
||||
f"dokumente/{safe_filename}", response.content
|
||||
)
|
||||
doc_data["downloaded"] = True
|
||||
else:
|
||||
doc_data["download_error"] = f"HTTP {response.status_code}"
|
||||
except Exception as e:
|
||||
doc_data["download_error"] = str(e)
|
||||
|
||||
if docs_data:
|
||||
zipf.writestr(
|
||||
"dokumente.json",
|
||||
json.dumps(docs_data, indent=2, ensure_ascii=False),
|
||||
)
|
||||
|
||||
# 4. Quarterly Confirmations with documents
|
||||
quarterly_confirmations = destinataer.quartalseinreichungen.all().order_by("-jahr", "-quartal")
|
||||
quarterly_data = []
|
||||
|
||||
for confirmation in quarterly_confirmations:
|
||||
confirmation_data = {
|
||||
"id": str(confirmation.id),
|
||||
"jahr": confirmation.jahr,
|
||||
"quartal": confirmation.quartal,
|
||||
"quartal_display": confirmation.get_quartal_display(),
|
||||
"status": confirmation.status,
|
||||
"status_display": confirmation.get_status_display(),
|
||||
"studiennachweis_erforderlich": confirmation.studiennachweis_erforderlich,
|
||||
"studiennachweis_eingereicht": confirmation.studiennachweis_eingereicht,
|
||||
"studiennachweis_bemerkung": confirmation.studiennachweis_bemerkung,
|
||||
"einkommenssituation_bestaetigt": confirmation.einkommenssituation_bestaetigt,
|
||||
"einkommenssituation_text": confirmation.einkommenssituation_text,
|
||||
"vermogenssituation_bestaetigt": confirmation.vermogenssituation_bestaetigt,
|
||||
"vermogenssituation_text": confirmation.vermogenssituation_text,
|
||||
"weitere_dokumente_beschreibung": confirmation.weitere_dokumente_beschreibung,
|
||||
"interne_notizen": confirmation.interne_notizen,
|
||||
"erstellt_am": confirmation.erstellt_am.isoformat(),
|
||||
"aktualisiert_am": confirmation.aktualisiert_am.isoformat(),
|
||||
"eingereicht_am": confirmation.eingereicht_am.isoformat() if confirmation.eingereicht_am else None,
|
||||
"geprueft_am": confirmation.geprueft_am.isoformat() if confirmation.geprueft_am else None,
|
||||
"geprueft_von": confirmation.geprueft_von.username if confirmation.geprueft_von else None,
|
||||
"faelligkeitsdatum": confirmation.faelligkeitsdatum.isoformat() if confirmation.faelligkeitsdatum else None,
|
||||
"completion_percentage": confirmation.get_completion_percentage(),
|
||||
"uploaded_files": []
|
||||
}
|
||||
|
||||
# Add uploaded files from quarterly confirmation
|
||||
quarterly_files = [
|
||||
("studiennachweis", confirmation.studiennachweis_datei),
|
||||
("einkommenssituation", confirmation.einkommenssituation_datei),
|
||||
("vermogenssituation", confirmation.vermogenssituation_datei),
|
||||
("weitere_dokumente", confirmation.weitere_dokumente),
|
||||
]
|
||||
|
||||
for file_type, file_field in quarterly_files:
|
||||
if file_field and os.path.exists(file_field.path):
|
||||
file_info = {
|
||||
"type": file_type,
|
||||
"name": os.path.basename(file_field.name),
|
||||
"path": file_field.name
|
||||
}
|
||||
confirmation_data["uploaded_files"].append(file_info)
|
||||
|
||||
# Add file to ZIP
|
||||
safe_filename = f"quarterly_{confirmation.jahr}_Q{confirmation.quartal}_{file_type}_{os.path.basename(file_field.name)}"
|
||||
zipf.write(
|
||||
file_field.path,
|
||||
f"vierteljahresnachweis/{safe_filename}"
|
||||
)
|
||||
|
||||
quarterly_data.append(confirmation_data)
|
||||
|
||||
if quarterly_data:
|
||||
zipf.writestr(
|
||||
"vierteljahresnachweis.json",
|
||||
json.dumps(quarterly_data, indent=2, ensure_ascii=False),
|
||||
)
|
||||
|
||||
# Prepare response
|
||||
with open(temp_file.name, "rb") as f:
|
||||
response = HttpResponse(f.read(), content_type="application/zip")
|
||||
filename = f"destinataer_{destinataer.nachname}_{destinataer.vorname}_{timezone.now().strftime('%Y%m%d_%H%M%S')}.zip"
|
||||
response["Content-Disposition"] = f'attachment; filename="{filename}"'
|
||||
return response
|
||||
|
||||
finally:
|
||||
# Clean up temp file
|
||||
try:
|
||||
os.unlink(temp_file.name)
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
1453
app/stiftung/views/dokumente.py
Normal file
1453
app/stiftung/views/dokumente.py
Normal file
File diff suppressed because it is too large
Load Diff
743
app/stiftung/views/finanzen.py
Normal file
743
app/stiftung/views/finanzen.py
Normal file
@@ -0,0 +1,743 @@
|
||||
# views/finanzen.py
|
||||
# Phase 0: Vision 2026 – Code-Refactoring
|
||||
|
||||
import csv
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
import time
|
||||
from datetime import datetime, timedelta, date
|
||||
from decimal import Decimal
|
||||
|
||||
import qrcode
|
||||
import qrcode.image.svg
|
||||
import requests
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.core.paginator import Paginator
|
||||
from django.db.models import (Avg, Count, DecimalField, F, IntegerField, Q,
|
||||
Sum, Value)
|
||||
from django.db.models.functions import Cast, Coalesce, NullIf, Replace
|
||||
from django.http import HttpResponse, JsonResponse
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from django_otp.decorators import otp_required
|
||||
from django_otp.plugins.otp_totp.models import TOTPDevice
|
||||
from django_otp.plugins.otp_static.models import StaticDevice, StaticToken
|
||||
from django_otp.util import random_hex
|
||||
from rest_framework.decorators import api_view
|
||||
from rest_framework.response import Response
|
||||
|
||||
from stiftung.models import (AppConfiguration, AuditLog, BackupJob, BankTransaction,
|
||||
BriefVorlage, CSVImport, Destinataer,
|
||||
DestinataerEmailEingang, DestinataerNotiz,
|
||||
DestinataerUnterstuetzung,
|
||||
DokumentLink, Foerderung, GeschichteBild, GeschichteSeite,
|
||||
Land, LandAbrechnung, LandVerpachtung, Paechter, Person,
|
||||
Rentmeister, StiftungsKalenderEintrag, StiftungsKonto,
|
||||
UnterstuetzungWiederkehrend, Veranstaltung,
|
||||
Veranstaltungsteilnehmer, Verwaltungskosten,
|
||||
VierteljahresNachweis)
|
||||
from stiftung.forms import (
|
||||
DestinataerForm, DestinataerUnterstuetzungForm, DestinataerNotizForm,
|
||||
FoerderungForm, GeschichteBildForm, GeschichteSeiteForm,
|
||||
LandForm, LandVerpachtungForm, LandAbrechnungForm,
|
||||
PaechterForm, DokumentLinkForm,
|
||||
RentmeisterForm, StiftungsKontoForm, VerwaltungskostenForm,
|
||||
BankTransactionForm, BankImportForm,
|
||||
UnterstuetzungForm, UnterstuetzungWiederkehrendForm,
|
||||
UnterstuetzungMarkAsPaidForm, VierteljahresNachweisForm,
|
||||
UserCreationForm, UserUpdateForm, PasswordChangeForm, UserPermissionForm,
|
||||
TwoFactorSetupForm, TwoFactorVerifyForm, TwoFactorDisableForm,
|
||||
BackupTokenRegenerateForm, PersonForm,
|
||||
VeranstaltungForm, VeranstaltungsteilnehmerForm,
|
||||
)
|
||||
|
||||
|
||||
@login_required
|
||||
def bericht_list(request):
|
||||
"""List available reports"""
|
||||
# Get available years from data
|
||||
jahre = sorted(
|
||||
set(
|
||||
list(Foerderung.objects.values_list("jahr", flat=True))
|
||||
+ list(LandVerpachtung.objects.values_list("pachtbeginn__year", flat=True))
|
||||
),
|
||||
reverse=True,
|
||||
)
|
||||
|
||||
# Statistics for overview tiles (removed legacy Person and Verpachtung)
|
||||
total_destinataere = Destinataer.objects.count()
|
||||
total_laendereien = Land.objects.count()
|
||||
total_verpachtungen = LandVerpachtung.objects.count()
|
||||
total_foerderungen = Foerderung.objects.count()
|
||||
|
||||
context = {
|
||||
"jahre": jahre,
|
||||
"title": "Berichte",
|
||||
"total_destinataere": total_destinataere,
|
||||
"total_laendereien": total_laendereien,
|
||||
"total_verpachtungen": total_verpachtungen,
|
||||
"total_foerderungen": total_foerderungen,
|
||||
}
|
||||
return render(request, "stiftung/bericht_list.html", context)
|
||||
|
||||
|
||||
@login_required
|
||||
def jahresbericht_generate(request, jahr):
|
||||
"""Generate annual report for a specific year"""
|
||||
# Get data for the year
|
||||
foerderungen = Foerderung.objects.filter(jahr=jahr).select_related("person")
|
||||
verpachtungen = LandVerpachtung.objects.filter(
|
||||
pachtbeginn__year__lte=jahr, pachtende__year__gte=jahr
|
||||
).select_related("land", "paechter")
|
||||
|
||||
# Calculate statistics
|
||||
total_foerderungen = foerderungen.aggregate(total=Sum("betrag"))["total"] or 0
|
||||
total_pachtzins = (
|
||||
verpachtungen.aggregate(total=Sum("pachtzins_pauschal"))["total"] or 0
|
||||
)
|
||||
|
||||
context = {
|
||||
"jahr": jahr,
|
||||
"foerderungen": foerderungen,
|
||||
"verpachtungen": verpachtungen,
|
||||
"total_foerderungen": total_foerderungen,
|
||||
"total_pachtzins": total_pachtzins,
|
||||
"title": f"Jahresbericht {jahr}",
|
||||
}
|
||||
return render(request, "stiftung/jahresbericht.html", context)
|
||||
|
||||
|
||||
@login_required
|
||||
def jahresbericht_generate_redirect(request):
|
||||
"""Redirects the GET form without path param to the proper URL using the provided query param 'jahr'."""
|
||||
jahr = request.GET.get("jahr")
|
||||
if jahr and str(jahr).isdigit():
|
||||
return redirect("stiftung:jahresbericht_generate", jahr=int(jahr))
|
||||
messages.error(request, "Bitte wählen Sie ein gültiges Jahr aus.")
|
||||
return redirect("stiftung:bericht_list")
|
||||
|
||||
|
||||
@login_required
|
||||
def jahresbericht_pdf(request, jahr):
|
||||
"""Generate PDF version of annual report"""
|
||||
from django.http import HttpResponse
|
||||
from django.template.loader import render_to_string
|
||||
from weasyprint import HTML
|
||||
|
||||
# Get data for the year
|
||||
foerderungen = Foerderung.objects.filter(jahr=jahr).select_related("person")
|
||||
verpachtungen = LandVerpachtung.objects.filter(
|
||||
pachtbeginn__year__lte=jahr, pachtende__year__gte=jahr
|
||||
).select_related("land", "paechter")
|
||||
|
||||
# Calculate statistics
|
||||
total_foerderungen = foerderungen.aggregate(total=Sum("betrag"))["total"] or 0
|
||||
total_pachtzins = (
|
||||
verpachtungen.aggregate(total=Sum("pachtzins_pauschal"))["total"] or 0
|
||||
)
|
||||
|
||||
context = {
|
||||
"jahr": jahr,
|
||||
"foerderungen": foerderungen,
|
||||
"verpachtungen": verpachtungen,
|
||||
"total_foerderungen": total_foerderungen,
|
||||
"total_pachtzins": total_pachtzins,
|
||||
}
|
||||
|
||||
# Render HTML
|
||||
html_string = render_to_string("stiftung/jahresbericht.html", context)
|
||||
|
||||
# Generate PDF
|
||||
pdf = HTML(string=html_string).write_pdf()
|
||||
|
||||
# Create response
|
||||
response = HttpResponse(pdf, content_type="application/pdf")
|
||||
response["Content-Disposition"] = f'attachment; filename="jahresbericht_{jahr}.pdf"'
|
||||
|
||||
return response
|
||||
|
||||
|
||||
# API Views for AJAX
|
||||
@login_required
|
||||
def geschaeftsfuehrung(request):
|
||||
"""Hauptansicht für die Geschäftsführung mit Übersicht"""
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from django.db.models import Count, Sum
|
||||
|
||||
from stiftung.models import Rentmeister, StiftungsKonto, Verwaltungskosten
|
||||
|
||||
# Rentmeister-Übersicht
|
||||
rentmeister = Rentmeister.objects.filter(aktiv=True).order_by("nachname", "vorname")
|
||||
|
||||
# Konten-Übersicht
|
||||
konten = StiftungsKonto.objects.filter(aktiv=True).order_by(
|
||||
"bank_name", "kontoname"
|
||||
)
|
||||
gesamtsaldo = konten.aggregate(total=Sum("saldo"))["total"] or 0
|
||||
|
||||
# Aktuelle Kosten (letzten 30 Tage)
|
||||
heute = datetime.now().date()
|
||||
vor_30_tagen = heute - timedelta(days=30)
|
||||
|
||||
aktuelle_kosten = Verwaltungskosten.objects.filter(
|
||||
datum__gte=vor_30_tagen
|
||||
).order_by("-datum")[:10]
|
||||
|
||||
# Statistiken
|
||||
kosten_summe_monat = (
|
||||
Verwaltungskosten.objects.filter(datum__gte=vor_30_tagen).aggregate(
|
||||
total=Sum("betrag")
|
||||
)["total"]
|
||||
or 0
|
||||
)
|
||||
|
||||
kosten_statistik = (
|
||||
Verwaltungskosten.objects.filter(datum__gte=vor_30_tagen)
|
||||
.values("kategorie")
|
||||
.annotate(summe=Sum("betrag"), anzahl=Count("id"))
|
||||
.order_by("-summe")
|
||||
)
|
||||
|
||||
context = {
|
||||
"rentmeister": rentmeister,
|
||||
"konten": konten,
|
||||
"gesamtsaldo": gesamtsaldo,
|
||||
"aktuelle_kosten": aktuelle_kosten,
|
||||
"kosten_summe_monat": kosten_summe_monat,
|
||||
"kosten_statistik": kosten_statistik,
|
||||
}
|
||||
|
||||
return render(request, "stiftung/geschaeftsfuehrung.html", context)
|
||||
|
||||
|
||||
@login_required
|
||||
def konto_list(request):
|
||||
"""Liste aller Stiftungskonten"""
|
||||
from django.db.models import Sum
|
||||
|
||||
from stiftung.models import StiftungsKonto
|
||||
|
||||
konten = StiftungsKonto.objects.all().order_by("bank_name", "kontoname")
|
||||
gesamtsaldo = konten.aggregate(total=Sum("saldo"))["total"] or 0
|
||||
|
||||
context = {
|
||||
"konten": konten,
|
||||
"gesamtsaldo": gesamtsaldo,
|
||||
}
|
||||
|
||||
return render(request, "stiftung/konto_list.html", context)
|
||||
|
||||
|
||||
@login_required
|
||||
def verwaltungskosten_list(request):
|
||||
"""Liste aller Verwaltungskosten"""
|
||||
from django.core.paginator import Paginator
|
||||
|
||||
from stiftung.models import Verwaltungskosten
|
||||
|
||||
kosten = Verwaltungskosten.objects.all().order_by("-datum", "-erstellt_am")
|
||||
|
||||
# Filter nach Kategorie
|
||||
kategorie_filter = request.GET.get("kategorie")
|
||||
if kategorie_filter:
|
||||
kosten = kosten.filter(kategorie=kategorie_filter)
|
||||
|
||||
# Filter nach Status
|
||||
status_filter = request.GET.get("status")
|
||||
if status_filter:
|
||||
kosten = kosten.filter(status=status_filter)
|
||||
|
||||
# Pagination
|
||||
paginator = Paginator(kosten, 25)
|
||||
page_number = request.GET.get("page")
|
||||
page_obj = paginator.get_page(page_number)
|
||||
|
||||
# Für Filter-Dropdowns
|
||||
kategorien = Verwaltungskosten.KATEGORIE_CHOICES
|
||||
status_choices = Verwaltungskosten.STATUS_CHOICES
|
||||
|
||||
context = {
|
||||
"page_obj": page_obj,
|
||||
"kategorien": kategorien,
|
||||
"status_choices": status_choices,
|
||||
"kategorie_filter": kategorie_filter,
|
||||
"status_filter": status_filter,
|
||||
}
|
||||
|
||||
return render(request, "stiftung/verwaltungskosten_list.html", context)
|
||||
|
||||
|
||||
@login_required
|
||||
def rentmeister_list(request):
|
||||
"""Liste aller Rentmeister"""
|
||||
from stiftung.models import Rentmeister
|
||||
|
||||
rentmeister = Rentmeister.objects.all().order_by("nachname", "vorname")
|
||||
|
||||
# Aktive/Inaktive aufteilen
|
||||
aktive_rentmeister = rentmeister.filter(aktiv=True)
|
||||
ehemalige_rentmeister = rentmeister.filter(aktiv=False)
|
||||
|
||||
context = {
|
||||
"aktive_rentmeister": aktive_rentmeister,
|
||||
"ehemalige_rentmeister": ehemalige_rentmeister,
|
||||
"total_count": rentmeister.count(),
|
||||
}
|
||||
|
||||
return render(request, "stiftung/rentmeister_list.html", context)
|
||||
|
||||
|
||||
@login_required
|
||||
def rentmeister_detail(request, pk):
|
||||
"""Detailansicht eines Rentmeisters mit seinen Ausgaben"""
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from django.db.models import Count, Q, Sum
|
||||
|
||||
from stiftung.models import Rentmeister, Verwaltungskosten
|
||||
|
||||
rentmeister = get_object_or_404(Rentmeister, pk=pk)
|
||||
|
||||
# Ausgaben des Rentmeisters
|
||||
ausgaben = Verwaltungskosten.objects.filter(rentmeister=rentmeister).order_by(
|
||||
"-datum"
|
||||
)
|
||||
|
||||
# Statistiken
|
||||
heute = datetime.now().date()
|
||||
aktueller_monat = heute.replace(day=1)
|
||||
aktuelles_jahr = heute.replace(month=1, day=1)
|
||||
|
||||
stats = {
|
||||
"gesamt_ausgaben": ausgaben.aggregate(total=Sum("betrag"))["total"] or 0,
|
||||
"monat_ausgaben": ausgaben.filter(datum__gte=aktueller_monat).aggregate(
|
||||
total=Sum("betrag")
|
||||
)["total"]
|
||||
or 0,
|
||||
"jahr_ausgaben": ausgaben.filter(datum__gte=aktuelles_jahr).aggregate(
|
||||
total=Sum("betrag")
|
||||
)["total"]
|
||||
or 0,
|
||||
"anzahl_ausgaben": ausgaben.count(),
|
||||
"offene_ausgaben": ausgaben.exclude(status="bezahlt").count(),
|
||||
}
|
||||
|
||||
# Kategorie-Aufschlüsselung
|
||||
kategorie_stats = (
|
||||
ausgaben.values("kategorie")
|
||||
.annotate(summe=Sum("betrag"), anzahl=Count("id"))
|
||||
.order_by("-summe")
|
||||
)
|
||||
|
||||
# Aktuelle Ausgaben (letzten 30 Tage)
|
||||
vor_30_tagen = heute - timedelta(days=30)
|
||||
aktuelle_ausgaben = ausgaben.filter(datum__gte=vor_30_tagen)[:10]
|
||||
|
||||
# Verknüpfte Dokumente laden
|
||||
from stiftung.models import DokumentLink
|
||||
|
||||
verknuepfte_dokumente = DokumentLink.objects.filter(
|
||||
rentmeister_id=rentmeister.id
|
||||
).order_by("-id")[
|
||||
:10
|
||||
] # Neueste 10 Dokumente
|
||||
|
||||
context = {
|
||||
"rentmeister": rentmeister,
|
||||
"ausgaben": ausgaben[:20], # Nur erste 20 für Übersicht
|
||||
"stats": stats,
|
||||
"kategorie_stats": kategorie_stats,
|
||||
"aktuelle_ausgaben": aktuelle_ausgaben,
|
||||
"verknuepfte_dokumente": verknuepfte_dokumente,
|
||||
}
|
||||
|
||||
return render(request, "stiftung/rentmeister_detail.html", context)
|
||||
|
||||
|
||||
@login_required
|
||||
def rentmeister_ausgaben(request, pk):
|
||||
"""Vollständige Ausgabenliste eines Rentmeisters mit PDF Export"""
|
||||
from django.core.paginator import Paginator
|
||||
from django.db import models
|
||||
from django.db.models import Count, Q, Sum
|
||||
|
||||
from stiftung.models import Rentmeister, Verwaltungskosten
|
||||
|
||||
rentmeister = get_object_or_404(Rentmeister, pk=pk)
|
||||
|
||||
# Handle PDF export request
|
||||
if request.method == "POST" and "export_pdf" in request.POST:
|
||||
selected_ids = request.POST.getlist("selected_expenses")
|
||||
if selected_ids:
|
||||
# Update status to 'in_bearbeitung' and log each change
|
||||
from stiftung.audit import log_action
|
||||
|
||||
expenses_to_update = Verwaltungskosten.objects.filter(
|
||||
id__in=selected_ids, rentmeister=rentmeister
|
||||
)
|
||||
|
||||
updated_count = 0
|
||||
for expense in expenses_to_update:
|
||||
old_status = expense.status
|
||||
expense.status = "in_bearbeitung"
|
||||
expense.save()
|
||||
updated_count += 1
|
||||
|
||||
# Log the status change
|
||||
log_action(
|
||||
request=request,
|
||||
action="update",
|
||||
entity_type="verwaltungskosten",
|
||||
entity_id=str(expense.pk),
|
||||
entity_name=expense.bezeichnung,
|
||||
description=f'Ausgaben-Status für PDF-Export geändert von "{old_status}" zu "in_bearbeitung"',
|
||||
changes={"status": {"old": old_status, "new": "in_bearbeitung"}},
|
||||
)
|
||||
|
||||
messages.success(
|
||||
request,
|
||||
f"{updated_count} Ausgaben wurden zur Bearbeitung markiert und sind bereit für PDF Export.",
|
||||
)
|
||||
return redirect(
|
||||
"stiftung:rentmeister_ausgaben_pdf",
|
||||
pk=pk,
|
||||
expense_ids=",".join(selected_ids),
|
||||
)
|
||||
|
||||
# Get expenses grouped by status
|
||||
ausgaben_by_status = {}
|
||||
for status_code, status_name in Verwaltungskosten.STATUS_CHOICES:
|
||||
ausgaben_by_status[status_code] = {
|
||||
"name": status_name,
|
||||
"ausgaben": Verwaltungskosten.objects.filter(
|
||||
rentmeister=rentmeister, status=status_code
|
||||
).order_by("-datum", "-erstellt_am"),
|
||||
"total": Verwaltungskosten.objects.filter(
|
||||
rentmeister=rentmeister, status=status_code
|
||||
).aggregate(total=Sum("betrag"))["total"]
|
||||
or 0,
|
||||
}
|
||||
|
||||
# Get statistics
|
||||
stats = Verwaltungskosten.objects.filter(rentmeister=rentmeister).aggregate(
|
||||
total_count=Count("id"),
|
||||
total_amount=Sum("betrag"),
|
||||
geplant_count=Count("id", filter=Q(status="geplant")),
|
||||
geplant_amount=Sum("betrag", filter=Q(status="geplant")),
|
||||
in_bearbeitung_count=Count("id", filter=Q(status="in_bearbeitung")),
|
||||
in_bearbeitung_amount=Sum("betrag", filter=Q(status="in_bearbeitung")),
|
||||
bezahlt_count=Count("id", filter=Q(status="bezahlt")),
|
||||
bezahlt_amount=Sum("betrag", filter=Q(status="bezahlt")),
|
||||
)
|
||||
|
||||
context = {
|
||||
"rentmeister": rentmeister,
|
||||
"ausgaben_by_status": ausgaben_by_status,
|
||||
"stats": stats,
|
||||
"kategorien": Verwaltungskosten.KATEGORIE_CHOICES,
|
||||
"status_choices": Verwaltungskosten.STATUS_CHOICES,
|
||||
}
|
||||
|
||||
return render(request, "stiftung/rentmeister_ausgaben.html", context)
|
||||
|
||||
|
||||
@login_required
|
||||
def rentmeister_create(request):
|
||||
"""Erstelle einen neuen Rentmeister"""
|
||||
from stiftung.forms import RentmeisterForm
|
||||
|
||||
if request.method == "POST":
|
||||
form = RentmeisterForm(request.POST)
|
||||
if form.is_valid():
|
||||
rentmeister = form.save()
|
||||
messages.success(
|
||||
request,
|
||||
f"Rentmeister {rentmeister.get_full_name()} wurde erfolgreich angelegt.",
|
||||
)
|
||||
return redirect("stiftung:rentmeister_detail", pk=rentmeister.pk)
|
||||
else:
|
||||
form = RentmeisterForm()
|
||||
|
||||
context = {
|
||||
"form": form,
|
||||
"title": "Neuen Rentmeister anlegen",
|
||||
"submit_text": "Rentmeister anlegen",
|
||||
}
|
||||
|
||||
return render(request, "stiftung/rentmeister_form.html", context)
|
||||
|
||||
|
||||
@login_required
|
||||
def rentmeister_edit(request, pk):
|
||||
"""Bearbeite einen bestehenden Rentmeister"""
|
||||
from stiftung.forms import RentmeisterForm
|
||||
from stiftung.models import Rentmeister
|
||||
|
||||
rentmeister = get_object_or_404(Rentmeister, pk=pk)
|
||||
|
||||
if request.method == "POST":
|
||||
form = RentmeisterForm(request.POST, instance=rentmeister)
|
||||
if form.is_valid():
|
||||
rentmeister = form.save()
|
||||
messages.success(
|
||||
request,
|
||||
f"Rentmeister {rentmeister.get_full_name()} wurde erfolgreich aktualisiert.",
|
||||
)
|
||||
return redirect("stiftung:rentmeister_detail", pk=rentmeister.pk)
|
||||
else:
|
||||
form = RentmeisterForm(instance=rentmeister)
|
||||
|
||||
context = {
|
||||
"form": form,
|
||||
"rentmeister": rentmeister,
|
||||
"title": f"{rentmeister.get_full_name()} bearbeiten",
|
||||
"submit_text": "Änderungen speichern",
|
||||
}
|
||||
|
||||
return render(request, "stiftung/rentmeister_form.html", context)
|
||||
|
||||
|
||||
@login_required
|
||||
def konto_create(request):
|
||||
"""Erstelle ein neues Stiftungskonto"""
|
||||
from stiftung.forms import StiftungsKontoForm
|
||||
|
||||
if request.method == "POST":
|
||||
form = StiftungsKontoForm(request.POST)
|
||||
if form.is_valid():
|
||||
konto = form.save()
|
||||
messages.success(
|
||||
request, f"Konto {konto.kontoname} wurde erfolgreich angelegt."
|
||||
)
|
||||
return redirect("stiftung:konto_list")
|
||||
else:
|
||||
form = StiftungsKontoForm()
|
||||
|
||||
context = {
|
||||
"form": form,
|
||||
"title": "Neues Konto anlegen",
|
||||
"submit_text": "Konto anlegen",
|
||||
}
|
||||
|
||||
return render(request, "stiftung/konto_form.html", context)
|
||||
|
||||
|
||||
@login_required
|
||||
def konto_edit(request, pk):
|
||||
"""Bearbeite ein bestehendes Stiftungskonto"""
|
||||
from stiftung.forms import StiftungsKontoForm
|
||||
from stiftung.models import StiftungsKonto
|
||||
|
||||
konto = get_object_or_404(StiftungsKonto, pk=pk)
|
||||
|
||||
if request.method == "POST":
|
||||
form = StiftungsKontoForm(request.POST, instance=konto)
|
||||
if form.is_valid():
|
||||
konto = form.save()
|
||||
messages.success(
|
||||
request, f"Konto {konto.kontoname} wurde erfolgreich aktualisiert."
|
||||
)
|
||||
return redirect("stiftung:konto_list")
|
||||
else:
|
||||
form = StiftungsKontoForm(instance=konto)
|
||||
|
||||
context = {
|
||||
"form": form,
|
||||
"konto": konto,
|
||||
"title": f"Konto {konto.kontoname} bearbeiten",
|
||||
"submit_text": "Änderungen speichern",
|
||||
}
|
||||
|
||||
return render(request, "stiftung/konto_form.html", context)
|
||||
|
||||
|
||||
@login_required
|
||||
def konto_detail(request, pk):
|
||||
"""Zeige Details eines Stiftungskontos"""
|
||||
from django.db import models
|
||||
from django.db.models import Count, Max, Q, Sum
|
||||
|
||||
from stiftung.models import BankTransaction, StiftungsKonto
|
||||
|
||||
konto = get_object_or_404(StiftungsKonto, pk=pk)
|
||||
|
||||
# Get transaction statistics
|
||||
transactions = BankTransaction.objects.filter(konto=konto)
|
||||
transaction_stats = transactions.aggregate(
|
||||
total_count=Count("id"),
|
||||
total_eingang=Sum("betrag", filter=Q(betrag__gt=0)),
|
||||
total_ausgang=Sum("betrag", filter=Q(betrag__lt=0)),
|
||||
last_transaction_date=Max("datum"),
|
||||
)
|
||||
|
||||
# Recent transactions
|
||||
recent_transactions = transactions.order_by("-datum", "-importiert_am")[:10]
|
||||
|
||||
context = {
|
||||
"konto": konto,
|
||||
"transaction_stats": transaction_stats,
|
||||
"recent_transactions": recent_transactions,
|
||||
}
|
||||
|
||||
return render(request, "stiftung/konto_detail.html", context)
|
||||
|
||||
|
||||
@login_required
|
||||
def verwaltungskosten_create(request):
|
||||
"""Erstelle neue Verwaltungskosten"""
|
||||
from stiftung.forms import VerwaltungskostenForm
|
||||
from stiftung.models import Rentmeister
|
||||
|
||||
# Check if we're coming from a specific Rentmeister
|
||||
rentmeister_id = request.GET.get("rentmeister")
|
||||
initial_data = {}
|
||||
redirect_url = "stiftung:verwaltungskosten_list"
|
||||
|
||||
if rentmeister_id:
|
||||
try:
|
||||
rentmeister = Rentmeister.objects.get(pk=rentmeister_id)
|
||||
initial_data["rentmeister"] = rentmeister
|
||||
redirect_url = "stiftung:rentmeister_detail"
|
||||
except Rentmeister.DoesNotExist:
|
||||
pass
|
||||
|
||||
if request.method == "POST":
|
||||
form = VerwaltungskostenForm(request.POST)
|
||||
if form.is_valid():
|
||||
kosten = form.save()
|
||||
messages.success(
|
||||
request,
|
||||
f'Verwaltungskosten "{kosten.bezeichnung}" wurden erfolgreich angelegt.',
|
||||
)
|
||||
if rentmeister_id:
|
||||
return redirect(redirect_url, pk=rentmeister_id)
|
||||
return redirect("stiftung:verwaltungskosten_list")
|
||||
else:
|
||||
form = VerwaltungskostenForm(initial=initial_data)
|
||||
|
||||
context = {
|
||||
"form": form,
|
||||
"title": "Neue Verwaltungskosten anlegen",
|
||||
"submit_text": "Kosten anlegen",
|
||||
}
|
||||
|
||||
return render(request, "stiftung/verwaltungskosten_form.html", context)
|
||||
|
||||
|
||||
@login_required
|
||||
def verwaltungskosten_edit(request, pk):
|
||||
"""Bearbeite bestehende Verwaltungskosten"""
|
||||
from stiftung.forms import VerwaltungskostenForm
|
||||
from stiftung.models import Verwaltungskosten
|
||||
|
||||
verwaltungskosten = get_object_or_404(Verwaltungskosten, pk=pk)
|
||||
|
||||
if request.method == "POST":
|
||||
form = VerwaltungskostenForm(request.POST, instance=verwaltungskosten)
|
||||
if form.is_valid():
|
||||
verwaltungskosten = form.save()
|
||||
messages.success(
|
||||
request,
|
||||
f'Verwaltungskosten "{verwaltungskosten.bezeichnung}" wurden erfolgreich aktualisiert.',
|
||||
)
|
||||
return redirect("stiftung:verwaltungskosten_list")
|
||||
else:
|
||||
form = VerwaltungskostenForm(instance=verwaltungskosten)
|
||||
|
||||
context = {
|
||||
"form": form,
|
||||
"verwaltungskosten": verwaltungskosten,
|
||||
"title": f"Verwaltungskosten bearbeiten: {verwaltungskosten.bezeichnung}",
|
||||
"submit_text": "Änderungen speichern",
|
||||
}
|
||||
|
||||
return render(request, "stiftung/verwaltungskosten_form.html", context)
|
||||
|
||||
|
||||
@login_required
|
||||
def verwaltungskosten_delete(request, pk):
|
||||
"""Lösche Verwaltungskosten"""
|
||||
from stiftung.models import Verwaltungskosten
|
||||
|
||||
verwaltungskosten = get_object_or_404(Verwaltungskosten, pk=pk)
|
||||
|
||||
if request.method == "POST":
|
||||
bezeichnung = verwaltungskosten.bezeichnung
|
||||
|
||||
# Log the deletion
|
||||
from stiftung.audit import log_action
|
||||
log_action(
|
||||
request=request,
|
||||
action="delete",
|
||||
entity_type="verwaltungskosten",
|
||||
entity_id=str(verwaltungskosten.pk),
|
||||
entity_name=bezeichnung,
|
||||
description=f'Verwaltungskosten "{bezeichnung}" wurden gelöscht',
|
||||
)
|
||||
|
||||
verwaltungskosten.delete()
|
||||
messages.success(
|
||||
request,
|
||||
f'Verwaltungskosten "{bezeichnung}" wurden erfolgreich gelöscht.',
|
||||
)
|
||||
return redirect("stiftung:verwaltungskosten_list")
|
||||
|
||||
context = {
|
||||
"verwaltungskosten": verwaltungskosten,
|
||||
"title": f"Verwaltungskosten löschen: {verwaltungskosten.bezeichnung}",
|
||||
}
|
||||
|
||||
return render(request, "stiftung/verwaltungskosten_delete.html", context)
|
||||
|
||||
|
||||
@login_required
|
||||
def mark_expense_paid(request):
|
||||
"""Markiere eine Ausgabe als bezahlt"""
|
||||
if request.method == "POST":
|
||||
expense_id = request.POST.get("expense_id")
|
||||
if expense_id:
|
||||
try:
|
||||
from stiftung.models import Verwaltungskosten
|
||||
|
||||
expense = Verwaltungskosten.objects.get(pk=expense_id)
|
||||
old_status = expense.status
|
||||
expense.status = "bezahlt"
|
||||
expense.save()
|
||||
|
||||
# Log the status change
|
||||
from stiftung.audit import log_action
|
||||
|
||||
log_action(
|
||||
request=request,
|
||||
action="update",
|
||||
entity_type="verwaltungskosten",
|
||||
entity_id=str(expense.pk),
|
||||
entity_name=expense.bezeichnung,
|
||||
description=f'Ausgaben-Status geändert von "{old_status}" zu "bezahlt"',
|
||||
changes={"status": {"old": old_status, "new": "bezahlt"}},
|
||||
)
|
||||
|
||||
messages.success(
|
||||
request,
|
||||
f'Ausgabe "{expense.bezeichnung}" wurde als bezahlt markiert.',
|
||||
)
|
||||
return redirect(
|
||||
"stiftung:rentmeister_ausgaben", pk=expense.rentmeister.pk
|
||||
)
|
||||
except Verwaltungskosten.DoesNotExist:
|
||||
messages.error(request, "Ausgabe nicht gefunden.")
|
||||
|
||||
return redirect("stiftung:verwaltungskosten_list")
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# ADMINISTRATION VIEWS
|
||||
# =============================================================================
|
||||
|
||||
|
||||
236
app/stiftung/views/foerderung.py
Normal file
236
app/stiftung/views/foerderung.py
Normal file
@@ -0,0 +1,236 @@
|
||||
# views/foerderung.py
|
||||
# Phase 0: Vision 2026 – Code-Refactoring
|
||||
|
||||
import csv
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
import time
|
||||
from datetime import datetime, timedelta, date
|
||||
from decimal import Decimal
|
||||
|
||||
import qrcode
|
||||
import qrcode.image.svg
|
||||
import requests
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.core.paginator import Paginator
|
||||
from django.db.models import (Avg, Count, DecimalField, F, IntegerField, Q,
|
||||
Sum, Value)
|
||||
from django.db.models.functions import Cast, Coalesce, NullIf, Replace
|
||||
from django.http import HttpResponse, JsonResponse
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from django_otp.decorators import otp_required
|
||||
from django_otp.plugins.otp_totp.models import TOTPDevice
|
||||
from django_otp.plugins.otp_static.models import StaticDevice, StaticToken
|
||||
from django_otp.util import random_hex
|
||||
from rest_framework.decorators import api_view
|
||||
from rest_framework.response import Response
|
||||
|
||||
from stiftung.models import (AppConfiguration, AuditLog, BackupJob, BankTransaction,
|
||||
BriefVorlage, CSVImport, Destinataer,
|
||||
DestinataerEmailEingang, DestinataerNotiz,
|
||||
DestinataerUnterstuetzung,
|
||||
DokumentLink, Foerderung, GeschichteBild, GeschichteSeite,
|
||||
Land, LandAbrechnung, LandVerpachtung, Paechter, Person,
|
||||
Rentmeister, StiftungsKalenderEintrag, StiftungsKonto,
|
||||
UnterstuetzungWiederkehrend, Veranstaltung,
|
||||
Veranstaltungsteilnehmer, Verwaltungskosten,
|
||||
VierteljahresNachweis)
|
||||
from stiftung.forms import (
|
||||
DestinataerForm, DestinataerUnterstuetzungForm, DestinataerNotizForm,
|
||||
FoerderungForm, GeschichteBildForm, GeschichteSeiteForm,
|
||||
LandForm, LandVerpachtungForm, LandAbrechnungForm,
|
||||
PaechterForm, DokumentLinkForm,
|
||||
RentmeisterForm, StiftungsKontoForm, VerwaltungskostenForm,
|
||||
BankTransactionForm, BankImportForm,
|
||||
UnterstuetzungForm, UnterstuetzungWiederkehrendForm,
|
||||
UnterstuetzungMarkAsPaidForm, VierteljahresNachweisForm,
|
||||
UserCreationForm, UserUpdateForm, PasswordChangeForm, UserPermissionForm,
|
||||
TwoFactorSetupForm, TwoFactorVerifyForm, TwoFactorDisableForm,
|
||||
BackupTokenRegenerateForm, PersonForm,
|
||||
VeranstaltungForm, VeranstaltungsteilnehmerForm,
|
||||
)
|
||||
|
||||
|
||||
@login_required
|
||||
def foerderung_list(request):
|
||||
"""List all funding grants with filtering and pagination"""
|
||||
foerderungen = Foerderung.objects.select_related(
|
||||
"destinataer", "verwendungsnachweis"
|
||||
).all()
|
||||
|
||||
# Check for export request - handle both GET and POST
|
||||
export_format = (
|
||||
request.POST.get("format")
|
||||
if request.method == "POST"
|
||||
else request.GET.get("format", "")
|
||||
)
|
||||
selected_ids_param = (
|
||||
request.POST.get("selected_entries", "")
|
||||
if request.method == "POST"
|
||||
else request.GET.get("selected_entries", "")
|
||||
)
|
||||
selected_ids = (
|
||||
[id for id in selected_ids_param.split(",") if id] if selected_ids_param else []
|
||||
)
|
||||
|
||||
# Filtering
|
||||
jahr = request.GET.get("jahr")
|
||||
kategorie = request.GET.get("kategorie")
|
||||
status = request.GET.get("status")
|
||||
destinataer = request.GET.get("destinataer")
|
||||
|
||||
if jahr:
|
||||
foerderungen = foerderungen.filter(jahr=int(jahr))
|
||||
if kategorie:
|
||||
foerderungen = foerderungen.filter(kategorie=kategorie)
|
||||
if status:
|
||||
foerderungen = foerderungen.filter(status=status)
|
||||
if destinataer:
|
||||
foerderungen = foerderungen.filter(destinataer__nachname__icontains=destinataer)
|
||||
|
||||
# Handle exports
|
||||
if export_format == "csv":
|
||||
return export_foerderungen_csv(request, foerderungen, selected_ids)
|
||||
elif export_format == "pdf":
|
||||
return export_foerderungen_pdf(request, foerderungen, selected_ids)
|
||||
|
||||
# Pagination
|
||||
paginator = Paginator(foerderungen, 25)
|
||||
page_number = request.GET.get("page")
|
||||
page_obj = paginator.get_page(page_number)
|
||||
|
||||
# Statistics
|
||||
total_betrag = foerderungen.aggregate(total=Sum("betrag"))["total"] or 0
|
||||
avg_betrag = foerderungen.aggregate(avg=Avg("betrag"))["avg"] or 0
|
||||
|
||||
# Year choices for filters
|
||||
jahre = sorted(
|
||||
set(list(Foerderung.objects.values_list("jahr", flat=True))), reverse=True
|
||||
)
|
||||
|
||||
context = {
|
||||
"page_obj": page_obj,
|
||||
"foerderungen": foerderungen, # Add for counting
|
||||
"total_betrag": total_betrag,
|
||||
"avg_betrag": avg_betrag,
|
||||
"kategorien": Foerderung.KATEGORIE_CHOICES,
|
||||
"status_choices": Foerderung.STATUS_CHOICES,
|
||||
"filter_jahr": jahr,
|
||||
"filter_kategorie": kategorie,
|
||||
"filter_status": status,
|
||||
"filter_person": destinataer,
|
||||
"jahre": jahre,
|
||||
}
|
||||
return render(request, "stiftung/foerderung_list.html", context)
|
||||
|
||||
|
||||
@login_required
|
||||
def foerderung_detail(request, pk):
|
||||
"""Show details of a specific funding grant"""
|
||||
foerderung = get_object_or_404(
|
||||
Foerderung.objects.select_related("person", "verwendungsnachweis"), pk=pk
|
||||
)
|
||||
|
||||
# Alle mit dieser Förderung verknüpften Dokumente laden
|
||||
verknuepfte_dokumente = DokumentLink.objects.filter(
|
||||
foerderung_id=foerderung.pk
|
||||
).order_by("kontext", "titel")
|
||||
|
||||
context = {
|
||||
"foerderung": foerderung,
|
||||
"verknuepfte_dokumente": verknuepfte_dokumente,
|
||||
"title": f"Förderung: {foerderung}",
|
||||
}
|
||||
return render(request, "stiftung/foerderung_detail.html", context)
|
||||
|
||||
|
||||
@login_required
|
||||
def foerderung_create(request):
|
||||
"""Create a new funding grant"""
|
||||
# Get destinataer from URL parameter if provided
|
||||
destinataer_id = request.GET.get("destinataer")
|
||||
initial = {}
|
||||
if destinataer_id:
|
||||
initial["destinataer"] = destinataer_id
|
||||
|
||||
if request.method == "POST":
|
||||
form = FoerderungForm(request.POST)
|
||||
if form.is_valid():
|
||||
foerderung = form.save()
|
||||
messages.success(
|
||||
request,
|
||||
f"Förderung für {foerderung.destinataer} wurde erfolgreich erstellt.",
|
||||
)
|
||||
return redirect("stiftung:foerderung_detail", pk=foerderung.pk)
|
||||
else:
|
||||
form = FoerderungForm(initial=initial)
|
||||
|
||||
context = {
|
||||
"form": form,
|
||||
"title": "Neue Förderung erstellen",
|
||||
}
|
||||
return render(request, "stiftung/foerderung_form.html", context)
|
||||
|
||||
|
||||
@login_required
|
||||
def foerderung_update(request, pk):
|
||||
"""Update an existing funding grant"""
|
||||
foerderung = get_object_or_404(Foerderung, pk=pk)
|
||||
|
||||
if request.method == "POST":
|
||||
form = FoerderungForm(request.POST, instance=foerderung)
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
messages.success(
|
||||
request,
|
||||
f"Förderung für {foerderung.person} wurde erfolgreich aktualisiert.",
|
||||
)
|
||||
return redirect("stiftung:foerderung_detail", pk=foerderung.pk)
|
||||
else:
|
||||
form = FoerderungForm(instance=foerderung)
|
||||
|
||||
context = {
|
||||
"form": form,
|
||||
"foerderung": foerderung,
|
||||
"title": f"Förderung bearbeiten: {foerderung}",
|
||||
}
|
||||
return render(request, "stiftung/foerderung_form.html", context)
|
||||
|
||||
|
||||
@login_required
|
||||
def foerderung_delete(request, pk):
|
||||
"""Delete a funding grant"""
|
||||
foerderung = get_object_or_404(Foerderung, pk=pk)
|
||||
|
||||
if request.method == "POST":
|
||||
# Get the recipient name before deletion
|
||||
recipient_name = (
|
||||
foerderung.destinataer.get_full_name()
|
||||
if foerderung.destinataer
|
||||
else (
|
||||
foerderung.person.get_full_name()
|
||||
if foerderung.person
|
||||
else "Unbekannter Empfänger"
|
||||
)
|
||||
)
|
||||
|
||||
foerderung.delete()
|
||||
messages.success(
|
||||
request, f"Förderung für {recipient_name} wurde erfolgreich gelöscht."
|
||||
)
|
||||
return redirect("stiftung:foerderung_list")
|
||||
|
||||
context = {
|
||||
"foerderung": foerderung,
|
||||
"title": f"Förderung löschen: {foerderung}",
|
||||
}
|
||||
return render(request, "stiftung/foerderung_confirm_delete.html", context)
|
||||
|
||||
|
||||
# DokumentLink Views
|
||||
710
app/stiftung/views/geschichte.py
Normal file
710
app/stiftung/views/geschichte.py
Normal file
@@ -0,0 +1,710 @@
|
||||
# views/geschichte.py
|
||||
# Phase 0: Vision 2026 – Code-Refactoring
|
||||
|
||||
import csv
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
import time
|
||||
from datetime import datetime, timedelta, date
|
||||
from decimal import Decimal
|
||||
|
||||
import qrcode
|
||||
import qrcode.image.svg
|
||||
import requests
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.core.paginator import Paginator
|
||||
from django.db.models import (Avg, Count, DecimalField, F, IntegerField, Q,
|
||||
Sum, Value)
|
||||
from django.db.models.functions import Cast, Coalesce, NullIf, Replace
|
||||
from django.http import HttpResponse, JsonResponse
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from django_otp.decorators import otp_required
|
||||
from django_otp.plugins.otp_totp.models import TOTPDevice
|
||||
from django_otp.plugins.otp_static.models import StaticDevice, StaticToken
|
||||
from django_otp.util import random_hex
|
||||
from rest_framework.decorators import api_view
|
||||
from rest_framework.response import Response
|
||||
|
||||
from stiftung.models import (AppConfiguration, AuditLog, BackupJob, BankTransaction,
|
||||
BriefVorlage, CSVImport, Destinataer,
|
||||
DestinataerEmailEingang, DestinataerNotiz,
|
||||
DestinataerUnterstuetzung,
|
||||
DokumentLink, Foerderung, GeschichteBild, GeschichteSeite,
|
||||
Land, LandAbrechnung, LandVerpachtung, Paechter, Person,
|
||||
Rentmeister, StiftungsKalenderEintrag, StiftungsKonto,
|
||||
UnterstuetzungWiederkehrend, Veranstaltung,
|
||||
Veranstaltungsteilnehmer, Verwaltungskosten,
|
||||
VierteljahresNachweis)
|
||||
from stiftung.forms import (
|
||||
DestinataerForm, DestinataerUnterstuetzungForm, DestinataerNotizForm,
|
||||
FoerderungForm, GeschichteBildForm, GeschichteSeiteForm,
|
||||
LandForm, LandVerpachtungForm, LandAbrechnungForm,
|
||||
PaechterForm, DokumentLinkForm,
|
||||
RentmeisterForm, StiftungsKontoForm, VerwaltungskostenForm,
|
||||
BankTransactionForm, BankImportForm,
|
||||
UnterstuetzungForm, UnterstuetzungWiederkehrendForm,
|
||||
UnterstuetzungMarkAsPaidForm, VierteljahresNachweisForm,
|
||||
UserCreationForm, UserUpdateForm, PasswordChangeForm, UserPermissionForm,
|
||||
TwoFactorSetupForm, TwoFactorVerifyForm, TwoFactorDisableForm,
|
||||
BackupTokenRegenerateForm, PersonForm,
|
||||
VeranstaltungForm, VeranstaltungsteilnehmerForm,
|
||||
)
|
||||
|
||||
|
||||
@login_required
|
||||
def geschichte_list(request):
|
||||
"""List all published history pages"""
|
||||
seiten = GeschichteSeite.objects.filter(ist_veroeffentlicht=True).order_by('sortierung', 'titel')
|
||||
|
||||
context = {
|
||||
'seiten': seiten,
|
||||
'title': 'Geschichte der Stiftung'
|
||||
}
|
||||
|
||||
return render(request, 'stiftung/geschichte/liste.html', context)
|
||||
|
||||
|
||||
@login_required
|
||||
def geschichte_detail(request, slug):
|
||||
"""Display a specific history page"""
|
||||
seite = get_object_or_404(GeschichteSeite, slug=slug, ist_veroeffentlicht=True)
|
||||
bilder = seite.bilder.all().order_by('sortierung', 'titel')
|
||||
|
||||
context = {
|
||||
'seite': seite,
|
||||
'bilder': bilder,
|
||||
'title': seite.titel
|
||||
}
|
||||
|
||||
return render(request, 'stiftung/geschichte/detail.html', context)
|
||||
|
||||
|
||||
@login_required
|
||||
def geschichte_create(request):
|
||||
"""Create a new history page"""
|
||||
if not request.user.has_perm('stiftung.add_geschichteseite'):
|
||||
messages.error(request, 'Sie haben keine Berechtigung, neue Geschichtsseiten zu erstellen.')
|
||||
return redirect('stiftung:geschichte_list')
|
||||
|
||||
if request.method == 'POST':
|
||||
form = GeschichteSeiteForm(request.POST)
|
||||
if form.is_valid():
|
||||
seite = form.save(commit=False)
|
||||
seite.erstellt_von = request.user
|
||||
seite.aktualisiert_von = request.user
|
||||
seite.save()
|
||||
|
||||
messages.success(request, f'Geschichtsseite "{seite.titel}" wurde erfolgreich erstellt.')
|
||||
return redirect('stiftung:geschichte_detail', slug=seite.slug)
|
||||
else:
|
||||
form = GeschichteSeiteForm()
|
||||
|
||||
context = {
|
||||
'form': form,
|
||||
'title': 'Neue Geschichtsseite'
|
||||
}
|
||||
|
||||
return render(request, 'stiftung/geschichte/form.html', context)
|
||||
|
||||
|
||||
@login_required
|
||||
def geschichte_edit(request, slug):
|
||||
"""Edit an existing history page"""
|
||||
seite = get_object_or_404(GeschichteSeite, slug=slug)
|
||||
|
||||
if not request.user.has_perm('stiftung.change_geschichteseite'):
|
||||
messages.error(request, 'Sie haben keine Berechtigung, diese Geschichtsseite zu bearbeiten.')
|
||||
return redirect('stiftung:geschichte_detail', slug=slug)
|
||||
|
||||
if request.method == 'POST':
|
||||
form = GeschichteSeiteForm(request.POST, instance=seite)
|
||||
if form.is_valid():
|
||||
seite = form.save(commit=False)
|
||||
seite.aktualisiert_von = request.user
|
||||
seite.save()
|
||||
|
||||
messages.success(request, f'Geschichtsseite "{seite.titel}" wurde erfolgreich aktualisiert.')
|
||||
return redirect('stiftung:geschichte_detail', slug=seite.slug)
|
||||
else:
|
||||
form = GeschichteSeiteForm(instance=seite)
|
||||
|
||||
context = {
|
||||
'form': form,
|
||||
'seite': seite,
|
||||
'title': f'Bearbeiten: {seite.titel}'
|
||||
}
|
||||
|
||||
return render(request, 'stiftung/geschichte/form.html', context)
|
||||
|
||||
|
||||
@login_required
|
||||
def geschichte_bild_upload(request, slug):
|
||||
"""Upload images to a history page"""
|
||||
seite = get_object_or_404(GeschichteSeite, slug=slug)
|
||||
|
||||
if not request.user.has_perm('stiftung.add_geschichtebild'):
|
||||
messages.error(request, 'Sie haben keine Berechtigung, Bilder hochzuladen.')
|
||||
return redirect('stiftung:geschichte_detail', slug=slug)
|
||||
|
||||
if request.method == 'POST':
|
||||
form = GeschichteBildForm(request.POST, request.FILES)
|
||||
if form.is_valid():
|
||||
bild = form.save(commit=False)
|
||||
bild.seite = seite
|
||||
bild.hochgeladen_von = request.user
|
||||
bild.save()
|
||||
|
||||
messages.success(request, f'Bild "{bild.titel}" wurde erfolgreich hochgeladen.')
|
||||
return redirect('stiftung:geschichte_detail', slug=slug)
|
||||
else:
|
||||
form = GeschichteBildForm()
|
||||
|
||||
context = {
|
||||
'form': form,
|
||||
'seite': seite,
|
||||
'title': f'Bild hochladen: {seite.titel}'
|
||||
}
|
||||
|
||||
return render(request, 'stiftung/geschichte/bild_form.html', context)
|
||||
|
||||
|
||||
@login_required
|
||||
def geschichte_bild_delete(request, slug, bild_id):
|
||||
"""Delete an image from a history page"""
|
||||
seite = get_object_or_404(GeschichteSeite, slug=slug)
|
||||
bild = get_object_or_404(GeschichteBild, id=bild_id, seite=seite)
|
||||
|
||||
if not request.user.has_perm('stiftung.delete_geschichtebild'):
|
||||
messages.error(request, 'Sie haben keine Berechtigung, Bilder zu löschen.')
|
||||
return redirect('stiftung:geschichte_detail', slug=slug)
|
||||
|
||||
if request.method == 'POST':
|
||||
bild_titel = bild.titel
|
||||
bild.delete()
|
||||
messages.success(request, f'Bild "{bild_titel}" wurde erfolgreich gelöscht.')
|
||||
return redirect('stiftung:geschichte_detail', slug=slug)
|
||||
|
||||
context = {
|
||||
'bild': bild,
|
||||
'seite': seite,
|
||||
'title': f'Bild löschen: {bild.titel}'
|
||||
}
|
||||
|
||||
return render(request, 'stiftung/geschichte/bild_delete.html', context)
|
||||
|
||||
|
||||
# Calendar Views
|
||||
@login_required
|
||||
def kalender_view(request):
|
||||
"""Main calendar view with different view types"""
|
||||
from stiftung.services.calendar_service import StiftungsKalenderService
|
||||
import calendar as cal
|
||||
|
||||
calendar_service = StiftungsKalenderService()
|
||||
|
||||
# Get current date and view parameters
|
||||
today = timezone.now().date()
|
||||
view_type = request.GET.get('view', 'month') # month, week, list, agenda
|
||||
year = int(request.GET.get('year', today.year))
|
||||
month = int(request.GET.get('month', today.month))
|
||||
|
||||
# Calculate date ranges based on view type
|
||||
if view_type == 'month':
|
||||
# Get events for the entire month
|
||||
start_date = date(year, month, 1)
|
||||
_, last_day = cal.monthrange(year, month)
|
||||
end_date = date(year, month, last_day)
|
||||
title_suffix = f"{cal.month_name[month]} {year}"
|
||||
|
||||
elif view_type == 'week':
|
||||
# Get current week
|
||||
week_start = today - timedelta(days=today.weekday())
|
||||
start_date = week_start
|
||||
end_date = week_start + timedelta(days=6)
|
||||
title_suffix = f"Woche vom {start_date.strftime('%d.%m')} - {end_date.strftime('%d.%m.%Y')}"
|
||||
|
||||
elif view_type == 'agenda':
|
||||
# Next 30 days
|
||||
start_date = today
|
||||
end_date = today + timedelta(days=30)
|
||||
title_suffix = "Nächste 30 Tage"
|
||||
|
||||
else: # list view
|
||||
# Next 90 days
|
||||
start_date = today
|
||||
end_date = today + timedelta(days=90)
|
||||
title_suffix = "Liste (nächste 90 Tage)"
|
||||
|
||||
# Get events for the date range
|
||||
events = calendar_service.get_all_events(start_date, end_date)
|
||||
|
||||
# Generate calendar grid for month view
|
||||
calendar_grid = None
|
||||
if view_type == 'month':
|
||||
calendar_grid = []
|
||||
first_day = date(year, month, 1)
|
||||
month_cal = cal.monthcalendar(year, month)
|
||||
|
||||
for week in month_cal:
|
||||
week_data = []
|
||||
for day in week:
|
||||
if day == 0:
|
||||
week_data.append(None)
|
||||
else:
|
||||
day_date = date(year, month, day)
|
||||
day_events = [e for e in events if e.date == day_date]
|
||||
week_data.append({
|
||||
'day': day,
|
||||
'date': day_date,
|
||||
'is_today': day_date == today,
|
||||
'events': day_events[:3], # Show max 3 events per day
|
||||
'event_count': len(day_events)
|
||||
})
|
||||
calendar_grid.append(week_data)
|
||||
|
||||
# Navigation dates for month view
|
||||
if month > 1:
|
||||
prev_month = month - 1
|
||||
prev_year = year
|
||||
else:
|
||||
prev_month = 12
|
||||
prev_year = year - 1
|
||||
|
||||
if month < 12:
|
||||
next_month = month + 1
|
||||
next_year = year
|
||||
else:
|
||||
next_month = 1
|
||||
next_year = year + 1
|
||||
|
||||
context = {
|
||||
'title': f'Kalender - {title_suffix}',
|
||||
'events': events,
|
||||
'calendar_grid': calendar_grid,
|
||||
'view_type': view_type,
|
||||
'year': year,
|
||||
'month': month,
|
||||
'today': today,
|
||||
'start_date': start_date,
|
||||
'end_date': end_date,
|
||||
'prev_year': prev_year,
|
||||
'prev_month': prev_month,
|
||||
'next_year': next_year,
|
||||
'next_month': next_month,
|
||||
'month_name': cal.month_name[month],
|
||||
'weekdays': ['Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa', 'So'],
|
||||
}
|
||||
|
||||
# Choose template based on view type
|
||||
if view_type == 'month':
|
||||
template = 'stiftung/kalender/month_view.html'
|
||||
elif view_type == 'week':
|
||||
template = 'stiftung/kalender/week_view.html'
|
||||
elif view_type == 'agenda':
|
||||
template = 'stiftung/kalender/agenda_view.html'
|
||||
else:
|
||||
template = 'stiftung/kalender/list_view.html'
|
||||
|
||||
return render(request, template, context)
|
||||
|
||||
|
||||
@login_required
|
||||
def kalender_create(request):
|
||||
"""Create new calendar event"""
|
||||
from stiftung.models import StiftungsKalenderEintrag
|
||||
|
||||
if request.method == 'POST':
|
||||
# Simple form handling - you can enhance this with Django forms
|
||||
titel = request.POST.get('titel')
|
||||
beschreibung = request.POST.get('beschreibung', '')
|
||||
datum = request.POST.get('datum')
|
||||
kategorie = request.POST.get('kategorie', 'termin')
|
||||
prioritaet = request.POST.get('prioritaet', 'normal')
|
||||
|
||||
if titel and datum:
|
||||
zeit_str = request.POST.get('zeit')
|
||||
uhrzeit = zeit_str if zeit_str else None
|
||||
ganztags = not bool(zeit_str)
|
||||
|
||||
StiftungsKalenderEintrag.objects.create(
|
||||
titel=titel,
|
||||
beschreibung=beschreibung,
|
||||
datum=datum,
|
||||
uhrzeit=uhrzeit,
|
||||
ganztags=ganztags,
|
||||
kategorie=kategorie,
|
||||
prioritaet=prioritaet,
|
||||
erstellt_von=request.user.username
|
||||
)
|
||||
messages.success(request, 'Kalendereintrag wurde erfolgreich erstellt.')
|
||||
return redirect('stiftung:kalender')
|
||||
else:
|
||||
messages.error(request, 'Titel und Datum sind erforderlich.')
|
||||
|
||||
context = {
|
||||
'title': 'Neuer Kalendereintrag',
|
||||
}
|
||||
|
||||
return render(request, 'stiftung/kalender/create.html', context)
|
||||
|
||||
|
||||
@login_required
|
||||
def kalender_detail(request, pk):
|
||||
"""Calendar event detail view"""
|
||||
from stiftung.models import StiftungsKalenderEintrag
|
||||
|
||||
event = get_object_or_404(StiftungsKalenderEintrag, pk=pk)
|
||||
|
||||
context = {
|
||||
'title': f'Kalendereintrag: {event.titel}',
|
||||
'event': event,
|
||||
}
|
||||
|
||||
return render(request, 'stiftung/kalender/detail.html', context)
|
||||
|
||||
|
||||
@login_required
|
||||
def kalender_edit(request, pk):
|
||||
"""Edit calendar event"""
|
||||
from stiftung.models import StiftungsKalenderEintrag
|
||||
|
||||
event = get_object_or_404(StiftungsKalenderEintrag, pk=pk)
|
||||
|
||||
if request.method == 'POST':
|
||||
event.titel = request.POST.get('titel', event.titel)
|
||||
event.beschreibung = request.POST.get('beschreibung', event.beschreibung)
|
||||
event.datum = request.POST.get('datum', event.datum)
|
||||
zeit_str = request.POST.get('zeit')
|
||||
if zeit_str:
|
||||
event.uhrzeit = zeit_str
|
||||
event.ganztags = False
|
||||
else:
|
||||
event.uhrzeit = None
|
||||
event.ganztags = True
|
||||
event.kategorie = request.POST.get('kategorie', event.kategorie)
|
||||
event.prioritaet = request.POST.get('prioritaet', event.prioritaet)
|
||||
event.erledigt = 'erledigt' in request.POST
|
||||
|
||||
event.save()
|
||||
messages.success(request, 'Kalendereintrag wurde aktualisiert.')
|
||||
return redirect('stiftung:kalender_detail', pk=pk)
|
||||
|
||||
context = {
|
||||
'title': f'Bearbeiten: {event.titel}',
|
||||
'event': event,
|
||||
}
|
||||
|
||||
return render(request, 'stiftung/kalender/edit.html', context)
|
||||
|
||||
|
||||
@login_required
|
||||
def kalender_delete(request, pk):
|
||||
"""Delete calendar event"""
|
||||
from stiftung.models import StiftungsKalenderEintrag
|
||||
|
||||
event = get_object_or_404(StiftungsKalenderEintrag, pk=pk)
|
||||
|
||||
if request.method == 'POST':
|
||||
event_titel = event.titel
|
||||
event.delete()
|
||||
messages.success(request, f'Kalendereintrag "{event_titel}" wurde gelöscht.')
|
||||
return redirect('stiftung:kalender')
|
||||
|
||||
context = {
|
||||
'title': f'Löschen: {event.titel}',
|
||||
'event': event,
|
||||
}
|
||||
|
||||
return render(request, 'stiftung/kalender/delete_confirm.html', context)
|
||||
|
||||
|
||||
@login_required
|
||||
def kalender_admin(request):
|
||||
"""Calendar administration with event sources and management"""
|
||||
from stiftung.models import StiftungsKalenderEintrag
|
||||
from stiftung.services.calendar_service import StiftungsKalenderService
|
||||
|
||||
# Get filter parameters
|
||||
show_custom = request.GET.get('show_custom', 'true') == 'true'
|
||||
show_payments = request.GET.get('show_payments', 'true') == 'true'
|
||||
show_leases = request.GET.get('show_leases', 'true') == 'true'
|
||||
show_birthdays = request.GET.get('show_birthdays', 'true') == 'true'
|
||||
category_filter = request.GET.get('category', '')
|
||||
priority_filter = request.GET.get('priority', '')
|
||||
|
||||
# Initialize calendar service
|
||||
calendar_service = StiftungsKalenderService()
|
||||
|
||||
# Get events based on filters
|
||||
from datetime import date, timedelta
|
||||
start_date = date.today() - timedelta(days=30)
|
||||
end_date = date.today() + timedelta(days=90)
|
||||
|
||||
all_events = []
|
||||
|
||||
# Custom calendar entries
|
||||
if show_custom:
|
||||
custom_events = calendar_service.get_calendar_events(start_date, end_date)
|
||||
all_events.extend(custom_events)
|
||||
|
||||
# Payment events
|
||||
if show_payments:
|
||||
payment_events = calendar_service.get_support_payment_events(start_date, end_date)
|
||||
all_events.extend(payment_events)
|
||||
|
||||
# Lease events
|
||||
if show_leases:
|
||||
lease_events = calendar_service.get_lease_events(start_date, end_date)
|
||||
all_events.extend(lease_events)
|
||||
|
||||
# Birthday events
|
||||
if show_birthdays:
|
||||
birthday_events = calendar_service.get_birthday_events(start_date, end_date)
|
||||
all_events.extend(birthday_events)
|
||||
|
||||
# Filter by category and priority if specified
|
||||
if category_filter:
|
||||
all_events = [e for e in all_events if getattr(e, 'category', '') == category_filter]
|
||||
|
||||
if priority_filter:
|
||||
all_events = [e for e in all_events if getattr(e, 'priority', '') == priority_filter]
|
||||
|
||||
# Sort events by date
|
||||
all_events.sort(key=lambda x: x.date)
|
||||
|
||||
# Get statistics
|
||||
custom_count = StiftungsKalenderEintrag.objects.count()
|
||||
total_events = len(all_events)
|
||||
|
||||
# Event source statistics
|
||||
stats = {
|
||||
'custom_events': len([e for e in all_events if getattr(e, 'source', '') == 'custom']),
|
||||
'payment_events': len([e for e in all_events if getattr(e, 'source', '') == 'payment']),
|
||||
'lease_events': len([e for e in all_events if getattr(e, 'source', '') == 'lease']),
|
||||
'birthday_events': len([e for e in all_events if getattr(e, 'source', '') == 'birthday']),
|
||||
'total_events': total_events,
|
||||
'custom_count': custom_count,
|
||||
}
|
||||
|
||||
context = {
|
||||
'title': 'Kalender Administration',
|
||||
'events': all_events,
|
||||
'stats': stats,
|
||||
'show_custom': show_custom,
|
||||
'show_payments': show_payments,
|
||||
'show_leases': show_leases,
|
||||
'show_birthdays': show_birthdays,
|
||||
'category_filter': category_filter,
|
||||
'priority_filter': priority_filter,
|
||||
'categories': StiftungsKalenderEintrag.KATEGORIE_CHOICES,
|
||||
'priorities': StiftungsKalenderEintrag.PRIORITAET_CHOICES,
|
||||
}
|
||||
|
||||
return render(request, 'stiftung/kalender/admin.html', context)
|
||||
|
||||
|
||||
@login_required
|
||||
def kalender_api_events(request):
|
||||
"""API endpoint for calendar events (JSON)"""
|
||||
from django.http import JsonResponse
|
||||
from stiftung.services.calendar_service import StiftungsKalenderService
|
||||
from datetime import datetime
|
||||
|
||||
calendar_service = StiftungsKalenderService()
|
||||
|
||||
# Get date range from request
|
||||
start_date = request.GET.get('start')
|
||||
end_date = request.GET.get('end')
|
||||
|
||||
if start_date and end_date:
|
||||
try:
|
||||
start_date = datetime.strptime(start_date, '%Y-%m-%d').date()
|
||||
end_date = datetime.strptime(end_date, '%Y-%m-%d').date()
|
||||
except ValueError:
|
||||
return JsonResponse({'error': 'Invalid date format'}, status=400)
|
||||
|
||||
events = calendar_service.get_all_events(start_date, end_date)
|
||||
else:
|
||||
events = calendar_service.get_all_events()
|
||||
|
||||
# Convert to FullCalendar format
|
||||
calendar_events = []
|
||||
for event in events:
|
||||
calendar_events.append({
|
||||
'id': getattr(event, 'id', str(event.title)),
|
||||
'title': event.title,
|
||||
'start': event.date.strftime('%Y-%m-%d'),
|
||||
'description': getattr(event, 'description', ''),
|
||||
'className': f"event-{event.category}",
|
||||
'backgroundColor': f"var(--bs-{event.color})",
|
||||
'borderColor': f"var(--bs-{event.color})",
|
||||
})
|
||||
|
||||
return JsonResponse(calendar_events, safe=False)
|
||||
|
||||
|
||||
# Calendar Views
|
||||
@login_required
|
||||
def kalender_view(request):
|
||||
"""Full calendar view with all events"""
|
||||
from stiftung.services.calendar_service import StiftungsKalenderService
|
||||
|
||||
calendar_service = StiftungsKalenderService()
|
||||
|
||||
# Get current month events by default
|
||||
today = timezone.now().date()
|
||||
events = calendar_service.get_events_for_month(today.year, today.month)
|
||||
|
||||
context = {
|
||||
'events': events,
|
||||
'title': 'Stiftungskalender',
|
||||
'current_month': today.strftime('%B %Y'),
|
||||
}
|
||||
|
||||
return render(request, 'stiftung/kalender/kalender.html', context)
|
||||
|
||||
|
||||
context = {
|
||||
'title': 'Kalendereintrag löschen'
|
||||
}
|
||||
return render(request, 'stiftung/kalender/delete.html', context)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# E-Mail-Eingang – Destinatäre
|
||||
# =============================================================================
|
||||
|
||||
@login_required
|
||||
def email_eingang_list(request):
|
||||
"""
|
||||
Übersicht aller eingegangenen E-Mails von Destinatären.
|
||||
Zeigt ungeklärte Absender zuerst, dann chronologisch absteigend.
|
||||
"""
|
||||
status_filter = request.GET.get("status", "")
|
||||
search = request.GET.get("q", "").strip()
|
||||
|
||||
qs = DestinataerEmailEingang.objects.select_related("destinataer", "quartalsnachweis")
|
||||
|
||||
if status_filter:
|
||||
qs = qs.filter(status=status_filter)
|
||||
if search:
|
||||
qs = qs.filter(
|
||||
Q(absender_email__icontains=search)
|
||||
| Q(absender_name__icontains=search)
|
||||
| Q(betreff__icontains=search)
|
||||
| Q(destinataer__vorname__icontains=search)
|
||||
| Q(destinataer__nachname__icontains=search)
|
||||
)
|
||||
|
||||
# Unbekannte Absender zuerst, dann nach Datum absteigend
|
||||
qs = qs.order_by(
|
||||
"status", # "unbekannt" kommt alphabetisch vor "verarbeitet" / "zugewiesen"
|
||||
"-eingangsdatum",
|
||||
)
|
||||
|
||||
paginator = Paginator(qs, 30)
|
||||
page_obj = paginator.get_page(request.GET.get("page"))
|
||||
|
||||
context = {
|
||||
"title": "E-Mail-Eingang (Destinatäre)",
|
||||
"page_obj": page_obj,
|
||||
"status_filter": status_filter,
|
||||
"search": search,
|
||||
"status_choices": DestinataerEmailEingang.STATUS_CHOICES,
|
||||
"counts": {
|
||||
"gesamt": DestinataerEmailEingang.objects.count(),
|
||||
"neu": DestinataerEmailEingang.objects.filter(status="neu").count(),
|
||||
"unbekannt": DestinataerEmailEingang.objects.filter(status="unbekannt").count(),
|
||||
"fehler": DestinataerEmailEingang.objects.filter(status="fehler").count(),
|
||||
},
|
||||
}
|
||||
return render(request, "stiftung/email_eingang/list.html", context)
|
||||
|
||||
|
||||
@login_required
|
||||
def email_eingang_detail(request, pk):
|
||||
"""Detailansicht einer eingegangenen E-Mail mit Möglichkeit zur manuellen Zuordnung."""
|
||||
eingang = get_object_or_404(DestinataerEmailEingang, pk=pk)
|
||||
|
||||
if request.method == "POST":
|
||||
action = request.POST.get("action")
|
||||
|
||||
if action == "assign_destinataer":
|
||||
dest_id = request.POST.get("destinataer_id")
|
||||
if dest_id:
|
||||
try:
|
||||
destinataer = Destinataer.objects.get(pk=dest_id)
|
||||
eingang.destinataer = destinataer
|
||||
eingang.status = "zugewiesen"
|
||||
eingang.save()
|
||||
messages.success(
|
||||
request,
|
||||
f"E-Mail wurde {destinataer} zugeordnet.",
|
||||
)
|
||||
except Destinataer.DoesNotExist:
|
||||
messages.error(request, "Destinatär nicht gefunden.")
|
||||
return redirect("email_eingang_detail", pk=pk)
|
||||
|
||||
elif action == "mark_verarbeitet":
|
||||
eingang.status = "verarbeitet"
|
||||
eingang.notizen = request.POST.get("notizen", eingang.notizen)
|
||||
eingang.save()
|
||||
messages.success(request, "E-Mail als verarbeitet markiert.")
|
||||
return redirect("email_eingang_list")
|
||||
|
||||
elif action == "save_notizen":
|
||||
eingang.notizen = request.POST.get("notizen", "")
|
||||
eingang.save()
|
||||
messages.success(request, "Notizen gespeichert.")
|
||||
return redirect("email_eingang_detail", pk=pk)
|
||||
|
||||
# Paperless-Links zusammenstellen
|
||||
paperless_links = eingang.get_paperless_links()
|
||||
|
||||
# DokumentLinks für diese E-Mail (über paperless_dokument_ids)
|
||||
dokument_links = []
|
||||
if eingang.paperless_dokument_ids:
|
||||
dokument_links = DokumentLink.objects.filter(
|
||||
paperless_document_id__in=eingang.paperless_dokument_ids
|
||||
)
|
||||
|
||||
# Alle aktiven Destinatäre für manuelle Zuordnung
|
||||
alle_destinataere = Destinataer.objects.filter(aktiv=True).order_by("nachname", "vorname")
|
||||
|
||||
context = {
|
||||
"title": f"E-Mail-Eingang: {eingang}",
|
||||
"eingang": eingang,
|
||||
"paperless_links": paperless_links,
|
||||
"dokument_links": dokument_links,
|
||||
"alle_destinataere": alle_destinataere,
|
||||
}
|
||||
return render(request, "stiftung/email_eingang/detail.html", context)
|
||||
|
||||
|
||||
@login_required
|
||||
def email_eingang_poll_trigger(request):
|
||||
"""Löst den IMAP-Poll-Task manuell aus (für Tests und manuelle Verarbeitung)."""
|
||||
if request.method == "POST":
|
||||
from stiftung.tasks import poll_destinataer_emails
|
||||
try:
|
||||
task = poll_destinataer_emails.delay()
|
||||
messages.success(
|
||||
request,
|
||||
f"E-Mail-Abruf wurde gestartet (Task-ID: {task.id}). "
|
||||
"Bitte Seite in ca. 30 Sekunden neu laden.",
|
||||
)
|
||||
except Exception as exc:
|
||||
messages.error(request, f"Fehler beim Starten des Tasks: {exc}")
|
||||
return redirect("email_eingang_list")
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Veranstaltungsmodul
|
||||
# ============================================================
|
||||
|
||||
1553
app/stiftung/views/land.py
Normal file
1553
app/stiftung/views/land.py
Normal file
File diff suppressed because it is too large
Load Diff
2139
app/stiftung/views/system.py
Normal file
2139
app/stiftung/views/system.py
Normal file
File diff suppressed because it is too large
Load Diff
1495
app/stiftung/views/unterstuetzungen.py
Normal file
1495
app/stiftung/views/unterstuetzungen.py
Normal file
File diff suppressed because it is too large
Load Diff
254
app/stiftung/views/veranstaltung.py
Normal file
254
app/stiftung/views/veranstaltung.py
Normal file
@@ -0,0 +1,254 @@
|
||||
# views/veranstaltung.py
|
||||
# Phase 0: Vision 2026 – Code-Refactoring
|
||||
|
||||
import csv
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
import time
|
||||
from datetime import datetime, timedelta, date
|
||||
from decimal import Decimal
|
||||
|
||||
import qrcode
|
||||
import qrcode.image.svg
|
||||
import requests
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.core.paginator import Paginator
|
||||
from django.db.models import (Avg, Count, DecimalField, F, IntegerField, Q,
|
||||
Sum, Value)
|
||||
from django.db.models.functions import Cast, Coalesce, NullIf, Replace
|
||||
from django.http import HttpResponse, JsonResponse
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from django_otp.decorators import otp_required
|
||||
from django_otp.plugins.otp_totp.models import TOTPDevice
|
||||
from django_otp.plugins.otp_static.models import StaticDevice, StaticToken
|
||||
from django_otp.util import random_hex
|
||||
from rest_framework.decorators import api_view
|
||||
from rest_framework.response import Response
|
||||
|
||||
from stiftung.models import (AppConfiguration, AuditLog, BackupJob, BankTransaction,
|
||||
BriefVorlage, CSVImport, Destinataer,
|
||||
DestinataerEmailEingang, DestinataerNotiz,
|
||||
DestinataerUnterstuetzung,
|
||||
DokumentLink, Foerderung, GeschichteBild, GeschichteSeite,
|
||||
Land, LandAbrechnung, LandVerpachtung, Paechter, Person,
|
||||
Rentmeister, StiftungsKalenderEintrag, StiftungsKonto,
|
||||
UnterstuetzungWiederkehrend, Veranstaltung,
|
||||
Veranstaltungsteilnehmer, Verwaltungskosten,
|
||||
VierteljahresNachweis)
|
||||
from stiftung.forms import (
|
||||
DestinataerForm, DestinataerUnterstuetzungForm, DestinataerNotizForm,
|
||||
FoerderungForm, GeschichteBildForm, GeschichteSeiteForm,
|
||||
LandForm, LandVerpachtungForm, LandAbrechnungForm,
|
||||
PaechterForm, DokumentLinkForm,
|
||||
RentmeisterForm, StiftungsKontoForm, VerwaltungskostenForm,
|
||||
BankTransactionForm, BankImportForm,
|
||||
UnterstuetzungForm, UnterstuetzungWiederkehrendForm,
|
||||
UnterstuetzungMarkAsPaidForm, VierteljahresNachweisForm,
|
||||
UserCreationForm, UserUpdateForm, PasswordChangeForm, UserPermissionForm,
|
||||
TwoFactorSetupForm, TwoFactorVerifyForm, TwoFactorDisableForm,
|
||||
BackupTokenRegenerateForm, PersonForm,
|
||||
VeranstaltungForm, VeranstaltungsteilnehmerForm,
|
||||
)
|
||||
|
||||
|
||||
@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
|
||||
|
||||
|
||||
@login_required
|
||||
def veranstaltung_serienbrief_vorschau(request, pk):
|
||||
"""HTML-Vorschau des Serienbriefs im Browser (kein PDF-Download)"""
|
||||
veranstaltung = get_object_or_404(Veranstaltung, pk=pk)
|
||||
teilnehmer = veranstaltung.teilnehmer.all().order_by("nachname", "vorname")
|
||||
return render(
|
||||
request,
|
||||
"stiftung/veranstaltung/serienbrief_vorschau.html",
|
||||
{
|
||||
"veranstaltung": veranstaltung,
|
||||
"teilnehmer": teilnehmer,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@login_required
|
||||
def veranstaltung_create(request):
|
||||
"""Neue Veranstaltung erstellen"""
|
||||
from stiftung.forms import VeranstaltungForm
|
||||
|
||||
if request.method == "POST":
|
||||
form = VeranstaltungForm(request.POST)
|
||||
if form.is_valid():
|
||||
veranstaltung = form.save()
|
||||
messages.success(request, f'Veranstaltung "{veranstaltung.titel}" wurde erstellt.')
|
||||
return redirect("stiftung:veranstaltung_detail", pk=veranstaltung.pk)
|
||||
else:
|
||||
messages.error(request, "Bitte korrigieren Sie die Fehler im Formular.")
|
||||
else:
|
||||
form = VeranstaltungForm()
|
||||
|
||||
return render(request, "stiftung/veranstaltung/form.html", {
|
||||
"form": form,
|
||||
"title": "Neue Veranstaltung erstellen",
|
||||
})
|
||||
|
||||
|
||||
@login_required
|
||||
def veranstaltung_update(request, pk):
|
||||
"""Veranstaltung bearbeiten (inkl. Serienbrief-Felder)"""
|
||||
from stiftung.forms import VeranstaltungForm
|
||||
|
||||
veranstaltung = get_object_or_404(Veranstaltung, pk=pk)
|
||||
|
||||
if request.method == "POST":
|
||||
form = VeranstaltungForm(request.POST, instance=veranstaltung)
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
messages.success(request, f'Veranstaltung "{veranstaltung.titel}" wurde aktualisiert.')
|
||||
return redirect("stiftung:veranstaltung_detail", pk=veranstaltung.pk)
|
||||
else:
|
||||
messages.error(request, "Bitte korrigieren Sie die Fehler im Formular.")
|
||||
else:
|
||||
form = VeranstaltungForm(instance=veranstaltung)
|
||||
|
||||
return render(request, "stiftung/veranstaltung/form.html", {
|
||||
"form": form,
|
||||
"veranstaltung": veranstaltung,
|
||||
"title": f"Veranstaltung bearbeiten: {veranstaltung.titel}",
|
||||
})
|
||||
|
||||
|
||||
@login_required
|
||||
def veranstaltung_delete(request, pk):
|
||||
"""Veranstaltung löschen"""
|
||||
veranstaltung = get_object_or_404(Veranstaltung, pk=pk)
|
||||
|
||||
if request.method == "POST":
|
||||
titel = veranstaltung.titel
|
||||
veranstaltung.delete()
|
||||
messages.success(request, f'Veranstaltung "{titel}" wurde gelöscht.')
|
||||
return redirect("stiftung:veranstaltung_list")
|
||||
|
||||
return render(request, "stiftung/veranstaltung/delete.html", {
|
||||
"veranstaltung": veranstaltung,
|
||||
})
|
||||
|
||||
|
||||
@login_required
|
||||
def teilnehmer_create(request, veranstaltung_pk):
|
||||
"""Teilnehmer zu einer Veranstaltung hinzufügen"""
|
||||
from stiftung.forms import VeranstaltungsteilnehmerForm
|
||||
|
||||
veranstaltung = get_object_or_404(Veranstaltung, pk=veranstaltung_pk)
|
||||
|
||||
if request.method == "POST":
|
||||
form = VeranstaltungsteilnehmerForm(request.POST)
|
||||
if form.is_valid():
|
||||
teilnehmer = form.save(commit=False)
|
||||
teilnehmer.veranstaltung = veranstaltung
|
||||
teilnehmer.save()
|
||||
messages.success(request, f"{teilnehmer.vorname} {teilnehmer.nachname} wurde hinzugefügt.")
|
||||
return redirect("stiftung:veranstaltung_detail", pk=veranstaltung.pk)
|
||||
else:
|
||||
messages.error(request, "Bitte korrigieren Sie die Fehler im Formular.")
|
||||
else:
|
||||
form = VeranstaltungsteilnehmerForm()
|
||||
|
||||
return render(request, "stiftung/veranstaltung/teilnehmer_form.html", {
|
||||
"form": form,
|
||||
"veranstaltung": veranstaltung,
|
||||
"title": "Teilnehmer hinzufügen",
|
||||
})
|
||||
|
||||
|
||||
@login_required
|
||||
def teilnehmer_update(request, veranstaltung_pk, pk):
|
||||
"""Teilnehmer bearbeiten"""
|
||||
from stiftung.forms import VeranstaltungsteilnehmerForm
|
||||
|
||||
veranstaltung = get_object_or_404(Veranstaltung, pk=veranstaltung_pk)
|
||||
teilnehmer = get_object_or_404(Veranstaltungsteilnehmer, pk=pk, veranstaltung=veranstaltung)
|
||||
|
||||
if request.method == "POST":
|
||||
form = VeranstaltungsteilnehmerForm(request.POST, instance=teilnehmer)
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
messages.success(request, f"{teilnehmer.vorname} {teilnehmer.nachname} wurde aktualisiert.")
|
||||
return redirect("stiftung:veranstaltung_detail", pk=veranstaltung.pk)
|
||||
else:
|
||||
messages.error(request, "Bitte korrigieren Sie die Fehler im Formular.")
|
||||
else:
|
||||
form = VeranstaltungsteilnehmerForm(instance=teilnehmer)
|
||||
|
||||
return render(request, "stiftung/veranstaltung/teilnehmer_form.html", {
|
||||
"form": form,
|
||||
"veranstaltung": veranstaltung,
|
||||
"teilnehmer": teilnehmer,
|
||||
"title": f"Teilnehmer bearbeiten: {teilnehmer.vorname} {teilnehmer.nachname}",
|
||||
})
|
||||
|
||||
|
||||
@login_required
|
||||
def teilnehmer_delete(request, veranstaltung_pk, pk):
|
||||
"""Teilnehmer aus Veranstaltung entfernen"""
|
||||
veranstaltung = get_object_or_404(Veranstaltung, pk=veranstaltung_pk)
|
||||
teilnehmer = get_object_or_404(Veranstaltungsteilnehmer, pk=pk, veranstaltung=veranstaltung)
|
||||
|
||||
if request.method == "POST":
|
||||
name = f"{teilnehmer.vorname} {teilnehmer.nachname}"
|
||||
teilnehmer.delete()
|
||||
messages.success(request, f"{name} wurde aus der Teilnehmerliste entfernt.")
|
||||
return redirect("stiftung:veranstaltung_detail", pk=veranstaltung.pk)
|
||||
|
||||
return render(request, "stiftung/veranstaltung/teilnehmer_delete.html", {
|
||||
"veranstaltung": veranstaltung,
|
||||
"teilnehmer": teilnehmer,
|
||||
})
|
||||
Reference in New Issue
Block a user