feat: Email-Eingangsverarbeitung für Destinatäre implementieren
Neues System zur automatischen Verarbeitung eingehender E-Mails von Destinatären. IMAP-Polling alle 15 Minuten via Celery Beat, automatische Zuordnung zu Destinatären anhand der E-Mail-Adresse, Upload von Anhängen zu Paperless-NGX. Umfasst: - DestinataerEmailEingang Model mit Status-Tracking - Celery Task für IMAP-Polling und Paperless-Integration - Web-UI (Liste + Detail) mit Such- und Filterfunktion - Admin-Interface mit Bulk-Actions - Agent-Dokumentation (SysAdmin, RentmeisterAI) - Dev-Environment Modernisierung (docker compose v2) Reviewed by: SysAdmin (STI-15), RentmeisterAI (STI-16) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -28,7 +28,8 @@ from rest_framework.decorators import api_view
|
||||
from rest_framework.response import Response
|
||||
|
||||
from .models import (AppConfiguration, CSVImport, Destinataer,
|
||||
DestinataerUnterstuetzung, DokumentLink, Foerderung, Land,
|
||||
DestinataerEmailEingang, DestinataerUnterstuetzung,
|
||||
DokumentLink, Foerderung, Land,
|
||||
LandAbrechnung, LandVerpachtung, Paechter, Person,
|
||||
StiftungsKonto, UnterstuetzungWiederkehrend, VierteljahresNachweis)
|
||||
|
||||
@@ -8513,3 +8514,131 @@ def kalender_view(request):
|
||||
'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")
|
||||
|
||||
Reference in New Issue
Block a user