- 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>
698 lines
28 KiB
Python
698 lines
28 KiB
Python
# 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
|
||
|
||
|