Phase 4: SEPA-Validierung (schwifty), Globale Suche (Cmd+K) & Jahresbericht-Modul

- SEPA-Export: IBAN/BIC-Validierung via schwifty, Schuldner-Konto aus StiftungsKonto
- Globale Suche: Cmd+K Modal über Destinatäre, Pächter, Ländereien, Förderungen, Dokumente
- Jahresbericht: Vollständige Jahresbilanz mit Einnahmen/Ausgaben/Netto, Unterstützungen,
  Landabrechnungen, Verwaltungskosten nach Kategorie, PDF-Export

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
SysAdmin Agent
2026-03-11 12:57:36 +00:00
parent a79a0989d6
commit 2be72c3990
8 changed files with 695 additions and 160 deletions

View File

@@ -1768,9 +1768,14 @@ def unterstuetzung_abschliessen(request, pk):
@login_required
def sepa_xml_export(request):
"""2c: SEPA pain.001 XML-Export für freigegebene Zahlungen."""
"""Phase 4: SEPA pain.001 XML-Export mit schwifty IBAN/BIC-Validierung."""
from xml.etree.ElementTree import Element, SubElement, tostring
import xml.dom.minidom
try:
from schwifty import IBAN as SchwiftyIBAN, BIC as SchwiftyBIC
schwifty_available = True
except ImportError:
schwifty_available = False
zahlungen = DestinataerUnterstuetzung.objects.filter(
status="freigegeben"
@@ -1780,10 +1785,32 @@ def sepa_xml_export(request):
messages.warning(request, "Keine freigegebenen Zahlungen für SEPA-Export vorhanden.")
return redirect("stiftung:zahlungs_pipeline")
# IBAN/BIC-Validierung mit schwifty
validierungsfehler = []
if schwifty_available:
for zahlung in zahlungen:
iban_raw = (zahlung.empfaenger_iban or zahlung.destinataer.iban or "").replace(" ", "")
if not iban_raw:
validierungsfehler.append(
f"{zahlung.destinataer.get_full_name()}: Keine IBAN hinterlegt"
)
continue
try:
SchwiftyIBAN(iban_raw)
except Exception:
validierungsfehler.append(
f"{zahlung.destinataer.get_full_name()}: Ungültige IBAN '{iban_raw}'"
)
if validierungsfehler:
for fehler in validierungsfehler:
messages.error(request, f"SEPA-Validierungsfehler: {fehler}")
return redirect("stiftung:zahlungs_pipeline")
heute = date.today()
msg_id = f"STIFTUNG-{heute.strftime('%Y%m%d%H%M%S')}"
nb_of_txs = zahlungen.count()
ctrl_sum = str(sum(z.betrag for z in zahlungen))
ctrl_sum = f"{sum(z.betrag for z in zahlungen):.2f}"
root = Element("Document", {
"xmlns": "urn:iso:std:iso:20022:tech:xsd:pain.001.001.03",
@@ -1810,18 +1837,34 @@ def sepa_xml_export(request):
dbtr = SubElement(pmt_inf, "Dbtr")
SubElement(dbtr, "Nm").text = "van Hees-Theyssen-Vogel'sche Stiftung"
# Schuldner-IBAN aus aktivem Stiftungskonto
if zahlungen.first() and zahlungen.first().konto and zahlungen.first().konto.iban:
dbtr_acct = SubElement(pmt_inf, "DbtrAcct")
dbtr_acct_id = SubElement(dbtr_acct, "Id")
SubElement(dbtr_acct_id, "IBAN").text = zahlungen.first().konto.iban.replace(" ", "")
dbtr_agt = SubElement(pmt_inf, "DbtrAgt")
fin_instn_id = SubElement(dbtr_agt, "FinInstnId")
bic_val = zahlungen.first().konto.bic.strip()
if schwifty_available and bic_val:
try:
bic_val = str(SchwiftyBIC(bic_val))
except Exception:
pass
SubElement(fin_instn_id, "BIC").text = bic_val or "NOTPROVIDED"
for zahlung in zahlungen:
cdt_trf = SubElement(pmt_inf, "CdtTrfTxInf")
pmt_id_el = SubElement(cdt_trf, "PmtId")
SubElement(pmt_id_el, "EndToEndId").text = str(zahlung.id)[:35]
amt = SubElement(cdt_trf, "Amt")
instd_amt = SubElement(amt, "InstdAmt", {"Ccy": "EUR"})
instd_amt.text = str(zahlung.betrag)
instd_amt.text = f"{zahlung.betrag:.2f}"
cdtr = SubElement(cdt_trf, "Cdtr")
SubElement(cdtr, "Nm").text = (zahlung.empfaenger_name or zahlung.destinataer.get_full_name())[:70]
cdtr_acct = SubElement(cdt_trf, "CdtrAcct")
cdtr_id = SubElement(cdtr_acct, "Id")
SubElement(cdtr_id, "IBAN").text = (zahlung.empfaenger_iban or zahlung.destinataer.iban or "").replace(" ", "")
iban_clean = (zahlung.empfaenger_iban or zahlung.destinataer.iban or "").replace(" ", "")
SubElement(cdtr_id, "IBAN").text = iban_clean
rmt_inf = SubElement(cdt_trf, "RmtInf")
SubElement(rmt_inf, "Ustrd").text = (zahlung.verwendungszweck or zahlung.beschreibung or "Stiftungsunterstützung")[:140]