- Save cover email body as DMS document with new 'email' context type - Show email body separately from attachments in email detail view - Add per-category DMS document assignment in quarterly confirmation (Studiennachweis, Einkommenssituation, Vermögenssituation) - Add VERSION file and context processor for automatic version display - Add MCP server, agent system, import/export, and new migrations - Update compose files and production environment template Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
815 lines
30 KiB
Python
815 lines
30 KiB
Python
# 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, EmailEingang, 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()
|
||
form.save_m2m()
|
||
|
||
# Link selected DMS documents
|
||
dok_ids = request.POST.getlist("dokument_ids")
|
||
if dok_ids:
|
||
from stiftung.models import DokumentDatei
|
||
seite.dokumente.set(DokumentDatei.objects.filter(pk__in=dok_ids))
|
||
|
||
messages.success(request, f'Geschichtsseite "{seite.titel}" wurde erfolgreich erstellt.')
|
||
return redirect('stiftung:geschichte_detail', slug=seite.slug)
|
||
else:
|
||
form = GeschichteSeiteForm()
|
||
|
||
# Verfuegbare Stiftungsgeschichte-Dokumente aus DMS
|
||
from stiftung.models import DokumentDatei
|
||
geschichte_dokumente = DokumentDatei.objects.filter(
|
||
kontext="stiftungsgeschichte"
|
||
).order_by("-erstellt_am")[:20]
|
||
|
||
context = {
|
||
'form': form,
|
||
'title': 'Neue Geschichtsseite',
|
||
'geschichte_dokumente': geschichte_dokumente,
|
||
'selected_dok_ids': [],
|
||
}
|
||
|
||
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()
|
||
form.save_m2m()
|
||
|
||
# Update linked DMS documents
|
||
dok_ids = request.POST.getlist("dokument_ids")
|
||
from stiftung.models import DokumentDatei
|
||
seite.dokumente.set(DokumentDatei.objects.filter(pk__in=dok_ids))
|
||
|
||
messages.success(request, f'Geschichtsseite "{seite.titel}" wurde erfolgreich aktualisiert.')
|
||
return redirect('stiftung:geschichte_detail', slug=seite.slug)
|
||
else:
|
||
form = GeschichteSeiteForm(instance=seite)
|
||
|
||
# Verfuegbare Stiftungsgeschichte-Dokumente aus DMS
|
||
from stiftung.models import DokumentDatei
|
||
geschichte_dokumente = DokumentDatei.objects.filter(
|
||
kontext="stiftungsgeschichte"
|
||
).order_by("-erstellt_am")[:20]
|
||
|
||
# IDs der bereits verknuepften Dokumente
|
||
selected_dok_ids = list(seite.dokumente.values_list("pk", flat=True))
|
||
|
||
context = {
|
||
'form': form,
|
||
'seite': seite,
|
||
'title': f'Bearbeiten: {seite.titel}',
|
||
'geschichte_dokumente': geschichte_dokumente,
|
||
'selected_dok_ids': selected_dok_ids,
|
||
}
|
||
|
||
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):
|
||
"""
|
||
Uebersicht aller eingegangenen E-Mails.
|
||
Filtert nach Status und Kategorie, zeigt ungeklaerte Absender zuerst.
|
||
"""
|
||
status_filter = request.GET.get("status", "")
|
||
kategorie_filter = request.GET.get("kategorie", "")
|
||
search = request.GET.get("q", "").strip()
|
||
|
||
qs = EmailEingang.objects.select_related("destinataer", "quartalsnachweis", "verwaltungskosten")
|
||
|
||
if status_filter:
|
||
qs = qs.filter(status=status_filter)
|
||
if kategorie_filter:
|
||
qs = qs.filter(kategorie=kategorie_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",
|
||
"-eingangsdatum",
|
||
)
|
||
|
||
paginator = Paginator(qs, 30)
|
||
page_obj = paginator.get_page(request.GET.get("page"))
|
||
|
||
context = {
|
||
"title": "E-Mail-Eingang",
|
||
"page_obj": page_obj,
|
||
"status_filter": status_filter,
|
||
"kategorie_filter": kategorie_filter,
|
||
"search": search,
|
||
"status_choices": EmailEingang.STATUS_CHOICES,
|
||
"kategorie_choices": EmailEingang.KATEGORIE_CHOICES,
|
||
"counts": {
|
||
"gesamt": EmailEingang.objects.count(),
|
||
"neu": EmailEingang.objects.filter(status="neu").count(),
|
||
"unbekannt": EmailEingang.objects.filter(status="unbekannt").count(),
|
||
"rechnung": EmailEingang.objects.filter(kategorie="rechnung").count(),
|
||
"fehler": EmailEingang.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 Zuordnung und Rechnungserfassung."""
|
||
eingang = get_object_or_404(EmailEingang, 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.kategorie = "destinataer"
|
||
eingang.status = "zugewiesen"
|
||
eingang.save()
|
||
eingang.dokument_dateien.filter(destinataer__isnull=True).update(
|
||
destinataer=destinataer
|
||
)
|
||
messages.success(request, f"E-Mail wurde {destinataer} zugeordnet.")
|
||
except Destinataer.DoesNotExist:
|
||
messages.error(request, "Destinataer nicht gefunden.")
|
||
return redirect("stiftung:email_eingang_detail", pk=pk)
|
||
|
||
elif action == "erfasse_rechnung":
|
||
# Erstelle Verwaltungskosten-Eintrag aus Email
|
||
bezeichnung = request.POST.get("bezeichnung", eingang.betreff[:200]).strip()
|
||
betrag = request.POST.get("betrag", "0").strip().replace(",", ".")
|
||
kategorie = request.POST.get("vk_kategorie", "rechnung_intern")
|
||
lieferant = request.POST.get("lieferant", eingang.absender_name or eingang.absender_email).strip()
|
||
rechnungsnummer = request.POST.get("rechnungsnummer", "").strip()
|
||
|
||
try:
|
||
from decimal import Decimal
|
||
vk = Verwaltungskosten(
|
||
bezeichnung=bezeichnung[:200],
|
||
kategorie=kategorie,
|
||
betrag=Decimal(betrag) if betrag else Decimal("0"),
|
||
datum=eingang.eingangsdatum.date(),
|
||
lieferant_firma=lieferant[:200],
|
||
rechnungsnummer=rechnungsnummer[:100],
|
||
status="erhalten",
|
||
beschreibung=f"Automatisch erfasst aus E-Mail-Eingang.\nBetreff: {eingang.betreff}\nAbsender: {eingang.absender_email}",
|
||
)
|
||
vk.save()
|
||
|
||
# Verknuepfe Email mit Verwaltungskosten
|
||
eingang.verwaltungskosten = vk
|
||
eingang.kategorie = "rechnung"
|
||
eingang.status = "rechnung_erfasst"
|
||
eingang.save()
|
||
|
||
# Verknuepfe angehaengte Dokumente mit Verwaltungskosten
|
||
for dok in eingang.dokument_dateien.all():
|
||
dok.verwaltungskosten = vk
|
||
dok.kontext = "rechnung"
|
||
dok.save()
|
||
|
||
messages.success(request, f'Rechnung "{bezeichnung}" erfasst (€{betrag}).')
|
||
except Exception as exc:
|
||
messages.error(request, f"Fehler beim Erfassen der Rechnung: {exc}")
|
||
return redirect("stiftung:email_eingang_detail", pk=pk)
|
||
|
||
elif action == "set_kategorie":
|
||
new_kategorie = request.POST.get("kategorie", "")
|
||
if new_kategorie in dict(EmailEingang.KATEGORIE_CHOICES):
|
||
eingang.kategorie = new_kategorie
|
||
eingang.save()
|
||
messages.success(request, f"Kategorie auf '{dict(EmailEingang.KATEGORIE_CHOICES)[new_kategorie]}' gesetzt.")
|
||
return redirect("stiftung: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("stiftung:email_eingang_list")
|
||
|
||
elif action == "save_notizen":
|
||
eingang.notizen = request.POST.get("notizen", "")
|
||
eingang.save()
|
||
messages.success(request, "Notizen gespeichert.")
|
||
return redirect("stiftung:email_eingang_detail", pk=pk)
|
||
|
||
# DMS-Dokumente: E-Mail-Body und Anhaenge trennen
|
||
alle_dms_dokumente = eingang.dokument_dateien.all().order_by("erstellt_am")
|
||
email_dokument = alle_dms_dokumente.filter(kontext="email").first()
|
||
anhaenge_dokumente = alle_dms_dokumente.exclude(kontext="email")
|
||
|
||
# Alle aktiven Destinataere fuer manuelle Zuordnung
|
||
alle_destinataere = Destinataer.objects.filter(aktiv=True).order_by("nachname", "vorname")
|
||
|
||
context = {
|
||
"title": f"E-Mail-Eingang: {eingang}",
|
||
"eingang": eingang,
|
||
"email_dokument": email_dokument,
|
||
"anhaenge_dokumente": anhaenge_dokumente,
|
||
"alle_destinataere": alle_destinataere,
|
||
"vk_kategorie_choices": Verwaltungskosten.KATEGORIE_CHOICES,
|
||
}
|
||
return render(request, "stiftung/email_eingang/detail.html", context)
|
||
|
||
|
||
@login_required
|
||
def email_eingang_poll_trigger(request):
|
||
"""Loest den IMAP-Poll manuell aus – sucht alle E-Mails der letzten 30 Tage."""
|
||
if request.method == "POST":
|
||
from stiftung.tasks import poll_emails
|
||
try:
|
||
result = poll_emails.apply(kwargs={"search_all_recent_days": 30}).get(timeout=300)
|
||
processed = result.get("processed", 0) if isinstance(result, dict) else 0
|
||
if result and result.get("status") == "skipped":
|
||
messages.warning(request, "IMAP ist nicht konfiguriert. Bitte Einstellungen unter Administration → E-Mail / IMAP prüfen.")
|
||
elif processed > 0:
|
||
error_count = result.get("errors", 0) if isinstance(result, dict) else 0
|
||
if error_count > 0:
|
||
messages.warning(request, f"{processed} E-Mail(s) importiert, aber {error_count} Fehler aufgetreten. Bitte Logs prüfen.")
|
||
else:
|
||
messages.success(request, f"{processed} neue E-Mail(s) importiert.")
|
||
else:
|
||
error_count = result.get("errors", 0) if isinstance(result, dict) else 0
|
||
if error_count > 0:
|
||
messages.warning(request, f"Keine neuen E-Mails importiert, aber {error_count} Fehler aufgetreten. Bitte Logs prüfen.")
|
||
else:
|
||
messages.info(request, "Keine neuen E-Mails gefunden.")
|
||
except Exception as exc:
|
||
messages.error(request, f"Fehler beim E-Mail-Abruf: {exc}")
|
||
return redirect("stiftung:email_eingang_list")
|
||
|
||
|
||
@login_required
|
||
def email_eingang_delete(request, pk):
|
||
"""Loescht eine eingegangene E-Mail."""
|
||
eingang = get_object_or_404(EmailEingang, pk=pk)
|
||
if request.method == "POST":
|
||
betreff = eingang.betreff or "(kein Betreff)"
|
||
eingang.delete()
|
||
messages.success(request, f'E-Mail "{betreff}" wurde gelöscht.')
|
||
return redirect("stiftung:email_eingang_list")
|
||
return redirect("stiftung:email_eingang_detail", pk=pk)
|
||
|
||
|
||
# ============================================================
|
||
# Veranstaltungsmodul
|
||
# ============================================================
|
||
|