From 6ecd59e5ea3ee53107e279b1dd3f3eac18d2319a Mon Sep 17 00:00:00 2001 From: dfuhrmann3 Date: Tue, 7 Jul 2026 14:54:17 +0200 Subject: [PATCH] feat: make payment means code (BT-81) dynamically selectable --- invoice_logic.py | 23 ++++++++++++++++------- main.py | 13 +++++++++++++ tests/test_invoice_logic.py | 36 ++++++++++++++++++++++++++++++++++++ 3 files changed, 65 insertions(+), 7 deletions(-) diff --git a/invoice_logic.py b/invoice_logic.py index ed06ba4..ca5aee2 100644 --- a/invoice_logic.py +++ b/invoice_logic.py @@ -58,6 +58,16 @@ VERKAEUFER = { "register_id": "HRB 6058 NP", # BT-30 (Handelsregisternummer, NICHT die USt-ID) } +# UN/EDIFACT 4461 Zahlungsmittelcodes (BT-81), Auswahl der fuer B2B/B2G relevanten Codes. +# "58" bleibt der Standardwert (SEPA-Ueberweisung), da alle bisherigen Rechnungen diesen nutzen. +PAYMENT_MEANS_CODES = { + "58": "SEPA-Überweisung", + "30": "Überweisung (allgemein)", + "31": "SEPA-Lastschrift", + "42": "Einzahlung auf Bankkonto", + "48": "Bankkarte (Kredit-/Debitkarte)", +} + def berechne_faelligkeit(invoice_date_str, term_days): """Fälligkeitsdatum = Rechnungsdatum + term_days. Gibt 'YYYY-MM-DD' zurück, oder leer, @@ -287,9 +297,9 @@ def generate_compliant_xrechnung(data): ET.SubElement(delivery, f"{{{CBC}}}ActualDeliveryDate").text = parse_to_strict_xml_date(data["delivery_date"]) payment_means = ET.SubElement(root, f"{{{CAC}}}PaymentMeans") - # BT-81 Zahlungsart: Code 58 = SEPA-Überweisung (statt 30, das für Überweisungen - # außerhalb des SEPA-Raums gedacht ist - VD-Valitool-169-Hinweis). - ET.SubElement(payment_means, f"{{{CBC}}}PaymentMeansCode").text = "58" + # BT-81 Zahlungsart: Standard bleibt 58 (SEPA-Ueberweisung), aber ueber + # PAYMENT_MEANS_CODES pro Rechnung waehlbar (siehe UN/EDIFACT 4461). + ET.SubElement(payment_means, f"{{{CBC}}}PaymentMeansCode").text = str(data.get("payment_means_code", "58")) payee_acc = ET.SubElement(payment_means, f"{{{CAC}}}PayeeFinancialAccount") ET.SubElement(payee_acc, f"{{{CBC}}}ID").text = str(data["iban"]).replace(" ", "") @@ -506,10 +516,9 @@ def generate_compliant_zugferd_cii(data): ET.SubElement(header_settle, f"{{{RAM}}}InvoiceCurrencyCode").text = "EUR" pay_means = ET.SubElement(header_settle, f"{{{RAM}}}SpecifiedTradeSettlementPaymentMeans") - # BT-81 Zahlungsart: Code 58 = SEPA-Überweisung. Code 30 ist für Überweisungen AUSSERHALB - # des SEPA-Raums gedacht (VD-Valitool-169-Hinweis) - da hier immer eine IBAN verwendet wird, - # ist 58 der korrekte Code. - ET.SubElement(pay_means, f"{{{RAM}}}TypeCode").text = "58" + # BT-81 Zahlungsart: Standard bleibt 58 (SEPA-Ueberweisung), aber ueber + # PAYMENT_MEANS_CODES pro Rechnung waehlbar (siehe UN/EDIFACT 4461). + ET.SubElement(pay_means, f"{{{RAM}}}TypeCode").text = str(data.get("payment_means_code", "58")) payee_acc = ET.SubElement(pay_means, f"{{{RAM}}}PayeePartyCreditorFinancialAccount") ET.SubElement(payee_acc, f"{{{RAM}}}IBANID").text = str(data["iban"]).replace(" ", "") diff --git a/main.py b/main.py index baad7f9..6a3f17f 100644 --- a/main.py +++ b/main.py @@ -11,6 +11,7 @@ from invoice_logic import ( InvoiceMetadata, VERKAEUFER, VERKAEUFER_IBAN, + PAYMENT_MEANS_CODES, berechne_faelligkeit, kaufmaennisches_runden, clean_german_float, @@ -252,6 +253,17 @@ with col2: r_zip = st.text_input("PLZ (BT-53)", value=data.get("recipient_zip", "")) r_city = st.text_input("Stadt (BT-52) :red[*]", value=data.get("recipient_city", "")) + st.markdown("---") + + payment_means_codes_list = list(PAYMENT_MEANS_CODES.keys()) + payment_means_labels = [f"{code} - {PAYMENT_MEANS_CODES[code]}" for code in payment_means_codes_list] + selected_payment_means_label = st.selectbox( + "Zahlungsmittel (BT-81)", + options=payment_means_labels, + index=payment_means_codes_list.index("58"), + ) + selected_payment_means_code = selected_payment_means_label.split(" - ")[0] + st.markdown("### 3. Positionen") if st.button("➕ Neue Position hinzufügen"): @@ -335,6 +347,7 @@ with col2: "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": VERKAEUFER_IBAN, + "payment_means_code": selected_payment_means_code, "delivery_note_id": dn_id, "order_id": o_id, "contract_id": c_id, diff --git a/tests/test_invoice_logic.py b/tests/test_invoice_logic.py index 1e95427..2b9b887 100644 --- a/tests/test_invoice_logic.py +++ b/tests/test_invoice_logic.py @@ -11,6 +11,7 @@ from invoice_logic import ( generate_compliant_xrechnung, generate_compliant_zugferd_cii, VERKAEUFER_IBAN, + PAYMENT_MEANS_CODES, ) @@ -94,3 +95,38 @@ def test_map_unece_unit_code_hours(): def test_parse_to_strict_xml_date_german_format(): assert parse_to_strict_xml_date("15.01.2026") == "2026-01-15" + + +def test_payment_means_codes_contains_expected_entries(): + assert PAYMENT_MEANS_CODES["58"] == "SEPA-Überweisung" + assert set(["30", "31", "42", "48", "58"]).issubset(PAYMENT_MEANS_CODES.keys()) + + +def test_generate_compliant_xrechnung_uses_dynamic_payment_means_code(): + data = _single_rate_invoice() + data["payment_means_code"] = "31" + xml_str = generate_compliant_xrechnung(data) + root = ET.fromstring(xml_str.encode("utf-8")) + ns = {"cbc": "urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2", + "cac": "urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2"} + code = root.find(".//cac:PaymentMeans/cbc:PaymentMeansCode", ns).text + assert code == "31" + + +def test_generate_compliant_xrechnung_defaults_payment_means_code_to_58(): + xml_str = generate_compliant_xrechnung(_single_rate_invoice()) + root = ET.fromstring(xml_str.encode("utf-8")) + ns = {"cbc": "urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2", + "cac": "urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2"} + code = root.find(".//cac:PaymentMeans/cbc:PaymentMeansCode", ns).text + assert code == "58" + + +def test_generate_compliant_zugferd_cii_uses_dynamic_payment_means_code(): + data = _single_rate_invoice() + data["payment_means_code"] = "48" + xml_str = generate_compliant_zugferd_cii(data) + root = ET.fromstring(xml_str.encode("utf-8")) + ns = {"ram": "urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100"} + code = root.find(".//ram:SpecifiedTradeSettlementPaymentMeans/ram:TypeCode", ns).text + assert code == "48"