diff --git a/invoice_logic.py b/invoice_logic.py
index 1b9eeb8..2f7b7a8 100644
--- a/invoice_logic.py
+++ b/invoice_logic.py
@@ -58,6 +58,7 @@ VERKAEUFER = {
"country": "DE", # BT-40
"vat_id": "DE218436692", # BT-31 (Umsatzsteuer-ID)
"register_id": "HRB 6058 NP", # BT-30 (Handelsregisternummer, NICHT die USt-ID)
+ "bic": "DEUTDEDB160", # BT-86 (BIC der Hausbank, Deutsche Bank)
}
# UN/EDIFACT 4461 Zahlungsmittelcodes (BT-81), Auswahl der fuer B2B/B2G relevanten Codes.
@@ -953,6 +954,11 @@ def generate_compliant_xrechnung(data):
ET.SubElement(payment_means, f"{{{CBC}}}PaymentMeansCode").text = str(data.get("payment_means_code", "58"))
payee_acc = ET.SubElement(payment_means, f"{{{CAC}}}PayeeFinancialAccount")
ET.SubElement(payee_acc, f"{{{CBC}}}ID").text = str(data["iban"]).replace(" ", "")
+ # BT-86 BIC der Verkaeuferbank (optional, aber von vielen Empfaengern erwartet).
+ _bic = str(data.get("bic") or VERKAEUFER["bic"]).replace(" ", "")
+ if _bic:
+ fin_branch = ET.SubElement(payee_acc, f"{{{CAC}}}FinancialInstitutionBranch")
+ ET.SubElement(fin_branch, f"{{{CBC}}}ID").text = _bic
# BT-20 Zahlungsbedingungen als Freitext in cac:PaymentTerms/cbc:Note (Vorkasse und/oder
# Skonto) - EN16931 kennt dafuer kein eigenes strukturiertes Feld. Muss laut UBL-Schema NACH
@@ -1013,6 +1019,9 @@ def generate_compliant_xrechnung(data):
ET.SubElement(monetary_total, f"{{{CBC}}}TaxInclusiveAmount", {"currencyID": "EUR"}).text = f"{data['total_gross_amount']:.2f}"
if data.get("allowance_amount", 0.0) > 0:
ET.SubElement(monetary_total, f"{{{CBC}}}AllowanceTotalAmount", {"currencyID": "EUR"}).text = f"{data['allowance_amount']:.2f}"
+ # BT-113 Vorauszahlung: mangels Anzahlungsdaten immer 0,00 - macht die Summensektion
+ # vollstaendig (viele Empfaenger/Validatoren erwarten das Feld). Muss VOR PayableAmount stehen.
+ ET.SubElement(monetary_total, f"{{{CBC}}}PrepaidAmount", {"currencyID": "EUR"}).text = "0.00"
ET.SubElement(monetary_total, f"{{{CBC}}}PayableAmount", {"currencyID": "EUR"}).text = f"{data['total_gross_amount']:.2f}"
for index, item in enumerate(data["items"], start=1):
@@ -1079,7 +1088,10 @@ def generate_compliant_zugferd_cii(data):
ET.SubElement(biz_process, f"{{{RAM}}}ID").text = "urn:fdc:peppol.eu:2017:poacc:billing:01:1.0"
guideline = ET.SubElement(ex_doc_ctx, f"{{{RAM}}}GuidelineSpecifiedDocumentContextParameter")
- ET.SubElement(guideline, f"{{{RAM}}}ID").text = "urn:cen.eu:en16931:2017#compliant#urn:xeinkauf.de:kosit:xrechnung_3.0"
+ # BT-24 Profil: ZUGFeRD/Factur-X wird B2B verschickt - daher das EN16931-Profil (COMFORT)
+ # deklarieren, NICHT das strengere KoSIT-XRechnung-Behoerdenprofil. So passt die
+ # Profildeklaration zum factur-x-Level 'en16931' und zur B2B-Ausrichtung (vgl. Lieferanten-XML).
+ ET.SubElement(guideline, f"{{{RAM}}}ID").text = "urn:cen.eu:en16931:2017"
ex_doc = ET.SubElement(root, f"{{{RSM}}}ExchangedDocument")
ET.SubElement(ex_doc, f"{{{RAM}}}ID").text = str(data["invoice_number"])
@@ -1237,6 +1249,11 @@ def generate_compliant_zugferd_cii(data):
ET.SubElement(pay_means, f"{{{RAM}}}TypeCode").text = str(data.get("payment_means_code", "58"))
payee_acc = ET.SubElement(pay_means, f"{{{RAM}}}PayeePartyCreditorFinancialAccount")
ET.SubElement(payee_acc, f"{{{RAM}}}IBANID").text = str(data["iban"]).replace(" ", "")
+ # BT-86 BIC der Verkaeuferbank (nach dem Konto, laut CII-Schema).
+ _bic = str(data.get("bic") or VERKAEUFER["bic"]).replace(" ", "")
+ if _bic:
+ fin_inst = ET.SubElement(pay_means, f"{{{RAM}}}PayeeSpecifiedCreditorFinancialInstitution")
+ ET.SubElement(fin_inst, f"{{{RAM}}}BICID").text = _bic
# BG-23: pro Steuersatz-Bucket ein eigenes ram:ApplicableTradeTax (Mehrfach-Steuersaetze).
# Fallback fuer Aufrufer ohne tax_buckets (Einzelsteuersatz, byte-identisch zu vor Phase 1):
@@ -1300,6 +1317,9 @@ def generate_compliant_zugferd_cii(data):
ET.SubElement(header_sum, f"{{{RAM}}}TaxBasisTotalAmount").text = f"{data['total_net_amount']:.2f}"
ET.SubElement(header_sum, f"{{{RAM}}}TaxTotalAmount", {"currencyID": "EUR"}).text = f"{data['total_vat_amount']:.2f}"
ET.SubElement(header_sum, f"{{{RAM}}}GrandTotalAmount").text = f"{data['total_gross_amount']:.2f}"
+ # BT-113 Vorauszahlung: mangels Anzahlungsdaten immer 0,00. Muss laut CII-Schema NACH
+ # GrandTotalAmount und VOR DuePayableAmount stehen (DuePayable = GrandTotal - Vorauszahlung).
+ ET.SubElement(header_sum, f"{{{RAM}}}TotalPrepaidAmount").text = "0.00"
ET.SubElement(header_sum, f"{{{RAM}}}DuePayableAmount").text = f"{data['total_gross_amount']:.2f}"
xml_str = ET.tostring(root, encoding="utf-8")
@@ -1371,31 +1391,3 @@ def add_pdfa_output_intent(pdf_bytes: bytes, icc_path: str = SRGB_ICC_PATH) -> b
return buf.getvalue()
finally:
pdf.close()
-
-
-def fix_xrechnung_xmp_conformance(pdf_bytes: bytes) -> bytes:
- """
- Die factur-x-Bibliothek schreibt standardmäßig fx:ConformanceLevel = 'EN 16931' und
- fx:Version = '1.0' in die XMP-Metadaten (generischer Factur-X-Standard). Da unsere CII-XML
- aber explizit das KoSIT-XRechnung-Profil deklariert (GuidelineID .../xrechnung_3.0), erwarten
- Validatoren hier stattdessen 'XRECHNUNG' und '3.0' - sonst schlagen z. B.
- VD-Valitool-H-022/H-024 fehl, weil PDF-Metadaten und eingebettetes XML nicht zusammenpassen.
- """
- pdf = pikepdf.open(io.BytesIO(pdf_bytes))
- try:
- if "/Metadata" in pdf.Root:
- xmp_bytes = pdf.Root.Metadata.read_bytes()
- xmp_str = xmp_bytes.decode("utf-8")
- xmp_str = xmp_str.replace(
- "EN 16931",
- "XRECHNUNG",
- ).replace(
- "1.0",
- "3.0",
- )
- pdf.Root.Metadata.write(xmp_str.encode("utf-8"))
- buf = io.BytesIO()
- pdf.save(buf)
- return buf.getvalue()
- finally:
- pdf.close()
diff --git a/main.py b/main.py
index 634975d..2fe4479 100644
--- a/main.py
+++ b/main.py
@@ -22,7 +22,6 @@ from invoice_logic import (
normalize_pdf_bytes,
pdf_has_embedded_facturx,
add_pdfa_output_intent,
- fix_xrechnung_xmp_conformance,
compute_invoice_totals,
resolve_buyer_endpoint,
extract_service_period,
@@ -60,7 +59,9 @@ def build_zugferd_pdf(invoice_json: str, original_pdf_bytes: bytes) -> bytes:
def _attempt(src_pdf_bytes):
# factur-x: XSD-Validierung der CII-XML, Einbetten, PDF/A-3-konforme XMP-Struktur; danach
- # OutputIntent (sRGB-ICC) und XRechnung-Konformitaets-Fix in den XMP-Metadaten.
+ # nur noch OutputIntent (sRGB-ICC). Die CII deklariert jetzt das EN16931-Profil (B2B), das
+ # exakt zum factur-x-Level 'en16931' passt - daher KEIN XMP-Konformitaets-Override mehr
+ # noetig (frueher wurde XMP auf 'XRECHNUNG'/'3.0' gezwungen, um zum alten Profil zu passen).
pdf_bytes = facturx_generate_from_binary(
src_pdf_bytes,
cii_bytes,
@@ -71,7 +72,6 @@ def build_zugferd_pdf(invoice_json: str, original_pdf_bytes: bytes) -> bytes:
afrelationship="data",
)
pdf_bytes = add_pdfa_output_intent(pdf_bytes)
- pdf_bytes = fix_xrechnung_xmp_conformance(pdf_bytes)
# Erfolgskontrolle: factur-x laesst bei manchen PDF-Strukturen die XML still weg bzw.
# produziert eine kaputte PDF - dann als Fehlschlag behandeln.
if not pdf_has_embedded_facturx(pdf_bytes):