From df015c9f97e782f379d03197ef6103259cecfaa9 Mon Sep 17 00:00:00 2001 From: dfuhrmann3 Date: Thu, 30 Jul 2026 12:01:10 +0200 Subject: [PATCH] =?UTF-8?q?Zahlungsziel=20als=20Klartext=20(BT-20)=20und?= =?UTF-8?q?=20Zahlungsreferenz=20(BT-83)=20erg=C3=A4nzt.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BT-20: Das Zahlungsziel erscheint zusätzlich zum strukturierten Fälligkeits- datum im Klartext ("Zahlbar innerhalb von 14 Tagen ohne Abzug."). Bei Vorkasse bewusst unterdrückt, da sich beide Angaben widersprechen würden. BT-83: Die Rechnungsnummer als Verwendungszweck für den automatischen Zahlungsabgleich - cbc:PaymentID (UBL) bzw. ram:PaymentReference (CII), jeweils an der schema-korrekten Position der Sequenz. Beides ergab der Vergleich gegen fünf Eingangs-E-Rechnungen: BT-20 lieferten vier von fünf Lieferanten, BT-83 zwei von fünf. Co-Authored-By: Claude Opus 5 --- invoice_logic.py | 39 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 36 insertions(+), 3 deletions(-) diff --git a/invoice_logic.py b/invoice_logic.py index b4fb2a5..39c1a49 100644 --- a/invoice_logic.py +++ b/invoice_logic.py @@ -157,13 +157,36 @@ def format_skonto_text(skonto_percent, skonto_days, invoice_date_str): return f"Bei Zahlung innerhalb von {days} Tagen gewähren wir {percent_str}% Skonto." +def format_payment_term_text(payment_term_days): + """Klartext des Zahlungsziels fuer BT-20 ('14' -> 'Zahlbar innerhalb von 14 Tagen ohne + Abzug.'). Das Faelligkeitsdatum steht strukturiert in BT-9; der Klartext daneben ist bei + Lieferanten die Regel (4 von 5 Eingangsrechnungen liefern ihn) und fuer den Empfaenger + lesbar. Gibt "" zurueck, wenn kein gueltiges Zahlungsziel angegeben ist - es wird NICHTS + geraten.""" + try: + days = int(str(payment_term_days).strip()) + except (TypeError, ValueError): + return "" + if days <= 0: + return "" + return f"Zahlbar innerhalb von {days} Tagen ohne Abzug." + + def build_payment_terms_note(data): - """Kombinierter Freitext fuer die Zahlungsbedingungen (BT-20): optionaler Vorkasse-Hinweis - und/oder eine Skonto-Vereinbarung. Beides sind Zahlungsbedingungen, fuer die EN16931 kein - eigenes strukturiertes Feld kennt. Gibt "" zurueck, wenn keiner von beiden gesetzt ist.""" + """Kombinierter Freitext fuer die Zahlungsbedingungen (BT-20): das Zahlungsziel im Klartext + ODER ein Vorkasse-Hinweis, jeweils optional ergaenzt um eine Skonto-Vereinbarung. Fuer all + das kennt EN16931 kein eigenes strukturiertes Feld. Gibt "" zurueck, wenn nichts davon + gesetzt ist. + + Vorkasse und Zahlungsziel schliessen sich bewusst aus: 'Zahlung per Vorkasse vor Lieferung' + und 'zahlbar innerhalb von 14 Tagen' wuerden sich widersprechen.""" parts = [] if data.get("vorkasse"): parts.append(VORKASSE_HINWEIS) + else: + term_text = format_payment_term_text(data.get("payment_term_days")) + if term_text: + parts.append(term_text) skonto_text = format_skonto_text( data.get("skonto_percent"), data.get("skonto_days"), data["invoice_date"] ) @@ -1050,6 +1073,11 @@ def generate_compliant_xrechnung(data): # 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")) + # BT-83 Zahlungsreferenz (Verwendungszweck): die Rechnungsnummer, damit der Empfaenger die + # Zahlung automatisch zuordnen kann. Muss laut UBL-Sequenz NACH PaymentMeansCode und VOR + # PayeeFinancialAccount stehen. + if str(data.get("invoice_number") or "").strip(): + ET.SubElement(payment_means, f"{{{CBC}}}PaymentID").text = str(data["invoice_number"]).strip() payee_acc = ET.SubElement(payment_means, f"{{{CAC}}}PayeeFinancialAccount") ET.SubElement(payee_acc, f"{{{CBC}}}ID").text = str(data["iban"]).replace(" ", "") # BT-86 BIC der Verkaeuferbank (optional, aber von vielen Empfaengern erwartet). @@ -1351,6 +1379,11 @@ def generate_compliant_zugferd_cii(data): ET.SubElement(dn_ref, f"{{{RAM}}}IssuerAssignedID").text = str(data["delivery_note_id"]).strip() header_settle = ET.SubElement(trade_trans, f"{{{RAM}}}ApplicableHeaderTradeSettlement") + # BT-83 Zahlungsreferenz (Verwendungszweck): die Rechnungsnummer, damit der Empfaenger die + # Zahlung automatisch zuordnen kann. Muss laut CII-Sequenz VOR InvoiceCurrencyCode stehen + # (die Sequenz ist CreditorReferenceID -> PaymentReference -> ... -> InvoiceCurrencyCode). + if str(data.get("invoice_number") or "").strip(): + ET.SubElement(header_settle, f"{{{RAM}}}PaymentReference").text = str(data["invoice_number"]).strip() ET.SubElement(header_settle, f"{{{RAM}}}InvoiceCurrencyCode").text = "EUR" pay_means = ET.SubElement(header_settle, f"{{{RAM}}}SpecifiedTradeSettlementPaymentMeans")