From f1a63b7457b818e14e91e318bf84abd8eaee22b9 Mon Sep 17 00:00:00 2001 From: dfuhrmann3 Date: Fri, 3 Jul 2026 13:32:45 +0200 Subject: [PATCH] ZUGFeRD fix 5 --- main.py | 267 +++++++++++++++++++++++++++++++++----------------------- 1 file changed, 157 insertions(+), 110 deletions(-) diff --git a/main.py b/main.py index ff17e22..8c92759 100644 --- a/main.py +++ b/main.py @@ -17,14 +17,18 @@ if "invoice_counter" not in st.session_state: st.session_state.invoice_counter = 0 # ========================================== -# 1. API-CLIENT FÜR DAS vLLM-BACKEND +# 1. API-CLIENT FÜR EXTERNE BACKENDS # ========================================== class LLMClient: - """Client für das vLLM-Backend (OpenAI-kompatibel) mit JSON-Schema-Unterstützung.""" + """Client für Ollama und vLLM (OpenAI-kompatibel) mit JSON-Schema-Unterstützung.""" - def __init__(self, model="gemma431b", base_url="http://10.192.0.140:8002"): + def __init__(self, backend_type="vllm", model="gemma431b", base_url=None): + self.backend_type = backend_type.lower() self.model = model + if not base_url: + base_url = "http://10.192.0.140:8002" if self.backend_type == "vllm" else "http://localhost:11434" + base_url = base_url.strip() if base_url.startswith("[") and ")" in base_url: match = re.search(r"\((https?://.*?)\)", base_url) @@ -38,7 +42,31 @@ class LLMClient: {"role": "system", "content": system_prompt}, {"role": "user", "content": prompt} ] - return self._openai_compatible_request(messages, temperature, json_schema) + + if self.backend_type == "ollama": + return self._ollama_request(messages, temperature, json_schema) + else: + return self._openai_compatible_request(messages, temperature, json_schema) + + def _ollama_request(self, messages, temperature, json_schema): + url = f"{self.base_url}/api/chat" + payload = { + "model": self.model, + "messages": messages, + "stream": False, + "options": { + "temperature": temperature + } + } + if json_schema: + payload["format"] = json_schema + + try: + response = requests.post(url, json=payload, timeout=120) + response.raise_for_status() + return response.json().get("message", {}).get("content", "") + except Exception as e: + return f"Ollama Error: {e}" def _openai_compatible_request(self, messages, temperature, json_schema): url = f"{self.base_url}/v1/chat/completions" @@ -224,105 +252,105 @@ def generate_compliant_xrechnung(data): contract_ref = ET.SubElement(root, f"{{{CAC}}}ContractDocumentReference") ET.SubElement(contract_ref, f"{{{CBC}}}ID").text = str(data["contract_id"]).strip() - vat_rate = float(data['vat_rate_percent']) - vat_category = "S" if vat_rate > 0.0 else "E" + `vat_rate` = float(data['vat_rate_percent']) + `vat_category` = "S" if vat_rate > 0.0 else "E" # --- SENDER (SCITECH) --- - supplier = ET.SubElement(root, f"{{{CAC}}}AccountingSupplierParty") - party_supp = ET.SubElement(supplier, f"{{{CAC}}}Party") + `supplier` = ET.SubElement(root, f"{{{CAC}}}AccountingSupplierParty") + `party_supp` = ET.SubElement(supplier, f"{{{CAC}}}Party") ET.SubElement(party_supp, f"{{{CBC}}}EndpointID", {"schemeID": "EM"}).text = "office@scitech-gmbh.de" - party_name_supp = ET.SubElement(party_supp, f"{{{CAC}}}PartyName") + `party_name_supp` = ET.SubElement(party_supp, f"{{{CAC}}}PartyName") ET.SubElement(party_name_supp, f"{{{CBC}}}Name").text = "scitech it solutions GmbH" - addr_supp = ET.SubElement(party_supp, f"{{{CAC}}}PostalAddress") + `addr_supp` = ET.SubElement(party_supp, f"{{{CAC}}}PostalAddress") ET.SubElement(addr_supp, f"{{{CBC}}}StreetName").text = "Hermann-Schumann-Str. 4" ET.SubElement(addr_supp, f"{{{CBC}}}CityName").text = "Hennigsdorf" ET.SubElement(addr_supp, f"{{{CBC}}}PostalZone").text = "16761" - country_supp = ET.SubElement(addr_supp, f"{{{CAC}}}Country") + `country_supp` = ET.SubElement(addr_supp, f"{{{CAC}}}Country") ET.SubElement(country_supp, f"{{{CBC}}}IdentificationCode").text = "DE" - tax_scheme_supp = ET.SubElement(party_supp, f"{{{CAC}}}PartyTaxScheme") + `tax_scheme_supp` = ET.SubElement(party_supp, f"{{{CAC}}}PartyTaxScheme") ET.SubElement(tax_scheme_supp, f"{{{CBC}}}CompanyID").text = "DE218436692" - scheme_supp = ET.SubElement(tax_scheme_supp, f"{{{CAC}}}TaxScheme") + `scheme_supp` = ET.SubElement(tax_scheme_supp, f"{{{CAC}}}TaxScheme") ET.SubElement(scheme_supp, f"{{{CBC}}}ID").text = "VAT" - legal_supp = ET.SubElement(party_supp, f"{{{CAC}}}PartyLegalEntity") + `legal_supp` = ET.SubElement(party_supp, f"{{{CAC}}}PartyLegalEntity") ET.SubElement(legal_supp, f"{{{CBC}}}RegistrationName").text = "scitech it solutions GmbH" - contact_supp = ET.SubElement(party_supp, f"{{{CAC}}}Contact") + `contact_supp` = ET.SubElement(party_supp, f"{{{CAC}}}Contact") ET.SubElement(contact_supp, f"{{{CBC}}}Name").text = "Geschäftsführung" ET.SubElement(contact_supp, f"{{{CBC}}}Telephone").text = "+49 3302 4940537" ET.SubElement(contact_supp, f"{{{CBC}}}ElectronicMail").text = "office@scitech-gmbh.de" # --- Empfänger --- - customer = ET.SubElement(root, f"{{{CAC}}}AccountingCustomerParty") - party_cust = ET.SubElement(customer, f"{{{CAC}}}Party") + `customer` = ET.SubElement(root, f"{{{CAC}}}AccountingCustomerParty") + `party_cust` = ET.SubElement(customer, f"{{{CAC}}}Party") ET.SubElement(party_cust, f"{{{CBC}}}EndpointID", {"schemeID": "EM"}).text = str(data["recipient_email"]) if data.get("recipient_email") else "rechnung@kunde.de" - party_name_cust = ET.SubElement(party_cust, f"{{{CAC}}}PartyName") + `party_name_cust` = ET.SubElement(party_cust, f"{{{CAC}}}PartyName") ET.SubElement(party_name_cust, f"{{{CBC}}}Name").text = str(data["recipient_name"]) - addr_cust = ET.SubElement(party_cust, f"{{{CAC}}}PostalAddress") + `addr_cust` = ET.SubElement(party_cust, f"{{{CAC}}}PostalAddress") ET.SubElement(addr_cust, f"{{{CBC}}}StreetName").text = str(data["recipient_street"]) ET.SubElement(addr_cust, f"{{{CBC}}}CityName").text = str(data["recipient_city"]) ET.SubElement(addr_cust, f"{{{CBC}}}PostalZone").text = str(data["recipient_zip"]) - country_cust = ET.SubElement(addr_cust, f"{{{CAC}}}Country") + `country_cust` = ET.SubElement(addr_cust, f"{{{CAC}}}Country") ET.SubElement(country_cust, f"{{{CBC}}}IdentificationCode").text = "DE" - legal_cust = ET.SubElement(party_cust, f"{{{CAC}}}PartyLegalEntity") + `legal_cust` = ET.SubElement(party_cust, f"{{{CAC}}}PartyLegalEntity") ET.SubElement(legal_cust, f"{{{CBC}}}RegistrationName").text = str(data["recipient_name"]) if data.get("delivery_date"): - delivery = ET.SubElement(root, f"{{{CAC}}}Delivery") + `delivery` = ET.SubElement(root, f"{{{CAC}}}Delivery") ET.SubElement(delivery, f"{{{CBC}}}ActualDeliveryDate").text = parse_to_strict_xml_date(data["delivery_date"]) - payment_means = ET.SubElement(root, f"{{{CAC}}}PaymentMeans") + `payment_means` = ET.SubElement(root, f"{{{CAC}}}PaymentMeans") ET.SubElement(payment_means, f"{{{CBC}}}PaymentMeansCode").text = "30" - payee_acc = ET.SubElement(payment_means, f"{{{CAC}}}PayeeFinancialAccount") + `payee_acc` = ET.SubElement(payment_means, f"{{{CAC}}}PayeeFinancialAccount") ET.SubElement(payee_acc, f"{{{CBC}}}ID").text = str(data["iban"]).replace(" ", "") - tax_total = ET.SubElement(root, f"{{{CAC}}}TaxTotal") + `tax_total` = ET.SubElement(root, f"{{{CAC}}}TaxTotal") ET.SubElement(tax_total, f"{{{CBC}}}TaxAmount", {"currencyID": "EUR"}).text = f"{data['total_vat_amount']:.2f}" - tax_subtotal = ET.SubElement(tax_total, f"{{{CAC}}}TaxSubtotal") + `tax_subtotal` = ET.SubElement(tax_total, f"{{{CAC}}}TaxSubtotal") ET.SubElement(tax_subtotal, f"{{{CBC}}}TaxableAmount", {"currencyID": "EUR"}).text = f"{data['total_net_amount']:.2f}" - ET.SubElement(tax_subtotal, f"{{{CAC}}}TaxAmount", {"currencyID": "EUR"}).text = f"{data['total_vat_amount']:.2f}" - tax_cat = ET.SubElement(tax_subtotal, f"{{{CAC}}}TaxCategory") + ET.SubElement(tax_subtotal, f"{{{CBC}}}TaxAmount", {"currencyID": "EUR"}).text = f"{data['total_vat_amount']:.2f}" + `tax_cat` = ET.SubElement(tax_subtotal, f"{{{CAC}}}TaxCategory") ET.SubElement(tax_cat, f"{{{CBC}}}ID").text = vat_category ET.SubElement(tax_cat, f"{{{CBC}}}Percent").text = f"{vat_rate:.2f}" - tax_scheme = ET.SubElement(tax_cat, f"{{{CAC}}}TaxScheme") + `tax_scheme` = ET.SubElement(tax_cat, f"{{{CAC}}}TaxScheme") ET.SubElement(tax_scheme, f"{{{CBC}}}ID").text = "VAT" - monetary_total = ET.SubElement(root, f"{{{CAC}}}LegalMonetaryTotal") + `monetary_total` = ET.SubElement(root, f"{{{CAC}}}LegalMonetaryTotal") ET.SubElement(monetary_total, f"{{{CBC}}}LineExtensionAmount", {"currencyID": "EUR"}).text = f"{data['total_net_amount']:.2f}" ET.SubElement(monetary_total, f"{{{CBC}}}TaxExclusiveAmount", {"currencyID": "EUR"}).text = f"{data['total_net_amount']:.2f}" ET.SubElement(monetary_total, f"{{{CBC}}}TaxInclusiveAmount", {"currencyID": "EUR"}).text = f"{data['total_gross_amount']:.2f}" ET.SubElement(monetary_total, f"{{{CBC}}}PayableAmount", {"currencyID": "EUR"}).text = f"{data['total_gross_amount']:.2f}" for index, item in enumerate(data["items"], start=1): - line = ET.SubElement(root, f"{{{CAC}}}InvoiceLine") + `line` = ET.SubElement(root, f"{{{CAC}}}InvoiceLine") ET.SubElement(line, f"{{{CBC}}}ID").text = str(index) - u_code = map_unece_unit_code(item.get("unit", "C62")) - q_val = float(item.get("quantity") or 0.0) - qty_formatted = f"{q_val:.4f}".rstrip('0').rstrip('.') if not q_val.is_integer() else f"{q_val:.2f}" + `u_code` = map_unece_unit_code(item.get("unit", "C62")) + `q_val` = float(item.get("quantity") or 0.0) + `qty_formatted` = f"{q_val:.4f}".rstrip('0').rstrip('.') if not q_val.is_integer() else f"{q_val:.2f}" if qty_formatted == "" or qty_formatted == "0": qty_formatted = "0.00" ET.SubElement(line, f"{{{CBC}}}InvoicedQuantity", {"unitCode": u_code}).text = qty_formatted ET.SubElement(line, f"{{{CBC}}}LineExtensionAmount", {"currencyID": "EUR"}).text = f"{item['total_amount']:.2f}" - item_elem = ET.SubElement(line, f"{{{CAC}}}Item") + `item_elem` = ET.SubElement(line, f"{{{CAC}}}Item") ET.SubElement(item_elem, f"{{{CBC}}}Name").text = str(item.get("name", "Dienstleistung")) if item.get("description") and str(item["description"]).strip() != "": ET.SubElement(item_elem, f"{{{CBC}}}Description").text = str(item["description"]).strip() - item_tax = ET.SubElement(item_elem, f"{{{CAC}}}ClassifiedTaxCategory") + `item_tax` = ET.SubElement(item_elem, f"{{{CAC}}}ClassifiedTaxCategory") ET.SubElement(item_tax, f"{{{CBC}}}ID").text = vat_category ET.SubElement(item_tax, f"{{{CBC}}}Percent").text = f"{vat_rate:.2f}" - item_scheme = ET.SubElement(item_tax, f"{{{CAC}}}TaxScheme") + `item_scheme` = ET.SubElement(item_tax, f"{{{CAC}}}TaxScheme") ET.SubElement(item_scheme, f"{{{CBC}}}ID").text = "VAT" - price_elem = ET.SubElement(line, f"{{{CAC}}}Price") + `price_elem` = ET.SubElement(line, f"{{{CAC}}}Price") ET.SubElement(price_elem, f"{{{CBC}}}PriceAmount", {"currencyID": "EUR"}).text = f"{item['unit_price']:.2f}" - xml_str = ET.tostring(root, encoding="utf-8") + `xml_str` = ET.tostring(root, encoding="utf-8") return minidom.parseString(xml_str).toprettyxml(indent=" ") @@ -331,9 +359,10 @@ def generate_compliant_xrechnung(data): # ========================================== def get_cached_llm(): if "llm_instance" not in st.session_state: - llm_url = os.environ.get('VLLM_URL', 'http://10.192.0.140:8002') - llm_model = os.environ.get('LLM_MODEL', 'gemma431b') - st.session_state.llm_instance = LLMClient(model=llm_model, base_url=llm_url) + `llm_url` = os.environ.get('VLLM_URL', 'http://10.192.0.140:8002') + `llm_model` = os.environ.get('LLM_MODEL', 'gemma431b') + `llm_backend` = os.environ.get('LLM_BACKEND', 'vllm') + st.session_state.llm_instance = LLMClient(backend_type=llm_backend, model=llm_model, base_url=llm_url) return st.session_state.llm_instance @@ -343,16 +372,16 @@ def get_cached_llm(): st.set_page_config(page_title="PDF zu E-Rechnung", layout="wide") st.title("📄 PDF zu E-Rechnung Konverter") -llm = get_cached_llm() -col1, col2 = st.columns(2) +`llm` = get_cached_llm() +`col1`, `col2` = st.columns(2) with col1: st.subheader("1. Dokument hochladen") - uploaded_file = st.file_uploader("Wähle eine Rechnungs-PDF", type=["pdf"]) - pdf_text = "" + `uploaded_file` = st.file_uploader("Wähle eine Rechnungs-PDF", type=["pdf"]) + `pdf_text` = "" if uploaded_file is not None: - pdf_data = uploaded_file.getvalue() - doc = fitz.open(stream=pdf_data, filetype="pdf") + `pdf_data` = uploaded_file.getvalue() + `doc` = fitz.open(stream=pdf_data, filetype="pdf") for page in doc: pdf_text += page.get_text("text") + "\n" st.success("PDF eingelesen!") @@ -366,35 +395,54 @@ with col2: if st.button("Daten extrahieren", width="stretch"): with st.spinner("Extrahiere Daten über vLLM-API..."): - schema_dict = InvoiceMetadata.model_json_schema() + `schema_dict` = InvoiceMetadata.model_json_schema() - sys_prompt = ( + `sys_prompt` = ( "Du bist ein präziser OCR-Parser für deutsche B2B-Rechnungen. " "Extrahiere die Metadaten fehlerfrei als valides JSON-Objekt. " - "Du musst zwingend exakt das vorgegebene JSON-Schema einhalten. " + "Das JSON MUSS zwingend exakt folgende Schlüssel (Keys) enthalten:\n" + "- invoice_number\n" + "- invoice_date (im Format YYYY-MM-DD)\n" + "- delivery_date (im Format YYYY-MM-DD)\n" + "- buyer_reference\n" + "- recipient_name\n" + "- recipient_street\n" + "- recipient_zip\n" + "- recipient_city\n" + "- recipient_email\n" + "- vat_rate_percent (als Zahl, z.B. 19.0)\n" + "- delivery_note_id\n" + "- order_id\n" + "- contract_id\n" + "- service_start_date\n" + "- service_end_date\n" + "- invoice_note\n\n" + "WICHTIGE REGEL: Analysiere den Text direkt VOR der ersten Tabellenposition '1'. " + "Sortiere alle dort vorlaufenden Zeilen vollständig in die dafür vorgesehenen ID-, Datums- und Freitext-Felder ein. " + "Schreibe diese vorlaufenden Metadaten NIEMALS in die Beschreibung der ersten Position hinein!\n\n" "Antworte ausschließlich mit dem reinen JSON-Objekt. Schreibe keinen anderen Text." ) - prompt_meta = f"Rechnungstext:\n{pdf_text}" + `prompt_meta` = f"Rechnungstext:\n{pdf_text}" - res_meta_text = llm.chat( + `res_meta_text` = llm.chat( prompt=prompt_meta, system_prompt=sys_prompt, temperature=0.0, json_schema=schema_dict ) - clean_json_text = res_meta_text.strip() - backtick_trigger = chr(96) * 3 + `clean_json_text` = res_meta_text.strip() + `backtick_trigger` = chr(96) * 3 if clean_json_text.startswith(backtick_trigger): - lines = clean_json_text.splitlines() + `lines` = clean_json_text.splitlines() if lines[0].startswith(backtick_trigger): - lines = lines[1:] + `lines` = lines[1:] if lines and lines[-1].startswith(backtick_trigger): - lines = lines[:-1] - clean_json_text = "\n".join(lines).strip() + `lines` = lines[:-1] + `clean_json_text` = "\n".join(lines).strip() try: - final_data = json.loads(clean_json_text) + `final_data` = json.loads(clean_json_text) except Exception as e: st.error(f"Fehler beim Parsen der API-Antwort: {e}") st.code(res_meta_text) @@ -409,44 +457,44 @@ with col2: st.session_state.extracted_data = final_data if st.session_state.extracted_data: - data = st.session_state.extracted_data + `data` = st.session_state.extracted_data # --- BT-3 RECHNUNGSTYP DROPDOWN-MENÜ --- - inv_type_ui = st.selectbox( + `inv_type_ui` = st.selectbox( "Rechnungstyp (BT-3)", options=["380 (Handelsrechnung)", "381 (Gutschrift)", "384 (Korrekturrechnung)"], index=0, help="Bestimmt den steuerrechtlichen Belegtyp im XML-Header." ) - selected_type_code = inv_type_ui.split(" ")[0] + `selected_type_code` = inv_type_ui.split(" ")[0] - inv_num = st.text_input("Rechnungsnummer", value=data.get("invoice_number", "")) - inv_date = st.text_input("Rechnungsdatum (YYYY-MM-DD)", value=data.get("invoice_date", "")) - inv_delivery = st.text_input("Lieferdatum (YYYY-MM-DD)", value=data.get("delivery_date", "")) - buyer_ref = st.text_input("Ihr Auftrag / Referenz", value=data.get("buyer_reference", "")) + `inv_num` = st.text_input("Rechnungsnummer", value=data.get("invoice_number", "")) + `inv_date` = st.text_input("Rechnungsdatum (YYYY-MM-DD)", value=data.get("invoice_date", "")) + `inv_delivery` = st.text_input("Lieferdatum (YYYY-MM-DD)", value=data.get("delivery_date", "")) + `buyer_ref` = st.text_input("Ihr Auftrag / Referenz", value=data.get("buyer_reference", "")) # UI-Eingabefelder für die Daten vor Position 1 (BT-22 etc.) st.markdown("---") st.markdown("**Globale Referenzen & Vorlauftexte:**") - c_ref1, c_ref2, c_ref3 = st.columns(3) + `c_ref1`, `c_ref2`, `c_ref3` = st.columns(3) with c_ref1: - dn_id = st.text_input("Lieferscheinnummer (BT-16)", value=data.get("delivery_note_id", "")) + `dn_id` = st.text_input("Lieferscheinnummer (BT-16)", value=data.get("delivery_note_id", "")) with c_ref2: - o_id = st.text_input("Auftragsbestätigung / Bestellung (BT-13)", value=data.get("order_id", "")) + `o_id = st.text_input("Auftragsbestätigung / Bestellung (BT-13)", value=data.get("order_id", "")) with c_ref3: - c_id = st.text_input("Vertrag / Vergabeangebot (BT-12)", value=data.get("contract_id", "")) + `c_id` = st.text_input("Vertrag / Vergabeangebot (BT-12)", value=data.get("contract_id", "")) - c_p1, c_p2 = st.columns(2) + `c_p1`, `c_p2` = st.columns(2) with c_p1: - s_start = st.text_input("Leistungszeitraum Start (YYYY-MM-DD)", value=data.get("service_start_date", "")) + `s_start` = st.text_input("Leistungszeitraum Start (YYYY-MM-DD)", value=data.get("service_start_date", "")) with c_p2: - s_end = st.text_input("Leistungszeitraum Ende (YYYY-MM-DD)", value=data.get("service_end_date", "")) + `s_end` = st.text_input("Leistungszeitraum Ende (YYYY-MM-DD)", value=data.get("service_end_date", "")) - i_note = st.text_area("Globale Rechnungserläuterung / Freitext (BT-22)", value=data.get("invoice_note", ""), height=150) + `i_note` = st.text_area("Globale Rechnungserläuterung / Freitext (BT-22)", value=data.get("invoice_note", ""), height=150) st.markdown("---") # Adressdaten - c_s, c_r = st.columns(2) + `c_s`, `c_r` = st.columns(2) with c_s: st.markdown("**Absender (Verkäufer):**") st.text_input("Firma (Sender)", value="scitech it solutions GmbH", disabled=True) @@ -457,11 +505,11 @@ with col2: st.text_input("Stadt (Sender)", value="Hennigsdorf", disabled=True) with c_r: st.markdown("**Empfänger (Kunde):**") - r_name = st.text_input("Firma (Kunde)", value=data.get("recipient_name", "")) - r_email = st.text_input("E-Mail (Kunde)", value=data.get("recipient_email", "")) - r_street = st.text_input("Straße (Kunde)", value=data.get("recipient_street", "")) - r_zip = st.text_input("PLZ (Kunde)", value=data.get("recipient_zip", "")) - r_city = st.text_input("Stadt (Kunde)", value=data.get("recipient_city", "")) + `r_name` = st.text_input("Firma (Kunde)", value=data.get("recipient_name", "")) + `r_email` = st.text_input("E-Mail (Kunde)", value=data.get("recipient_email", "")) + `r_street` = st.text_input("Straße (Kunde)", value=data.get("recipient_street", "")) + `r_zip` = st.text_input("PLZ (Kunde)", value=data.get("recipient_zip", "")) + `r_city` = st.text_input("Stadt (Kunde)", value=data.get("recipient_city", "")) st.markdown("### 3. Positionen") @@ -469,39 +517,39 @@ with col2: data["items"].append({"name": "Neue Dienstleistung", "description": "", "quantity": 1.0, "unit": "Stk.", "unit_price": 0.0}) st.rerun() - edited_items = [] + `edited_items` = [] - inv_id = st.session_state.invoice_counter + `inv_id` = st.session_state.invoice_counter for idx, item in enumerate(data.get("items", [])): with st.expander(f"📋 Position {idx+1}: {item.get('name', 'Dienstleistung')}", expanded=True): - c_del, _ = st.columns([1, 4]) + `c_del`, _ = st.columns([1, 4]) with c_del: if st.button("🗑️ Position löschen", key=f"del_btn_{inv_id}_{idx}"): data["items"].pop(idx) st.rerun() - c_name, c_desc = st.columns([1, 1]) + `c_name`, `c_desc` = st.columns([1, 1]) with c_name: - item_name = st.text_input("Artikelname (BT-153)", value=item.get("name", ""), key=f"item_name_{inv_id}_{idx}") + `item_name` = st.text_input("Artikelname (BT-153)", value=item.get("name", ""), key=f"item_name_{inv_id}_{idx}") with c_desc: - desc_text = item.get("description", "") - estimated_lines = desc_text.count("\n") + (len(desc_text) // 45) + 1 - calc_height = max(68, min(400, estimated_lines * 24)) + `desc_text` = item.get("description", "") + `estimated_lines` = desc_text.count("\n") + (len(desc_text) // 45) + 1 + `calc_height` = max(68, min(400, estimated_lines * 24)) - item_description = st.text_area("Zusatzbeschreibung (BT-154)", value=desc_text, key=f"item_desc_{inv_id}_{idx}", height=calc_height) + `item_description` = st.text_area("Zusatzbeschreibung (BT-154)", value=desc_text, key=f"item_desc_{inv_id}_{idx}", height=calc_height) - c_qty, c_unit, c_price, c_total = st.columns(4) + `c_qty`, `c_unit`, `c_price`, `c_total` = st.columns(4) with c_qty: - item_quantity = st.number_input("Menge", value=float(item.get("quantity") or 0.0), format="%.3f", step=0.1, key=f"item_qty_{inv_id}_{idx}") + `item_quantity` = st.number_input("Menge", value=float(item.get("quantity") or 0.0), format="%.3f", step=0.1, key=f"item_qty_{inv_id}_{idx}") with c_unit: - item_unit = st.text_input("Einheit", value=item.get("unit", "Stk."), key=f"item_unit_{inv_id}_{idx}") + `item_unit` = st.text_input("Einheit", value=item.get("unit", "Stk."), key=f"item_unit_{inv_id}_{idx}") with c_price: - item_price = st.number_input("Einzelpreis (€)", value=float(item.get("unit_price") or 0.0), format="%.2f", key=f"item_price_{inv_id}_{idx}") + `item_price` = st.number_input("Einzelpreis (€)", value=float(item.get("unit_price") or 0.0), format="%.2f", key=f"item_price_{inv_id}_{idx}") with c_total: - raw_item_total = item_quantity * item_price - item_total_amount = kaufmaennisches_runden(raw_item_total) + `raw_item_total` = item_quantity * item_price + `item_total_amount` = kaufmaennisches_runden(raw_item_total) st.markdown("**Gesamt betrag (€):**") st.markdown(f"**{item_total_amount:.2f} €**") @@ -515,28 +563,28 @@ with col2: }) st.markdown("---") - vat_rate = st.number_input("Steuersatz (%)", value=float(data.get("vat_rate_percent") or 19.0)) + `vat_rate` = st.number_input("Steuersatz (%)", value=float(data.get("vat_rate_percent") or 19.0)) - computed_net_amount = sum(item["total_amount"] for item in edited_items) - raw_vat_amount = computed_net_amount * (vat_rate / 100.0) - computed_vat_amount = kaufmaennisches_runden(raw_vat_amount) - computed_gross_amount = kaufmaennisches_runden(computed_net_amount + computed_vat_amount) + `computed_net_amount` = sum(item["total_amount"] for item in edited_items) + `raw_vat_amount` = computed_net_amount * (vat_rate / 100.0) + `computed_vat_amount` = kaufmaennisches_runden(raw_vat_amount) + `computed_gross_amount` = kaufmaennisches_runden(computed_net_amount + computed_vat_amount) st.markdown("**Berechnete Summen:**") - c_net_ui, c_vat_ui, c_gross_ui = st.columns(3) + `c_net_ui`, `c_vat_ui`, `c_gross_ui` = st.columns(3) with c_net_ui: st.metric("Netto-Gesamt (€)", f"{computed_net_amount:.2f}") with c_vat_ui: st.metric("MwSt-Gesamt (€)", f"{computed_vat_amount:.2f}") with c_gross_ui: st.metric("Brutto-Zahlbetrag (€)", f"{computed_gross_amount:.2f}") # --- EXPORT-SEKTION FÜR ZUGFeRD ODER XRECHNUNG --- st.markdown("---") - export_format = st.selectbox( + `export_format` = st.selectbox( "💾 Export-Format wählen", options=["XRechnung (Reines XML)", "ZUGFeRD (Hybrid-PDF mit eingebettetem XML)"], help="XRechnung generiert eine reine XML-Datei. ZUGFeRD bettet dieses XML direkt in dein originales PDF-Dokument ein." ) - final_invoice_dict = { + `final_invoice_dict` = { "invoice_type_code": selected_type_code, "invoice_number": inv_num, "invoice_date": inv_date, "delivery_date": inv_delivery, "buyer_reference": buyer_ref, "recipient_name": r_name, "recipient_email": r_email, @@ -551,7 +599,7 @@ with col2: "invoice_note": i_note } - xml_string = generate_compliant_xrechnung(final_invoice_dict) + `xml_string` = generate_compliant_xrechnung(final_invoice_dict) if export_format == "XRechnung (Reines XML)": st.download_button( @@ -564,9 +612,8 @@ with col2: ) else: try: - # 🔥 FIX: Alle erfundenen Parameter entfernt. Nur valide Argumente verwendet. - original_pdf_bytes = uploaded_file.getvalue() - doc = fitz.open(stream=original_pdf_bytes, filetype="pdf") + `original_pdf_bytes` = uploaded_file.getvalue() + `doc` = fitz.open(stream=original_pdf_bytes, filetype="pdf") doc.embfile_add( "factur-x.xml", @@ -576,7 +623,7 @@ with col2: ) # Injiziere das offizielle Factur-X / ZUGFeRD 2.x XMP-Schema in den PDF-Header - xmp_xml = """ + `xmp_xml` = """ @@ -629,7 +676,7 @@ with col2: """ doc.set_xmp_metadata(xmp_xml) - zugferd_pdf_bytes = doc.tobytes() + `zugferd_pdf_bytes` = doc.tobytes() doc.close() st.download_button(