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):