feat: make payment means code (BT-81) dynamically selectable

This commit is contained in:
dfuhrmann3 2026-07-07 14:54:17 +02:00
parent ae986b4f3f
commit 6ecd59e5ea
3 changed files with 65 additions and 7 deletions

View File

@ -58,6 +58,16 @@ VERKAEUFER = {
"register_id": "HRB 6058 NP", # BT-30 (Handelsregisternummer, NICHT die USt-ID) "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): def berechne_faelligkeit(invoice_date_str, term_days):
"""Fälligkeitsdatum = Rechnungsdatum + term_days. Gibt 'YYYY-MM-DD' zurück, oder leer, """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"]) 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")
# BT-81 Zahlungsart: Code 58 = SEPA-Überweisung (statt 30, das für Überweisungen # BT-81 Zahlungsart: Standard bleibt 58 (SEPA-Ueberweisung), aber ueber
# außerhalb des SEPA-Raums gedacht ist - VD-Valitool-169-Hinweis). # PAYMENT_MEANS_CODES pro Rechnung waehlbar (siehe UN/EDIFACT 4461).
ET.SubElement(payment_means, f"{{{CBC}}}PaymentMeansCode").text = "58" ET.SubElement(payment_means, f"{{{CBC}}}PaymentMeansCode").text = str(data.get("payment_means_code", "58"))
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(" ", "") 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" ET.SubElement(header_settle, f"{{{RAM}}}InvoiceCurrencyCode").text = "EUR"
pay_means = ET.SubElement(header_settle, f"{{{RAM}}}SpecifiedTradeSettlementPaymentMeans") pay_means = ET.SubElement(header_settle, f"{{{RAM}}}SpecifiedTradeSettlementPaymentMeans")
# BT-81 Zahlungsart: Code 58 = SEPA-Überweisung. Code 30 ist für Überweisungen AUSSERHALB # BT-81 Zahlungsart: Standard bleibt 58 (SEPA-Ueberweisung), aber ueber
# des SEPA-Raums gedacht (VD-Valitool-169-Hinweis) - da hier immer eine IBAN verwendet wird, # PAYMENT_MEANS_CODES pro Rechnung waehlbar (siehe UN/EDIFACT 4461).
# ist 58 der korrekte Code. ET.SubElement(pay_means, f"{{{RAM}}}TypeCode").text = str(data.get("payment_means_code", "58"))
ET.SubElement(pay_means, f"{{{RAM}}}TypeCode").text = "58"
payee_acc = ET.SubElement(pay_means, f"{{{RAM}}}PayeePartyCreditorFinancialAccount") payee_acc = ET.SubElement(pay_means, f"{{{RAM}}}PayeePartyCreditorFinancialAccount")
ET.SubElement(payee_acc, f"{{{RAM}}}IBANID").text = str(data["iban"]).replace(" ", "") ET.SubElement(payee_acc, f"{{{RAM}}}IBANID").text = str(data["iban"]).replace(" ", "")

13
main.py
View File

@ -11,6 +11,7 @@ from invoice_logic import (
InvoiceMetadata, InvoiceMetadata,
VERKAEUFER, VERKAEUFER,
VERKAEUFER_IBAN, VERKAEUFER_IBAN,
PAYMENT_MEANS_CODES,
berechne_faelligkeit, berechne_faelligkeit,
kaufmaennisches_runden, kaufmaennisches_runden,
clean_german_float, clean_german_float,
@ -252,6 +253,17 @@ with col2:
r_zip = st.text_input("PLZ (BT-53)", value=data.get("recipient_zip", "")) 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", "")) 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") st.markdown("### 3. Positionen")
if st.button("➕ Neue Position hinzufügen"): 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, "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, "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, "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, "delivery_note_id": dn_id,
"order_id": o_id, "order_id": o_id,
"contract_id": c_id, "contract_id": c_id,

View File

@ -11,6 +11,7 @@ from invoice_logic import (
generate_compliant_xrechnung, generate_compliant_xrechnung,
generate_compliant_zugferd_cii, generate_compliant_zugferd_cii,
VERKAEUFER_IBAN, 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(): def test_parse_to_strict_xml_date_german_format():
assert parse_to_strict_xml_date("15.01.2026") == "2026-01-15" 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"