Erweitert um optionalen Leistungsnachweis pdf merge
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
b41554f1f1
commit
88c327c977
|
|
@ -1459,6 +1459,33 @@ def normalize_pdf_bytes(pdf_bytes: bytes) -> bytes:
|
||||||
return pdf_bytes
|
return pdf_bytes
|
||||||
|
|
||||||
|
|
||||||
|
def append_pdf_pages(base_pdf_bytes: bytes, extra_pdf_bytes: bytes) -> bytes:
|
||||||
|
"""Haengt alle Seiten von extra_pdf_bytes hinten an base_pdf_bytes an (z.B. einen
|
||||||
|
Leistungsnachweis an die Rechnung) und gibt die zusammengefuehrte PDF zurueck. Aus der
|
||||||
|
angehaengten PDF wird nichts ausgelesen - sie ist reiner Seiteninhalt.
|
||||||
|
|
||||||
|
Bewusst OHNE try/except: schlaegt das Zusammenfuehren fehl (kaputte oder passwortgeschuetzte
|
||||||
|
Datei), muss der Export sichtbar scheitern. Eine stillschweigend ohne Leistungsnachweis
|
||||||
|
exportierte Rechnung waere schlimmer als eine Fehlermeldung - anders als bei
|
||||||
|
normalize_pdf_bytes, wo das Zurueckfallen auf die Eingabe harmlos ist."""
|
||||||
|
with pikepdf.open(io.BytesIO(base_pdf_bytes)) as base, \
|
||||||
|
pikepdf.open(io.BytesIO(extra_pdf_bytes)) as extra:
|
||||||
|
base.pages.extend(extra.pages)
|
||||||
|
buf = io.BytesIO()
|
||||||
|
base.save(buf)
|
||||||
|
return buf.getvalue()
|
||||||
|
|
||||||
|
|
||||||
|
def count_pdf_pages(pdf_bytes: bytes) -> int:
|
||||||
|
"""Seitenzahl einer PDF. Dient nur einer Anzeige-Meldung und gibt bei unlesbaren Bytes 0
|
||||||
|
zurueck, statt den bereits erfolgreichen Export mit einer Exception zu kippen."""
|
||||||
|
try:
|
||||||
|
with pikepdf.open(io.BytesIO(pdf_bytes)) as pdf:
|
||||||
|
return len(pdf.pages)
|
||||||
|
except Exception:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
def pdf_has_embedded_facturx(pdf_bytes: bytes) -> bool:
|
def pdf_has_embedded_facturx(pdf_bytes: bytes) -> bool:
|
||||||
"""True, wenn die PDF wieder lesbar ist UND die eingebettete 'factur-x.xml' enthaelt. Dient als
|
"""True, wenn die PDF wieder lesbar ist UND die eingebettete 'factur-x.xml' enthaelt. Dient als
|
||||||
Erfolgskontrolle nach der ZUGFeRD-Erzeugung - factur-x kann bei manchen PDF-Strukturen die XML
|
Erfolgskontrolle nach der ZUGFeRD-Erzeugung - factur-x kann bei manchen PDF-Strukturen die XML
|
||||||
|
|
|
||||||
56
main.py
56
main.py
|
|
@ -23,6 +23,8 @@ from invoice_logic import (
|
||||||
generate_compliant_zugferd_cii,
|
generate_compliant_zugferd_cii,
|
||||||
normalize_pdf_bytes,
|
normalize_pdf_bytes,
|
||||||
pdf_has_embedded_facturx,
|
pdf_has_embedded_facturx,
|
||||||
|
append_pdf_pages,
|
||||||
|
count_pdf_pages,
|
||||||
add_pdfa_output_intent,
|
add_pdfa_output_intent,
|
||||||
compute_invoice_totals,
|
compute_invoice_totals,
|
||||||
resolve_buyer_endpoint,
|
resolve_buyer_endpoint,
|
||||||
|
|
@ -125,6 +127,18 @@ with col1:
|
||||||
st.pdf(uploaded_file)
|
st.pdf(uploaded_file)
|
||||||
with st.expander("Gefundener Text"): st.text(pdf_text)
|
with st.expander("Gefundener Text"): st.text(pdf_text)
|
||||||
|
|
||||||
|
# Optionaler Zusatzbeleg (i.d.R. Leistungsnachweis): wird beim ZUGFeRD-Export unveraendert
|
||||||
|
# hinten an die Rechnung gehaengt, sodass EINE PDF entsteht. Bewusst NICHT Teil der
|
||||||
|
# _last_file_id-Logik oben - sonst wuerde sein Upload die bereits geprueften Rechnungsdaten
|
||||||
|
# verwerfen. Aus dieser PDF wird nichts ausgelesen.
|
||||||
|
attachment_file = st.file_uploader(
|
||||||
|
"Leistungsnachweis anhängen (optional)", type=["pdf"], key="attachment_pdf"
|
||||||
|
)
|
||||||
|
st.caption(
|
||||||
|
"Optional. Die Seiten dieser PDF werden beim ZUGFeRD-Export unverändert ans Ende der "
|
||||||
|
"Rechnung gehängt - es entsteht eine einzelne PDF. Es werden daraus keine Daten ausgelesen."
|
||||||
|
)
|
||||||
|
|
||||||
with col2:
|
with col2:
|
||||||
st.subheader("2. Strukturierte Daten validieren")
|
st.subheader("2. Strukturierte Daten validieren")
|
||||||
if "extracted_data" not in st.session_state: st.session_state.extracted_data = None
|
if "extracted_data" not in st.session_state: st.session_state.extracted_data = None
|
||||||
|
|
@ -609,6 +623,15 @@ with col2:
|
||||||
# Ein-Klick-Export: die Exportdatei wird vorab erzeugt und direkt in den Download-Button
|
# 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.
|
# gelegt - ein Klick startet also sofort den Download, ohne separaten Zwischen-Button.
|
||||||
if "UBL XML" in export_format:
|
if "UBL XML" in export_format:
|
||||||
|
# Eine XRechnung ist eine reine XML-Datei - dort laesst sich kein PDF-Anhang
|
||||||
|
# unterbringen. Nicht blockieren (der Nutzer darf das bewusst wollen), aber
|
||||||
|
# deutlich darauf hinweisen, damit der Nachweis nicht unbemerkt verloren geht.
|
||||||
|
if attachment_file is not None:
|
||||||
|
st.warning(
|
||||||
|
"⚠️ Ihr Leistungsnachweis kann in eine XRechnung-XML nicht eingebettet werden "
|
||||||
|
"und entfällt bei diesem Export. Wählen Sie das ZUGFeRD-PDF-Format, wenn der "
|
||||||
|
"Nachweis mitgeliefert werden soll."
|
||||||
|
)
|
||||||
xml_data = generate_compliant_xrechnung(final_invoice_dict) if not export_disabled else ""
|
xml_data = generate_compliant_xrechnung(final_invoice_dict) if not export_disabled else ""
|
||||||
st.download_button(
|
st.download_button(
|
||||||
"🔒 XRechnung exportieren & herunterladen",
|
"🔒 XRechnung exportieren & herunterladen",
|
||||||
|
|
@ -622,21 +645,52 @@ with col2:
|
||||||
else:
|
else:
|
||||||
zugferd_pdf_bytes = None
|
zugferd_pdf_bytes = None
|
||||||
zugferd_error = None
|
zugferd_error = None
|
||||||
|
attachment_error = None
|
||||||
if not export_disabled and uploaded_file is not None:
|
if not export_disabled and uploaded_file is not None:
|
||||||
try:
|
try:
|
||||||
uploaded_file.seek(0)
|
uploaded_file.seek(0)
|
||||||
original_pdf_bytes = uploaded_file.read()
|
original_pdf_bytes = uploaded_file.read()
|
||||||
|
|
||||||
|
# Optionalen Leistungsnachweis ZUERST anhaengen, DANN die factur-x.xml
|
||||||
|
# einbetten: so laeuft die Erfolgskontrolle (pdf_has_embedded_facturx) und
|
||||||
|
# die Zwei-Versuche-Logik in build_zugferd_pdf ueber das fertige Dokument.
|
||||||
|
# Umgekehrt (erst einbetten, dann anhaengen) bliebe ein beschaedigender
|
||||||
|
# Anhang unbemerkt.
|
||||||
|
if attachment_file is not None:
|
||||||
|
try:
|
||||||
|
attachment_file.seek(0)
|
||||||
|
original_pdf_bytes = append_pdf_pages(
|
||||||
|
original_pdf_bytes, attachment_file.read()
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
# Eigene Meldung: sonst sieht es so aus, als sei die RECHNUNG kaputt.
|
||||||
|
attachment_error = str(e)
|
||||||
|
raise
|
||||||
|
|
||||||
zugferd_pdf_bytes = build_zugferd_pdf(
|
zugferd_pdf_bytes = build_zugferd_pdf(
|
||||||
json.dumps(final_invoice_dict, default=str, sort_keys=True),
|
json.dumps(final_invoice_dict, default=str, sort_keys=True),
|
||||||
original_pdf_bytes,
|
original_pdf_bytes,
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
if attachment_error is None:
|
||||||
zugferd_error = str(e)
|
zugferd_error = str(e)
|
||||||
elif not export_disabled and uploaded_file is None:
|
elif not export_disabled and uploaded_file is None:
|
||||||
zugferd_error = "Original-PDF nicht mehr geladen. Bitte die PDF erneut hochladen."
|
zugferd_error = "Original-PDF nicht mehr geladen. Bitte die PDF erneut hochladen."
|
||||||
|
|
||||||
if zugferd_error:
|
if attachment_error:
|
||||||
|
st.error(
|
||||||
|
f"❌ Der Leistungsnachweis konnte nicht angehängt werden: {attachment_error}\n\n"
|
||||||
|
"Die PDF ist möglicherweise beschädigt oder passwortgeschützt. Bitte entfernen "
|
||||||
|
"Sie die Datei oder laden Sie eine andere hoch. Der Export wurde abgebrochen, "
|
||||||
|
"damit keine Rechnung ohne den Nachweis entsteht."
|
||||||
|
)
|
||||||
|
elif zugferd_error:
|
||||||
st.error(f"Fehler bei der ZUGFeRD-Generierung: {zugferd_error}")
|
st.error(f"Fehler bei der ZUGFeRD-Generierung: {zugferd_error}")
|
||||||
|
elif zugferd_pdf_bytes and attachment_file is not None:
|
||||||
|
st.success(
|
||||||
|
f"✅ Leistungsnachweis angehängt - die exportierte PDF enthält "
|
||||||
|
f"{count_pdf_pages(zugferd_pdf_bytes)} Seiten."
|
||||||
|
)
|
||||||
|
|
||||||
st.download_button(
|
st.download_button(
|
||||||
"🔒 ZUGFeRD PDF exportieren & herunterladen",
|
"🔒 ZUGFeRD PDF exportieren & herunterladen",
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue