diff --git a/main.py b/main.py index 12ac34c..a76ca91 100644 --- a/main.py +++ b/main.py @@ -69,7 +69,8 @@ class InvoiceMetadata(BaseModel): invoice_number: str = Field(description="Die Rechnungsnummer nach 'Nummer:' (z.B. R26-219)") invoice_date: str = Field(description="Das Rechnungsdatum nach 'Datum:' im Format YYYY-MM-DD") delivery_date: str = Field(description="Das Lieferdatum nach 'Lieferdatum:' im Format YYYY-MM-DD") - buyer_reference: str = Field(default="", description="Käuferreferenz/Leitweg-ID: NUR eintragen, wenn im Text explizit ein Wert nach 'Ihr Auftrag:' o.ä. als eigene Referenz des Käufers/Empfängers steht (z.B. 05.06.2026-Herr.Knospe). NIEMALS die Nummer einer Auftragsbestätigung/Bestellung hier eintragen - die gehört ausschließlich in order_id. Wenn keine separate Käuferreferenz im Text erkennbar ist: leer lassen.") + customer_number: str = Field(default="", description="Kundennummer, wie sie beim Empfänger hinterlegt ist (z.B. der exakte Wert nach 'Kunden-Nr:'). Wird als Käuferreferenz (BT-10) verwendet. Falls im Text keine Kundennummer erkennbar ist: leer lassen.") + buyer_reference: str = Field(default="", description="Bestellnummer des KÄUFERS für diesen Auftrag (BT-13): der exakte Wert nach 'Ihr Auftrag:' o.ä. (z.B. 05.06.2026-Herr.Knospe). NICHT die Kundennummer (die gehört in customer_number) und NICHT unsere eigene Auftragsbestätigungsnummer (die gehört in order_id). Wenn im Text keine solche Käufer-Bestellnummer erkennbar ist: leer lassen.") recipient_name: str = Field(description="Vollständige Firma des Kunden/Empfängers (z.B. Stadtwerke Hennigsdorf GmbH)") recipient_street: str = Field(description="Straße und Hausnummer des Kunden") recipient_zip: str = Field(description="Postleitzahl (PLZ) des Kunden") @@ -78,7 +79,7 @@ class InvoiceMetadata(BaseModel): vat_rate_percent: float = Field(description="Der primäre Umsatzsteuersatz in Prozent (z.B. 19.0)") delivery_note_id: str = Field(default="", description="Gefundene Lieferscheinnummer (z.B. LS26-035) VOR Position 1. Sonst leer lassen.") - order_id: str = Field(default="", description="Gefundene Nummer der Auftragsbestätigung oder Bestellung (z.B. AB26-080) VOR Position 1. Sonst leer lassen.") + order_id: str = Field(default="", description="Gefundene EIGENE Auftragsbestätigungs-/Verkaufsauftragsnummer (BT-14, z.B. AB26-080) VOR Position 1. Das ist unsere eigene Nummer, NICHT die Bestellnummer des Käufers. Sonst leer lassen.") contract_id: str = Field(default="", description="Gefundene Nummer von Vergabeangebot, Vertrag oder Ausschreibung (z.B. AN26-130) VOR Position 1. Sonst leer lassen.") service_start_date: str = Field(default="", description="Startdatum eines explizit genannten Einsatz- oder Leistungszeitraums VOR Position 1 im Format YYYY-MM-DD. Sonst leer.") service_end_date: str = Field(default="", description="Enddatum eines explizit genannten Einsatz- oder Leistungszeitraums VOR Position 1 im Format YYYY-MM-DD. Sonst leer.") @@ -216,20 +217,29 @@ def generate_compliant_xrechnung(data): ET.SubElement(root, f"{{{CBC}}}DocumentCurrencyCode").text = "EUR" # BT-10 Käuferreferenz: NUR eintragen, wenn tatsächlich ein Wert vorliegt. Für private/ - # geschäftliche Empfänger (B2B) ist dieses Feld optional (0..1); nur bei Behörden (B2G) - # ist eine echte Leitweg-ID zwingend (BR-DE-15) - die kommt dann aber vom Empfänger selbst - # und wird nicht erfunden. - if data.get("buyer_reference") and str(data["buyer_reference"]).strip() != "": - ET.SubElement(root, f"{{{CBC}}}BuyerReference").text = str(data["buyer_reference"]).strip() + # geschäftliche Empfänger (B2B) reicht die Kundennummer, die der Kunde bei euch hat; nur bei + # Behörden (B2G) ist eine echte Leitweg-ID vorgesehen - die kommt dann vom Empfänger selbst. + if data.get("customer_number") and str(data["customer_number"]).strip() != "": + ET.SubElement(root, f"{{{CBC}}}BuyerReference").text = str(data["customer_number"]).strip() if data.get("service_start_date") and data.get("service_end_date"): period_elem = ET.SubElement(root, f"{{{CAC}}}InvoicePeriod") ET.SubElement(period_elem, f"{{{CBC}}}StartDate").text = parse_to_strict_xml_date(data["service_start_date"]) ET.SubElement(period_elem, f"{{{CBC}}}EndDate").text = parse_to_strict_xml_date(data["service_end_date"]) - - if data.get("order_id") and str(data["order_id"]).strip() != "": + + # BT-13 "Purchase order reference" (Bestellnummer des KÄUFERS, z.B. nach "Ihr Auftrag:") und + # BT-14 "Sales order reference" (unsere EIGENE Auftrags-/Verkaufsauftragsnummer) leben beide + # unter demselben cac:OrderReference-Element. cac:OrderReference/cbc:ID ist laut UBL-Schema + # zwingend, sobald OrderReference verwendet wird - liegt keine Käufer-Bestellnummer (BT-13) + # vor, aber eine eigene Nummer (BT-14), ist laut Peppol-Spezifikation der Platzhalter "NA" + # vorgeschrieben. + has_bt13 = data.get("buyer_reference") and str(data["buyer_reference"]).strip() != "" + has_bt14 = data.get("order_id") and str(data["order_id"]).strip() != "" + if has_bt13 or has_bt14: order_ref = ET.SubElement(root, f"{{{CAC}}}OrderReference") - ET.SubElement(order_ref, f"{{{CBC}}}ID").text = str(data["order_id"]).strip() + ET.SubElement(order_ref, f"{{{CBC}}}ID").text = str(data["buyer_reference"]).strip() if has_bt13 else "NA" + if has_bt14: + ET.SubElement(order_ref, f"{{{CBC}}}SalesOrderID").text = str(data["order_id"]).strip() if data.get("delivery_note_id") and str(data["delivery_note_id"]).strip() != "": despatch_ref = ET.SubElement(root, f"{{{CAC}}}DespatchDocumentReference") @@ -427,8 +437,8 @@ def generate_compliant_zugferd_cii(data): header_agree = ET.SubElement(trade_trans, f"{{{RAM}}}ApplicableHeaderTradeAgreement") # BT-10 Käuferreferenz: NUR eintragen, wenn tatsächlich ein Wert vorliegt (siehe UBL-Generator # für die ausführliche Begründung). - if data.get("buyer_reference") and str(data["buyer_reference"]).strip() != "": - ET.SubElement(header_agree, f"{{{RAM}}}BuyerReference").text = str(data["buyer_reference"]).strip() + if data.get("customer_number") and str(data["customer_number"]).strip() != "": + ET.SubElement(header_agree, f"{{{RAM}}}BuyerReference").text = str(data["customer_number"]).strip() supplier_party = ET.SubElement(header_agree, f"{{{RAM}}}SellerTradeParty") ET.SubElement(supplier_party, f"{{{RAM}}}Name").text = "scitech it solutions GmbH" @@ -478,8 +488,16 @@ def generate_compliant_zugferd_cii(data): ) if data.get("order_id") and str(data["order_id"]).strip() != "": - order_ref = ET.SubElement(header_agree, f"{{{RAM}}}BuyerOrderReferencedDocument") + # BT-14 "Sales order reference" - unsere EIGENE Auftrags-/Verkaufsauftragsnummer. + # In CII ein eigenständiges Element. Muss laut XSD VOR BuyerOrderReferencedDocument stehen. + order_ref = ET.SubElement(header_agree, f"{{{RAM}}}SellerOrderReferencedDocument") ET.SubElement(order_ref, f"{{{RAM}}}IssuerAssignedID").text = str(data["order_id"]).strip() + + if data.get("buyer_reference") and str(data["buyer_reference"]).strip() != "": + # BT-13 "Purchase order reference" - die Bestellnummer des KÄUFERS (z.B. nach + # "Ihr Auftrag:"), NICHT unsere eigene Nummer (die ist BT-14/order_id oben). + buyer_order_ref = ET.SubElement(header_agree, f"{{{RAM}}}BuyerOrderReferencedDocument") + ET.SubElement(buyer_order_ref, f"{{{RAM}}}IssuerAssignedID").text = str(data["buyer_reference"]).strip() if data.get("contract_id") and str(data["contract_id"]).strip() != "": contract_ref = ET.SubElement(header_agree, f"{{{RAM}}}ContractReferencedDocument") @@ -664,6 +682,7 @@ with col2: "- invoice_date (im Format YYYY-MM-DD)\n" "- delivery_date (im Format YYYY-MM-DD)\n" "- buyer_reference\n" + "- customer_number\n" "- recipient_name\n" "- recipient_street\n" "- recipient_zip\n" @@ -727,10 +746,24 @@ with col2: ) 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 (BT-1)", value=data.get("invoice_number", "")) + inv_date = st.text_input("Rechnungsdatum (BT-2, YYYY-MM-DD)", value=data.get("invoice_date", "")) + inv_delivery = st.text_input("Lieferdatum (BT-72, YYYY-MM-DD)", value=data.get("delivery_date", "")) + cust_num = st.text_input( + "Kundennummer / Leitweg-ID (BT-10)", + value=data.get("customer_number", ""), + help="Eure Kundennummer beim Empfänger (Standardfall für B2B) oder eine Leitweg-ID, falls " + "der Empfänger eine Behörde ist. Sonst leer lassen - das Feld wird dann im XML komplett " + "weggelassen." + ) + buyer_ref = st.text_input( + "Ihr Auftrag / Bestellnummer (BT-13)", + value=data.get("buyer_reference", ""), + help="Die vom KÄUFER vorgegebene Bestellnummer für diesen Auftrag (z.B. nach 'Ihr Auftrag:'). " + "NICHT die Kundennummer (die gehört ins Feld oben) und NICHT die eigene Auftragsbestätigungs-" + "/Verkaufsauftragsnummer (die gehört ins Feld 'Auftragsbestätigung / Verkaufsauftrag (BT-14)' " + "weiter unten). Sonst leer lassen." + ) # UI-Eingabefelder für die Daten vor Position 1 (BT-22 etc.) st.markdown("---") @@ -739,15 +772,15 @@ 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 / Bestellung (BT-13)", value=data.get("order_id", "")) + o_id = st.text_input("Auftragsbestätigung / Verkaufsauftrag - unsere Nummer (BT-14)", value=data.get("order_id", "")) with c_ref3: c_id = st.text_input("Vertrag / Vergabeangebot (BT-12)", value=data.get("contract_id", "")) 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 (BT-73, 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 (BT-74, 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) st.markdown("---") @@ -756,21 +789,21 @@ with col2: 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) - st.text_input("E-Mail (Sender)", value="office@scitech-gmbh.de", disabled=True) - st.text_input("Telefon (Sender)", value="+49 3302 4940537", disabled=True) - st.text_input("Straße (Sender)", value="Hermann-Schumann-Str. 4", disabled=True) - st.text_input("PLZ (Sender)", value="16761", disabled=True) - st.text_input("Stadt (Sender)", value="Hennigsdorf", disabled=True) - st.text_input("Umsatzsteuer-ID (Sender)", value="DE218436692", disabled=True) - st.text_input("Registernummer (Sender)", value="HRB 6058 NP", disabled=True) + st.text_input("Firma (Sender) (BT-27)", value="scitech it solutions GmbH", disabled=True) + st.text_input("E-Mail (Sender) (BT-34)", value="office@scitech-gmbh.de", disabled=True) + st.text_input("Telefon (Sender) (BT-42)", value="+49 3302 4940537", disabled=True) + st.text_input("Straße (Sender) (BT-35)", value="Hermann-Schumann-Str. 4", disabled=True) + st.text_input("PLZ (Sender) (BT-38)", value="16761", disabled=True) + 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) 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) (BT-44)", value=data.get("recipient_name", "")) + r_email = st.text_input("E-Mail (Kunde) (BT-49)", value=data.get("recipient_email", "")) + r_street = st.text_input("Straße (Kunde) (BT-50)", value=data.get("recipient_street", "")) + r_zip = st.text_input("PLZ (Kunde) (BT-53)", value=data.get("recipient_zip", "")) + r_city = st.text_input("Stadt (Kunde) (BT-52)", value=data.get("recipient_city", "")) st.markdown("### 3. Positionen") @@ -801,11 +834,11 @@ with col2: 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 (BT-129)", 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 (BT-130)", 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 (BT-146, €)", 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) @@ -822,7 +855,7 @@ 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 (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) @@ -846,7 +879,7 @@ with col2: 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, + "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",