This commit is contained in:
dfuhrmann3 2026-07-06 10:30:35 +02:00
parent a5c27f5475
commit 00fe916c06
1 changed files with 76 additions and 4 deletions

80
main.py
View File

@ -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(
"<fx:ConformanceLevel>EN 16931</fx:ConformanceLevel>",
"<fx:ConformanceLevel>XRECHNUNG</fx:ConformanceLevel>",
).replace(
"<fx:Version>1.0</fx:Version>",
"<fx:Version>3.0.2</fx:Version>",
)
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",