Generalize email system with invoice workflow and Stiftungsgeschichte category

- Rename DestinataerEmailEingang → EmailEingang with category support
  (destinataer, rechnung, land_pacht, stiftungsgeschichte, allgemein)
- Add invoice capture workflow: create Verwaltungskosten from email,
  link DMS documents as invoice attachments, track payment status
- Add Stiftungsgeschichte email category with auto-detection patterns
  (Ahnenforschung, Genealogie, Chronik, etc.) and DMS integration
- Update poll_emails task with category detection and DMS context mapping
- Show available history documents in Geschichte editor sidebar
- Consolidate DMS views, remove legacy dokument templates
- Update all detail/form templates for DMS document linking
- Add deploy.sh script and streamline compose.yml

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
SysAdmin Agent
2026-03-12 10:17:14 +00:00
parent f4fc512ad3
commit e6f4c5ba1b
44 changed files with 1076 additions and 3428 deletions

View File

@@ -11,8 +11,6 @@ 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
@@ -35,7 +33,7 @@ from stiftung.models import (AppConfiguration, AuditLog, BackupJob, BankTransact
BriefVorlage, CSVImport, Destinataer,
DestinataerEmailEingang, DestinataerNotiz,
DestinataerUnterstuetzung,
DokumentLink, Foerderung, GeschichteBild, GeschichteSeite,
DokumentDatei, DokumentLink, Foerderung, GeschichteBild, GeschichteSeite,
Land, LandAbrechnung, LandVerpachtung, Paechter, Person,
Rentmeister, StiftungsKalenderEintrag, StiftungsKonto,
UnterstuetzungWiederkehrend, Veranstaltung,
@@ -143,8 +141,8 @@ def paechter_detail(request, pk):
paechter = get_object_or_404(Paechter, pk=pk)
# Alle mit diesem Pächter verknüpften Dokumente laden
verknuepfte_dokumente = DokumentLink.objects.filter(
paechter_id=paechter.pk
verknuepfte_dokumente = DokumentDatei.objects.filter(
paechter=paechter
).order_by("kontext", "titel")
# Neue LandVerpachtungen für diesen Pächter laden
@@ -392,7 +390,7 @@ def land_detail(request, pk):
land = get_object_or_404(Land, pk=pk)
# Alle mit dieser Länderei verknüpften Dokumente laden
verknuepfte_dokumente = DokumentLink.objects.filter(land_id=land.pk).order_by(
verknuepfte_dokumente = DokumentDatei.objects.filter(land=land).order_by(
"kontext", "titel"
)
@@ -584,8 +582,8 @@ def land_verpachtung_detail(request, pk):
verpachtung = get_object_or_404(LandVerpachtung, pk=pk)
# Alle mit dieser Verpachtung verknüpften Dokumente laden
verknuepfte_dokumente = DokumentLink.objects.filter(
land_verpachtung_id=verpachtung.pk
verknuepfte_dokumente = DokumentDatei.objects.filter(
verpachtung=verpachtung
).order_by("kontext", "titel")
context = {
@@ -747,55 +745,26 @@ def paechter_export(request, pk):
json.dumps(entity_data, indent=2, ensure_ascii=False),
)
# 2. Linked documents from Paperless
dokumente = DokumentLink.objects.filter(paechter_id=paechter.pk)
# 2. DMS-Dokumente (Django-natives DMS)
dokumente = DokumentDatei.objects.filter(paechter=paechter)
docs_data = []
for doc in dokumente:
doc_data = {
"paperless_id": doc.paperless_document_id,
"dms_id": str(doc.id),
"titel": doc.titel,
"kontext": doc.get_kontext_display(),
"beschreibung": doc.beschreibung,
"dateiname": doc.dateiname_original,
}
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:
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"
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}"
if doc.datei:
safe_filename = doc.dateiname_original or str(doc.id)
zipf.writestr(f"dokumente/{safe_filename}", doc.datei.read())
doc_data["included"] = True
except Exception as e:
doc_data["download_error"] = str(e)
doc_data["error"] = str(e)
if docs_data:
zipf.writestr(
@@ -871,55 +840,26 @@ def land_export(request, pk):
"land_data.json", json.dumps(entity_data, indent=2, ensure_ascii=False)
)
# 2. Linked documents from Paperless
dokumente = DokumentLink.objects.filter(land_id=land.pk)
# 2. DMS-Dokumente (Django-natives DMS)
dokumente = DokumentDatei.objects.filter(land=land)
docs_data = []
for doc in dokumente:
doc_data = {
"paperless_id": doc.paperless_document_id,
"dms_id": str(doc.id),
"titel": doc.titel,
"kontext": doc.get_kontext_display(),
"beschreibung": doc.beschreibung,
"dateiname": doc.dateiname_original,
}
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:
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"
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}"
if doc.datei:
safe_filename = doc.dateiname_original or str(doc.id)
zipf.writestr(f"dokumente/{safe_filename}", doc.datei.read())
doc_data["included"] = True
except Exception as e:
doc_data["download_error"] = str(e)
doc_data["error"] = str(e)
if docs_data:
zipf.writestr(
@@ -996,55 +936,26 @@ def verpachtung_export(request, pk):
json.dumps(entity_data, indent=2, ensure_ascii=False),
)
# 2. Linked documents from Paperless
dokumente = DokumentLink.objects.filter(verpachtung_id=verpachtung.pk)
# 2. DMS-Dokumente (Django-natives DMS)
dokumente = DokumentDatei.objects.filter(verpachtung=verpachtung)
docs_data = []
for doc in dokumente:
doc_data = {
"paperless_id": doc.paperless_document_id,
"dms_id": str(doc.id),
"titel": doc.titel,
"kontext": doc.get_kontext_display(),
"beschreibung": doc.beschreibung,
"dateiname": doc.dateiname_original,
}
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:
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"
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}"
if doc.datei:
safe_filename = doc.dateiname_original or str(doc.id)
zipf.writestr(f"dokumente/{safe_filename}", doc.datei.read())
doc_data["included"] = True
except Exception as e:
doc_data["download_error"] = str(e)
doc_data["error"] = str(e)
if docs_data:
zipf.writestr(
@@ -1438,8 +1349,8 @@ def verpachtung_detail(request, pk):
verpachtung = get_object_or_404(LandVerpachtung, pk=pk)
# Alle mit dieser Verpachtung verknüpften Dokumente laden
verknuepfte_dokumente = DokumentLink.objects.filter(
land_verpachtung_id=verpachtung.pk
verknuepfte_dokumente = DokumentDatei.objects.filter(
verpachtung=verpachtung
).order_by("kontext", "titel")
context = {