diff --git a/main.py b/main.py index bddec63..02b9da9 100644 --- a/main.py +++ b/main.py @@ -89,6 +89,30 @@ def get_cached_llm(): return st.session_state.llm_instance +@st.cache_data(show_spinner=False) +def build_zugferd_pdf(invoice_json: str, original_pdf_bytes: bytes) -> bytes: + """Erzeugt die ZUGFeRD-Hybrid-PDF (factur-x + PDF/A-3). Gecacht ueber (Rechnungsdaten-JSON, + Original-PDF-Bytes), damit diese vergleichsweise teure Erzeugung nur laeuft, wenn sich die + Rechnung tatsaechlich aendert - so kann der Export-Button direkt herunterladen, ohne bei + jedem Rerun neu zu generieren.""" + invoice_dict = json.loads(invoice_json) + cii_xml_string = generate_compliant_zugferd_cii(invoice_dict) + # factur-x: XSD-Validierung der CII-XML, korrektes Einbetten und PDF/A-3-konforme XMP-Struktur. + pdf_bytes = facturx_generate_from_binary( + original_pdf_bytes, + cii_xml_string.encode("utf-8"), + flavor="factur-x", + level="en16931", + check_xsd=True, + check_schematron=False, # benötigt optional 'saxonche' für Schematron-Regeln + afrelationship="data", + ) + # Zusätzliches OutputIntent (sRGB-ICC) + XRechnung-Konformitaets-Fix in den XMP-Metadaten. + pdf_bytes = add_pdfa_output_intent(pdf_bytes) + pdf_bytes = fix_xrechnung_xmp_conformance(pdf_bytes) + return pdf_bytes + + # ========================================== # 5. STREAMLIT FRONTEND # ========================================== @@ -109,6 +133,7 @@ with col1: st.success("PDF eingelesen!") uploaded_file.seek(0) st.pdf(uploaded_file) + with st.expander("Gefundener Text"): st.text(pdf_text) with col2: st.subheader("2. Strukturierte Daten validieren") @@ -203,8 +228,8 @@ with col2: # Reiter-Navigation: gruppiert die Eingabefelder, damit man nicht die ganze Seite # runterscrollen muss. Summen & Export stehen bewusst UNTERHALB der Tabs (immer sichtbar). - tab_basis, tab_parteien, tab_positionen, tab_pdftext = st.tabs( - ["Basis", "Parteien", "Positionen", "PDF-Text"] + tab_basis, tab_parteien, tab_positionen = st.tabs( + ["Basis", "Parteien", "Positionen"] ) with tab_basis: @@ -359,9 +384,6 @@ with col2: with c_disc4: skonto_days = st.text_input("Skonto-Frist (Tage)", value="") - with tab_pdftext: - st.text(pdf_text if pdf_text.strip() else "(Kein PDF-Text verfügbar)") - # --- SUMMEN & EXPORT: bewusst AUSSERHALB der Tabs, damit sie unabhängig vom aktiven # Reiter immer sichtbar und ohne Scrollen erreichbar sind. --- st.markdown("---") @@ -439,51 +461,53 @@ with col2: export_disabled = len(blocker) > 0 + # Ein-Klick-Export: die Exportdatei wird vorab erzeugt und direkt in den Download-Button + # gelegt - ein Klick startet also sofort den Download, ohne separaten Zwischen-Button. if "UBL XML" in export_format: - if st.button("🔒 XRechnung exportieren", type="primary", width="stretch", disabled=export_disabled): - xml_data = generate_compliant_xrechnung(final_invoice_dict) - st.download_button(label="📥 Download XRechnung XML", data=xml_data, file_name=f"XRechnung_{inv_num}.xml", mime="text/xml") + xml_data = generate_compliant_xrechnung(final_invoice_dict) if not export_disabled else "" + st.download_button( + "🔒 XRechnung exportieren & herunterladen", + data=xml_data, + file_name=f"XRechnung_{inv_num}.xml", + mime="text/xml", + type="primary", + width="stretch", + disabled=export_disabled, + ) else: - if st.button("🔒 ZUGFeRD PDF exportieren", type="primary", width="stretch", disabled=export_disabled): + zugferd_pdf_bytes = None + zugferd_error = None + if not export_disabled and uploaded_file is not None: try: - cii_xml_string = generate_compliant_zugferd_cii(final_invoice_dict) - uploaded_file.seek(0) original_pdf_bytes = uploaded_file.read() - - # 1) factur-x übernimmt: XSD-Validierung der CII-XML, korrektes Einbetten - # (Filespec mit AFRelationship=/Data, /F + /UF, Subtype text/xml) und die - # komplette PDF/A-3-konforme XMP-Metadatenstruktur (pdfaid:part/conformance, - # fx:-Schema-Erweiterung). Bricht die XML-Validierung fehl, wird hier ein - # klarer Fehler geworfen statt einer stillen, ungültigen PDF. - zugferd_pdf_bytes = facturx_generate_from_binary( + zugferd_pdf_bytes = build_zugferd_pdf( + json.dumps(final_invoice_dict, default=str, sort_keys=True), original_pdf_bytes, - cii_xml_string.encode("utf-8"), - flavor="factur-x", - level="en16931", - check_xsd=True, - check_schematron=False, # benötigt optional 'saxonche' für Schematron-Regeln - afrelationship="data", - ) - - # 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", - data=zugferd_pdf_bytes, - file_name=f"ZUGFeRD_{inv_num}.pdf", - mime="application/pdf" - ) - st.info( - "ℹ️ Hinweis: Die eingebettete CII-XML wurde gegen das offizielle EN16931-XSD-Schema " - "validiert. Für eine vollständige PDF/A-3-Zertifizierung (z. B. Schriftarten-Einbettung " - "der Ursprungs-PDF) empfiehlt sich zusätzlich eine Prüfung mit veraPDF." ) except Exception as e: - st.error(f"Fehler bei der ZUGFeRD-Generierung: {e}") + zugferd_error = str(e) + elif not export_disabled and uploaded_file is None: + zugferd_error = "Original-PDF nicht mehr geladen. Bitte die PDF erneut hochladen." + + if zugferd_error: + st.error(f"Fehler bei der ZUGFeRD-Generierung: {zugferd_error}") + + st.download_button( + "🔒 ZUGFeRD PDF exportieren & herunterladen", + data=zugferd_pdf_bytes if zugferd_pdf_bytes else b"", + file_name=f"ZUGFeRD_{inv_num}.pdf", + mime="application/pdf", + type="primary", + width="stretch", + disabled=export_disabled or zugferd_pdf_bytes is None, + ) + if zugferd_pdf_bytes: + st.info( + "ℹ️ Hinweis: Die eingebettete CII-XML wurde gegen das offizielle EN16931-XSD-Schema " + "validiert. Für eine vollständige PDF/A-3-Zertifizierung (z. B. Schriftarten-Einbettung " + "der Ursprungs-PDF) empfiehlt sich zusätzlich eine Prüfung mit veraPDF." + ) # ========================================== # FOOTER