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