feat(ui): PDF-Text back under PDF preview; one-click export+download
- Extracted-text expander moved back to left column under PDF preview (PDF-Text tab removed, now 3 tabs) - Export button now generates and downloads in a single click via st.download_button - ZUGFeRD generation cached (st.cache_data) so it doesn't rerun on every keystroke Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
c07eb860fa
commit
1992c30991
108
main.py
108
main.py
|
|
@ -89,6 +89,30 @@ def get_cached_llm():
|
||||||
return st.session_state.llm_instance
|
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
|
# 5. STREAMLIT FRONTEND
|
||||||
# ==========================================
|
# ==========================================
|
||||||
|
|
@ -109,6 +133,7 @@ with col1:
|
||||||
st.success("PDF eingelesen!")
|
st.success("PDF eingelesen!")
|
||||||
uploaded_file.seek(0)
|
uploaded_file.seek(0)
|
||||||
st.pdf(uploaded_file)
|
st.pdf(uploaded_file)
|
||||||
|
with st.expander("Gefundener Text"): st.text(pdf_text)
|
||||||
|
|
||||||
with col2:
|
with col2:
|
||||||
st.subheader("2. Strukturierte Daten validieren")
|
st.subheader("2. Strukturierte Daten validieren")
|
||||||
|
|
@ -203,8 +228,8 @@ with col2:
|
||||||
|
|
||||||
# Reiter-Navigation: gruppiert die Eingabefelder, damit man nicht die ganze Seite
|
# Reiter-Navigation: gruppiert die Eingabefelder, damit man nicht die ganze Seite
|
||||||
# runterscrollen muss. Summen & Export stehen bewusst UNTERHALB der Tabs (immer sichtbar).
|
# runterscrollen muss. Summen & Export stehen bewusst UNTERHALB der Tabs (immer sichtbar).
|
||||||
tab_basis, tab_parteien, tab_positionen, tab_pdftext = st.tabs(
|
tab_basis, tab_parteien, tab_positionen = st.tabs(
|
||||||
["Basis", "Parteien", "Positionen", "PDF-Text"]
|
["Basis", "Parteien", "Positionen"]
|
||||||
)
|
)
|
||||||
|
|
||||||
with tab_basis:
|
with tab_basis:
|
||||||
|
|
@ -359,9 +384,6 @@ with col2:
|
||||||
with c_disc4:
|
with c_disc4:
|
||||||
skonto_days = st.text_input("Skonto-Frist (Tage)", value="")
|
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
|
# --- SUMMEN & EXPORT: bewusst AUSSERHALB der Tabs, damit sie unabhängig vom aktiven
|
||||||
# Reiter immer sichtbar und ohne Scrollen erreichbar sind. ---
|
# Reiter immer sichtbar und ohne Scrollen erreichbar sind. ---
|
||||||
st.markdown("---")
|
st.markdown("---")
|
||||||
|
|
@ -439,51 +461,53 @@ with col2:
|
||||||
|
|
||||||
export_disabled = len(blocker) > 0
|
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 "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) if not export_disabled else ""
|
||||||
xml_data = generate_compliant_xrechnung(final_invoice_dict)
|
st.download_button(
|
||||||
st.download_button(label="📥 Download XRechnung XML", data=xml_data, file_name=f"XRechnung_{inv_num}.xml", mime="text/xml")
|
"🔒 XRechnung exportieren & herunterladen",
|
||||||
|
data=xml_data,
|
||||||
|
file_name=f"XRechnung_{inv_num}.xml",
|
||||||
|
mime="text/xml",
|
||||||
|
type="primary",
|
||||||
|
width="stretch",
|
||||||
|
disabled=export_disabled,
|
||||||
|
)
|
||||||
else:
|
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:
|
try:
|
||||||
cii_xml_string = generate_compliant_zugferd_cii(final_invoice_dict)
|
|
||||||
|
|
||||||
uploaded_file.seek(0)
|
uploaded_file.seek(0)
|
||||||
original_pdf_bytes = uploaded_file.read()
|
original_pdf_bytes = uploaded_file.read()
|
||||||
|
zugferd_pdf_bytes = build_zugferd_pdf(
|
||||||
# 1) factur-x übernimmt: XSD-Validierung der CII-XML, korrektes Einbetten
|
json.dumps(final_invoice_dict, default=str, sort_keys=True),
|
||||||
# (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(
|
|
||||||
original_pdf_bytes,
|
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:
|
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
|
# FOOTER
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue