From 4a8ec11d3e7344d68b9e481b2fb89302b44897e8 Mon Sep 17 00:00:00 2001 From: Marc-Constantin Enke Date: Mon, 2 Mar 2026 14:13:52 +0100 Subject: [PATCH 01/14] feat(Expense Claim): create sepa payment --- banking/custom/expense_claim.py | 132 ++++++++++++++++++++++++++++++++ banking/hooks.py | 19 ++++- 2 files changed, 150 insertions(+), 1 deletion(-) create mode 100644 banking/custom/expense_claim.py diff --git a/banking/custom/expense_claim.py b/banking/custom/expense_claim.py new file mode 100644 index 00000000..980a6b28 --- /dev/null +++ b/banking/custom/expense_claim.py @@ -0,0 +1,132 @@ +from datetime import date +from typing import TYPE_CHECKING + +import frappe +from frappe.model.mapper import get_mapped_doc +from frappe.utils import flt +from frappe.utils.data import getdate + +from banking.ebics.doctype.sepa_payment_order.sepa_payment_order import PaymentOrderStatus + +if TYPE_CHECKING: + from hrms.hr.doctype.expense_claim.expense_claim import ExpenseClaim + from hrms.hr.doctype.expense_claim_detail.expense_claim_detail import ExpenseClaimDetail + + from banking.ebics.doctype.sepa_payment.sepa_payment import SEPAPayment + + +@frappe.whitelist() +def make_sepa_payment_order(source_name: str, target_doc=None): + def set_missing_values(source, target): + if not target.bank_account: + bank_account = frappe.db.get_value( + "Bank Account", + {"is_company_account": 1, "company": source.company, "disabled": 0}, + ["name", "iban", "bank"], + order_by="is_default DESC", + as_dict=True, + ) + if bank_account: + target.bank_account = bank_account.get("name") + target.iban = bank_account.get("iban") + target.bank = bank_account.get("bank") + + def process_payment( + source: "ExpenseClaimDetail", target: "SEPAPayment", source_parent: "ExpenseClaim" + ): + claim = source_parent + target.recipient = claim.employee_name + target.purpose = _get_employee_purpose(claim) + + bank_account = _get_recipients_bank_account(claim) + if not bank_account or not bank_account.get("iban"): + frappe.throw( + frappe._("No Bank Account with IBAN found for Employee {0}.").format(claim.employee) + ) + if bank_account: + if bank_account.get("bank"): + swift_number, bank_name = frappe.db.get_value( + "Bank", bank_account["bank"], ["swift_number", "bank_name"] + ) + target.swift_number = swift_number + target.bank_name = bank_name + target.iban = bank_account.get("iban") + + target.currency = frappe.db.get_value("Company", claim.company, "default_currency") + target.eref = target.reference_name + target.amount = get_sepa_payment_amount( + claim, + method="get_mapped_doc", + reference_row_name=source.name, + execution_date=getdate(), + ) + + return get_mapped_doc( + "Expense Claim", + source_name, + { + "Expense Claim": { + "doctype": "SEPA Payment Order", + "validation": { + "docstatus": ("=", 1), + "approval_status": ("=", "Approved"), + "status": ("!=", "Paid"), + }, + }, + "Expense Claim Detail": { + "doctype": "SEPA Payment", + "field_map": { + "name": "reference_row_name", + "parent": "reference_name", + "parenttype": "reference_doctype", + }, + "condition": lambda row: row.idx == 1, + "postprocess": process_payment, + }, + }, + target_doc, + postprocess=set_missing_values, + ) + + +def _get_employee_purpose(claim: "ExpenseClaim"): + """Return the bank transfer purpose for an expense claim reimbursement. + + Example: "EC-00001, 2025-03-01" + """ + reference = ", ".join( + str(ref).strip() + for ref in [claim.name, claim.posting_date] + if ref + ) + return reference.strip() + + +def _get_recipients_bank_account(claim: "ExpenseClaim"): + return frappe.db.get_value( + "Bank Account", + {"party_type": "Employee", "party": claim.employee, "disabled": 0}, + ["iban", "bank"], + order_by="is_default DESC", + as_dict=True, + ) + + +@frappe.whitelist() +def make_bulk_sepa_payment_order(source_names: str): + target_doc = None + for source_name in frappe.parse_json(source_names): + if not isinstance(source_name, str): + raise TypeError + + target_doc = make_sepa_payment_order(source_name, target_doc) + + return target_doc + +def get_sepa_payment_amount( + doc: "ExpenseClaim", method: str, reference_row_name: str, execution_date: date +) -> float: + """Return outstanding amount (grand_total minus reimbursed). No discount logic.""" + precision = doc.precision("grand_total") + outstanding = flt(doc.grand_total, precision) - flt(doc.total_amount_reimbursed, precision) + return max(0, outstanding) diff --git a/banking/hooks.py b/banking/hooks.py index 72d299c1..fb4fe47b 100644 --- a/banking/hooks.py +++ b/banking/hooks.py @@ -30,12 +30,16 @@ # include js in doctype views doctype_js = { "Bank": "custom/bank.js", + "Expense Claim": "custom/expense_claim.js", "Purchase Invoice": "custom/purchase_invoice.js", "Employee": "custom/employee.js", "Supplier": "custom/supplier.js", "Bank Reconciliation Tool": "custom/bank_reconciliation_tool.js", } -doctype_list_js = {"Purchase Invoice": "custom/purchase_invoice_list.js"} +doctype_list_js = { + "Expense Claim": "custom/expense_claim_list.js", + "Purchase Invoice": "custom/purchase_invoice_list.js", +} # doctype_tree_js = {"doctype" : "public/js/doctype_tree.js"} # doctype_calendar_js = {"doctype" : "public/js/doctype_calendar.js"} @@ -115,6 +119,9 @@ "Employee": { "validate": "banking.custom.employee.validate", }, + "Expense Claim": { + "get_sepa_payment_amount": "banking.custom.expense_claim.get_sepa_payment_amount", + }, "Purchase Invoice": { "sepa_payment_order_status_changed": "banking.custom.purchase_invoice.sepa_payment_order_status_changed", "get_sepa_payment_amount": "banking.custom.purchase_invoice.get_sepa_payment_amount", @@ -236,6 +243,16 @@ get_payment_entries = "banking.klarna_kosma_integration.doctype.bank_reconciliation_tool_beta.unpaid_vouchers.get_payment_entries" alyf_banking_custom_records = [ + { + "doctype": "DocType Link", + "parent": "Expense Claim", + "parentfield": "links", + "parenttype": "Customize Form", + "group": "Payment", + "link_doctype": "SEPA Payment Order", + "link_fieldname": "reference_name", + "custom": 1, + }, { "doctype": "DocType Link", "parent": "Purchase Invoice", From ba5350fdc4b3933c7e3eb557859112600a977353 Mon Sep 17 00:00:00 2001 From: Marc-Constantin Enke Date: Mon, 2 Mar 2026 14:14:39 +0100 Subject: [PATCH 02/14] feat(Expense Claim): create bulk sepa from list view --- banking/custom/expense_claim_list.js | 52 ++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 banking/custom/expense_claim_list.js diff --git a/banking/custom/expense_claim_list.js b/banking/custom/expense_claim_list.js new file mode 100644 index 00000000..03c6b22b --- /dev/null +++ b/banking/custom/expense_claim_list.js @@ -0,0 +1,52 @@ +const old_onload = frappe.listview_settings["Expense Claim"].onload; +const old_add_fields = frappe.listview_settings["Expense Claim"].add_fields || []; +frappe.listview_settings["Expense Claim"].add_fields = [ + ...old_add_fields, + "approval_status", +]; + +frappe.listview_settings["Expense Claim"].onload = function (listview) { + if (old_onload) old_onload(listview); + + if (frappe.model.can_create("SEPA Payment Order")) { + listview.page.add_action_item(__("SEPA Payment Order"), () => { + const claims_to_pay = listview + .get_checked_items() + .filter( + (item) => + item.status !== "Paid" && + item.docstatus === 1 && + item.approval_status === "Approved" + ) + .map((item) => item.name); + if (!claims_to_pay.length) { + frappe.msgprint( + __( + "Only submitted, approved and unpaid Expense Claims can be used. Rejected or already paid claims were ignored." + ), + __("SEPA Payment Order") + ); + return; + } + frappe.call({ + type: "POST", + method: "banking.custom.expense_claim.make_bulk_sepa_payment_order", + args: { + source_names: claims_to_pay, + }, + freeze: true, + freeze_message: __("Creating SEPA Payment Order ..."), + callback: function (r) { + if (!r.exc && r.message) { + frappe.model.sync(r.message); + frappe.get_doc( + r.message.doctype, + r.message.name + ).__run_link_triggers = true; + frappe.set_route("Form", r.message.doctype, r.message.name); + } + }, + }); + }); + } +}; From 7ff8c8f7345fef0b08a97d3af2adcfcaf98c3597 Mon Sep 17 00:00:00 2001 From: Marc-Constantin Enke Date: Mon, 2 Mar 2026 14:14:59 +0100 Subject: [PATCH 03/14] feat(Expense Claim): add button for creating sepa payment --- banking/custom/expense_claim.js | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 banking/custom/expense_claim.js diff --git a/banking/custom/expense_claim.js b/banking/custom/expense_claim.js new file mode 100644 index 00000000..e90e9da0 --- /dev/null +++ b/banking/custom/expense_claim.js @@ -0,0 +1,32 @@ +frappe.ui.form.on("Expense Claim", { + setup(frm) { + frm.make_methods = frm.make_methods || {}; + frm.make_methods["SEPA Payment Order"] = () => { + frm.trigger("make_sepa_payment_order"); + }; + }, + + refresh(frm) { + const has_outstanding = + frm.doc.status !== "Paid" && + frm.doc.docstatus === 1 && + frm.doc.approval_status === "Approved" && + flt(frm.doc.grand_total) - flt(frm.doc.total_amount_reimbursed) > 0; + + if (has_outstanding) { + frm.add_custom_button( + __("SEPA Payment Order"), + () => frm.trigger("make_sepa_payment_order"), + __("Create"), + ); + } + }, + + make_sepa_payment_order(frm) { + frappe.model.open_mapped_doc({ + method: "banking.custom.expense_claim.make_sepa_payment_order", + frm: frm, + freeze_message: __("Creating SEPA Payment Order ..."), + }); + }, +}); From 96f27685c3620e1391888510d67b87a4caed953b Mon Sep 17 00:00:00 2001 From: Marc-Constantin Enke Date: Mon, 2 Mar 2026 15:03:24 +0100 Subject: [PATCH 04/14] style: formatting --- banking/custom/expense_claim.js | 2 +- banking/custom/expense_claim.py | 17 ++++------------- banking/custom/expense_claim_list.js | 3 ++- 3 files changed, 7 insertions(+), 15 deletions(-) diff --git a/banking/custom/expense_claim.js b/banking/custom/expense_claim.js index e90e9da0..7a9f0760 100644 --- a/banking/custom/expense_claim.js +++ b/banking/custom/expense_claim.js @@ -17,7 +17,7 @@ frappe.ui.form.on("Expense Claim", { frm.add_custom_button( __("SEPA Payment Order"), () => frm.trigger("make_sepa_payment_order"), - __("Create"), + __("Create") ); } }, diff --git a/banking/custom/expense_claim.py b/banking/custom/expense_claim.py index 980a6b28..e55646fa 100644 --- a/banking/custom/expense_claim.py +++ b/banking/custom/expense_claim.py @@ -6,8 +6,6 @@ from frappe.utils import flt from frappe.utils.data import getdate -from banking.ebics.doctype.sepa_payment_order.sepa_payment_order import PaymentOrderStatus - if TYPE_CHECKING: from hrms.hr.doctype.expense_claim.expense_claim import ExpenseClaim from hrms.hr.doctype.expense_claim_detail.expense_claim_detail import ExpenseClaimDetail @@ -31,18 +29,14 @@ def set_missing_values(source, target): target.iban = bank_account.get("iban") target.bank = bank_account.get("bank") - def process_payment( - source: "ExpenseClaimDetail", target: "SEPAPayment", source_parent: "ExpenseClaim" - ): + def process_payment(source: "ExpenseClaimDetail", target: "SEPAPayment", source_parent: "ExpenseClaim"): claim = source_parent target.recipient = claim.employee_name target.purpose = _get_employee_purpose(claim) bank_account = _get_recipients_bank_account(claim) if not bank_account or not bank_account.get("iban"): - frappe.throw( - frappe._("No Bank Account with IBAN found for Employee {0}.").format(claim.employee) - ) + frappe.throw(frappe._("No Bank Account with IBAN found for Employee {0}.").format(claim.employee)) if bank_account: if bank_account.get("bank"): swift_number, bank_name = frappe.db.get_value( @@ -94,11 +88,7 @@ def _get_employee_purpose(claim: "ExpenseClaim"): Example: "EC-00001, 2025-03-01" """ - reference = ", ".join( - str(ref).strip() - for ref in [claim.name, claim.posting_date] - if ref - ) + reference = ", ".join(str(ref).strip() for ref in [claim.name, claim.posting_date] if ref) return reference.strip() @@ -123,6 +113,7 @@ def make_bulk_sepa_payment_order(source_names: str): return target_doc + def get_sepa_payment_amount( doc: "ExpenseClaim", method: str, reference_row_name: str, execution_date: date ) -> float: diff --git a/banking/custom/expense_claim_list.js b/banking/custom/expense_claim_list.js index 03c6b22b..f8641bf7 100644 --- a/banking/custom/expense_claim_list.js +++ b/banking/custom/expense_claim_list.js @@ -1,5 +1,6 @@ const old_onload = frappe.listview_settings["Expense Claim"].onload; -const old_add_fields = frappe.listview_settings["Expense Claim"].add_fields || []; +const old_add_fields = + frappe.listview_settings["Expense Claim"].add_fields || []; frappe.listview_settings["Expense Claim"].add_fields = [ ...old_add_fields, "approval_status", From 2a80b5491e9937f2bc3a5d4ef12b1663ffbd164b Mon Sep 17 00:00:00 2001 From: Marc-Constantin Enke Date: Wed, 4 Mar 2026 10:20:09 +0100 Subject: [PATCH 05/14] feat(Expense Claim): add custom field and hook for sepa status --- banking/custom_fields.py | 12 ++++++++++++ banking/hooks.py | 1 + banking/patches.txt | 2 +- 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/banking/custom_fields.py b/banking/custom_fields.py index 841b5daa..dd001b7b 100644 --- a/banking/custom_fields.py +++ b/banking/custom_fields.py @@ -66,6 +66,18 @@ def get_custom_fields(): depends_on="eval:doc.docstatus === 0 && doc.business_trip_employee && !doc.employee_bank_account && doc.pay_to_employee && frappe.model.can_create('Bank Account')", ), ], + "Expense Claim": [ + dict( + fieldname="sepa_payment_order_status", + label=_("SEPA Payment Order Status"), + fieldtype="Select", + options="\n".join(PaymentOrderStatus), + insert_after="payable_account", + no_copy=1, + read_only=1, + allow_on_submit=1, + ), + ], "Payment Schedule": [ dict( fieldname="sepa_payment_order_status", diff --git a/banking/hooks.py b/banking/hooks.py index fb4fe47b..6c2ee4c9 100644 --- a/banking/hooks.py +++ b/banking/hooks.py @@ -120,6 +120,7 @@ "validate": "banking.custom.employee.validate", }, "Expense Claim": { + "sepa_payment_order_status_changed": "banking.custom.expense_claim.sepa_payment_order_status_changed", "get_sepa_payment_amount": "banking.custom.expense_claim.get_sepa_payment_amount", }, "Purchase Invoice": { diff --git a/banking/patches.txt b/banking/patches.txt index 15a2faee..aaebaaab 100644 --- a/banking/patches.txt +++ b/banking/patches.txt @@ -1,5 +1,5 @@ [pre_model_sync] -banking.patches.recreate_custom_fields # 2025-10-23 +banking.patches.recreate_custom_fields # 2026-03-04 banking.patches.remove_spaces_from_iban [post_model_sync] From 9027a5a4902ab65f1e69c4d59c68844dd1d18c36 Mon Sep 17 00:00:00 2001 From: Marc-Constantin Enke Date: Wed, 4 Mar 2026 10:21:12 +0100 Subject: [PATCH 06/14] feat(Expense Claim): add sepa payment order status change --- banking/custom/expense_claim.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/banking/custom/expense_claim.py b/banking/custom/expense_claim.py index e55646fa..31d39bca 100644 --- a/banking/custom/expense_claim.py +++ b/banking/custom/expense_claim.py @@ -6,6 +6,8 @@ from frappe.utils import flt from frappe.utils.data import getdate +from banking.ebics.doctype.sepa_payment_order.sepa_payment_order import PaymentOrderStatus + if TYPE_CHECKING: from hrms.hr.doctype.expense_claim.expense_claim import ExpenseClaim from hrms.hr.doctype.expense_claim_detail.expense_claim_detail import ExpenseClaimDetail @@ -114,10 +116,18 @@ def make_bulk_sepa_payment_order(source_names: str): return target_doc +def sepa_payment_order_status_changed( + doc: "ExpenseClaim", method: str, reference_row_name: str, status: PaymentOrderStatus +): + """Called via hooks when a linked SEPA Payment Order changes.""" + doc.sepa_payment_order_status = status.value + doc.save(ignore_permissions=True) + + def get_sepa_payment_amount( doc: "ExpenseClaim", method: str, reference_row_name: str, execution_date: date ) -> float: - """Return outstanding amount (grand_total minus reimbursed). No discount logic.""" + """Return outstanding amount. No discount logic.""" precision = doc.precision("grand_total") outstanding = flt(doc.grand_total, precision) - flt(doc.total_amount_reimbursed, precision) return max(0, outstanding) From 5a5bdc4a874a2421c431789ebc9b74b1707fe353 Mon Sep 17 00:00:00 2001 From: Marc-Constantin Enke Date: Wed, 4 Mar 2026 10:21:23 +0100 Subject: [PATCH 07/14] feat: adjust filters for sepa status --- banking/custom/expense_claim.js | 1 + banking/custom/expense_claim_list.js | 6 ++++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/banking/custom/expense_claim.js b/banking/custom/expense_claim.js index 7a9f0760..59ae045e 100644 --- a/banking/custom/expense_claim.js +++ b/banking/custom/expense_claim.js @@ -11,6 +11,7 @@ frappe.ui.form.on("Expense Claim", { frm.doc.status !== "Paid" && frm.doc.docstatus === 1 && frm.doc.approval_status === "Approved" && + !frm.doc.sepa_payment_order_status && flt(frm.doc.grand_total) - flt(frm.doc.total_amount_reimbursed) > 0; if (has_outstanding) { diff --git a/banking/custom/expense_claim_list.js b/banking/custom/expense_claim_list.js index f8641bf7..29b07c6c 100644 --- a/banking/custom/expense_claim_list.js +++ b/banking/custom/expense_claim_list.js @@ -4,6 +4,7 @@ const old_add_fields = frappe.listview_settings["Expense Claim"].add_fields = [ ...old_add_fields, "approval_status", + "sepa_payment_order_status", ]; frappe.listview_settings["Expense Claim"].onload = function (listview) { @@ -17,13 +18,14 @@ frappe.listview_settings["Expense Claim"].onload = function (listview) { (item) => item.status !== "Paid" && item.docstatus === 1 && - item.approval_status === "Approved" + item.approval_status === "Approved" && + !item.sepa_payment_order_status ) .map((item) => item.name); if (!claims_to_pay.length) { frappe.msgprint( __( - "Only submitted, approved and unpaid Expense Claims can be used. Rejected or already paid claims were ignored." + "Only submitted, approved and unpaid Expense Claims without an existing SEPA Payment Order can be used. Rejected, paid or already linked claims were ignored." ), __("SEPA Payment Order") ); From e47420c5d1c56cb3c451d0a2d353dde8a532ae6c Mon Sep 17 00:00:00 2001 From: Marc <147735520+MarcCon@users.noreply.github.com> Date: Mon, 16 Mar 2026 10:22:40 +0100 Subject: [PATCH 08/14] style: linter fix Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com> --- banking/custom/expense_claim.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/banking/custom/expense_claim.py b/banking/custom/expense_claim.py index 31d39bca..e1d0dfc2 100644 --- a/banking/custom/expense_claim.py +++ b/banking/custom/expense_claim.py @@ -16,7 +16,7 @@ @frappe.whitelist() -def make_sepa_payment_order(source_name: str, target_doc=None): +def make_sepa_payment_order(source_name: str, target_doc: str | Document | None = None): def set_missing_values(source, target): if not target.bank_account: bank_account = frappe.db.get_value( From 56bc3cad86bb343f4aef58fe2d7f0a3372c7d005 Mon Sep 17 00:00:00 2001 From: Marc-Constantin Enke Date: Mon, 16 Mar 2026 10:27:30 +0100 Subject: [PATCH 09/14] style: linter fix 2 --- banking/custom/expense_claim.py | 1 + 1 file changed, 1 insertion(+) diff --git a/banking/custom/expense_claim.py b/banking/custom/expense_claim.py index e1d0dfc2..0b629392 100644 --- a/banking/custom/expense_claim.py +++ b/banking/custom/expense_claim.py @@ -2,6 +2,7 @@ from typing import TYPE_CHECKING import frappe +from frappe.model.document import Document from frappe.model.mapper import get_mapped_doc from frappe.utils import flt from frappe.utils.data import getdate From 4d597fd64b551a1a49566efb3b974902ed09ef43 Mon Sep 17 00:00:00 2001 From: Marc-Constantin Enke Date: Fri, 27 Mar 2026 08:53:34 +0100 Subject: [PATCH 10/14] fix: call patch --- banking/patches.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/banking/patches.txt b/banking/patches.txt index aaebaaab..e9170634 100644 --- a/banking/patches.txt +++ b/banking/patches.txt @@ -8,7 +8,7 @@ execute:frappe.delete_doc_if_exists("DocType", "Bank Consent") execute:frappe.delete_doc_if_exists("DocType", "Klarna Kosma Session") execute:frappe.delete_doc_if_exists("Custom Field", "Bank Account-kosma_account_id") execute:frappe.delete_doc_if_exists("Custom Field", "Bank Transaction-kosma_party_name") -execute:from banking.install import insert_custom_records; insert_custom_records() +execute:from banking.install import insert_custom_records; insert_custom_records() #2026-03-27 banking.patches.set_voucher_matching_defaults banking.patches.download_batch_transactions execute:from banking.install import make_property_setters; make_property_setters() From 3727e5da0dd5becf270bd03ec69ce9466fd7155f Mon Sep 17 00:00:00 2001 From: Marc-Constantin Enke Date: Fri, 27 Mar 2026 08:54:50 +0100 Subject: [PATCH 11/14] fix(Expense Claim): get iban from employee --- banking/custom/expense_claim.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/banking/custom/expense_claim.py b/banking/custom/expense_claim.py index 0b629392..3a0a16e2 100644 --- a/banking/custom/expense_claim.py +++ b/banking/custom/expense_claim.py @@ -2,6 +2,7 @@ from typing import TYPE_CHECKING import frappe +from frappe import _ from frappe.model.document import Document from frappe.model.mapper import get_mapped_doc from frappe.utils import flt @@ -38,16 +39,21 @@ def process_payment(source: "ExpenseClaimDetail", target: "SEPAPayment", source_ target.purpose = _get_employee_purpose(claim) bank_account = _get_recipients_bank_account(claim) - if not bank_account or not bank_account.get("iban"): - frappe.throw(frappe._("No Bank Account with IBAN found for Employee {0}.").format(claim.employee)) - if bank_account: + + if bank_account and bank_account.get("iban"): + target.iban = bank_account.get("iban") if bank_account.get("bank"): swift_number, bank_name = frappe.db.get_value( "Bank", bank_account["bank"], ["swift_number", "bank_name"] ) target.swift_number = swift_number target.bank_name = bank_name - target.iban = bank_account.get("iban") + else: + employee_iban = frappe.db.get_value("Employee", claim.employee, "iban") + if employee_iban: + target.iban = employee_iban + else: + frappe.throw(_("No IBAN found for Employee {0}.").format(claim.employee)) target.currency = frappe.db.get_value("Company", claim.company, "default_currency") target.eref = target.reference_name From 1bd4270593436932eaa624e7094b5f89f46f0710 Mon Sep 17 00:00:00 2001 From: Marc-Constantin Enke Date: Thu, 2 Apr 2026 12:38:09 +0200 Subject: [PATCH 12/14] fix(Expense Claim): prevent crash when hrms is absent --- banking/custom/expense_claim_list.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/banking/custom/expense_claim_list.js b/banking/custom/expense_claim_list.js index 29b07c6c..77c94ef5 100644 --- a/banking/custom/expense_claim_list.js +++ b/banking/custom/expense_claim_list.js @@ -1,3 +1,5 @@ +frappe.listview_settings["Expense Claim"] = + frappe.listview_settings["Expense Claim"] || {}; const old_onload = frappe.listview_settings["Expense Claim"].onload; const old_add_fields = frappe.listview_settings["Expense Claim"].add_fields || []; From 82d175f96dccc745837334e07f84ef94b0dc2c6e Mon Sep 17 00:00:00 2001 From: Marc-Constantin Enke Date: Thu, 2 Apr 2026 13:02:05 +0200 Subject: [PATCH 13/14] fix(Expense Claim): prevent duplicate SEPA Payment Order creation --- banking/custom/expense_claim.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/banking/custom/expense_claim.py b/banking/custom/expense_claim.py index 3a0a16e2..1c6ef0b8 100644 --- a/banking/custom/expense_claim.py +++ b/banking/custom/expense_claim.py @@ -19,6 +19,9 @@ @frappe.whitelist() def make_sepa_payment_order(source_name: str, target_doc: str | Document | None = None): + if frappe.db.get_value("Expense Claim", source_name, "sepa_payment_order_status"): + frappe.throw(_("A SEPA Payment Order already exists for Expense Claim {0}.").format(source_name)) + def set_missing_values(source, target): if not target.bank_account: bank_account = frappe.db.get_value( From c9386362f57bdd8333c71bb30b72abe410cb9769 Mon Sep 17 00:00:00 2001 From: Marc-Constantin Enke Date: Thu, 2 Apr 2026 13:24:26 +0200 Subject: [PATCH 14/14] fix(Expense Claim): prevent payment order with amount 0 --- banking/custom/expense_claim.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/banking/custom/expense_claim.py b/banking/custom/expense_claim.py index 1c6ef0b8..74bfb950 100644 --- a/banking/custom/expense_claim.py +++ b/banking/custom/expense_claim.py @@ -86,7 +86,8 @@ def process_payment(source: "ExpenseClaimDetail", target: "SEPAPayment", source_ "parent": "reference_name", "parenttype": "reference_doctype", }, - "condition": lambda row: row.idx == 1, + "condition": lambda row: row.idx == 1 + and flt(row.parent_doc.grand_total) - flt(row.parent_doc.total_amount_reimbursed) > 0.0, "postprocess": process_payment, }, },