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",