From e80d011c88495a26e3aa63546fb55f348cf9a8dc Mon Sep 17 00:00:00 2001 From: dfuhrmann3 Date: Mon, 6 Jul 2026 14:09:15 +0200 Subject: [PATCH] Test --- main.py | 108 +++++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 75 insertions(+), 33 deletions(-) diff --git a/main.py b/main.py index a76ca91..079a655 100644 --- a/main.py +++ b/main.py @@ -89,6 +89,24 @@ class InvoiceMetadata(BaseModel): # ========================================== # 3. MATHEMATISCHE & PARSING-FUNKTIONEN # ========================================== + +# Zentrale Geschäftskonstanten - hier zentral pflegen statt an mehreren Stellen im Code. +ZAHLUNGSZIEL_TAGE = 14 +ZAHLUNGSBEDINGUNG_TEXT = f"Zahlbar innerhalb {ZAHLUNGSZIEL_TAGE} Tagen ohne Abzug." +VERKAEUFER_IBAN = "DE06120700240456207000" + + +def berechne_faelligkeit(invoice_date_str): + """Fälligkeitsdatum = Rechnungsdatum + ZAHLUNGSZIEL_TAGE. Gibt 'YYYY-MM-DD' zurück. + Fällt bei unlesbarem Rechnungsdatum auf heute+Ziel zurück (nur als letzter Notnagel).""" + base = parse_to_strict_xml_date(invoice_date_str) + try: + base_date = datetime.strptime(base, "%Y-%m-%d") + except (ValueError, TypeError): + base_date = datetime.today() + return (base_date + timedelta(days=ZAHLUNGSZIEL_TAGE)).strftime("%Y-%m-%d") + + def kaufmaennisches_runden(betrag, nachkommastellen=2): if not betrag: return 0.0 @@ -170,13 +188,16 @@ def map_unece_unit_code(unit_str): return "C62" def parse_to_strict_xml_date(date_str): + """Normalisiert ein Datum auf 'YYYY-MM-DD'. Gibt leeren String zurück, wenn das Datum + fehlt ODER nicht geparst werden kann - NICHT still auf 'heute' fallen, damit fehlerhafte + Datumsangaben sichtbar werden statt unbemerkt durchzurutschen.""" if not date_str or str(date_str).strip() == "": return "" clean_str = str(date_str).strip() for fmt in ("%Y-%m-%d", "%d.%m.%Y", "%d-%m-%Y"): try: return datetime.strptime(clean_str, fmt).strftime("%Y-%m-%d") except ValueError: continue - return datetime.today().strftime("%Y-%m-%d") + return "" def format_cii_date(date_str): clean = parse_to_strict_xml_date(date_str) @@ -203,12 +224,7 @@ def generate_compliant_xrechnung(data): ET.SubElement(root, f"{{{CBC}}}ID").text = str(data["invoice_number"]) ET.SubElement(root, f"{{{CBC}}}IssueDate").text = parse_to_strict_xml_date(data["invoice_date"]) - try: - base_date = datetime.strptime(parse_to_strict_xml_date(data["invoice_date"]), "%Y-%m-%d") - computed_due = (base_date + timedelta(days=14)).strftime("%Y-%m-%d") - except Exception: - computed_due = (datetime.today() + timedelta(days=14)).strftime("%Y-%m-%d") - ET.SubElement(root, f"{{{CBC}}}DueDate").text = computed_due + ET.SubElement(root, f"{{{CBC}}}DueDate").text = berechne_faelligkeit(data["invoice_date"]) ET.SubElement(root, f"{{{CBC}}}InvoiceTypeCode").text = str(data.get("invoice_type_code", "380")) @@ -282,7 +298,7 @@ def generate_compliant_xrechnung(data): # --- Empfänger --- 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" + ET.SubElement(party_cust, f"{{{CBC}}}EndpointID", {"schemeID": "EM"}).text = str(data.get("recipient_email", "")).strip() 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"]) @@ -307,7 +323,7 @@ def generate_compliant_xrechnung(data): # CII/ZUGFeRD-Fassung. BR-CO-25 ist bereits durch das gesetzte cbc:DueDate (BT-9) erfüllt; # dieser Text ist zusätzliche Klarstellung, keine Pflicht. payment_terms_ubl = ET.SubElement(root, f"{{{CAC}}}PaymentTerms") - ET.SubElement(payment_terms_ubl, f"{{{CBC}}}Note").text = "Zahlbar innerhalb 14 Tagen ohne Abzug." + ET.SubElement(payment_terms_ubl, f"{{{CBC}}}Note").text = ZAHLUNGSBEDINGUNG_TEXT tax_total = ET.SubElement(root, f"{{{CAC}}}TaxTotal") ET.SubElement(tax_total, f"{{{CBC}}}TaxAmount", {"currencyID": "EUR"}).text = f"{data['total_vat_amount']:.2f}" @@ -481,11 +497,10 @@ def generate_compliant_zugferd_cii(data): 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. + # XSD nach PostalTradeAddress stehen. KEIN erfundener Fallback - fehlt die Adresse, wird das + # (leere) Pflichtfeld sichtbar und die UI blockiert den Export mit Warnung. 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" - ) + ET.SubElement(cust_uri, f"{{{RAM}}}URIID", {"schemeID": "EM"}).text = str(data.get("recipient_email", "")).strip() if data.get("order_id") and str(data["order_id"]).strip() != "": # BT-14 "Sales order reference" - unsere EIGENE Auftrags-/Verkaufsauftragsnummer. @@ -543,15 +558,11 @@ def generate_compliant_zugferd_cii(data): 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") + # Fälligkeitsdatum ODER Zahlungsbedingungen angegeben werden). Zentrale Berechnung, analog + # zur XRechnung(UBL)-Generierung. + due_date_str = berechne_faelligkeit(data["invoice_date"]).replace("-", "") payment_terms = ET.SubElement(header_settle, f"{{{RAM}}}SpecifiedTradePaymentTerms") - ET.SubElement(payment_terms, f"{{{RAM}}}Description").text = "Zahlbar innerhalb 14 Tagen ohne Abzug." + ET.SubElement(payment_terms, f"{{{RAM}}}Description").text = ZAHLUNGSBEDINGUNG_TEXT due_date_elem = ET.SubElement(payment_terms, f"{{{RAM}}}DueDateDateTime") ET.SubElement(due_date_elem, f"{{{UDT}}}DateTimeString", {"format": "102"}).text = due_date_str @@ -740,7 +751,7 @@ with col2: # --- BT-3 RECHNUNGSTYP DROPDOWN-MENÜ --- inv_type_ui = st.selectbox( "Rechnungstyp (BT-3)", - options=["380 (Handelsrechnung)", "381 (Gutschrift)", "384 (Korrekturrechnung)"], + options=["380 (Handelsrechnung)", "381 (Gutschrift)", "384 (Korrekturrechnung)", "326 (Teilrechnung)"], index=0, help="Bestimmt den steuerrechtlichen Belegtyp im XML-Header." ) @@ -772,7 +783,7 @@ with col2: with c_ref1: dn_id = st.text_input("Lieferscheinnummer (BT-16)", value=data.get("delivery_note_id", "")) with c_ref2: - o_id = st.text_input("Auftragsbestätigung / Verkaufsauftrag - unsere Nummer (BT-14)", value=data.get("order_id", "")) + o_id = st.text_input("Auftragsbestätigung / Verkaufsauftrag (BT-14)", value=data.get("order_id", "")) with c_ref3: c_id = st.text_input("Vertrag / Vergabeangebot (BT-12)", value=data.get("contract_id", "")) @@ -797,6 +808,7 @@ with col2: st.text_input("Stadt (Sender) (BT-37)", value="Hennigsdorf", disabled=True) st.text_input("Umsatzsteuer-ID (Sender) (BT-31)", value="DE218436692", disabled=True) st.text_input("Registernummer (Sender) (BT-30)", value="HRB 6058 NP", disabled=True) + st.text_input("IBAN (Sender) (BT-84)", value=VERKAEUFER_IBAN, disabled=True) with c_r: st.markdown("**Empfänger (Kunde):**") r_name = st.text_input("Firma (Kunde) (BT-44)", value=data.get("recipient_name", "")) @@ -842,7 +854,7 @@ with col2: with c_total: raw_item_total = item_quantity * item_price item_total_amount = kaufmaennisches_runden(raw_item_total) - st.markdown("**Gesamt betrag (€):**") + st.markdown("**Gesamtbetrag (€):**") st.markdown(f"**{item_total_amount:.2f} €**") edited_items.append({ @@ -857,10 +869,15 @@ with col2: st.markdown("---") vat_rate = st.number_input("Steuersatz (BT-152, %)", 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) + # Summen mit Decimal bilden, damit die Netto-Summe exakt der Summe der (bereits gerundeten) + # Positionsbeträge entspricht. Reine float-Summation kann minimal driften und dann zu + # BR-CO-10-Validierungsfehlern führen (Summe der Zeilen != TaxExclusiveAmount). + net_dec = sum((Decimal(str(item["total_amount"])) for item in edited_items), Decimal("0")) + vat_dec = (net_dec * Decimal(str(vat_rate)) / Decimal("100")).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP) + gross_dec = (net_dec + vat_dec).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP) + computed_net_amount = float(net_dec.quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)) + computed_vat_amount = float(vat_dec) + computed_gross_amount = float(gross_dec) st.markdown("**Berechnete Summen:**") c_net_ui, c_vat_ui, c_gross_ui = st.columns(3) @@ -882,7 +899,7 @@ with col2: "buyer_reference": buyer_ref, "customer_number": cust_num, "recipient_name": r_name, "recipient_email": r_email, "recipient_street": r_street, "recipient_zip": r_zip, "recipient_city": r_city, "items": edited_items, "total_net_amount": computed_net_amount, "total_vat_amount": computed_vat_amount, - "vat_rate_percent": vat_rate, "total_gross_amount": computed_gross_amount, "iban": "DE06120700240456207000", + "vat_rate_percent": vat_rate, "total_gross_amount": computed_gross_amount, "iban": VERKAEUFER_IBAN, "delivery_note_id": dn_id, "order_id": o_id, "contract_id": c_id, @@ -890,13 +907,38 @@ with col2: "service_end_date": s_end, "invoice_note": i_note } - + + # --- PFLICHTFELD-VALIDIERUNG vor dem Export --- + # Sammelt blockierende Probleme (Export nicht möglich), damit fehlerhafte/fehlende + # Pflichtangaben sichtbar werden, statt still mit Platzhaltern oder falschen Werten + # in die Rechnung zu wandern. + blocker = [] + if not str(inv_num).strip(): + blocker.append("Rechnungsnummer (BT-1) fehlt.") + if parse_to_strict_xml_date(inv_date) == "": + blocker.append("Rechnungsdatum (BT-2) fehlt oder ist unlesbar (erwartet: YYYY-MM-DD). " + "Bitte korrigieren - es wird KEIN Datum automatisch eingesetzt.") + if not str(r_email).strip(): + blocker.append("E-Mail des Kunden (BT-49) fehlt. Diese elektronische Adresse ist für " + "XRechnung/ZUGFeRD ein Pflichtfeld (PEPPOL-EN16931-R010) und wird NICHT " + "mit einem Platzhalter befüllt. Bitte eintragen.") + if inv_delivery and parse_to_strict_xml_date(inv_delivery) == "": + blocker.append("Lieferdatum (BT-72) ist unlesbar (erwartet: YYYY-MM-DD). Bitte korrigieren oder leeren.") + if not edited_items: + blocker.append("Die Rechnung enthält keine Position (mindestens eine ist erforderlich).") + + if blocker: + st.warning("⚠️ Export blockiert - folgende Pflichtangaben fehlen oder sind fehlerhaft:\n\n" + + "\n".join(f"- {b}" for b in blocker)) + + export_disabled = len(blocker) > 0 + if "UBL XML" in export_format: - if st.button("🔒 XRechnung exportieren", type="primary", width="stretch"): + 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") else: - if st.button("🔒 ZUGFeRD PDF exportieren", type="primary", width="stretch"): + if st.button("🔒 ZUGFeRD PDF exportieren", type="primary", width="stretch", disabled=export_disabled): try: cii_xml_string = generate_compliant_zugferd_cii(final_invoice_dict) @@ -944,7 +986,7 @@ st.markdown("---") st.caption( "📝 **Hinweise:**\n\n" "**Human-in-the-Loop:** " - "Das Tool dient as Automations-Assistent. Vor dem finalen XML-Export ist eine visuelle Endkontrolle der extrahierten Daten " + "Das Tool dient als Automations-Assistent. Vor dem finalen XML-Export ist eine visuelle Endkontrolle der extrahierten Daten " "(insbesondere Rechnungsnummer und Empfängeradresse) durch den Sachbearbeiter zwingend erforderlich.\n\n" "**GoBD-Konformität:** " "Das Tool generiert ausschließlich das strukturierte XML-Format. Für die Einhaltung der GoBD-Richtlinien "