From 00fe916c06b3f89368bf5ba3da5290ac4e9ca59b Mon Sep 17 00:00:00 2001 From: dfuhrmann3 Date: Mon, 6 Jul 2026 10:30:35 +0200 Subject: [PATCH] Test --- main.py | 80 ++++++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 76 insertions(+), 4 deletions(-) diff --git a/main.py b/main.py index 6abf6b0..b905914 100644 --- a/main.py +++ b/main.py @@ -355,6 +355,12 @@ def generate_compliant_zugferd_cii(data): root = ET.Element(f"{{{RSM}}}CrossIndustryInvoice") ex_doc_ctx = ET.SubElement(root, f"{{{RSM}}}ExchangedDocumentContext") + + # BT-23 Geschäftsprozess (PEPPOL-EN16931-R001 Pflicht). Muss laut XSD VOR + # GuidelineSpecifiedDocumentContextParameter stehen. + biz_process = ET.SubElement(ex_doc_ctx, f"{{{RAM}}}BusinessProcessSpecifiedDocumentContextParameter") + 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" @@ -410,13 +416,27 @@ def generate_compliant_zugferd_cii(data): supp_legal = ET.SubElement(supplier_party, f"{{{RAM}}}SpecifiedLegalOrganization") ET.SubElement(supp_legal, f"{{{RAM}}}ID").text = "DE218436692" - + + # BG-6 SELLER CONTACT (BR-DE-2 Pflicht für XRechnung). Muss laut XSD nach + # SpecifiedLegalOrganization und vor PostalTradeAddress stehen. + supp_contact = ET.SubElement(supplier_party, f"{{{RAM}}}DefinedTradeContact") + ET.SubElement(supp_contact, f"{{{RAM}}}PersonName").text = "Geschäftsführung" + supp_contact_tel = ET.SubElement(supp_contact, f"{{{RAM}}}TelephoneUniversalCommunication") + ET.SubElement(supp_contact_tel, f"{{{RAM}}}CompleteNumber").text = "+49 3302 4940537" + supp_contact_mail = ET.SubElement(supp_contact, f"{{{RAM}}}EmailURIUniversalCommunication") + ET.SubElement(supp_contact_mail, f"{{{RAM}}}URIID").text = "office@scitech-gmbh.de" + supp_addr = ET.SubElement(supplier_party, f"{{{RAM}}}PostalTradeAddress") ET.SubElement(supp_addr, f"{{{RAM}}}PostcodeCode").text = "16761" ET.SubElement(supp_addr, f"{{{RAM}}}LineOne").text = "Hermann-Schumann-Str. 4" ET.SubElement(supp_addr, f"{{{RAM}}}CityName").text = "Hennigsdorf" ET.SubElement(supp_addr, f"{{{RAM}}}CountryID").text = "DE" - + + # BT-34 Elektronische Adresse des Verkäufers (PEPPOL-EN16931-R020 Pflicht). Muss laut + # XSD nach PostalTradeAddress und vor SpecifiedTaxRegistration stehen. + supp_uri = ET.SubElement(supplier_party, f"{{{RAM}}}URIUniversalCommunication") + ET.SubElement(supp_uri, f"{{{RAM}}}URIID", {"schemeID": "EM"}).text = "office@scitech-gmbh.de" + supp_tax = ET.SubElement(supplier_party, f"{{{RAM}}}SpecifiedTaxRegistration") ET.SubElement(supp_tax, f"{{{RAM}}}ID", {"schemeID": "VA"}).text = "DE218436692" @@ -428,6 +448,13 @@ def generate_compliant_zugferd_cii(data): ET.SubElement(cust_addr, f"{{{RAM}}}LineOne").text = str(data["recipient_street"]) ET.SubElement(cust_addr, f"{{{RAM}}}CityName").text = str(data["recipient_city"]) ET.SubElement(cust_addr, f"{{{RAM}}}CountryID").text = "DE" + + # BT-49 Elektronische Adresse des Käufers (PEPPOL-EN16931-R010 Pflicht). Muss laut + # XSD nach PostalTradeAddress stehen. Fallback analog zur XRechnung(UBL)-Generierung. + cust_uri = ET.SubElement(customer_party, f"{{{RAM}}}URIUniversalCommunication") + ET.SubElement(cust_uri, f"{{{RAM}}}URIID", {"schemeID": "EM"}).text = ( + str(data["recipient_email"]) if data.get("recipient_email") else "rechnung@kunde.de" + ) if data.get("order_id") and str(data["order_id"]).strip() != "": order_ref = ET.SubElement(header_agree, f"{{{RAM}}}BuyerOrderReferencedDocument") @@ -472,7 +499,20 @@ def generate_compliant_zugferd_cii(data): ET.SubElement(start_pt, f"{{{UDT}}}DateTimeString", {"format": "102"}).text = format_cii_date(data["service_start_date"]) end_pt = ET.SubElement(period, f"{{{RAM}}}EndDateTime") ET.SubElement(end_pt, f"{{{UDT}}}DateTimeString", {"format": "102"}).text = format_cii_date(data["service_end_date"]) - + + # BT-9 Fälligkeitsdatum (BR-CO-25 Pflicht: bei positivem Zahlbetrag muss entweder ein + # Fälligkeitsdatum ODER Zahlungsbedingungen angegeben werden). Default: 14 Tage netto, + # analog zur bereits funktionierenden XRechnung(UBL)-Generierung. + try: + base_date = datetime.strptime(format_cii_date(data["invoice_date"]), "%Y%m%d") + due_date_str = (base_date + timedelta(days=14)).strftime("%Y%m%d") + except Exception: + due_date_str = (datetime.today() + timedelta(days=14)).strftime("%Y%m%d") + payment_terms = ET.SubElement(header_settle, f"{{{RAM}}}SpecifiedTradePaymentTerms") + ET.SubElement(payment_terms, f"{{{RAM}}}Description").text = "Zahlbar innerhalb 14 Tagen ohne Abzug." + due_date_elem = ET.SubElement(payment_terms, f"{{{RAM}}}DueDateDateTime") + ET.SubElement(due_date_elem, f"{{{UDT}}}DateTimeString", {"format": "102"}).text = due_date_str + header_sum = ET.SubElement(header_settle, f"{{{RAM}}}SpecifiedTradeSettlementHeaderMonetarySummation") ET.SubElement(header_sum, f"{{{RAM}}}LineTotalAmount").text = f"{data['total_net_amount']:.2f}" ET.SubElement(header_sum, f"{{{RAM}}}TaxBasisTotalAmount").text = f"{data['total_net_amount']:.2f}" @@ -520,7 +560,38 @@ def add_pdfa_output_intent(pdf_bytes: bytes, icc_path: str = SRGB_ICC_PATH) -> b pdf.save(buf) return buf.getvalue() finally: - pdf.close()# ========================================== + 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.2' - 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.2", + ) + pdf.Root.Metadata.write(xmp_str.encode("utf-8")) + buf = io.BytesIO() + pdf.save(buf) + return buf.getvalue() + finally: + pdf.close() + + +# ========================================== # 4. API CLIENT INITIALISIERUNG # ========================================== def get_cached_llm(): @@ -791,6 +862,7 @@ with col2: # 2) Zusätzliches OutputIntent (sRGB-ICC-Profil) ergänzen. Das ist für echte # PDF/A-3-Konformität zwingend, wird von factur-x aber nicht selbst gesetzt. zugferd_pdf_bytes = add_pdfa_output_intent(zugferd_pdf_bytes) + zugferd_pdf_bytes = fix_xrechnung_xmp_conformance(zugferd_pdf_bytes) st.download_button( label="📥 Download ZUGFeRD Hybrid-PDF",