Test
This commit is contained in:
parent
a5c27f5475
commit
00fe916c06
80
main.py
80
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(
|
||||
"<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",
|
||||
|
|
|
|||
Loading…
Reference in New Issue