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:
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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user