From 06ac632af118a94cbf6b73dcb2ff2c4a02c87de1 Mon Sep 17 00:00:00 2001
From: 0xD0M1M0 <76812428+0xD0M1M0@users.noreply.github.com>
Date: Tue, 16 Dec 2025 12:52:21 +0000
Subject: [PATCH 01/38] feat: bank reconciliation rule
---
banking/custom/bank_account.js | 42 +++
banking/custom_fields.py | 10 +
banking/hooks.py | 5 +
.../bank_reconciliation_rule/__init__.py | 0
.../bank_reconciliation_rule.js | 69 +++++
.../bank_reconciliation_rule.json | 135 ++++++++++
.../bank_reconciliation_rule.py | 18 ++
.../bank_reconciliation_rule_list.js | 23 ++
.../test_bank_reconciliation_rule.py | 9 +
banking/overrides/bank_account.py | 12 +
banking/overrides/bank_transaction.py | 249 ++++++++++++++++++
banking/patches.txt | 2 +-
12 files changed, 573 insertions(+), 1 deletion(-)
create mode 100644 banking/custom/bank_account.js
create mode 100644 banking/klarna_kosma_integration/doctype/bank_reconciliation_rule/__init__.py
create mode 100644 banking/klarna_kosma_integration/doctype/bank_reconciliation_rule/bank_reconciliation_rule.js
create mode 100644 banking/klarna_kosma_integration/doctype/bank_reconciliation_rule/bank_reconciliation_rule.json
create mode 100644 banking/klarna_kosma_integration/doctype/bank_reconciliation_rule/bank_reconciliation_rule.py
create mode 100644 banking/klarna_kosma_integration/doctype/bank_reconciliation_rule/bank_reconciliation_rule_list.js
create mode 100644 banking/klarna_kosma_integration/doctype/bank_reconciliation_rule/test_bank_reconciliation_rule.py
diff --git a/banking/custom/bank_account.js b/banking/custom/bank_account.js
new file mode 100644
index 00000000..c76b2c19
--- /dev/null
+++ b/banking/custom/bank_account.js
@@ -0,0 +1,42 @@
+// Copyright (c) 2025, ALYF GmbH and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on("Bank Account", {
+ refresh(frm) {
+ set_bank_fee_filter(frm);
+ },
+
+ account(frm) {
+ set_bank_fee_filter(frm);
+ },
+});
+
+function set_bank_fee_filter(frm) {
+ if (!frm.doc.account) return;
+
+ (async () => {
+ const {
+ message: { account_currency: currency },
+ } = await frappe.db.get_value(
+ "Account",
+ frm.doc.account,
+ "account_currency"
+ );
+
+ const {
+ message: { company: company },
+ } = await frappe.db.get_value(
+ "Bank Account",
+ frm.doc.bank_account,
+ "company"
+ );
+
+ frm.set_query("bank_fee_account", () => ({
+ filters: {
+ account_type: "Cost of Goods Sold",
+ account_currency: currency,
+ company: company,
+ },
+ }));
+ })();
+}
diff --git a/banking/custom_fields.py b/banking/custom_fields.py
index 841b5daa..a95c938a 100644
--- a/banking/custom_fields.py
+++ b/banking/custom_fields.py
@@ -87,4 +87,14 @@ def get_custom_fields():
insert_after="transaction_id",
),
],
+ "Bank Account": [
+ dict(
+ fieldname="bank_fee_account",
+ fieldtype="Link",
+ label="Bank Fee Account",
+ options="Account",
+ depends_on="is_company_account",
+ insert_after="account_subtype",
+ ),
+ ],
}
diff --git a/banking/hooks.py b/banking/hooks.py
index e111c869..00015f40 100644
--- a/banking/hooks.py
+++ b/banking/hooks.py
@@ -34,6 +34,7 @@
"Employee": "custom/employee.js",
"Supplier": "custom/supplier.js",
"Bank Reconciliation Tool": "custom/bank_reconciliation_tool.js",
+ "Bank Account": "custom/bank_account.js",
}
doctype_list_js = {"Purchase Invoice": "custom/purchase_invoice_list.js"}
# doctype_tree_js = {"doctype" : "public/js/doctype_tree.js"}
@@ -108,9 +109,13 @@
doc_events = {
"Bank Transaction": {
"on_update_after_submit": "banking.overrides.bank_transaction.on_update_after_submit",
+ "before_validate": "banking.overrides.bank_transaction.before_validate",
+ "before_submit": "banking.overrides.bank_transaction.before_submit",
+ "on_cancel": "banking.overrides.bank_transaction.on_cancel",
},
"Bank Account": {
"before_validate": "banking.overrides.bank_account.before_validate",
+ "validate": "banking.overrides.bank_account.validate",
},
"Employee": {
"validate": "banking.custom.employee.validate",
diff --git a/banking/klarna_kosma_integration/doctype/bank_reconciliation_rule/__init__.py b/banking/klarna_kosma_integration/doctype/bank_reconciliation_rule/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/banking/klarna_kosma_integration/doctype/bank_reconciliation_rule/bank_reconciliation_rule.js b/banking/klarna_kosma_integration/doctype/bank_reconciliation_rule/bank_reconciliation_rule.js
new file mode 100644
index 00000000..4bab987b
--- /dev/null
+++ b/banking/klarna_kosma_integration/doctype/bank_reconciliation_rule/bank_reconciliation_rule.js
@@ -0,0 +1,69 @@
+// Copyright (c) 2025, ALYF GmbH and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on("Bank Reconciliation Rule", {
+ onload: function (frm) {
+ render_bt_filters(frm);
+ },
+ refresh(frm) {
+ set_target_account_filter(frm);
+ },
+ bank_account(frm) {
+ set_target_account_filter(frm);
+ },
+});
+
+let render_bt_filters = function (frm) {
+ const parent = frm.fields_dict.filter_area.$wrapper;
+ parent.empty();
+
+ const filters =
+ frm.doc.filters && frm.doc.filters !== "[]"
+ ? JSON.parse(frm.doc.filters)
+ : [];
+
+ frappe.model.with_doctype("Bank Transaction", () => {
+ const filter_group = new frappe.ui.FilterGroup({
+ parent: parent,
+ doctype: "Bank Transaction",
+ on_change: () => {
+ frm.set_value("filters", JSON.stringify(filter_group.get_filters()));
+ },
+ });
+
+ filter_group.add_filters_to_filter_group(filters);
+ });
+};
+
+function set_target_account_filter(frm) {
+ if (!frm.doc.bank_account) return;
+
+ (async () => {
+ const {
+ message: { account },
+ } = await frappe.db.get_value(
+ "Bank Account",
+ frm.doc.bank_account,
+ "account"
+ );
+
+ const {
+ message: { account_currency: currency },
+ } = await frappe.db.get_value("Account", account, "account_currency");
+
+ const {
+ message: { company: company },
+ } = await frappe.db.get_value(
+ "Bank Account",
+ frm.doc.bank_account,
+ "company"
+ );
+
+ frm.set_query("target_account", () => ({
+ filters: {
+ account_currency: currency,
+ company: company,
+ },
+ }));
+ })();
+}
diff --git a/banking/klarna_kosma_integration/doctype/bank_reconciliation_rule/bank_reconciliation_rule.json b/banking/klarna_kosma_integration/doctype/bank_reconciliation_rule/bank_reconciliation_rule.json
new file mode 100644
index 00000000..36d8235e
--- /dev/null
+++ b/banking/klarna_kosma_integration/doctype/bank_reconciliation_rule/bank_reconciliation_rule.json
@@ -0,0 +1,135 @@
+{
+ "actions": [],
+ "creation": "2025-10-23 13:00:49.226504",
+ "doctype": "DocType",
+ "engine": "InnoDB",
+ "field_order": [
+ "account_settings_section",
+ "bank_account",
+ "target_account",
+ "disabled",
+ "filters_section",
+ "filter_description",
+ "filters",
+ "filter_area",
+ "amended_from"
+ ],
+ "fields": [
+ {
+ "fieldname": "bank_account",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Bank Account",
+ "link_filters": "[[\"Bank Account\",\"is_company_account\",\"=\",1]]",
+ "options": "Bank Account",
+ "reqd": 1
+ },
+ {
+ "fieldname": "account_settings_section",
+ "fieldtype": "Section Break",
+ "label": "Account Settings"
+ },
+ {
+ "fieldname": "target_account",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Target Account",
+ "options": "Account",
+ "reqd": 1
+ },
+ {
+ "fieldname": "filters_section",
+ "fieldtype": "Section Break",
+ "label": "Filters"
+ },
+ {
+ "fieldname": "filters",
+ "fieldtype": "Code",
+ "hidden": 1,
+ "label": "Filters",
+ "options": "JSON",
+ "read_only": 1
+ },
+ {
+ "fieldname": "filter_area",
+ "fieldtype": "HTML"
+ },
+ {
+ "fieldname": "filter_description",
+ "fieldtype": "HTML",
+ "options": "Automatically create a journal entry if the submitted document matches these filters.
Be careful: if the filter is not strict enough accounting errors will happen.
"
+ },
+ {
+ "fieldname": "amended_from",
+ "fieldtype": "Link",
+ "label": "Amended From",
+ "no_copy": 1,
+ "options": "Bank Reconciliation Rule",
+ "print_hide": 1,
+ "read_only": 1,
+ "search_index": 1
+ },
+ {
+ "allow_on_submit": 1,
+ "default": "0",
+ "fieldname": "disabled",
+ "fieldtype": "Check",
+ "label": "Disabled"
+ }
+ ],
+ "grid_page_length": 50,
+ "index_web_pages_for_search": 1,
+ "is_submittable": 1,
+ "links": [],
+ "modified": "2025-12-16 12:33:40.308798",
+ "modified_by": "Administrator",
+ "module": "Klarna Kosma Integration",
+ "name": "Bank Reconciliation Rule",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "amend": 1,
+ "cancel": 1,
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "System Manager",
+ "share": 1,
+ "submit": 1,
+ "write": 1
+ },
+ {
+ "cancel": 1,
+ "create": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Accounts Manager",
+ "share": 1,
+ "submit": 1,
+ "write": 1
+ },
+ {
+ "create": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Accounts User",
+ "share": 1,
+ "write": 1
+ }
+ ],
+ "row_format": "Dynamic",
+ "rows_threshold_for_grid_search": 20,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "states": []
+}
\ No newline at end of file
diff --git a/banking/klarna_kosma_integration/doctype/bank_reconciliation_rule/bank_reconciliation_rule.py b/banking/klarna_kosma_integration/doctype/bank_reconciliation_rule/bank_reconciliation_rule.py
new file mode 100644
index 00000000..dc837609
--- /dev/null
+++ b/banking/klarna_kosma_integration/doctype/bank_reconciliation_rule/bank_reconciliation_rule.py
@@ -0,0 +1,18 @@
+# Copyright (c) 2025, ALYF GmbH and contributors
+# For license information, please see license.txt
+
+import frappe
+from frappe import _
+from frappe.model.document import Document
+
+
+class BankReconciliationRule(Document):
+ def validate(self):
+ bank_account = frappe.db.get_value("Bank Account", self.bank_account, "account")
+ bank_account_currency = frappe.db.get_value("Account", bank_account, "account_currency")
+ target_account_currency = frappe.db.get_value("Account", self.target_account, "account_currency")
+ if bank_account_currency != target_account_currency:
+ frappe.throw(_("Bank Account and Target Account need to be in the same currency!"))
+ # self.filters is a code field with json, so it has in an emtpy stage "[]" inside
+ if self.filters is None or len(self.filters) <= 2:
+ frappe.throw(_("Please define at least one filter!"))
diff --git a/banking/klarna_kosma_integration/doctype/bank_reconciliation_rule/bank_reconciliation_rule_list.js b/banking/klarna_kosma_integration/doctype/bank_reconciliation_rule/bank_reconciliation_rule_list.js
new file mode 100644
index 00000000..f215c265
--- /dev/null
+++ b/banking/klarna_kosma_integration/doctype/bank_reconciliation_rule/bank_reconciliation_rule_list.js
@@ -0,0 +1,23 @@
+// Copyright (c) 2025, ALYF GmbH and contributors
+// For license information, please see license.txt
+
+frappe.listview_settings["Bank Reconciliation Rule"] = {
+ add_fields: ["docstatus", "disabled"],
+ has_indicator_for_draft: true,
+
+ get_indicator: function (doc) {
+ // Submitted documents
+ if (doc.docstatus === 1) {
+ if (doc.disabled === 1) {
+ return [__("Disabled"), "orange", "disabled,=,1"];
+ } else {
+ return [__("Active"), "green", "docstatus,=,1"];
+ }
+ }
+
+ // Draft documents
+ if (doc.docstatus === 0) {
+ return [__("Draft"), "blue", "docstatus,=,0"];
+ }
+ },
+};
diff --git a/banking/klarna_kosma_integration/doctype/bank_reconciliation_rule/test_bank_reconciliation_rule.py b/banking/klarna_kosma_integration/doctype/bank_reconciliation_rule/test_bank_reconciliation_rule.py
new file mode 100644
index 00000000..77dc6280
--- /dev/null
+++ b/banking/klarna_kosma_integration/doctype/bank_reconciliation_rule/test_bank_reconciliation_rule.py
@@ -0,0 +1,9 @@
+# Copyright (c) 2025, ALYF GmbH and Contributors
+# See license.txt
+
+# import frappe
+from frappe.tests.utils import FrappeTestCase
+
+
+class TestBankReconciliationRule(FrappeTestCase):
+ pass
diff --git a/banking/overrides/bank_account.py b/banking/overrides/bank_account.py
index 6b856435..d7f9a0d8 100644
--- a/banking/overrides/bank_account.py
+++ b/banking/overrides/bank_account.py
@@ -1,4 +1,16 @@
+import frappe
+from frappe import _
+
+
def before_validate(doc, method):
"""Remove spaces from IBAN"""
if doc.iban:
doc.iban = doc.iban.replace(" ", "")
+
+
+def validate(doc, method):
+ if doc.account and doc.bank_fee_account:
+ bank_account_currency = frappe.db.get_value("Account", doc.account, "account_currency")
+ bank_fee_currency = frappe.db.get_value("Account", doc.bank_fee_account, "account_currency")
+ if bank_account_currency != bank_fee_currency:
+ frappe.throw(_("Company Account and Bank Fee Account must be in the same currency!"))
diff --git a/banking/overrides/bank_transaction.py b/banking/overrides/bank_transaction.py
index 38fc3544..ca3fcc2d 100644
--- a/banking/overrides/bank_transaction.py
+++ b/banking/overrides/bank_transaction.py
@@ -1,8 +1,12 @@
+import json
+
import frappe
from erpnext.accounts.doctype.bank_transaction.bank_transaction import BankTransaction
+from erpnext.accounts.doctype.journal_entry.journal_entry import JournalEntry
from frappe import _
from frappe.core.utils import find
from frappe.utils import flt, getdate
+from frappe.utils.data import evaluate_filters
class CustomBankTransaction(BankTransaction):
@@ -72,3 +76,248 @@ def on_update_after_submit(doc, event):
),
title=_("Over Allocation"),
)
+
+
+def before_validate(doc, method):
+ ensure_positive_deposit_withdrawal_fees(doc)
+
+
+def on_cancel(doc, method):
+ # Cancel the journal entries created by this Bank Transaction
+ auto_created_journal_entries = frappe.get_all(
+ "Journal Entry",
+ filters={"cheque_no": doc.name},
+ pluck="name",
+ )
+
+ for journal_entry in auto_created_journal_entries:
+ try:
+ doc = frappe.get_doc("Journal Entry", journal_entry)
+ if doc.docstatus == 1:
+ doc.cancel()
+ except Exception as e:
+ frappe.msgprint(f"Failed to cancel {journal_entry}: {e}")
+
+
+def before_submit(doc, method):
+ date = doc.date or frappe.utils.nowdate()
+
+ if not doc.bank_account:
+ frappe.throw(_("No bank account - verify data!"))
+
+ if doc.deposit == 0 and doc.withdrawal == 0:
+ return
+
+ if doc.deposit < 0 or doc.withdrawal < 0:
+ frappe.throw(_("Debit or Credit is negative. Verify input data!"))
+
+ # Generic data catching
+ # Get Company
+ company_doc = frappe.get_cached_doc("Company", doc.company)
+
+ # Bank account debit/credit
+ account = frappe.db.get_value("Bank Account", doc.bank_account, "account")
+ # End Generic data catching
+
+ # Set initial values
+ debit, credit = (doc.deposit, 0) if doc.deposit > 0 else (0, doc.withdrawal)
+ # Create a journal entry for the bank fees
+ if doc.bank_account:
+ bank_fee_account = frappe.db.get_value("Bank Account", doc.bank_account, "bank_fee_account")
+ if not bank_fee_account:
+ frappe.throw(_("Please set the bank fee account in the bank account."))
+
+ # First Step: Book visible bank fees
+ if doc.included_fee > 0 and bank_fee_account:
+ included_fee = doc.included_fee
+ # only correct the credit value if set, as the debit value (deposit) is never including the fee.
+ if credit > 0:
+ credit = credit - included_fee
+ if debit > 0:
+ debit = debit - included_fee
+ je_fee_name = create_fee_journal_entry(
+ doc, company_doc, date, account, bank_fee_account, included_fee
+ )
+ doc.append(
+ "payment_entries",
+ {
+ "payment_document": "Journal Entry",
+ "payment_entry": je_fee_name,
+ "allocated_amount": included_fee,
+ },
+ )
+ # Set manually the un-/allocated amounts, as this value is already set and needs to be updated
+ doc.allocated_amount = included_fee
+ doc.unallocated_amount = debit + credit
+ if doc.unallocated_amount == 0:
+ doc.status = "Reconciled"
+ else:
+ included_fee = 0
+
+ # Second step: Automatic reconcilation based on the Bank Reconciliation Rules
+ bank_reconciliation_rules = frappe.db.get_list(
+ "Bank Reconciliation Rule",
+ filters={
+ "disabled": 0,
+ "bank_account": doc.bank_account,
+ "docstatus": 1,
+ },
+ fields=["name", "target_account", "filters"],
+ as_list=True,
+ )
+ for br_rule in bank_reconciliation_rules:
+ # Check if line matches filter
+ if br_rule[2]:
+ filters = json.loads(br_rule[2])
+ if filters:
+ condition_met = evaluate_filters(doc, filters)
+ if condition_met:
+ rule = br_rule[0]
+ target_account = br_rule[1]
+ je_auto_name = create_automatic_journal_entry(
+ doc, company_doc, date, account, target_account, rule, debit, credit
+ )
+ doc.append(
+ "payment_entries",
+ {
+ "payment_document": "Journal Entry",
+ "payment_entry": je_auto_name,
+ "allocated_amount": debit + credit,
+ },
+ )
+ # Set manually the un-/allocated amounts, as this value is already set and needs to be updated
+ doc.allocated_amount = doc.allocated_amount + debit + credit
+ # Set remaining debit and credit to 0, so no cash in transit is generated
+ debit = 0
+ credit = 0
+ doc.unallocated_amount = 0
+ doc.status = "Reconciled"
+ break
+
+ if debit == 0 and credit == 0:
+ return
+
+ journal_entry = frappe.new_doc("Journal Entry")
+ journal_entry.voucher_type = "Bank Entry"
+ journal_entry.title = f"BT ID {doc.name}"
+ journal_entry.posting_date = date
+ journal_entry.company = doc.company
+ journal_entry.user_remark = f"Auto-created from BT: {doc.name}"
+ journal_entry.cheque_no = doc.name
+ journal_entry.cheque_date = date
+ journal_entry.multi_currency = 1
+ journal_entry.clearance_date = frappe.utils.today()
+
+ journal_entry.append(
+ "accounts",
+ {
+ "account": account,
+ "bank_account": doc.bank_account,
+ "debit_in_account_currency": debit,
+ "credit_in_account_currency": credit,
+ "cost_center": company_doc.cost_center,
+ },
+ )
+
+ # If by any filter, no accounting entries (single lines) are present, do not create a journal entry.
+ if len(journal_entry.accounts) > 0:
+ journal_entry.insert()
+
+ # Create Exchange Gain/Loss Line
+ JournalEntry.get_balance(journal_entry, difference_account=company_doc.exchange_gain_loss_account)
+
+ journal_entry.submit()
+
+ else:
+ frappe.msgprint(
+ _("No journal entry was created, as no data was present in the Accounting Entries table.")
+ )
+
+
+def create_automatic_journal_entry(doc, company_doc, date, account, target_account, rule, debit, credit):
+ journal_entry = frappe.new_doc("Journal Entry")
+ journal_entry.voucher_type = "Journal Entry"
+ journal_entry.title = f"BT ID {doc.name}"
+ journal_entry.posting_date = date
+ journal_entry.company = doc.company
+ journal_entry.user_remark = f"Auto-created from BT: {doc.name} by automatic rule {rule}"
+ journal_entry.cheque_no = doc.name
+ journal_entry.cheque_date = date
+ journal_entry.multi_currency = 1
+ journal_entry.clearance_date = frappe.utils.today()
+
+ # Bank account entry
+ journal_entry.append(
+ "accounts",
+ {
+ "account": account,
+ "bank_account": doc.bank_account,
+ "debit_in_account_currency": debit,
+ "credit_in_account_currency": credit,
+ "cost_center": company_doc.cost_center,
+ },
+ )
+
+ # Target account entry
+ journal_entry.append(
+ "accounts",
+ {
+ "account": target_account,
+ "bank_account": "",
+ "debit_in_account_currency": credit,
+ "credit_in_account_currency": debit,
+ "cost_center": company_doc.cost_center,
+ },
+ )
+
+ journal_entry.submit()
+
+ return journal_entry.name
+
+
+def create_fee_journal_entry(doc, company_doc, date, account, bank_fee_account, included_fee):
+ journal_entry = frappe.new_doc("Journal Entry")
+ journal_entry.voucher_type = "Journal Entry"
+ journal_entry.title = f"BT ID {doc.name}"
+ journal_entry.posting_date = date
+ journal_entry.company = doc.company
+ journal_entry.user_remark = f"Auto-created from BT: {doc.name}"
+ journal_entry.cheque_no = doc.name
+ journal_entry.cheque_date = date
+ journal_entry.multi_currency = 1
+ journal_entry.clearance_date = frappe.utils.today()
+
+ # Bank fee entry
+ journal_entry.append(
+ "accounts",
+ {
+ "account": account,
+ "bank_account": doc.bank_account,
+ "debit_in_account_currency": 0,
+ "credit_in_account_currency": included_fee,
+ "cost_center": company_doc.cost_center,
+ },
+ )
+
+ # Fee account entry
+ journal_entry.append(
+ "accounts",
+ {
+ "account": bank_fee_account,
+ "bank_account": "",
+ "debit_in_account_currency": included_fee,
+ "credit_in_account_currency": 0,
+ "cost_center": company_doc.cost_center,
+ },
+ )
+
+ journal_entry.submit()
+
+ return journal_entry.name
+
+
+def ensure_positive_deposit_withdrawal_fees(doc):
+ doc.deposit = abs(flt(doc.deposit) or 0.0)
+ doc.withdrawal = abs(flt(doc.withdrawal) or 0.0)
+ doc.included_fee = abs(flt(doc.included_fee) or 0.0)
+ doc.excluded_fee = abs(flt(doc.excluded_fee) or 0.0)
diff --git a/banking/patches.txt b/banking/patches.txt
index 15a2faee..bf0a6a01 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 # 2025-12-16
banking.patches.remove_spaces_from_iban
[post_model_sync]
From 3923fbc12f49084c7568c8dc7bac9cb3934f7f91 Mon Sep 17 00:00:00 2001
From: 0xD0M1M0 <76812428+0xD0M1M0@users.noreply.github.com>
Date: Tue, 16 Dec 2025 18:36:18 +0000
Subject: [PATCH 02/38] fix: recall handle_excluded_fee
---
banking/overrides/bank_transaction.py | 7 +++++--
1 file changed, 5 insertions(+), 2 deletions(-)
diff --git a/banking/overrides/bank_transaction.py b/banking/overrides/bank_transaction.py
index ca3fcc2d..32ab2ade 100644
--- a/banking/overrides/bank_transaction.py
+++ b/banking/overrides/bank_transaction.py
@@ -79,7 +79,7 @@ def on_update_after_submit(doc, event):
def before_validate(doc, method):
- ensure_positive_deposit_withdrawal_fees(doc)
+ ensure_positive_deposit_withdrawal_fees(doc, method)
def on_cancel(doc, method):
@@ -316,8 +316,11 @@ def create_fee_journal_entry(doc, company_doc, date, account, bank_fee_account,
return journal_entry.name
-def ensure_positive_deposit_withdrawal_fees(doc):
+def ensure_positive_deposit_withdrawal_fees(doc, method):
doc.deposit = abs(flt(doc.deposit) or 0.0)
doc.withdrawal = abs(flt(doc.withdrawal) or 0.0)
doc.included_fee = abs(flt(doc.included_fee) or 0.0)
doc.excluded_fee = abs(flt(doc.excluded_fee) or 0.0)
+ if method == "before_validate":
+ # Re-call this function as the original function runs before this one and values are not converted
+ BankTransaction.handle_excluded_fee(doc)
From 2c5512ad33e01ed2675e8fc1437bf4e44ca6283a Mon Sep 17 00:00:00 2001
From: 0xD0M1M0 <76812428+0xD0M1M0@users.noreply.github.com>
Date: Wed, 17 Dec 2025 21:49:28 +0000
Subject: [PATCH 03/38] refactor: refactor functions
---
.../bank_reconciliation_rule.py | 11 +-
banking/locale/de.po | 154 ++++++---
banking/locale/main.pot | 138 +++++++--
banking/overrides/bank_account.py | 4 +
banking/overrides/bank_transaction.py | 293 +++++++-----------
5 files changed, 349 insertions(+), 251 deletions(-)
diff --git a/banking/klarna_kosma_integration/doctype/bank_reconciliation_rule/bank_reconciliation_rule.py b/banking/klarna_kosma_integration/doctype/bank_reconciliation_rule/bank_reconciliation_rule.py
index dc837609..72d242d6 100644
--- a/banking/klarna_kosma_integration/doctype/bank_reconciliation_rule/bank_reconciliation_rule.py
+++ b/banking/klarna_kosma_integration/doctype/bank_reconciliation_rule/bank_reconciliation_rule.py
@@ -8,11 +8,18 @@
class BankReconciliationRule(Document):
def validate(self):
+ self.validate_account_currencies()
+ self.validate_filters()
+
+ def validate_account_currencies(self):
bank_account = frappe.db.get_value("Bank Account", self.bank_account, "account")
bank_account_currency = frappe.db.get_value("Account", bank_account, "account_currency")
target_account_currency = frappe.db.get_value("Account", self.target_account, "account_currency")
+
if bank_account_currency != target_account_currency:
frappe.throw(_("Bank Account and Target Account need to be in the same currency!"))
- # self.filters is a code field with json, so it has in an emtpy stage "[]" inside
- if self.filters is None or len(self.filters) <= 2:
+
+ def validate_filters(self):
+ # self.filters is a code field with json, so it has "[]" when empty
+ if not self.filters or len(self.filters) <= 2:
frappe.throw(_("Please define at least one filter!"))
diff --git a/banking/locale/de.po b/banking/locale/de.po
index 2d05e4f3..2270bb4a 100644
--- a/banking/locale/de.po
+++ b/banking/locale/de.po
@@ -1,4 +1,4 @@
-# Translations template for ALYF Banking.
+# German translations for ALYF Banking.
# Copyright (C) 2025 ALYF GmbH
# This file is distributed under the same license as the ALYF Banking project.
# FIRST AUTHOR , 2025.
@@ -7,14 +7,17 @@ msgid ""
msgstr ""
"Project-Id-Version: ALYF Banking VERSION\n"
"Report-Msgid-Bugs-To: hallo@alyf.de\n"
-"POT-Creation-Date: 2025-11-07 14:18+0053\n"
-"PO-Revision-Date: 2025-01-31 19:26+0053\n"
+"POT-Creation-Date: 2025-12-17 21:46+0000\n"
+"PO-Revision-Date: 2025-12-17 22:47+0100\n"
"Last-Translator: hallo@alyf.de\n"
"Language-Team: hallo@alyf.de\n"
+"Language: de\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"Generated-By: Babel 2.13.1\n"
+"X-Generator: Poedit 3.8\n"
#: banking/ebics/doctype/ebics_request/ebics_request.js:63
msgid "EBICS User '{0}' has Download Batch Transactions enabled. Batch details are stored in separate EBICS Requests, so this re-import might be incomplete. Do you want to continue?"
@@ -45,7 +48,7 @@ msgstr "ALYF Banking"
msgid "Account Currency"
msgstr "Kontowährung"
-#: banking/public/js/bank_reconciliation_beta/actions_panel/details_tab.js:140
+#: banking/public/js/bank_reconciliation_beta/actions_panel/details_tab.js:130
msgid "Account Holder"
msgstr "Kontoinhaber"
@@ -54,13 +57,20 @@ msgstr "Kontoinhaber"
msgid "Account Opening Balance"
msgstr "Kontostand zum Eröffnungstag"
+#. Label of a Section Break field in DocType 'Bank Reconciliation Rule'
+#: banking/klarna_kosma_integration/doctype/bank_reconciliation_rule/bank_reconciliation_rule.json
+msgid "Account Settings"
+msgstr "Konteneinstellungen"
+
#. Name of a role
#: banking/ebics/doctype/ebics_user/ebics_user.json
+#: banking/klarna_kosma_integration/doctype/bank_reconciliation_rule/bank_reconciliation_rule.json
#: banking/klarna_kosma_integration/doctype/bank_reconciliation_tool_beta/bank_reconciliation_tool_beta.json
msgid "Accounts Manager"
msgstr ""
#. Name of a role
+#: banking/klarna_kosma_integration/doctype/bank_reconciliation_rule/bank_reconciliation_rule.json
#: banking/klarna_kosma_integration/doctype/bank_reconciliation_tool_beta/bank_reconciliation_tool_beta.json
msgid "Accounts User"
msgstr ""
@@ -72,6 +82,10 @@ msgstr ""
msgid "Actions"
msgstr "Aktionen"
+#: banking/klarna_kosma_integration/doctype/bank_reconciliation_rule/bank_reconciliation_rule_list.js:14
+msgid "Active"
+msgstr ""
+
#. Label of a Data field in DocType 'Banking Settings'
#: banking/klarna_kosma_integration/doctype/banking_settings/banking_settings.json
msgid "Admin URL"
@@ -83,7 +97,9 @@ msgid "Advanced"
msgstr "Erweitert"
#. Label of a Link field in DocType 'SEPA Payment Order'
+#. Label of a Link field in DocType 'Bank Reconciliation Rule'
#: banking/ebics/doctype/sepa_payment_order/sepa_payment_order.json
+#: banking/klarna_kosma_integration/doctype/bank_reconciliation_rule/bank_reconciliation_rule.json
msgid "Amended From"
msgstr ""
@@ -138,6 +154,16 @@ msgstr "Automatischer Abgleich ..."
msgid "Auto reconcile bank transactions based on matching reference numbers?"
msgstr "Möchten Sie Banktransaktionen automatisch abgleichen, basierend auf übereinstimmenden Referenznummern?"
+#: banking/overrides/bank_transaction.py:214
+msgid "Auto-created from BT: {0}"
+msgstr "Automatisch erstellt von BT: {0}"
+
+#. Content of the 'filter_description' (HTML) field in DocType 'Bank
+#. Reconciliation Rule'
+#: banking/klarna_kosma_integration/doctype/bank_reconciliation_rule/bank_reconciliation_rule.json
+msgid "Automatically create a journal entry if the submitted document matches these filters.
Be careful: if the filter is not strict enough accounting errors will happen.
"
+msgstr "Erstellen Sie automatisch einen Buchungssatz, wenn das gebuchte Dokument diesen Filtern entspricht.
Achtung: Wenn der Filter nicht strikt genug ist, können Buchungsfehler auftreten.
"
+
#. Label of a Link field in DocType 'EBICS User'
#. Label of a Link field in DocType 'SEPA Payment Order'
#. Label of a Link field in DocType 'Bank Reconciliation Tool Beta'
@@ -149,13 +175,19 @@ msgid "Bank"
msgstr "Bank"
#. Label of a Link field in DocType 'SEPA Payment Order'
+#. Label of a Link field in DocType 'Bank Reconciliation Rule'
#. Label of a Link field in DocType 'Bank Reconciliation Tool Beta'
#: banking/ebics/doctype/sepa_payment_order/sepa_payment_order.json
+#: banking/klarna_kosma_integration/doctype/bank_reconciliation_rule/bank_reconciliation_rule.json
#: banking/klarna_kosma_integration/doctype/bank_reconciliation_tool_beta/bank_reconciliation_tool_beta.json
-#: banking/public/js/bank_reconciliation_beta/actions_panel/details_tab.js:81
+#: banking/public/js/bank_reconciliation_beta/actions_panel/details_tab.js:78
msgid "Bank Account"
msgstr "Bankkonto"
+#: banking/klarna_kosma_integration/doctype/bank_reconciliation_rule/bank_reconciliation_rule.py:20
+msgid "Bank Account and Target Account need to be in the same currency!"
+msgstr "Bankkonto und Zielkonto müssen die gleiche Währung haben!"
+
#: banking/ebics/utils.py:215
msgid "Bank Account not found for IBAN {0}"
msgstr "Bankkonto nicht gefunden für IBAN {0}"
@@ -177,6 +209,11 @@ msgstr ""
msgid "Bank Reconciliation"
msgstr "Bankabgleich"
+#. Name of a DocType
+#: banking/klarna_kosma_integration/doctype/bank_reconciliation_rule/bank_reconciliation_rule.json
+msgid "Bank Reconciliation Rule"
+msgstr "Bankabgleich Regeln"
+
#. Name of a DocType
#: banking/klarna_kosma_integration/doctype/bank_reconciliation_tool_beta/bank_reconciliation_tool_beta.json
msgid "Bank Reconciliation Tool Beta"
@@ -186,7 +223,7 @@ msgstr "Bankabgleich Beta"
msgid "Bank Transaction {0} Matched"
msgstr "Banktransaktion {0} zugeordnet"
-#: banking/overrides/bank_transaction.py:12
+#: banking/overrides/bank_transaction.py:15
msgid "Bank Transaction {0} is already fully reconciled"
msgstr "Banktransaktion {0} ist bereits vollständig verbucht"
@@ -202,7 +239,7 @@ msgstr "Banktransaktion {0} teilweise verbucht."
msgid "Bank Transaction {0} reconciled with a new {1}"
msgstr "Banktransaktion {0} mit einem neuen {1} abgeglichen"
-#: banking/public/js/bank_reconciliation_beta/actions_panel/details_tab.js:64
+#: banking/public/js/bank_reconciliation_beta/actions_panel/details_tab.js:62
msgid "Bank Transaction {0} updated"
msgstr "Banktransaktion {0} aktualisiert"
@@ -328,6 +365,10 @@ msgstr "Schlusssaldo"
msgid "Company"
msgstr ""
+#: banking/overrides/bank_account.py:20
+msgid "Company Account and Bank Fee Account must be in the same currency!"
+msgstr "Firmenkonto und Bankgebührenkonto müssen die gleiche Währung haben!"
+
#: banking/ebics/doctype/ebics_user/ebics_user.js:182
msgid "Continue"
msgstr "Weiter"
@@ -366,7 +407,7 @@ msgstr "Anmeldedaten"
#. Label of a Link field in DocType 'SEPA Payment'
#: banking/ebics/doctype/sepa_payment/sepa_payment.json
-#: banking/public/js/bank_reconciliation_beta/actions_panel/details_tab.js:131
+#: banking/public/js/bank_reconciliation_beta/actions_panel/details_tab.js:122
msgid "Currency"
msgstr "Währung"
@@ -386,18 +427,22 @@ msgstr ""
msgid "DOWNLOADED"
msgstr "HERUNTERGELADEN"
-#: banking/public/js/bank_reconciliation_beta/actions_panel/details_tab.js:90
+#: banking/public/js/bank_reconciliation_beta/actions_panel/details_tab.js:86
#: banking/public/js/bank_reconciliation_beta/actions_panel/match_tab.js:488
#: banking/public/js/bank_reconciliation_beta/panel_manager.js:128
msgid "Date"
msgstr "Datum"
-#: banking/public/js/bank_reconciliation_beta/actions_panel/details_tab.js:97
+#: banking/overrides/bank_transaction.py:79
+msgid "Debit or Credit is negative. Verify input data!"
+msgstr "Haben oder Soll sind negative. Verifiziere die eingegebenen Daten!"
+
+#: banking/public/js/bank_reconciliation_beta/actions_panel/details_tab.js:92
#: banking/public/js/bank_reconciliation_beta/panel_manager.js:130
msgid "Deposit"
msgstr "Einzahlung"
-#: banking/public/js/bank_reconciliation_beta/actions_panel/details_tab.js:116
+#: banking/public/js/bank_reconciliation_beta/actions_panel/details_tab.js:109
msgid "Description"
msgstr "Beschreibung"
@@ -414,6 +459,12 @@ msgstr "Entwicklermodus ist aktiviert. Bitte deaktivieren Sie ihn, um die automa
msgid "Did you mean {0}?"
msgstr "Meinten Sie {0}?"
+#. Label of a Check field in DocType 'Bank Reconciliation Rule'
+#: banking/klarna_kosma_integration/doctype/bank_reconciliation_rule/bank_reconciliation_rule.json
+#: banking/klarna_kosma_integration/doctype/bank_reconciliation_rule/bank_reconciliation_rule_list.js:12
+msgid "Disabled"
+msgstr ""
+
#. Label of a Select field in DocType 'Banking Reference Mapping'
#. Label of a Link field in DocType 'Voucher Matching Default'
#: banking/klarna_kosma_integration/doctype/banking_reference_mapping/banking_reference_mapping.json
@@ -449,10 +500,11 @@ msgid "Downloaded"
msgstr "Heruntergeladen"
#: banking/ebics/doctype/sepa_payment_order/sepa_payment_order_list.js:22
+#: banking/klarna_kosma_integration/doctype/bank_reconciliation_rule/bank_reconciliation_rule_list.js:20
msgid "Draft"
msgstr ""
-#: banking/overrides/bank_transaction.py:33
+#: banking/overrides/bank_transaction.py:36
msgid "Due to Period Closing, you cannot reconcile unpaid vouchers with a Bank Transaction before {0}"
msgstr "Aufgrund des Periodenschlusses können Sie keine unbezahlten Belege mit einer Banktransaktion vor dem {0} abgleichen"
@@ -518,11 +570,11 @@ msgstr ""
#: banking/ebics/doctype/ebics_user/ebics_user.js:114
msgid "Ebics 2.5 (H004)"
-msgstr ""
+msgstr "Ebics 2.5 (H004)"
#: banking/ebics/doctype/ebics_user/ebics_user.js:118
msgid "Ebics 3.0 (H005)"
-msgstr ""
+msgstr "Ebics 3.0 (H005)"
#: banking/klarna_kosma_integration/doctype/banking_settings/banking_settings.js:70
msgid "Ebics Users"
@@ -607,6 +659,10 @@ msgstr ""
msgid "Failed"
msgstr "Fehlgeschlagen"
+#: banking/overrides/bank_transaction.py:125
+msgid "Failed to cancel {0}: {1}"
+msgstr "Fehler beim Absagen von {0}: {1}"
+
#: banking/public/js/bank_reconciliation_beta/actions_panel/create_tab.js:91
msgid "Failed to create {0} against {1}"
msgstr "Fehler beim Erstellen von {0} gegen {1}"
@@ -627,7 +683,7 @@ msgstr "Fehler beim Abgleich von {0}"
msgid "Failed to remove EBICS user registration."
msgstr "EBICS-Benutzerregistrierung konnte nicht entfernt werden."
-#: banking/public/js/bank_reconciliation_beta/actions_panel/details_tab.js:51
+#: banking/public/js/bank_reconciliation_beta/actions_panel/details_tab.js:49
msgid "Failed to update {0}"
msgstr "Fehler beim Aktualisieren von {0}"
@@ -654,7 +710,10 @@ msgstr "Filter erforderlich"
msgid "Filter by Reference Date"
msgstr "Nach Referenzdatum filtern"
+#. Label of a Section Break field in DocType 'Bank Reconciliation Rule'
+#. Label of a Code field in DocType 'Bank Reconciliation Rule'
#. Label of a Section Break field in DocType 'Bank Reconciliation Tool Beta'
+#: banking/klarna_kosma_integration/doctype/bank_reconciliation_rule/bank_reconciliation_rule.json
#: banking/klarna_kosma_integration/doctype/bank_reconciliation_tool_beta/bank_reconciliation_tool_beta.json
msgid "Filters"
msgstr "Filter"
@@ -696,15 +755,15 @@ msgstr "Banktransaktionen abrufen"
#. Option for the 'Protocol Version' (Select) field in DocType 'EBICS User'
#: banking/ebics/doctype/ebics_user/ebics_user.json
msgid "H004"
-msgstr ""
+msgstr "H004"
#. Option for the 'Protocol Version' (Select) field in DocType 'EBICS User'
#: banking/ebics/doctype/ebics_user/ebics_user.json
msgid "H005"
-msgstr ""
+msgstr "H005"
#: banking/public/js/bank_reconciliation_beta/actions_panel/create_tab.js:288
-#: banking/public/js/bank_reconciliation_beta/actions_panel/details_tab.js:207
+#: banking/public/js/bank_reconciliation_beta/actions_panel/details_tab.js:190
#: banking/public/js/bank_reconciliation_beta/actions_panel/match_tab.js:453
msgid "Hidden field for alignment"
msgstr "Verstecktes Feld für Ausrichtung"
@@ -715,7 +774,7 @@ msgstr "Verstecktes Feld für Ausrichtung"
#: banking/ebics/doctype/sepa_payment_order/sepa_payment_order.json
#: banking/public/js/utils.js:16
msgid "IBAN"
-msgstr ""
+msgstr "IBAN"
#: banking/custom/employee.py:11
msgid "IBAN Warning: Not used in SEPA Payment Orders"
@@ -729,7 +788,7 @@ msgstr "IBAN auf Mitarbeiter wird nicht in SEPA-Zahlungsaufträgen verwendet. Bi
msgid "IBAN {0} is invalid."
msgstr "IBAN {0} ist ungültig."
-#: banking/public/js/bank_reconciliation_beta/actions_panel/details_tab.js:73
+#: banking/public/js/bank_reconciliation_beta/actions_panel/details_tab.js:71
msgid "ID"
msgstr ""
@@ -845,7 +904,11 @@ msgstr "Keine Daten verfügbar"
msgid "No Transactions found for the current filters."
msgstr "Keine Transaktionen gefunden für die aktuellen Filter."
-#: banking/public/js/bank_reconciliation_beta/actions_panel/details_tab.js:32
+#: banking/overrides/bank_transaction.py:73
+msgid "No bank account - verify data!"
+msgstr "Kein Bankkonto - verifiziere die Daten!"
+
+#: banking/public/js/bank_reconciliation_beta/actions_panel/details_tab.js:30
msgid "No changes to update"
msgstr "Keine Änderungen zum Aktualisieren"
@@ -876,7 +939,7 @@ msgstr "Auftragsart"
msgid "Outstanding"
msgstr "Offen"
-#: banking/overrides/bank_transaction.py:73
+#: banking/overrides/bank_transaction.py:107
msgid "Over Allocation"
msgstr "Überbuchung"
@@ -898,20 +961,20 @@ msgstr "Teilweise abgeglichen"
msgid "Partner ID"
msgstr "Partner-ID"
-#: banking/public/js/bank_reconciliation_beta/actions_panel/details_tab.js:197
+#: banking/public/js/bank_reconciliation_beta/actions_panel/details_tab.js:180
#: banking/public/js/bank_reconciliation_beta/actions_panel/match_tab.js:496
msgid "Party"
msgstr "Partei"
-#: banking/public/js/bank_reconciliation_beta/actions_panel/details_tab.js:148
+#: banking/public/js/bank_reconciliation_beta/actions_panel/details_tab.js:137
msgid "Party Account Number"
msgstr "Partei-Kontonummer"
-#: banking/public/js/bank_reconciliation_beta/actions_panel/details_tab.js:156
+#: banking/public/js/bank_reconciliation_beta/actions_panel/details_tab.js:144
msgid "Party IBAN"
msgstr "Partei-IBAN"
-#: banking/public/js/bank_reconciliation_beta/actions_panel/details_tab.js:179
+#: banking/public/js/bank_reconciliation_beta/actions_panel/details_tab.js:166
msgid "Party Type"
msgstr "Partei-Typ"
@@ -966,6 +1029,10 @@ msgstr "Bitte fügen Sie dem Land {0} einen zweistelligen Ländercode hinzu"
msgid "Please confirm that the following keys are identical to the ones mentioned on your bank's letter:"
msgstr "Bitte bestätigen Sie, dass die folgenden Schlüssel mit denen auf dem Schreiben Ihrer Bank übereinstimmen:"
+#: banking/klarna_kosma_integration/doctype/bank_reconciliation_rule/bank_reconciliation_rule.py:25
+msgid "Please define at least one filter!"
+msgstr "Bitte definiere mindestens einen Filter!"
+
#. Description of the 'section_break_qjlc' (Section Break) field in DocType
#. 'EBICS User'
#: banking/ebics/doctype/ebics_user/ebics_user.json
@@ -993,6 +1060,10 @@ msgstr "Bitte wählen Sie Belege des gleichen Typs aus, um sie abzugleichen"
msgid "Please set the 'Bank Account' filter"
msgstr "Bitte setzen Sie den 'Bankkonto'-Filter"
+#: banking/overrides/bank_transaction.py:132
+msgid "Please set the bank fee account in the bank account."
+msgstr "Bitte setze ein Bankgebührenkonto am Bankkonto."
+
#. Label of a Password field in DocType 'Banking Settings'
#: banking/klarna_kosma_integration/doctype/banking_settings/banking_settings.json
msgid "Portal API Token"
@@ -1088,7 +1159,7 @@ msgstr "Referenzname"
#. Label of a Data field in DocType 'SEPA Payment Order'
#: banking/ebics/doctype/sepa_payment_order/sepa_payment_order.json
#: banking/public/js/bank_reconciliation_beta/actions_panel/create_tab.js:181
-#: banking/public/js/bank_reconciliation_beta/actions_panel/details_tab.js:170
+#: banking/public/js/bank_reconciliation_beta/actions_panel/details_tab.js:157
msgid "Reference Number"
msgstr "Referenznummer"
@@ -1160,6 +1231,10 @@ msgstr "SWIFT-Nummer"
msgid "Sales Invoice"
msgstr ""
+#: banking/public/js/bank_reconciliation_beta/actions_panel/details_tab.js:199
+msgid "Save"
+msgstr ""
+
#. Label of a Section Break field in DocType 'SEPA Payment Order'
#: banking/ebics/doctype/sepa_payment_order/sepa_payment_order.json
msgid "Sender"
@@ -1235,10 +1310,6 @@ msgstr "Passwort speichern"
msgid "Store the passphrase in the ERPNext database to enable automated, regular download of bank statements."
msgstr "Passwort in der ERPNext-Datenbank speichern, um den automatisierten, regelmäßigen Download von Kontoauszügen zu ermöglichen."
-#: banking/public/js/bank_reconciliation_beta/actions_panel/details_tab.js:216
-msgid "Submit"
-msgstr ""
-
#: banking/klarna_kosma_integration/doctype/banking_settings/banking_settings.js:70
msgid "Subscriber"
msgstr "Abonnent"
@@ -1282,12 +1353,18 @@ msgstr "System"
#: banking/ebics/doctype/ebics_request/ebics_request.json
#: banking/ebics/doctype/ebics_user/ebics_user.json
#: banking/ebics/doctype/sepa_payment_order/sepa_payment_order.json
+#: banking/klarna_kosma_integration/doctype/bank_reconciliation_rule/bank_reconciliation_rule.json
#: banking/klarna_kosma_integration/doctype/bank_reconciliation_tool_beta/bank_reconciliation_tool_beta.json
#: banking/klarna_kosma_integration/doctype/banking_settings/banking_settings.json
msgid "System Manager"
msgstr ""
-#: banking/overrides/bank_transaction.py:70
+#. Label of a Link field in DocType 'Bank Reconciliation Rule'
+#: banking/klarna_kosma_integration/doctype/bank_reconciliation_rule/bank_reconciliation_rule.json
+msgid "Target Account"
+msgstr "Zielkonto"
+
+#: banking/overrides/bank_transaction.py:104
msgid "The Bank Transaction is over-allocated by {0} at row {1}."
msgstr "Die Banktransaktion ist um {0} bei Zeile {1} überbucht."
@@ -1317,7 +1394,7 @@ msgstr "Der Server hat einen Fehler verursacht. Bitte versuchen Sie es später e
msgid "These DocTypes are pre-enabled in the 'Match Voucher' tab."
msgstr "Diese Belegtypen sind im Tab 'Beleg-Zuordnung' voreingestellt."
-#: banking/public/js/bank_reconciliation_beta/actions_panel/details_tab.js:123
+#: banking/public/js/bank_reconciliation_beta/actions_panel/details_tab.js:115
msgid "To Allocate"
msgstr "Offener Betrag"
@@ -1366,7 +1443,7 @@ msgstr "Nicht zugewiesener Betrag"
msgid "Unpaid Vouchers"
msgstr "Unbezahlte Belege"
-#: banking/public/js/bank_reconciliation_beta/actions_panel/details_tab.js:165
+#: banking/public/js/bank_reconciliation_beta/actions_panel/details_tab.js:152
msgid "Update"
msgstr "Aktualisieren"
@@ -1374,7 +1451,7 @@ msgstr "Aktualisieren"
msgid "Update Amounts"
msgstr "Beträge aktualisieren"
-#: banking/public/js/bank_reconciliation_beta/actions_panel/details_tab.js:48
+#: banking/public/js/bank_reconciliation_beta/actions_panel/details_tab.js:46
msgid "Updating ..."
msgstr "Aktualisieren ..."
@@ -1447,7 +1524,7 @@ msgstr "Wir versuchen, alle Transaktionen der abgeschlossenen Tage im ausgewähl
msgid "We'll try to download new transactions from today, using camt.052."
msgstr "Wir versuchen, neue Transaktionen von heute via camt.052 herunterzuladen."
-#: banking/public/js/bank_reconciliation_beta/actions_panel/details_tab.js:105
+#: banking/public/js/bank_reconciliation_beta/actions_panel/details_tab.js:99
#: banking/public/js/bank_reconciliation_beta/panel_manager.js:129
msgid "Withdrawal"
msgstr "Auszahlung"
@@ -1456,6 +1533,10 @@ msgstr "Auszahlung"
msgid "You selected protocol version {0}, but the bank {1} only supports {2}."
msgstr "Sie haben Protokollversion {0} ausgewählt, aber die Bank {1} unterstützt nur {2}."
+#: banking/overrides/bank_transaction.py:213
+msgid "by automatic rule {0}"
+msgstr "durch automatische Regel {0}"
+
#: banking/klarna_kosma_integration/doctype/bank_reconciliation_tool_beta/bank_reconciliation_tool_beta.js:270
#: banking/klarna_kosma_integration/doctype/bank_reconciliation_tool_beta/bank_reconciliation_tool_beta.js:304
msgid "to import bank transactions for {0}."
@@ -1470,4 +1551,3 @@ msgstr ""
#: banking/klarna_kosma_integration/workspace/alyf_banking/alyf_banking.json
msgid "{} Unreconciled"
msgstr "{} Nicht abgeglichen"
-
diff --git a/banking/locale/main.pot b/banking/locale/main.pot
index fde35ea9..24e5b7a5 100644
--- a/banking/locale/main.pot
+++ b/banking/locale/main.pot
@@ -7,8 +7,8 @@ msgid ""
msgstr ""
"Project-Id-Version: ALYF Banking VERSION\n"
"Report-Msgid-Bugs-To: hallo@alyf.de\n"
-"POT-Creation-Date: 2025-11-07 14:18+0053\n"
-"PO-Revision-Date: 2025-11-07 14:18+0053\n"
+"POT-Creation-Date: 2025-12-17 21:46+0000\n"
+"PO-Revision-Date: 2025-12-17 21:46+0000\n"
"Last-Translator: hallo@alyf.de\n"
"Language-Team: hallo@alyf.de\n"
"MIME-Version: 1.0\n"
@@ -45,7 +45,7 @@ msgstr ""
msgid "Account Currency"
msgstr ""
-#: banking/public/js/bank_reconciliation_beta/actions_panel/details_tab.js:140
+#: banking/public/js/bank_reconciliation_beta/actions_panel/details_tab.js:130
msgid "Account Holder"
msgstr ""
@@ -54,13 +54,20 @@ msgstr ""
msgid "Account Opening Balance"
msgstr ""
+#. Label of a Section Break field in DocType 'Bank Reconciliation Rule'
+#: banking/klarna_kosma_integration/doctype/bank_reconciliation_rule/bank_reconciliation_rule.json
+msgid "Account Settings"
+msgstr ""
+
#. Name of a role
#: banking/ebics/doctype/ebics_user/ebics_user.json
+#: banking/klarna_kosma_integration/doctype/bank_reconciliation_rule/bank_reconciliation_rule.json
#: banking/klarna_kosma_integration/doctype/bank_reconciliation_tool_beta/bank_reconciliation_tool_beta.json
msgid "Accounts Manager"
msgstr ""
#. Name of a role
+#: banking/klarna_kosma_integration/doctype/bank_reconciliation_rule/bank_reconciliation_rule.json
#: banking/klarna_kosma_integration/doctype/bank_reconciliation_tool_beta/bank_reconciliation_tool_beta.json
msgid "Accounts User"
msgstr ""
@@ -72,6 +79,10 @@ msgstr ""
msgid "Actions"
msgstr ""
+#: banking/klarna_kosma_integration/doctype/bank_reconciliation_rule/bank_reconciliation_rule_list.js:14
+msgid "Active"
+msgstr ""
+
#. Label of a Data field in DocType 'Banking Settings'
#: banking/klarna_kosma_integration/doctype/banking_settings/banking_settings.json
msgid "Admin URL"
@@ -83,7 +94,9 @@ msgid "Advanced"
msgstr ""
#. Label of a Link field in DocType 'SEPA Payment Order'
+#. Label of a Link field in DocType 'Bank Reconciliation Rule'
#: banking/ebics/doctype/sepa_payment_order/sepa_payment_order.json
+#: banking/klarna_kosma_integration/doctype/bank_reconciliation_rule/bank_reconciliation_rule.json
msgid "Amended From"
msgstr ""
@@ -138,6 +151,16 @@ msgstr ""
msgid "Auto reconcile bank transactions based on matching reference numbers?"
msgstr ""
+#: banking/overrides/bank_transaction.py:214
+msgid "Auto-created from BT: {0}"
+msgstr ""
+
+#. Content of the 'filter_description' (HTML) field in DocType 'Bank
+#. Reconciliation Rule'
+#: banking/klarna_kosma_integration/doctype/bank_reconciliation_rule/bank_reconciliation_rule.json
+msgid "Automatically create a journal entry if the submitted document matches these filters.
Be careful: if the filter is not strict enough accounting errors will happen.
"
+msgstr ""
+
#. Label of a Link field in DocType 'EBICS User'
#. Label of a Link field in DocType 'SEPA Payment Order'
#. Label of a Link field in DocType 'Bank Reconciliation Tool Beta'
@@ -149,13 +172,19 @@ msgid "Bank"
msgstr ""
#. Label of a Link field in DocType 'SEPA Payment Order'
+#. Label of a Link field in DocType 'Bank Reconciliation Rule'
#. Label of a Link field in DocType 'Bank Reconciliation Tool Beta'
#: banking/ebics/doctype/sepa_payment_order/sepa_payment_order.json
+#: banking/klarna_kosma_integration/doctype/bank_reconciliation_rule/bank_reconciliation_rule.json
#: banking/klarna_kosma_integration/doctype/bank_reconciliation_tool_beta/bank_reconciliation_tool_beta.json
-#: banking/public/js/bank_reconciliation_beta/actions_panel/details_tab.js:81
+#: banking/public/js/bank_reconciliation_beta/actions_panel/details_tab.js:78
msgid "Bank Account"
msgstr ""
+#: banking/klarna_kosma_integration/doctype/bank_reconciliation_rule/bank_reconciliation_rule.py:20
+msgid "Bank Account and Target Account need to be in the same currency!"
+msgstr ""
+
#: banking/ebics/utils.py:215
msgid "Bank Account not found for IBAN {0}"
msgstr ""
@@ -177,6 +206,11 @@ msgstr ""
msgid "Bank Reconciliation"
msgstr ""
+#. Name of a DocType
+#: banking/klarna_kosma_integration/doctype/bank_reconciliation_rule/bank_reconciliation_rule.json
+msgid "Bank Reconciliation Rule"
+msgstr ""
+
#. Name of a DocType
#: banking/klarna_kosma_integration/doctype/bank_reconciliation_tool_beta/bank_reconciliation_tool_beta.json
msgid "Bank Reconciliation Tool Beta"
@@ -186,7 +220,7 @@ msgstr ""
msgid "Bank Transaction {0} Matched"
msgstr ""
-#: banking/overrides/bank_transaction.py:12
+#: banking/overrides/bank_transaction.py:15
msgid "Bank Transaction {0} is already fully reconciled"
msgstr ""
@@ -202,7 +236,7 @@ msgstr ""
msgid "Bank Transaction {0} reconciled with a new {1}"
msgstr ""
-#: banking/public/js/bank_reconciliation_beta/actions_panel/details_tab.js:64
+#: banking/public/js/bank_reconciliation_beta/actions_panel/details_tab.js:62
msgid "Bank Transaction {0} updated"
msgstr ""
@@ -328,6 +362,10 @@ msgstr ""
msgid "Company"
msgstr ""
+#: banking/overrides/bank_account.py:20
+msgid "Company Account and Bank Fee Account must be in the same currency!"
+msgstr ""
+
#: banking/ebics/doctype/ebics_user/ebics_user.js:182
msgid "Continue"
msgstr ""
@@ -366,7 +404,7 @@ msgstr ""
#. Label of a Link field in DocType 'SEPA Payment'
#: banking/ebics/doctype/sepa_payment/sepa_payment.json
-#: banking/public/js/bank_reconciliation_beta/actions_panel/details_tab.js:131
+#: banking/public/js/bank_reconciliation_beta/actions_panel/details_tab.js:122
msgid "Currency"
msgstr ""
@@ -386,18 +424,22 @@ msgstr ""
msgid "DOWNLOADED"
msgstr ""
-#: banking/public/js/bank_reconciliation_beta/actions_panel/details_tab.js:90
+#: banking/public/js/bank_reconciliation_beta/actions_panel/details_tab.js:86
#: banking/public/js/bank_reconciliation_beta/actions_panel/match_tab.js:488
#: banking/public/js/bank_reconciliation_beta/panel_manager.js:128
msgid "Date"
msgstr ""
-#: banking/public/js/bank_reconciliation_beta/actions_panel/details_tab.js:97
+#: banking/overrides/bank_transaction.py:79
+msgid "Debit or Credit is negative. Verify input data!"
+msgstr ""
+
+#: banking/public/js/bank_reconciliation_beta/actions_panel/details_tab.js:92
#: banking/public/js/bank_reconciliation_beta/panel_manager.js:130
msgid "Deposit"
msgstr ""
-#: banking/public/js/bank_reconciliation_beta/actions_panel/details_tab.js:116
+#: banking/public/js/bank_reconciliation_beta/actions_panel/details_tab.js:109
msgid "Description"
msgstr ""
@@ -414,6 +456,12 @@ msgstr ""
msgid "Did you mean {0}?"
msgstr ""
+#. Label of a Check field in DocType 'Bank Reconciliation Rule'
+#: banking/klarna_kosma_integration/doctype/bank_reconciliation_rule/bank_reconciliation_rule.json
+#: banking/klarna_kosma_integration/doctype/bank_reconciliation_rule/bank_reconciliation_rule_list.js:12
+msgid "Disabled"
+msgstr ""
+
#. Label of a Select field in DocType 'Banking Reference Mapping'
#. Label of a Link field in DocType 'Voucher Matching Default'
#: banking/klarna_kosma_integration/doctype/banking_reference_mapping/banking_reference_mapping.json
@@ -449,10 +497,11 @@ msgid "Downloaded"
msgstr ""
#: banking/ebics/doctype/sepa_payment_order/sepa_payment_order_list.js:22
+#: banking/klarna_kosma_integration/doctype/bank_reconciliation_rule/bank_reconciliation_rule_list.js:20
msgid "Draft"
msgstr ""
-#: banking/overrides/bank_transaction.py:33
+#: banking/overrides/bank_transaction.py:36
msgid "Due to Period Closing, you cannot reconcile unpaid vouchers with a Bank Transaction before {0}"
msgstr ""
@@ -607,6 +656,10 @@ msgstr ""
msgid "Failed"
msgstr ""
+#: banking/overrides/bank_transaction.py:125
+msgid "Failed to cancel {0}: {1}"
+msgstr ""
+
#: banking/public/js/bank_reconciliation_beta/actions_panel/create_tab.js:91
msgid "Failed to create {0} against {1}"
msgstr ""
@@ -627,7 +680,7 @@ msgstr ""
msgid "Failed to remove EBICS user registration."
msgstr ""
-#: banking/public/js/bank_reconciliation_beta/actions_panel/details_tab.js:51
+#: banking/public/js/bank_reconciliation_beta/actions_panel/details_tab.js:49
msgid "Failed to update {0}"
msgstr ""
@@ -654,7 +707,10 @@ msgstr ""
msgid "Filter by Reference Date"
msgstr ""
+#. Label of a Section Break field in DocType 'Bank Reconciliation Rule'
+#. Label of a Code field in DocType 'Bank Reconciliation Rule'
#. Label of a Section Break field in DocType 'Bank Reconciliation Tool Beta'
+#: banking/klarna_kosma_integration/doctype/bank_reconciliation_rule/bank_reconciliation_rule.json
#: banking/klarna_kosma_integration/doctype/bank_reconciliation_tool_beta/bank_reconciliation_tool_beta.json
msgid "Filters"
msgstr ""
@@ -704,7 +760,7 @@ msgid "H005"
msgstr ""
#: banking/public/js/bank_reconciliation_beta/actions_panel/create_tab.js:288
-#: banking/public/js/bank_reconciliation_beta/actions_panel/details_tab.js:207
+#: banking/public/js/bank_reconciliation_beta/actions_panel/details_tab.js:190
#: banking/public/js/bank_reconciliation_beta/actions_panel/match_tab.js:453
msgid "Hidden field for alignment"
msgstr ""
@@ -729,7 +785,7 @@ msgstr ""
msgid "IBAN {0} is invalid."
msgstr ""
-#: banking/public/js/bank_reconciliation_beta/actions_panel/details_tab.js:73
+#: banking/public/js/bank_reconciliation_beta/actions_panel/details_tab.js:71
msgid "ID"
msgstr ""
@@ -845,7 +901,11 @@ msgstr ""
msgid "No Transactions found for the current filters."
msgstr ""
-#: banking/public/js/bank_reconciliation_beta/actions_panel/details_tab.js:32
+#: banking/overrides/bank_transaction.py:73
+msgid "No bank account - verify data!"
+msgstr ""
+
+#: banking/public/js/bank_reconciliation_beta/actions_panel/details_tab.js:30
msgid "No changes to update"
msgstr ""
@@ -876,7 +936,7 @@ msgstr ""
msgid "Outstanding"
msgstr ""
-#: banking/overrides/bank_transaction.py:73
+#: banking/overrides/bank_transaction.py:107
msgid "Over Allocation"
msgstr ""
@@ -898,20 +958,20 @@ msgstr ""
msgid "Partner ID"
msgstr ""
-#: banking/public/js/bank_reconciliation_beta/actions_panel/details_tab.js:197
+#: banking/public/js/bank_reconciliation_beta/actions_panel/details_tab.js:180
#: banking/public/js/bank_reconciliation_beta/actions_panel/match_tab.js:496
msgid "Party"
msgstr ""
-#: banking/public/js/bank_reconciliation_beta/actions_panel/details_tab.js:148
+#: banking/public/js/bank_reconciliation_beta/actions_panel/details_tab.js:137
msgid "Party Account Number"
msgstr ""
-#: banking/public/js/bank_reconciliation_beta/actions_panel/details_tab.js:156
+#: banking/public/js/bank_reconciliation_beta/actions_panel/details_tab.js:144
msgid "Party IBAN"
msgstr ""
-#: banking/public/js/bank_reconciliation_beta/actions_panel/details_tab.js:179
+#: banking/public/js/bank_reconciliation_beta/actions_panel/details_tab.js:166
msgid "Party Type"
msgstr ""
@@ -966,6 +1026,10 @@ msgstr ""
msgid "Please confirm that the following keys are identical to the ones mentioned on your bank's letter:"
msgstr ""
+#: banking/klarna_kosma_integration/doctype/bank_reconciliation_rule/bank_reconciliation_rule.py:25
+msgid "Please define at least one filter!"
+msgstr ""
+
#. Description of the 'section_break_qjlc' (Section Break) field in DocType
#. 'EBICS User'
#: banking/ebics/doctype/ebics_user/ebics_user.json
@@ -993,6 +1057,10 @@ msgstr ""
msgid "Please set the 'Bank Account' filter"
msgstr ""
+#: banking/overrides/bank_transaction.py:132
+msgid "Please set the bank fee account in the bank account."
+msgstr ""
+
#. Label of a Password field in DocType 'Banking Settings'
#: banking/klarna_kosma_integration/doctype/banking_settings/banking_settings.json
msgid "Portal API Token"
@@ -1088,7 +1156,7 @@ msgstr ""
#. Label of a Data field in DocType 'SEPA Payment Order'
#: banking/ebics/doctype/sepa_payment_order/sepa_payment_order.json
#: banking/public/js/bank_reconciliation_beta/actions_panel/create_tab.js:181
-#: banking/public/js/bank_reconciliation_beta/actions_panel/details_tab.js:170
+#: banking/public/js/bank_reconciliation_beta/actions_panel/details_tab.js:157
msgid "Reference Number"
msgstr ""
@@ -1160,6 +1228,10 @@ msgstr ""
msgid "Sales Invoice"
msgstr ""
+#: banking/public/js/bank_reconciliation_beta/actions_panel/details_tab.js:199
+msgid "Save"
+msgstr ""
+
#. Label of a Section Break field in DocType 'SEPA Payment Order'
#: banking/ebics/doctype/sepa_payment_order/sepa_payment_order.json
msgid "Sender"
@@ -1235,10 +1307,6 @@ msgstr ""
msgid "Store the passphrase in the ERPNext database to enable automated, regular download of bank statements."
msgstr ""
-#: banking/public/js/bank_reconciliation_beta/actions_panel/details_tab.js:216
-msgid "Submit"
-msgstr ""
-
#: banking/klarna_kosma_integration/doctype/banking_settings/banking_settings.js:70
msgid "Subscriber"
msgstr ""
@@ -1282,12 +1350,18 @@ msgstr ""
#: banking/ebics/doctype/ebics_request/ebics_request.json
#: banking/ebics/doctype/ebics_user/ebics_user.json
#: banking/ebics/doctype/sepa_payment_order/sepa_payment_order.json
+#: banking/klarna_kosma_integration/doctype/bank_reconciliation_rule/bank_reconciliation_rule.json
#: banking/klarna_kosma_integration/doctype/bank_reconciliation_tool_beta/bank_reconciliation_tool_beta.json
#: banking/klarna_kosma_integration/doctype/banking_settings/banking_settings.json
msgid "System Manager"
msgstr ""
-#: banking/overrides/bank_transaction.py:70
+#. Label of a Link field in DocType 'Bank Reconciliation Rule'
+#: banking/klarna_kosma_integration/doctype/bank_reconciliation_rule/bank_reconciliation_rule.json
+msgid "Target Account"
+msgstr ""
+
+#: banking/overrides/bank_transaction.py:104
msgid "The Bank Transaction is over-allocated by {0} at row {1}."
msgstr ""
@@ -1317,7 +1391,7 @@ msgstr ""
msgid "These DocTypes are pre-enabled in the 'Match Voucher' tab."
msgstr ""
-#: banking/public/js/bank_reconciliation_beta/actions_panel/details_tab.js:123
+#: banking/public/js/bank_reconciliation_beta/actions_panel/details_tab.js:115
msgid "To Allocate"
msgstr ""
@@ -1366,7 +1440,7 @@ msgstr ""
msgid "Unpaid Vouchers"
msgstr ""
-#: banking/public/js/bank_reconciliation_beta/actions_panel/details_tab.js:165
+#: banking/public/js/bank_reconciliation_beta/actions_panel/details_tab.js:152
msgid "Update"
msgstr ""
@@ -1374,7 +1448,7 @@ msgstr ""
msgid "Update Amounts"
msgstr ""
-#: banking/public/js/bank_reconciliation_beta/actions_panel/details_tab.js:48
+#: banking/public/js/bank_reconciliation_beta/actions_panel/details_tab.js:46
msgid "Updating ..."
msgstr ""
@@ -1447,7 +1521,7 @@ msgstr ""
msgid "We'll try to download new transactions from today, using camt.052."
msgstr ""
-#: banking/public/js/bank_reconciliation_beta/actions_panel/details_tab.js:105
+#: banking/public/js/bank_reconciliation_beta/actions_panel/details_tab.js:99
#: banking/public/js/bank_reconciliation_beta/panel_manager.js:129
msgid "Withdrawal"
msgstr ""
@@ -1456,6 +1530,10 @@ msgstr ""
msgid "You selected protocol version {0}, but the bank {1} only supports {2}."
msgstr ""
+#: banking/overrides/bank_transaction.py:213
+msgid "by automatic rule {0}"
+msgstr ""
+
#: banking/klarna_kosma_integration/doctype/bank_reconciliation_tool_beta/bank_reconciliation_tool_beta.js:270
#: banking/klarna_kosma_integration/doctype/bank_reconciliation_tool_beta/bank_reconciliation_tool_beta.js:304
msgid "to import bank transactions for {0}."
diff --git a/banking/overrides/bank_account.py b/banking/overrides/bank_account.py
index d7f9a0d8..c65a93dd 100644
--- a/banking/overrides/bank_account.py
+++ b/banking/overrides/bank_account.py
@@ -9,6 +9,10 @@ def before_validate(doc, method):
def validate(doc, method):
+ validate_account_currencies(doc)
+
+
+def validate_account_currencies(doc):
if doc.account and doc.bank_fee_account:
bank_account_currency = frappe.db.get_value("Account", doc.account, "account_currency")
bank_fee_currency = frappe.db.get_value("Account", doc.bank_fee_account, "account_currency")
diff --git a/banking/overrides/bank_transaction.py b/banking/overrides/bank_transaction.py
index 32ab2ade..d6cf79d6 100644
--- a/banking/overrides/bank_transaction.py
+++ b/banking/overrides/bank_transaction.py
@@ -2,7 +2,6 @@
import frappe
from erpnext.accounts.doctype.bank_transaction.bank_transaction import BankTransaction
-from erpnext.accounts.doctype.journal_entry.journal_entry import JournalEntry
from frappe import _
from frappe.core.utils import find
from frappe.utils import flt, getdate
@@ -63,6 +62,37 @@ def is_duplicate_reference(self, voucher_type, voucher_name):
)
+def before_validate(doc, method):
+ ensure_positive_deposit_withdrawal_fees(doc, method)
+
+
+def before_submit(doc, method):
+ date = doc.date or frappe.utils.nowdate()
+
+ if not doc.bank_account:
+ frappe.throw(_("No bank account - verify data!"))
+
+ if doc.deposit == 0 and doc.withdrawal == 0:
+ return
+
+ if doc.deposit < 0 or doc.withdrawal < 0:
+ frappe.throw(_("Debit or Credit is negative. Verify input data!"))
+
+ # Generic data catching
+ # Get Company
+ company_doc = frappe.get_cached_doc("Company", doc.company)
+
+ # Bank account debit/credit
+ account = frappe.db.get_value("Bank Account", doc.bank_account, "account")
+ # End Generic data catching
+
+ # Set initial values
+ debit, credit = (doc.deposit, 0) if doc.deposit > 0 else (0, doc.withdrawal)
+
+ doc = create_je_bank_fees(doc, company_doc, date, account, debit, credit)
+ doc = create_je_automatic_rules(doc, company_doc, date, account, debit, credit)
+
+
def on_update_after_submit(doc, event):
"""Validate if the Bank Transaction is over-allocated."""
to_allocate = flt(doc.withdrawal or doc.deposit)
@@ -78,10 +108,6 @@ def on_update_after_submit(doc, event):
)
-def before_validate(doc, method):
- ensure_positive_deposit_withdrawal_fees(doc, method)
-
-
def on_cancel(doc, method):
# Cancel the journal entries created by this Bank Transaction
auto_created_journal_entries = frappe.get_all(
@@ -96,151 +122,96 @@ def on_cancel(doc, method):
if doc.docstatus == 1:
doc.cancel()
except Exception as e:
- frappe.msgprint(f"Failed to cancel {journal_entry}: {e}")
-
-
-def before_submit(doc, method):
- date = doc.date or frappe.utils.nowdate()
-
- if not doc.bank_account:
- frappe.throw(_("No bank account - verify data!"))
-
- if doc.deposit == 0 and doc.withdrawal == 0:
- return
+ frappe.msgprint(_("Failed to cancel {0}: {1}").format(journal_entry, e))
- if doc.deposit < 0 or doc.withdrawal < 0:
- frappe.throw(_("Debit or Credit is negative. Verify input data!"))
-
- # Generic data catching
- # Get Company
- company_doc = frappe.get_cached_doc("Company", doc.company)
-
- # Bank account debit/credit
- account = frappe.db.get_value("Bank Account", doc.bank_account, "account")
- # End Generic data catching
- # Set initial values
- debit, credit = (doc.deposit, 0) if doc.deposit > 0 else (0, doc.withdrawal)
+def create_je_bank_fees(doc, company_doc, date, account, debit, credit):
# Create a journal entry for the bank fees
- if doc.bank_account:
- bank_fee_account = frappe.db.get_value("Bank Account", doc.bank_account, "bank_fee_account")
- if not bank_fee_account:
- frappe.throw(_("Please set the bank fee account in the bank account."))
-
- # First Step: Book visible bank fees
- if doc.included_fee > 0 and bank_fee_account:
- included_fee = doc.included_fee
- # only correct the credit value if set, as the debit value (deposit) is never including the fee.
- if credit > 0:
- credit = credit - included_fee
- if debit > 0:
- debit = debit - included_fee
- je_fee_name = create_fee_journal_entry(
- doc, company_doc, date, account, bank_fee_account, included_fee
- )
- doc.append(
- "payment_entries",
- {
- "payment_document": "Journal Entry",
- "payment_entry": je_fee_name,
- "allocated_amount": included_fee,
- },
- )
- # Set manually the un-/allocated amounts, as this value is already set and needs to be updated
- doc.allocated_amount = included_fee
- doc.unallocated_amount = debit + credit
- if doc.unallocated_amount == 0:
- doc.status = "Reconciled"
- else:
- included_fee = 0
-
- # Second step: Automatic reconcilation based on the Bank Reconciliation Rules
- bank_reconciliation_rules = frappe.db.get_list(
- "Bank Reconciliation Rule",
- filters={
- "disabled": 0,
- "bank_account": doc.bank_account,
- "docstatus": 1,
+ bank_fee_account = frappe.db.get_value("Bank Account", doc.bank_account, "bank_fee_account")
+ if not bank_fee_account:
+ frappe.throw(_("Please set the bank fee account in the bank account."))
+
+ # First Step: Book visible bank fees
+ if doc.included_fee > 0 and bank_fee_account:
+ included_fee = doc.included_fee
+ # only correct the credit value if set, as the debit value (deposit) is never including the fee.
+ if credit > 0:
+ credit = credit - included_fee
+ if debit > 0:
+ debit = debit - included_fee
+ je_fee_name = create_automatic_journal_entry(
+ doc, company_doc, date, account, bank_fee_account, None, 0, included_fee
+ )
+ doc.append(
+ "payment_entries",
+ {
+ "payment_document": "Journal Entry",
+ "payment_entry": je_fee_name,
+ "allocated_amount": included_fee,
},
- fields=["name", "target_account", "filters"],
- as_list=True,
)
- for br_rule in bank_reconciliation_rules:
- # Check if line matches filter
- if br_rule[2]:
- filters = json.loads(br_rule[2])
- if filters:
- condition_met = evaluate_filters(doc, filters)
- if condition_met:
- rule = br_rule[0]
- target_account = br_rule[1]
- je_auto_name = create_automatic_journal_entry(
- doc, company_doc, date, account, target_account, rule, debit, credit
- )
- doc.append(
- "payment_entries",
- {
- "payment_document": "Journal Entry",
- "payment_entry": je_auto_name,
- "allocated_amount": debit + credit,
- },
- )
- # Set manually the un-/allocated amounts, as this value is already set and needs to be updated
- doc.allocated_amount = doc.allocated_amount + debit + credit
- # Set remaining debit and credit to 0, so no cash in transit is generated
- debit = 0
- credit = 0
- doc.unallocated_amount = 0
- doc.status = "Reconciled"
- break
-
- if debit == 0 and credit == 0:
- return
+ # Set manually the un-/allocated amounts, as this value is already set and needs to be updated
+ doc.allocated_amount = included_fee
+ doc.unallocated_amount = debit + credit
+ if doc.unallocated_amount == 0:
+ doc.status = "Reconciled"
- journal_entry = frappe.new_doc("Journal Entry")
- journal_entry.voucher_type = "Bank Entry"
- journal_entry.title = f"BT ID {doc.name}"
- journal_entry.posting_date = date
- journal_entry.company = doc.company
- journal_entry.user_remark = f"Auto-created from BT: {doc.name}"
- journal_entry.cheque_no = doc.name
- journal_entry.cheque_date = date
- journal_entry.multi_currency = 1
- journal_entry.clearance_date = frappe.utils.today()
+ return doc
- journal_entry.append(
- "accounts",
- {
- "account": account,
+
+def create_je_automatic_rules(doc, company_doc, date, account, debit, credit):
+ # Second step: Automatic reconcilation based on the Bank Reconciliation Rules
+ bank_reconciliation_rules = frappe.db.get_list(
+ "Bank Reconciliation Rule",
+ filters={
+ "disabled": 0,
"bank_account": doc.bank_account,
- "debit_in_account_currency": debit,
- "credit_in_account_currency": credit,
- "cost_center": company_doc.cost_center,
+ "docstatus": 1,
},
+ fields=["name", "target_account", "filters"],
+ as_list=True,
)
-
- # If by any filter, no accounting entries (single lines) are present, do not create a journal entry.
- if len(journal_entry.accounts) > 0:
- journal_entry.insert()
-
- # Create Exchange Gain/Loss Line
- JournalEntry.get_balance(journal_entry, difference_account=company_doc.exchange_gain_loss_account)
-
- journal_entry.submit()
-
- else:
- frappe.msgprint(
- _("No journal entry was created, as no data was present in the Accounting Entries table.")
- )
-
-
-def create_automatic_journal_entry(doc, company_doc, date, account, target_account, rule, debit, credit):
+ for br_rule in bank_reconciliation_rules:
+ # Check if line matches filter
+ if br_rule[2]:
+ filters = json.loads(br_rule[2])
+ if filters:
+ condition_met = evaluate_filters(doc, filters)
+ if condition_met:
+ rule = br_rule[0]
+ target_account = br_rule[1]
+ je_auto_name = create_automatic_journal_entry(
+ doc, company_doc, date, account, target_account, rule, debit, credit
+ )
+ doc.append(
+ "payment_entries",
+ {
+ "payment_document": "Journal Entry",
+ "payment_entry": je_auto_name,
+ "allocated_amount": debit + credit,
+ },
+ )
+ # Set manually the un-/allocated amounts, as this value is already set and needs to be updated
+ doc.allocated_amount = doc.allocated_amount + debit + credit
+ # Set remaining debit and credit to 0, so no cash in transit is generated
+ debit = 0
+ credit = 0
+ doc.unallocated_amount = 0
+ doc.status = "Reconciled"
+ break
+
+ return doc
+
+
+def create_automatic_journal_entry(
+ doc, company_doc, date, account, target_account, rule=None, debit=0, credit=0
+):
journal_entry = frappe.new_doc("Journal Entry")
journal_entry.voucher_type = "Journal Entry"
- journal_entry.title = f"BT ID {doc.name}"
journal_entry.posting_date = date
journal_entry.company = doc.company
- journal_entry.user_remark = f"Auto-created from BT: {doc.name} by automatic rule {rule}"
+ rule_part = " " + _("by automatic rule {0}").format(rule) if rule else ""
+ journal_entry.user_remark = _("Auto-created from BT: {0}").format(doc.name) + rule_part
journal_entry.cheque_no = doc.name
journal_entry.cheque_date = date
journal_entry.multi_currency = 1
@@ -253,7 +224,7 @@ def create_automatic_journal_entry(doc, company_doc, date, account, target_accou
"account": account,
"bank_account": doc.bank_account,
"debit_in_account_currency": debit,
- "credit_in_account_currency": credit,
+ "credit_in_account_currency": credit, # included_fee
"cost_center": company_doc.cost_center,
},
)
@@ -264,7 +235,7 @@ def create_automatic_journal_entry(doc, company_doc, date, account, target_accou
{
"account": target_account,
"bank_account": "",
- "debit_in_account_currency": credit,
+ "debit_in_account_currency": credit, # included_fee
"credit_in_account_currency": debit,
"cost_center": company_doc.cost_center,
},
@@ -275,52 +246,10 @@ def create_automatic_journal_entry(doc, company_doc, date, account, target_accou
return journal_entry.name
-def create_fee_journal_entry(doc, company_doc, date, account, bank_fee_account, included_fee):
- journal_entry = frappe.new_doc("Journal Entry")
- journal_entry.voucher_type = "Journal Entry"
- journal_entry.title = f"BT ID {doc.name}"
- journal_entry.posting_date = date
- journal_entry.company = doc.company
- journal_entry.user_remark = f"Auto-created from BT: {doc.name}"
- journal_entry.cheque_no = doc.name
- journal_entry.cheque_date = date
- journal_entry.multi_currency = 1
- journal_entry.clearance_date = frappe.utils.today()
-
- # Bank fee entry
- journal_entry.append(
- "accounts",
- {
- "account": account,
- "bank_account": doc.bank_account,
- "debit_in_account_currency": 0,
- "credit_in_account_currency": included_fee,
- "cost_center": company_doc.cost_center,
- },
- )
-
- # Fee account entry
- journal_entry.append(
- "accounts",
- {
- "account": bank_fee_account,
- "bank_account": "",
- "debit_in_account_currency": included_fee,
- "credit_in_account_currency": 0,
- "cost_center": company_doc.cost_center,
- },
- )
-
- journal_entry.submit()
-
- return journal_entry.name
-
-
def ensure_positive_deposit_withdrawal_fees(doc, method):
doc.deposit = abs(flt(doc.deposit) or 0.0)
doc.withdrawal = abs(flt(doc.withdrawal) or 0.0)
doc.included_fee = abs(flt(doc.included_fee) or 0.0)
doc.excluded_fee = abs(flt(doc.excluded_fee) or 0.0)
- if method == "before_validate":
- # Re-call this function as the original function runs before this one and values are not converted
- BankTransaction.handle_excluded_fee(doc)
+ # Re-call this function as the original function runs before this one and values are not converted
+ BankTransaction.handle_excluded_fee(doc)
From 253ea0e11af8766ffa028ac2b42d67677ea2a848 Mon Sep 17 00:00:00 2001
From: 0xD0M1M0 <76812428+0xD0M1M0@users.noreply.github.com>
Date: Wed, 17 Dec 2025 21:50:20 +0000
Subject: [PATCH 04/38] chore: remove comments
---
banking/overrides/bank_transaction.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/banking/overrides/bank_transaction.py b/banking/overrides/bank_transaction.py
index d6cf79d6..eef72fc4 100644
--- a/banking/overrides/bank_transaction.py
+++ b/banking/overrides/bank_transaction.py
@@ -224,7 +224,7 @@ def create_automatic_journal_entry(
"account": account,
"bank_account": doc.bank_account,
"debit_in_account_currency": debit,
- "credit_in_account_currency": credit, # included_fee
+ "credit_in_account_currency": credit,
"cost_center": company_doc.cost_center,
},
)
@@ -235,7 +235,7 @@ def create_automatic_journal_entry(
{
"account": target_account,
"bank_account": "",
- "debit_in_account_currency": credit, # included_fee
+ "debit_in_account_currency": credit,
"credit_in_account_currency": debit,
"cost_center": company_doc.cost_center,
},
From c9c79976a07a5c22ffe2b93f15832dde968018e0 Mon Sep 17 00:00:00 2001
From: 0xD0M1M0 <76812428+0xD0M1M0@users.noreply.github.com>
Date: Wed, 17 Dec 2025 22:27:12 +0000
Subject: [PATCH 05/38] fix: create_je_bank_fees only if included fee is set
---
banking/overrides/bank_transaction.py | 52 ++++++++++++++-------------
1 file changed, 27 insertions(+), 25 deletions(-)
diff --git a/banking/overrides/bank_transaction.py b/banking/overrides/bank_transaction.py
index eef72fc4..5f9798c5 100644
--- a/banking/overrides/bank_transaction.py
+++ b/banking/overrides/bank_transaction.py
@@ -126,35 +126,37 @@ def on_cancel(doc, method):
def create_je_bank_fees(doc, company_doc, date, account, debit, credit):
- # Create a journal entry for the bank fees
+ # First step: Create a journal entry for included bank fees
+ included_fee = doc.included_fee
+
+ if included_fee <= 0:
+ return
+
bank_fee_account = frappe.db.get_value("Bank Account", doc.bank_account, "bank_fee_account")
if not bank_fee_account:
frappe.throw(_("Please set the bank fee account in the bank account."))
- # First Step: Book visible bank fees
- if doc.included_fee > 0 and bank_fee_account:
- included_fee = doc.included_fee
- # only correct the credit value if set, as the debit value (deposit) is never including the fee.
- if credit > 0:
- credit = credit - included_fee
- if debit > 0:
- debit = debit - included_fee
- je_fee_name = create_automatic_journal_entry(
- doc, company_doc, date, account, bank_fee_account, None, 0, included_fee
- )
- doc.append(
- "payment_entries",
- {
- "payment_document": "Journal Entry",
- "payment_entry": je_fee_name,
- "allocated_amount": included_fee,
- },
- )
- # Set manually the un-/allocated amounts, as this value is already set and needs to be updated
- doc.allocated_amount = included_fee
- doc.unallocated_amount = debit + credit
- if doc.unallocated_amount == 0:
- doc.status = "Reconciled"
+ # only correct the credit value if set, as the debit value (deposit) is never including the fee.
+ if credit > 0:
+ credit = credit - included_fee
+ if debit > 0:
+ debit = debit - included_fee
+ je_fee_name = create_automatic_journal_entry(
+ doc, company_doc, date, account, bank_fee_account, None, 0, included_fee
+ )
+ doc.append(
+ "payment_entries",
+ {
+ "payment_document": "Journal Entry",
+ "payment_entry": je_fee_name,
+ "allocated_amount": included_fee,
+ },
+ )
+ # Set manually the un-/allocated amounts, as this value is already set and needs to be updated
+ doc.allocated_amount = included_fee
+ doc.unallocated_amount = debit + credit
+ if doc.unallocated_amount == 0:
+ doc.status = "Reconciled"
return doc
From 7b0037e1db2c523537da424ce2d9f9cadabd3657 Mon Sep 17 00:00:00 2001
From: 0xD0M1M0 <76812428+0xD0M1M0@users.noreply.github.com>
Date: Thu, 18 Dec 2025 07:26:40 +0000
Subject: [PATCH 06/38] fix: bank_account
---
banking/overrides/bank_transaction.py | 8 ++------
1 file changed, 2 insertions(+), 6 deletions(-)
diff --git a/banking/overrides/bank_transaction.py b/banking/overrides/bank_transaction.py
index 5f9798c5..df88e479 100644
--- a/banking/overrides/bank_transaction.py
+++ b/banking/overrides/bank_transaction.py
@@ -89,8 +89,8 @@ def before_submit(doc, method):
# Set initial values
debit, credit = (doc.deposit, 0) if doc.deposit > 0 else (0, doc.withdrawal)
- doc = create_je_bank_fees(doc, company_doc, date, account, debit, credit)
- doc = create_je_automatic_rules(doc, company_doc, date, account, debit, credit)
+ create_je_bank_fees(doc, company_doc, date, account, debit, credit)
+ create_je_automatic_rules(doc, company_doc, date, account, debit, credit)
def on_update_after_submit(doc, event):
@@ -158,8 +158,6 @@ def create_je_bank_fees(doc, company_doc, date, account, debit, credit):
if doc.unallocated_amount == 0:
doc.status = "Reconciled"
- return doc
-
def create_je_automatic_rules(doc, company_doc, date, account, debit, credit):
# Second step: Automatic reconcilation based on the Bank Reconciliation Rules
@@ -202,8 +200,6 @@ def create_je_automatic_rules(doc, company_doc, date, account, debit, credit):
doc.status = "Reconciled"
break
- return doc
-
def create_automatic_journal_entry(
doc, company_doc, date, account, target_account, rule=None, debit=0, credit=0
From 1134e574e4f7ffea69db81d113ed98903c68dffd Mon Sep 17 00:00:00 2001
From: 0xD0M1M0 <76812428+0xD0M1M0@users.noreply.github.com>
Date: Tue, 6 Jan 2026 12:15:19 +0000
Subject: [PATCH 07/38] fix: deposit fees and rule credit value corrected
---
banking/overrides/bank_transaction.py | 28 +++++++++++++++++----------
1 file changed, 18 insertions(+), 10 deletions(-)
diff --git a/banking/overrides/bank_transaction.py b/banking/overrides/bank_transaction.py
index df88e479..8ed292de 100644
--- a/banking/overrides/bank_transaction.py
+++ b/banking/overrides/bank_transaction.py
@@ -88,9 +88,11 @@ def before_submit(doc, method):
# Set initial values
debit, credit = (doc.deposit, 0) if doc.deposit > 0 else (0, doc.withdrawal)
+ included_fee = doc.included_fee or 0
create_je_bank_fees(doc, company_doc, date, account, debit, credit)
- create_je_automatic_rules(doc, company_doc, date, account, debit, credit)
+ credit_no_fee = max(0, credit - included_fee)
+ create_je_automatic_rules(doc, company_doc, date, account, debit, credit_no_fee)
def on_update_after_submit(doc, event):
@@ -136,25 +138,31 @@ def create_je_bank_fees(doc, company_doc, date, account, debit, credit):
if not bank_fee_account:
frappe.throw(_("Please set the bank fee account in the bank account."))
- # only correct the credit value if set, as the debit value (deposit) is never including the fee.
- if credit > 0:
- credit = credit - included_fee
- if debit > 0:
- debit = debit - included_fee
je_fee_name = create_automatic_journal_entry(
doc, company_doc, date, account, bank_fee_account, None, 0, included_fee
)
+
+ if credit > 0:
+ # only correct the credit value if set, as the debit value (deposit) is never including the fee.
+ credit_no_fee = credit - included_fee
+ # Set manually the un-/allocated amounts, as this value is already set and needs to be updated
+ doc.allocated_amount = included_fee
+ doc.unallocated_amount = debit + (credit_no_fee or 0)
+ allocated_amount = included_fee
+ else:
+ allocated_amount = 0
+
+ # Add the journal entry for a deposit fee with an allocated_amount of 0, as the fee is not included in the deposit itself.
+ # Otherwise this would cause the un-/allocated amounts to fail.
doc.append(
"payment_entries",
{
"payment_document": "Journal Entry",
"payment_entry": je_fee_name,
- "allocated_amount": included_fee,
+ "allocated_amount": allocated_amount,
},
)
- # Set manually the un-/allocated amounts, as this value is already set and needs to be updated
- doc.allocated_amount = included_fee
- doc.unallocated_amount = debit + credit
+
if doc.unallocated_amount == 0:
doc.status = "Reconciled"
From fa85b90c66bd5e63a02140d1da8b90e71f2fbe63 Mon Sep 17 00:00:00 2001
From: 0xD0M1M0 <76812428+0xD0M1M0@users.noreply.github.com>
Date: Tue, 6 Jan 2026 12:18:07 +0000
Subject: [PATCH 08/38] feat: added test cases
---
.../test_bank_reconciliation_rule.py | 61 ++-
banking/overrides/test_bank_account.py | 47 ++
banking/overrides/test_bank_transaction.py | 500 ++++++++++++++++++
3 files changed, 606 insertions(+), 2 deletions(-)
create mode 100644 banking/overrides/test_bank_account.py
create mode 100644 banking/overrides/test_bank_transaction.py
diff --git a/banking/klarna_kosma_integration/doctype/bank_reconciliation_rule/test_bank_reconciliation_rule.py b/banking/klarna_kosma_integration/doctype/bank_reconciliation_rule/test_bank_reconciliation_rule.py
index 77dc6280..61e4c823 100644
--- a/banking/klarna_kosma_integration/doctype/bank_reconciliation_rule/test_bank_reconciliation_rule.py
+++ b/banking/klarna_kosma_integration/doctype/bank_reconciliation_rule/test_bank_reconciliation_rule.py
@@ -1,9 +1,66 @@
# Copyright (c) 2025, ALYF GmbH and Contributors
# See license.txt
-# import frappe
+import frappe
from frappe.tests.utils import FrappeTestCase
class TestBankReconciliationRule(FrappeTestCase):
- pass
+ def test_validate_account_currencies(self):
+ account_1 = frappe.get_doc(
+ {
+ "doctype": "Account",
+ "account_name": "_Test_Account_EUR",
+ "account_currency": "EUR",
+ "parent_account": frappe.get_all("Account", filters={"is_group": 1}, pluck="name")[0],
+ }
+ ).insert(ignore_permissions=True, ignore_mandatory=True, ignore_links=True)
+
+ account_2 = frappe.get_doc(
+ {
+ "doctype": "Account",
+ "account_name": "_Test_Account_USD",
+ "account_currency": "USD",
+ "parent_account": frappe.get_all("Account", filters={"is_group": 1}, pluck="name")[0],
+ }
+ ).insert(ignore_permissions=True, ignore_mandatory=True, ignore_links=True)
+
+ ba = frappe.get_doc(
+ {
+ "doctype": "Bank Account",
+ "account_name": "_Test_B_Account",
+ "account": account_1.name,
+ "bank": "_Test_Bank",
+ "is_company_account": 1,
+ }
+ ).insert(ignore_permissions=True, ignore_links=True)
+
+ brr_doc = frappe.new_doc("Bank Reconciliation Rule")
+ brr_doc.bank_account = ba.name
+ brr_doc.target_account = account_2.name
+
+ with self.assertRaisesRegex(
+ frappe.ValidationError,
+ "Bank Account and Target Account need to be in the same currency!",
+ ):
+ brr_doc.validate_account_currencies()
+
+ frappe.db.delete("Bank Account", ba.name)
+ frappe.db.delete("Account", account_1.name)
+ frappe.db.delete("Account", account_2.name)
+
+ def test_validate_filters(self):
+ brr_doc = frappe.new_doc("Bank Reconciliation Rule")
+ brr_doc.filters = None
+ with self.assertRaisesRegex(
+ frappe.ValidationError,
+ "Please define at least one filter!",
+ ):
+ brr_doc.validate_filters()
+
+ brr_doc.filters = []
+ with self.assertRaisesRegex(
+ frappe.ValidationError,
+ "Please define at least one filter!",
+ ):
+ brr_doc.validate_filters()
diff --git a/banking/overrides/test_bank_account.py b/banking/overrides/test_bank_account.py
new file mode 100644
index 00000000..9dc7ac79
--- /dev/null
+++ b/banking/overrides/test_bank_account.py
@@ -0,0 +1,47 @@
+# Copyright (c) 2025, ALYF GmbH and Contributors
+# See license.txt
+
+import frappe
+from frappe.tests.utils import FrappeTestCase
+
+
+class TestBankReconciliationRule(FrappeTestCase):
+ def test_validate_account_currencies(self):
+ account_1 = frappe.get_doc(
+ {
+ "doctype": "Account",
+ "account_name": "_Test_Account_EUR",
+ "account_currency": "EUR",
+ "parent_account": frappe.get_all("Account", filters={"is_group": 1}, pluck="name")[0],
+ }
+ ).insert(ignore_permissions=True, ignore_mandatory=True, ignore_links=True)
+
+ account_2 = frappe.get_doc(
+ {
+ "doctype": "Account",
+ "account_name": "_Test_Account_USD",
+ "account_currency": "USD",
+ "parent_account": frappe.get_all("Account", filters={"is_group": 1}, pluck="name")[0],
+ }
+ ).insert(ignore_permissions=True, ignore_mandatory=True, ignore_links=True)
+
+ ba = frappe.get_doc(
+ {
+ "doctype": "Bank Account",
+ "account_name": "_Test_B_Account",
+ "account": account_1.name,
+ "bank": "_Test_Bank",
+ "is_company_account": 1,
+ "bank_fee_account": account_2.name,
+ }
+ )
+
+ with self.assertRaisesRegex(
+ frappe.ValidationError,
+ "Company Account and Bank Fee Account must be in the same currency!",
+ ):
+ ba.insert(ignore_permissions=True, ignore_links=True)
+
+ frappe.db.delete("Bank Account", ba.name)
+ frappe.db.delete("Account", account_1.name)
+ frappe.db.delete("Account", account_2.name)
diff --git a/banking/overrides/test_bank_transaction.py b/banking/overrides/test_bank_transaction.py
new file mode 100644
index 00000000..db55881a
--- /dev/null
+++ b/banking/overrides/test_bank_transaction.py
@@ -0,0 +1,500 @@
+# Copyright (c) 2025, ALYF GmbH and Contributors
+# See license.txt
+
+from unittest.mock import patch
+
+import frappe
+from frappe.tests.utils import FrappeTestCase
+
+
+class TestBankReconciliationRule(FrappeTestCase):
+ def test_ensure_positive_deposit_withdrawal_fees(self):
+ from banking.overrides.bank_transaction import ensure_positive_deposit_withdrawal_fees
+
+ doc = frappe.new_doc("Bank Transaction")
+ doc.deposit = -2.0
+ doc.withdrawal = 0.0
+ doc.included_fee = -1.0
+ doc.excluded_fee = -1.0
+
+ ensure_positive_deposit_withdrawal_fees(doc, None)
+
+ self.assertEqual(doc.deposit, 1.0)
+ self.assertEqual(doc.withdrawal, 0.0)
+ self.assertEqual(doc.included_fee, 2.0)
+ self.assertEqual(doc.excluded_fee, 0.0)
+
+ doc.deposit = 0.0
+ doc.withdrawal = -1.0
+ doc.included_fee = -1.0
+ doc.excluded_fee = -1.0
+
+ ensure_positive_deposit_withdrawal_fees(doc, None)
+
+ self.assertEqual(doc.deposit, 0.0)
+ self.assertEqual(doc.withdrawal, 2.0)
+ self.assertEqual(doc.included_fee, 2.0)
+ self.assertEqual(doc.excluded_fee, 0.0)
+
+ doc.deposit = None
+ doc.withdrawal = None
+ doc.included_fee = None
+ doc.excluded_fee = None
+
+ ensure_positive_deposit_withdrawal_fees(doc, None)
+
+ self.assertEqual(doc.deposit, 0.0)
+ self.assertEqual(doc.withdrawal, 0.0)
+ self.assertEqual(doc.included_fee, 0.0)
+ self.assertEqual(doc.excluded_fee, 0.0)
+
+ @patch("banking.overrides.bank_transaction.create_je_automatic_rules")
+ @patch("banking.overrides.bank_transaction.create_je_bank_fees")
+ def test_before_submit(self, mock_create_bank_fees, mock_create_auto_rules):
+ from banking.overrides.bank_transaction import before_submit
+
+ account_1 = frappe.get_doc(
+ {
+ "doctype": "Account",
+ "account_name": "_Test_Account_EUR",
+ "account_currency": "EUR",
+ "parent_account": frappe.get_all("Account", filters={"is_group": 1}, pluck="name")[0],
+ }
+ ).insert(ignore_permissions=True, ignore_mandatory=True, ignore_links=True)
+
+ account_2 = frappe.get_doc(
+ {
+ "doctype": "Account",
+ "account_name": "_Test_Account_EUR_Fee",
+ "account_currency": "EUR",
+ "parent_account": frappe.get_all("Account", filters={"is_group": 1}, pluck="name")[0],
+ }
+ ).insert(ignore_permissions=True, ignore_mandatory=True, ignore_links=True)
+
+ ba = frappe.get_doc(
+ {
+ "doctype": "Bank Account",
+ "account_name": "_Test_B_Account",
+ "account": account_1.name,
+ "bank": "_Test_Bank",
+ "is_company_account": 1,
+ "bank_fee_account": account_2.name,
+ }
+ ).insert(ignore_permissions=True, ignore_mandatory=True, ignore_links=True)
+
+ bt = frappe.get_doc(
+ {
+ "doctype": "Bank Transaction",
+ "company": "_Test Company",
+ }
+ )
+
+ with self.assertRaisesRegex(
+ frappe.ValidationError,
+ "No bank account - verify data!",
+ ):
+ before_submit(bt, None)
+
+ bt.bank_account = ba.name
+ bt.deposit = -1.0
+
+ with self.assertRaisesRegex(
+ frappe.ValidationError,
+ "Debit or Credit is negative. Verify input data!",
+ ):
+ before_submit(bt, None)
+
+ bt.deposit = 0.0
+ bt.withdrawal = -1.0
+
+ with self.assertRaisesRegex(
+ frappe.ValidationError,
+ "Debit or Credit is negative. Verify input data!",
+ ):
+ before_submit(bt, None)
+
+ bt.withdrawal = 1.0
+
+ before_submit(bt, None)
+ mock_create_bank_fees.assert_called_once()
+ mock_create_auto_rules.assert_called_once()
+
+ frappe.db.delete("Bank Account", ba.name)
+ frappe.db.delete("Account", account_1.name)
+ frappe.db.delete("Account", account_2.name)
+
+ @patch("banking.overrides.bank_transaction.create_automatic_journal_entry")
+ def test_create_je_bank_fees_withdrawal(self, mock_create_je):
+ from banking.overrides.bank_transaction import create_je_bank_fees
+
+ mock_create_je.return_value = "JE-TEST-0001"
+ date = "2025-01-01"
+
+ account_1 = frappe.get_doc(
+ {
+ "doctype": "Account",
+ "account_name": "_Test_Account_EUR",
+ "account_currency": "EUR",
+ "parent_account": frappe.get_all("Account", filters={"is_group": 1}, pluck="name")[0],
+ }
+ ).insert(ignore_permissions=True, ignore_mandatory=True, ignore_links=True)
+
+ account_2 = frappe.get_doc(
+ {
+ "doctype": "Account",
+ "account_name": "_Test_Account_EUR_Fee",
+ "account_currency": "EUR",
+ "parent_account": frappe.get_all("Account", filters={"is_group": 1}, pluck="name")[0],
+ }
+ ).insert(ignore_permissions=True, ignore_mandatory=True, ignore_links=True)
+
+ ba = frappe.get_doc(
+ {
+ "doctype": "Bank Account",
+ "account_name": "_Test_B_Account",
+ "account": account_1.name,
+ "bank": "_Test_Bank",
+ "is_company_account": 1,
+ "bank_fee_account": account_2.name,
+ }
+ ).insert(ignore_permissions=True, ignore_mandatory=True, ignore_links=True)
+
+ bt = frappe.get_doc(
+ {
+ "doctype": "Bank Transaction",
+ "company": "_Test Company",
+ "withdrawal": 5.0,
+ "included_fee": 1.0,
+ "bank_account": ba.name,
+ }
+ ).insert(ignore_permissions=True, ignore_mandatory=True, ignore_links=True)
+
+ company_doc = frappe.get_cached_doc("Company", bt.company)
+
+ # Assert regular entry
+ create_je_bank_fees(bt, company_doc, date, account_1, 0, bt.withdrawal)
+
+ mock_create_je.assert_called_once_with(
+ bt,
+ company_doc,
+ date,
+ account_1,
+ account_2.name,
+ None,
+ 0,
+ 1.0,
+ )
+
+ self.assertEqual(len(bt.payment_entries), 1)
+ self.assertEqual(bt.payment_entries[0].payment_entry, "JE-TEST-0001")
+ self.assertEqual(bt.payment_entries[0].allocated_amount, 1.0)
+ self.assertEqual(bt.allocated_amount, 1.0)
+ self.assertEqual(bt.unallocated_amount, 4.0)
+ self.assertEqual(bt.status, "Pending")
+
+ # Assert fee = full amount entry
+ bt.withdrawal = 1.0
+ create_je_bank_fees(bt, company_doc, date, account_1, 0, bt.withdrawal)
+
+ self.assertEqual(bt.allocated_amount, 1.0)
+ self.assertEqual(bt.unallocated_amount, 0.0)
+ self.assertEqual(bt.status, "Reconciled")
+
+ frappe.db.delete("Bank Account", ba.name)
+ frappe.db.delete("Account", account_1.name)
+ frappe.db.delete("Account", account_2.name)
+
+ @patch("banking.overrides.bank_transaction.create_automatic_journal_entry")
+ def test_create_je_bank_fees_deposit(self, mock_create_je):
+ from banking.overrides.bank_transaction import create_je_bank_fees
+
+ mock_create_je.return_value = "JE-TEST-0001"
+ date = "2025-01-01"
+
+ account_1 = frappe.get_doc(
+ {
+ "doctype": "Account",
+ "account_name": "_Test_Account_EUR",
+ "account_currency": "EUR",
+ "parent_account": frappe.get_all("Account", filters={"is_group": 1}, pluck="name")[0],
+ }
+ ).insert(ignore_permissions=True, ignore_mandatory=True, ignore_links=True)
+
+ account_2 = frappe.get_doc(
+ {
+ "doctype": "Account",
+ "account_name": "_Test_Account_EUR_Fee",
+ "account_currency": "EUR",
+ "parent_account": frappe.get_all("Account", filters={"is_group": 1}, pluck="name")[0],
+ }
+ ).insert(ignore_permissions=True, ignore_mandatory=True, ignore_links=True)
+
+ ba = frappe.get_doc(
+ {
+ "doctype": "Bank Account",
+ "account_name": "_Test_B_Account",
+ "account": account_1.name,
+ "bank": "_Test_Bank",
+ "is_company_account": 1,
+ "bank_fee_account": account_2.name,
+ }
+ ).insert(ignore_permissions=True, ignore_mandatory=True, ignore_links=True)
+
+ bt = frappe.get_doc(
+ {
+ "doctype": "Bank Transaction",
+ "company": "_Test Company",
+ "deposit": 5.0,
+ "included_fee": 1.0,
+ "bank_account": ba.name,
+ }
+ ).insert(ignore_permissions=True, ignore_mandatory=True, ignore_links=True)
+
+ company_doc = frappe.get_cached_doc("Company", bt.company)
+
+ # Assert regular entry
+ create_je_bank_fees(bt, company_doc, date, account_1, bt.deposit, 0)
+
+ mock_create_je.assert_called_once_with(
+ bt,
+ company_doc,
+ date,
+ account_1,
+ account_2.name,
+ None,
+ 0,
+ 1.0,
+ )
+
+ self.assertEqual(len(bt.payment_entries), 1)
+ self.assertEqual(bt.payment_entries[0].payment_entry, "JE-TEST-0001")
+ self.assertEqual(bt.payment_entries[0].allocated_amount, 0.0)
+ self.assertEqual(bt.allocated_amount, 0.0)
+ self.assertEqual(bt.unallocated_amount, 5.0)
+ self.assertEqual(bt.status, "Pending")
+
+ frappe.db.delete("Bank Account", ba.name)
+ frappe.db.delete("Account", account_1.name)
+ frappe.db.delete("Account", account_2.name)
+
+ @patch("banking.overrides.bank_transaction.create_automatic_journal_entry")
+ def test_create_je_automatic_rules_withdrawal(self, mock_create_je):
+ from banking.overrides.bank_transaction import create_je_automatic_rules
+
+ frappe.db.delete("Bank Reconciliation Rule")
+
+ mock_create_je.return_value = "JE-TEST-0001"
+ date = "2025-01-01"
+
+ account_1 = frappe.get_doc(
+ {
+ "doctype": "Account",
+ "account_name": "_Test_Account_EUR",
+ "account_currency": "EUR",
+ "parent_account": frappe.get_all("Account", filters={"is_group": 1}, pluck="name")[0],
+ }
+ ).insert(ignore_permissions=True, ignore_mandatory=True, ignore_links=True)
+
+ account_2 = frappe.get_doc(
+ {
+ "doctype": "Account",
+ "account_name": "_Test_Account_EUR_Fee",
+ "account_currency": "EUR",
+ "parent_account": frappe.get_all("Account", filters={"is_group": 1}, pluck="name")[0],
+ }
+ ).insert(ignore_permissions=True, ignore_mandatory=True, ignore_links=True)
+
+ ba = frappe.get_doc(
+ {
+ "doctype": "Bank Account",
+ "account_name": "_Test_B_Account",
+ "account": account_1.name,
+ "bank": "_Test_Bank",
+ "is_company_account": 1,
+ "bank_fee_account": account_1.name,
+ }
+ ).insert(ignore_permissions=True, ignore_mandatory=True, ignore_links=True)
+
+ # brr_1 => correct
+ brr_1 = frappe.get_doc(
+ {
+ "doctype": "Bank Reconciliation Rule",
+ "disabled": 0,
+ "bank_account": ba.name,
+ "docstatus": 1,
+ "target_account": account_2.name,
+ "filters": '[["Bank Transaction","description","=","FLAG-TRUE",false]]',
+ }
+ ).insert(ignore_permissions=True, ignore_mandatory=True)
+
+ # brr_2 => fail
+ frappe.get_doc(
+ {
+ "doctype": "Bank Reconciliation Rule",
+ "disabled": 1,
+ "bank_account": ba.name,
+ "docstatus": 1,
+ "target_account": account_1.name,
+ "filters": '[["Bank Transaction","description","=","FLAG-TRUE",false]]',
+ }
+ ).insert(ignore_permissions=True, ignore_mandatory=True)
+
+ # brr_3 => fail
+ frappe.get_doc(
+ {
+ "doctype": "Bank Reconciliation Rule",
+ "disabled": 0,
+ "bank_account": ba.name,
+ "docstatus": 0,
+ "target_account": account_1.name,
+ "filters": '[["Bank Transaction","description","=","FLAG-TRUE",false]]',
+ }
+ ).insert(ignore_permissions=True, ignore_mandatory=True)
+
+ bt = frappe.get_doc(
+ {
+ "doctype": "Bank Transaction",
+ "company": "_Test Company",
+ "withdrawal": 5.0,
+ "included_fee": 1.0,
+ "bank_account": ba.name,
+ "description": "FLAG-TRUE",
+ }
+ ).insert(ignore_permissions=True, ignore_mandatory=True, ignore_links=True)
+
+ company_doc = frappe.get_cached_doc("Company", bt.company)
+
+ # Assert regular entry
+ create_je_automatic_rules(bt, company_doc, date, account_1, 0, bt.withdrawal - bt.included_fee)
+
+ mock_create_je.assert_called_once_with(
+ bt,
+ company_doc,
+ date,
+ account_1,
+ account_2.name,
+ brr_1.name,
+ 0.0,
+ 4.0,
+ )
+
+ self.assertEqual(len(bt.payment_entries), 1)
+ self.assertEqual(bt.payment_entries[0].payment_entry, "JE-TEST-0001")
+ self.assertEqual(bt.payment_entries[0].allocated_amount, 4.0)
+ self.assertEqual(bt.allocated_amount, 4.0)
+ self.assertEqual(bt.status, "Reconciled")
+
+ frappe.db.delete("Bank Account", ba.name)
+ frappe.db.delete("Account", account_1.name)
+ frappe.db.delete("Account", account_2.name)
+
+ @patch("banking.overrides.bank_transaction.create_automatic_journal_entry")
+ def test_create_je_automatic_rules_deposit(self, mock_create_je):
+ from banking.overrides.bank_transaction import create_je_automatic_rules
+
+ frappe.db.delete("Bank Reconciliation Rule")
+
+ mock_create_je.return_value = "JE-TEST-0001"
+ date = "2025-01-01"
+
+ account_1 = frappe.get_doc(
+ {
+ "doctype": "Account",
+ "account_name": "_Test_Account_EUR",
+ "account_currency": "EUR",
+ "parent_account": frappe.get_all("Account", filters={"is_group": 1}, pluck="name")[0],
+ }
+ ).insert(ignore_permissions=True, ignore_mandatory=True, ignore_links=True)
+
+ account_2 = frappe.get_doc(
+ {
+ "doctype": "Account",
+ "account_name": "_Test_Account_EUR_Fee",
+ "account_currency": "EUR",
+ "parent_account": frappe.get_all("Account", filters={"is_group": 1}, pluck="name")[0],
+ }
+ ).insert(ignore_permissions=True, ignore_mandatory=True, ignore_links=True)
+
+ ba = frappe.get_doc(
+ {
+ "doctype": "Bank Account",
+ "account_name": "_Test_B_Account",
+ "account": account_1.name,
+ "bank": "_Test_Bank",
+ "is_company_account": 1,
+ "bank_fee_account": account_1.name,
+ }
+ ).insert(ignore_permissions=True, ignore_mandatory=True, ignore_links=True)
+
+ # brr_1 => correct
+ brr_1 = frappe.get_doc(
+ {
+ "doctype": "Bank Reconciliation Rule",
+ "disabled": 0,
+ "bank_account": ba.name,
+ "docstatus": 1,
+ "target_account": account_2.name,
+ "filters": '[["Bank Transaction","description","=","FLAG-TRUE",false]]',
+ }
+ ).insert(ignore_permissions=True, ignore_mandatory=True)
+
+ # brr_2 => fail
+ frappe.get_doc(
+ {
+ "doctype": "Bank Reconciliation Rule",
+ "disabled": 1,
+ "bank_account": ba.name,
+ "docstatus": 1,
+ "target_account": account_1.name,
+ "filters": '[["Bank Transaction","description","=","FLAG-TRUE",false]]',
+ }
+ ).insert(ignore_permissions=True, ignore_mandatory=True)
+
+ # brr_3 => fail
+ frappe.get_doc(
+ {
+ "doctype": "Bank Reconciliation Rule",
+ "disabled": 0,
+ "bank_account": ba.name,
+ "docstatus": 0,
+ "target_account": account_1.name,
+ "filters": '[["Bank Transaction","description","=","FLAG-TRUE",false]]',
+ }
+ ).insert(ignore_permissions=True, ignore_mandatory=True)
+
+ bt = frappe.get_doc(
+ {
+ "doctype": "Bank Transaction",
+ "company": "_Test Company",
+ "deposit": 5.0,
+ "included_fee": 1.0,
+ "bank_account": ba.name,
+ "description": "FLAG-TRUE",
+ }
+ ).insert(ignore_permissions=True, ignore_mandatory=True, ignore_links=True)
+
+ company_doc = frappe.get_cached_doc("Company", bt.company)
+
+ # Assert regular entry
+ create_je_automatic_rules(bt, company_doc, date, account_1, bt.deposit, 0)
+
+ mock_create_je.assert_called_once_with(
+ bt,
+ company_doc,
+ date,
+ account_1,
+ account_2.name,
+ brr_1.name,
+ 5.0,
+ 0.0,
+ )
+
+ self.assertEqual(len(bt.payment_entries), 1)
+ self.assertEqual(bt.payment_entries[0].payment_entry, "JE-TEST-0001")
+ self.assertEqual(bt.payment_entries[0].allocated_amount, 5.0)
+ self.assertEqual(bt.allocated_amount, 5.0)
+ self.assertEqual(bt.status, "Reconciled")
+
+ frappe.db.delete("Bank Account", ba.name)
+ frappe.db.delete("Account", account_1.name)
+ frappe.db.delete("Account", account_2.name)
From d05f9fd89dda427ccae80741b3b69542f73dd118 Mon Sep 17 00:00:00 2001
From: barredterra <14891507+barredterra@users.noreply.github.com>
Date: Wed, 14 Jan 2026 01:27:34 +0100
Subject: [PATCH 09/38] fix(Bank Account): various fixes
- filter by broader "Expense" type instead of "COGS"
- Show Fee account only when Company Account is set (se we can always fetch the relevant filter values)
- Fetch company and currency in the same query
---
banking/custom/bank_account.js | 51 +++++++++++++++-------------------
banking/custom_fields.py | 2 +-
2 files changed, 23 insertions(+), 30 deletions(-)
diff --git a/banking/custom/bank_account.js b/banking/custom/bank_account.js
index c76b2c19..001a7e02 100644
--- a/banking/custom/bank_account.js
+++ b/banking/custom/bank_account.js
@@ -3,40 +3,33 @@
frappe.ui.form.on("Bank Account", {
refresh(frm) {
- set_bank_fee_filter(frm);
+ frm.trigger("set_fee_account_query");
},
account(frm) {
- set_bank_fee_filter(frm);
+ frm.trigger("set_fee_account_query");
},
-});
-
-function set_bank_fee_filter(frm) {
- if (!frm.doc.account) return;
- (async () => {
- const {
- message: { account_currency: currency },
- } = await frappe.db.get_value(
- "Account",
- frm.doc.account,
- "account_currency"
- );
+ async set_fee_account_query(frm) {
+ if (!frm.doc.account) {
+ return;
+ }
const {
- message: { company: company },
- } = await frappe.db.get_value(
- "Bank Account",
- frm.doc.bank_account,
- "company"
- );
+ message: { account_currency: currency, company: company },
+ } = await frappe.db.get_value("Account", frm.doc.account, [
+ "account_currency",
+ "company",
+ ]);
- frm.set_query("bank_fee_account", () => ({
- filters: {
- account_type: "Cost of Goods Sold",
- account_currency: currency,
- company: company,
- },
- }));
- })();
-}
+ frm.set_query("bank_fee_account", (doc) => {
+ return {
+ filters: {
+ root_type: "Expense",
+ account_currency: currency,
+ company: company,
+ },
+ };
+ });
+ },
+});
diff --git a/banking/custom_fields.py b/banking/custom_fields.py
index a95c938a..68f4ca3c 100644
--- a/banking/custom_fields.py
+++ b/banking/custom_fields.py
@@ -93,7 +93,7 @@ def get_custom_fields():
fieldtype="Link",
label="Bank Fee Account",
options="Account",
- depends_on="is_company_account",
+ depends_on="eval:doc.is_company_account && doc.account",
insert_after="account_subtype",
),
],
From 2c8f0ce10c274d4e6fc42ba40e9937f1ae693e26 Mon Sep 17 00:00:00 2001
From: barredterra <14891507+barredterra@users.noreply.github.com>
Date: Wed, 14 Jan 2026 01:28:58 +0100
Subject: [PATCH 10/38] feat: add rule to workspace
---
.../workspace/alyf_banking/alyf_banking.json | 14 ++++++++++++--
1 file changed, 12 insertions(+), 2 deletions(-)
diff --git a/banking/klarna_kosma_integration/workspace/alyf_banking/alyf_banking.json b/banking/klarna_kosma_integration/workspace/alyf_banking/alyf_banking.json
index b595bc42..ed97d353 100644
--- a/banking/klarna_kosma_integration/workspace/alyf_banking/alyf_banking.json
+++ b/banking/klarna_kosma_integration/workspace/alyf_banking/alyf_banking.json
@@ -45,7 +45,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Setup",
- "link_count": 2,
+ "link_count": 3,
"link_type": "DocType",
"onboard": 0,
"type": "Card Break"
@@ -69,9 +69,19 @@
"link_type": "DocType",
"onboard": 0,
"type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Bank Reconciliation Rule",
+ "link_count": 0,
+ "link_to": "Bank Reconciliation Rule",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
}
],
- "modified": "2025-08-05 19:58:14.464908",
+ "modified": "2026-01-14 01:28:31.573172",
"modified_by": "Administrator",
"module": "Klarna Kosma Integration",
"name": "ALYF Banking",
From 88d187395c930500bd4e4dedd3aa25054e9a5d0f Mon Sep 17 00:00:00 2001
From: barredterra <14891507+barredterra@users.noreply.github.com>
Date: Wed, 14 Jan 2026 01:35:45 +0100
Subject: [PATCH 11/38] fix(Bank Reconciliation Rule): filter_description, type
hints
---
.../bank_reconciliation_rule.json | 4 ++--
.../bank_reconciliation_rule.py | 15 +++++++++++++++
2 files changed, 17 insertions(+), 2 deletions(-)
diff --git a/banking/klarna_kosma_integration/doctype/bank_reconciliation_rule/bank_reconciliation_rule.json b/banking/klarna_kosma_integration/doctype/bank_reconciliation_rule/bank_reconciliation_rule.json
index 36d8235e..bc6840cf 100644
--- a/banking/klarna_kosma_integration/doctype/bank_reconciliation_rule/bank_reconciliation_rule.json
+++ b/banking/klarna_kosma_integration/doctype/bank_reconciliation_rule/bank_reconciliation_rule.json
@@ -57,7 +57,7 @@
{
"fieldname": "filter_description",
"fieldtype": "HTML",
- "options": "Automatically create a journal entry if the submitted document matches these filters.
Be careful: if the filter is not strict enough accounting errors will happen.
"
+ "options": "Automatically create a Journal Entry if the submitted document matches these filters.
Note: if the filter is not strict enough, accounting errors will occur.
"
},
{
"fieldname": "amended_from",
@@ -81,7 +81,7 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
- "modified": "2025-12-16 12:33:40.308798",
+ "modified": "2026-01-14 01:31:36.550231",
"modified_by": "Administrator",
"module": "Klarna Kosma Integration",
"name": "Bank Reconciliation Rule",
diff --git a/banking/klarna_kosma_integration/doctype/bank_reconciliation_rule/bank_reconciliation_rule.py b/banking/klarna_kosma_integration/doctype/bank_reconciliation_rule/bank_reconciliation_rule.py
index 72d242d6..5c984311 100644
--- a/banking/klarna_kosma_integration/doctype/bank_reconciliation_rule/bank_reconciliation_rule.py
+++ b/banking/klarna_kosma_integration/doctype/bank_reconciliation_rule/bank_reconciliation_rule.py
@@ -7,6 +7,21 @@
class BankReconciliationRule(Document):
+ # begin: auto-generated types
+ # This code is auto-generated. Do not modify anything in this block.
+
+ from typing import TYPE_CHECKING
+
+ if TYPE_CHECKING:
+ from frappe.types import DF
+
+ amended_from: DF.Link | None
+ bank_account: DF.Link
+ disabled: DF.Check
+ filters: DF.Code | None
+ target_account: DF.Link
+
+ # end: auto-generated types
def validate(self):
self.validate_account_currencies()
self.validate_filters()
From fa5339ba449d0d62201b02a39ad2052edf782366 Mon Sep 17 00:00:00 2001
From: barredterra <14891507+barredterra@users.noreply.github.com>
Date: Wed, 14 Jan 2026 01:43:38 +0100
Subject: [PATCH 12/38] fix(Bank Reconciliation Rule): show only bank accounts
with company account
---
.../bank_reconciliation_rule/bank_reconciliation_rule.json | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/banking/klarna_kosma_integration/doctype/bank_reconciliation_rule/bank_reconciliation_rule.json b/banking/klarna_kosma_integration/doctype/bank_reconciliation_rule/bank_reconciliation_rule.json
index bc6840cf..e8cfb809 100644
--- a/banking/klarna_kosma_integration/doctype/bank_reconciliation_rule/bank_reconciliation_rule.json
+++ b/banking/klarna_kosma_integration/doctype/bank_reconciliation_rule/bank_reconciliation_rule.json
@@ -20,7 +20,7 @@
"fieldtype": "Link",
"in_list_view": 1,
"label": "Bank Account",
- "link_filters": "[[\"Bank Account\",\"is_company_account\",\"=\",1]]",
+ "link_filters": "[[\"Bank Account\",\"is_company_account\",\"=\",1],[\"Bank Account\",\"account\",\"is\",\"set\"]]",
"options": "Bank Account",
"reqd": 1
},
@@ -81,7 +81,7 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
- "modified": "2026-01-14 01:31:36.550231",
+ "modified": "2026-01-14 01:42:33.190842",
"modified_by": "Administrator",
"module": "Klarna Kosma Integration",
"name": "Bank Reconciliation Rule",
From 3577e7d5d8d7ec9924539dfcd9b7ef22b3c19608 Mon Sep 17 00:00:00 2001
From: barredterra <14891507+barredterra@users.noreply.github.com>
Date: Wed, 14 Jan 2026 01:45:32 +0100
Subject: [PATCH 13/38] refactor(Bank Reconciliation Rule): set target account
query
---
.../bank_reconciliation_rule.js | 64 +++++++++----------
1 file changed, 29 insertions(+), 35 deletions(-)
diff --git a/banking/klarna_kosma_integration/doctype/bank_reconciliation_rule/bank_reconciliation_rule.js b/banking/klarna_kosma_integration/doctype/bank_reconciliation_rule/bank_reconciliation_rule.js
index 4bab987b..2b287fdd 100644
--- a/banking/klarna_kosma_integration/doctype/bank_reconciliation_rule/bank_reconciliation_rule.js
+++ b/banking/klarna_kosma_integration/doctype/bank_reconciliation_rule/bank_reconciliation_rule.js
@@ -6,10 +6,37 @@ frappe.ui.form.on("Bank Reconciliation Rule", {
render_bt_filters(frm);
},
refresh(frm) {
- set_target_account_filter(frm);
+ frm.trigger("set_target_account_query");
},
bank_account(frm) {
- set_target_account_filter(frm);
+ frm.trigger("set_target_account_query");
+ },
+ async set_target_account_query(frm) {
+ if (!frm.doc.bank_account) {
+ return;
+ }
+
+ const {
+ message: { account },
+ } = await frappe.db.get_value(
+ "Bank Account",
+ frm.doc.bank_account,
+ "account"
+ );
+
+ const {
+ message: { account_currency: currency, company: company },
+ } = await frappe.db.get_value("Account", account, [
+ "account_currency",
+ "company",
+ ]);
+
+ frm.set_query("target_account", () => ({
+ filters: {
+ account_currency: currency,
+ company: company,
+ },
+ }));
},
});
@@ -34,36 +61,3 @@ let render_bt_filters = function (frm) {
filter_group.add_filters_to_filter_group(filters);
});
};
-
-function set_target_account_filter(frm) {
- if (!frm.doc.bank_account) return;
-
- (async () => {
- const {
- message: { account },
- } = await frappe.db.get_value(
- "Bank Account",
- frm.doc.bank_account,
- "account"
- );
-
- const {
- message: { account_currency: currency },
- } = await frappe.db.get_value("Account", account, "account_currency");
-
- const {
- message: { company: company },
- } = await frappe.db.get_value(
- "Bank Account",
- frm.doc.bank_account,
- "company"
- );
-
- frm.set_query("target_account", () => ({
- filters: {
- account_currency: currency,
- company: company,
- },
- }));
- })();
-}
From 276ba763a7ae7a389f5f8230cf306cfe3c2caa03 Mon Sep 17 00:00:00 2001
From: barredterra <14891507+barredterra@users.noreply.github.com>
Date: Wed, 14 Jan 2026 02:07:44 +0100
Subject: [PATCH 14/38] refactor(Bank Reconciliation Rule): render_bt_filters
---
.../bank_reconciliation_rule.js | 45 +++++++++----------
1 file changed, 22 insertions(+), 23 deletions(-)
diff --git a/banking/klarna_kosma_integration/doctype/bank_reconciliation_rule/bank_reconciliation_rule.js b/banking/klarna_kosma_integration/doctype/bank_reconciliation_rule/bank_reconciliation_rule.js
index 2b287fdd..609c734c 100644
--- a/banking/klarna_kosma_integration/doctype/bank_reconciliation_rule/bank_reconciliation_rule.js
+++ b/banking/klarna_kosma_integration/doctype/bank_reconciliation_rule/bank_reconciliation_rule.js
@@ -3,7 +3,7 @@
frappe.ui.form.on("Bank Reconciliation Rule", {
onload: function (frm) {
- render_bt_filters(frm);
+ frm.trigger("render_bt_filters");
},
refresh(frm) {
frm.trigger("set_target_account_query");
@@ -38,26 +38,25 @@ frappe.ui.form.on("Bank Reconciliation Rule", {
},
}));
},
-});
-
-let render_bt_filters = function (frm) {
- const parent = frm.fields_dict.filter_area.$wrapper;
- parent.empty();
-
- const filters =
- frm.doc.filters && frm.doc.filters !== "[]"
- ? JSON.parse(frm.doc.filters)
- : [];
-
- frappe.model.with_doctype("Bank Transaction", () => {
- const filter_group = new frappe.ui.FilterGroup({
- parent: parent,
- doctype: "Bank Transaction",
- on_change: () => {
- frm.set_value("filters", JSON.stringify(filter_group.get_filters()));
- },
+ render_bt_filters(frm) {
+ const parent = frm.fields_dict.filter_area.$wrapper;
+ parent.empty();
+
+ const filters =
+ frm.doc.filters && frm.doc.filters !== "[]"
+ ? JSON.parse(frm.doc.filters)
+ : [];
+
+ frappe.model.with_doctype("Bank Transaction", () => {
+ const filter_group = new frappe.ui.FilterGroup({
+ parent: parent,
+ doctype: "Bank Transaction",
+ on_change: () => {
+ frm.set_value("filters", JSON.stringify(filter_group.get_filters()));
+ },
+ });
+
+ filter_group.add_filters_to_filter_group(filters);
});
-
- filter_group.add_filters_to_filter_group(filters);
- });
-};
+ },
+});
From 8242db8cd918e77dec3d3974121ffe5ead139521 Mon Sep 17 00:00:00 2001
From: barredterra <14891507+barredterra@users.noreply.github.com>
Date: Wed, 14 Jan 2026 02:08:07 +0100
Subject: [PATCH 15/38] feat(Bank Reconciliation Rule): make filters read-only
when submitted
---
.../bank_reconciliation_rule/bank_reconciliation_rule.js | 7 +++++++
1 file changed, 7 insertions(+)
diff --git a/banking/klarna_kosma_integration/doctype/bank_reconciliation_rule/bank_reconciliation_rule.js b/banking/klarna_kosma_integration/doctype/bank_reconciliation_rule/bank_reconciliation_rule.js
index 609c734c..03b4b17f 100644
--- a/banking/klarna_kosma_integration/doctype/bank_reconciliation_rule/bank_reconciliation_rule.js
+++ b/banking/klarna_kosma_integration/doctype/bank_reconciliation_rule/bank_reconciliation_rule.js
@@ -57,6 +57,13 @@ frappe.ui.form.on("Bank Reconciliation Rule", {
});
filter_group.add_filters_to_filter_group(filters);
+
+ if (frm.doc.docstatus === 1) {
+ parent.find(".filter-action-buttons").remove();
+ parent.find(".divider").remove();
+ parent.find(".remove-filter").remove();
+ parent.find(".form-control").prop("disabled", true);
+ }
});
},
});
From 3d9a67e0547d9d20b7a0bb9d108c5ee895a52270 Mon Sep 17 00:00:00 2001
From: 0xD0M1M0 <76812428+0xD0M1M0@users.noreply.github.com>
Date: Fri, 16 Jan 2026 15:19:00 +0100
Subject: [PATCH 16/38] fix: set clearance date after submit
clearance_date is set to None during submit, set afterwards to clear journal entry
---
banking/overrides/bank_transaction.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/banking/overrides/bank_transaction.py b/banking/overrides/bank_transaction.py
index 8ed292de..52178517 100644
--- a/banking/overrides/bank_transaction.py
+++ b/banking/overrides/bank_transaction.py
@@ -221,7 +221,6 @@ def create_automatic_journal_entry(
journal_entry.cheque_no = doc.name
journal_entry.cheque_date = date
journal_entry.multi_currency = 1
- journal_entry.clearance_date = frappe.utils.today()
# Bank account entry
journal_entry.append(
@@ -248,6 +247,7 @@ def create_automatic_journal_entry(
)
journal_entry.submit()
+ frappe.db.set_value("Journal Entry", journal_entry.name, "clearance_date", frappe.utils.today())
return journal_entry.name
From 17ebb1c14e6f9727a163455d65d27253411192ed Mon Sep 17 00:00:00 2001
From: barredterra <14891507+barredterra@users.noreply.github.com>
Date: Tue, 27 Jan 2026 19:23:25 +0100
Subject: [PATCH 17/38] refactor: test case for Bank Reconciliation Rule
---
.../bank_reconciliation_rule.py | 15 ++++-
.../test_bank_reconciliation_rule.py | 61 +++++++++----------
2 files changed, 41 insertions(+), 35 deletions(-)
diff --git a/banking/klarna_kosma_integration/doctype/bank_reconciliation_rule/bank_reconciliation_rule.py b/banking/klarna_kosma_integration/doctype/bank_reconciliation_rule/bank_reconciliation_rule.py
index 5c984311..3e633653 100644
--- a/banking/klarna_kosma_integration/doctype/bank_reconciliation_rule/bank_reconciliation_rule.py
+++ b/banking/klarna_kosma_integration/doctype/bank_reconciliation_rule/bank_reconciliation_rule.py
@@ -3,9 +3,18 @@
import frappe
from frappe import _
+from frappe.exceptions import ValidationError
from frappe.model.document import Document
+class NoFiltersError(ValidationError):
+ pass
+
+
+class CurrencyMismatchError(ValidationError):
+ pass
+
+
class BankReconciliationRule(Document):
# begin: auto-generated types
# This code is auto-generated. Do not modify anything in this block.
@@ -32,9 +41,11 @@ def validate_account_currencies(self):
target_account_currency = frappe.db.get_value("Account", self.target_account, "account_currency")
if bank_account_currency != target_account_currency:
- frappe.throw(_("Bank Account and Target Account need to be in the same currency!"))
+ frappe.throw(
+ _("Bank Account and Target Account need to be in the same currency!"), CurrencyMismatchError
+ )
def validate_filters(self):
# self.filters is a code field with json, so it has "[]" when empty
if not self.filters or len(self.filters) <= 2:
- frappe.throw(_("Please define at least one filter!"))
+ frappe.throw(_("Please define at least one filter!"), NoFiltersError)
diff --git a/banking/klarna_kosma_integration/doctype/bank_reconciliation_rule/test_bank_reconciliation_rule.py b/banking/klarna_kosma_integration/doctype/bank_reconciliation_rule/test_bank_reconciliation_rule.py
index 61e4c823..664cc33d 100644
--- a/banking/klarna_kosma_integration/doctype/bank_reconciliation_rule/test_bank_reconciliation_rule.py
+++ b/banking/klarna_kosma_integration/doctype/bank_reconciliation_rule/test_bank_reconciliation_rule.py
@@ -4,26 +4,17 @@
import frappe
from frappe.tests.utils import FrappeTestCase
+from banking.klarna_kosma_integration.doctype.bank_reconciliation_rule.bank_reconciliation_rule import (
+ CurrencyMismatchError,
+ NoFiltersError,
+)
+
class TestBankReconciliationRule(FrappeTestCase):
def test_validate_account_currencies(self):
- account_1 = frappe.get_doc(
- {
- "doctype": "Account",
- "account_name": "_Test_Account_EUR",
- "account_currency": "EUR",
- "parent_account": frappe.get_all("Account", filters={"is_group": 1}, pluck="name")[0],
- }
- ).insert(ignore_permissions=True, ignore_mandatory=True, ignore_links=True)
-
- account_2 = frappe.get_doc(
- {
- "doctype": "Account",
- "account_name": "_Test_Account_USD",
- "account_currency": "USD",
- "parent_account": frappe.get_all("Account", filters={"is_group": 1}, pluck="name")[0],
- }
- ).insert(ignore_permissions=True, ignore_mandatory=True, ignore_links=True)
+ parent_account = frappe.db.get_value("Account", {"is_group": 1})
+ account_1 = create_account("EUR", parent_account)
+ account_2 = create_account("USD", parent_account)
ba = frappe.get_doc(
{
@@ -39,28 +30,32 @@ def test_validate_account_currencies(self):
brr_doc.bank_account = ba.name
brr_doc.target_account = account_2.name
- with self.assertRaisesRegex(
- frappe.ValidationError,
- "Bank Account and Target Account need to be in the same currency!",
- ):
+ with self.assertRaises(CurrencyMismatchError):
brr_doc.validate_account_currencies()
- frappe.db.delete("Bank Account", ba.name)
- frappe.db.delete("Account", account_1.name)
- frappe.db.delete("Account", account_2.name)
+ ba.delete(delete_permanently=True, ignore_permissions=True)
+ account_1.delete(delete_permanently=True, ignore_permissions=True)
+ account_2.delete(delete_permanently=True, ignore_permissions=True)
def test_validate_filters(self):
brr_doc = frappe.new_doc("Bank Reconciliation Rule")
brr_doc.filters = None
- with self.assertRaisesRegex(
- frappe.ValidationError,
- "Please define at least one filter!",
- ):
+ with self.assertRaises(NoFiltersError):
brr_doc.validate_filters()
- brr_doc.filters = []
- with self.assertRaisesRegex(
- frappe.ValidationError,
- "Please define at least one filter!",
- ):
+ brr_doc.filters = "[]"
+ with self.assertRaises(NoFiltersError):
brr_doc.validate_filters()
+
+
+def create_account(currency: str, parent_account: str):
+ account = frappe.new_doc("Account")
+ account.update(
+ {
+ "account_name": f"_Test_Account_{currency}",
+ "account_currency": currency,
+ "parent_account": parent_account,
+ }
+ )
+ account.insert(ignore_permissions=True, ignore_mandatory=True, ignore_links=True)
+ return account
From 3b096af9f3d43a9dd64be4debda21fb8050d30e1 Mon Sep 17 00:00:00 2001
From: barredterra <14891507+barredterra@users.noreply.github.com>
Date: Tue, 27 Jan 2026 20:16:12 +0100
Subject: [PATCH 18/38] refactor: move exceptions and helpers to general utils
---
banking/exceptions.py | 10 ++++++
.../bank_reconciliation_rule.py | 6 ++--
.../test_bank_reconciliation_rule.py | 32 ++++---------------
banking/utils.py | 23 +++++++++++++
4 files changed, 41 insertions(+), 30 deletions(-)
create mode 100644 banking/exceptions.py
diff --git a/banking/exceptions.py b/banking/exceptions.py
new file mode 100644
index 00000000..f73ac70f
--- /dev/null
+++ b/banking/exceptions.py
@@ -0,0 +1,10 @@
+# Copyright (c) 2025, ALYF GmbH and Contributors
+# See license.txt
+
+from frappe.exceptions import ValidationError
+
+
+class CurrencyMismatchError(ValidationError):
+ """Raised when two accounts unexpectedly have different currencies."""
+
+ pass
diff --git a/banking/klarna_kosma_integration/doctype/bank_reconciliation_rule/bank_reconciliation_rule.py b/banking/klarna_kosma_integration/doctype/bank_reconciliation_rule/bank_reconciliation_rule.py
index 3e633653..45e3b9b9 100644
--- a/banking/klarna_kosma_integration/doctype/bank_reconciliation_rule/bank_reconciliation_rule.py
+++ b/banking/klarna_kosma_integration/doctype/bank_reconciliation_rule/bank_reconciliation_rule.py
@@ -6,12 +6,10 @@
from frappe.exceptions import ValidationError
from frappe.model.document import Document
+from banking.exceptions import CurrencyMismatchError
-class NoFiltersError(ValidationError):
- pass
-
-class CurrencyMismatchError(ValidationError):
+class NoFiltersError(ValidationError):
pass
diff --git a/banking/klarna_kosma_integration/doctype/bank_reconciliation_rule/test_bank_reconciliation_rule.py b/banking/klarna_kosma_integration/doctype/bank_reconciliation_rule/test_bank_reconciliation_rule.py
index 664cc33d..3fff8fce 100644
--- a/banking/klarna_kosma_integration/doctype/bank_reconciliation_rule/test_bank_reconciliation_rule.py
+++ b/banking/klarna_kosma_integration/doctype/bank_reconciliation_rule/test_bank_reconciliation_rule.py
@@ -4,27 +4,20 @@
import frappe
from frappe.tests.utils import FrappeTestCase
+from banking.exceptions import CurrencyMismatchError
from banking.klarna_kosma_integration.doctype.bank_reconciliation_rule.bank_reconciliation_rule import (
- CurrencyMismatchError,
NoFiltersError,
)
+from banking.utils import create_bank_account, create_currency_account
class TestBankReconciliationRule(FrappeTestCase):
def test_validate_account_currencies(self):
parent_account = frappe.db.get_value("Account", {"is_group": 1})
- account_1 = create_account("EUR", parent_account)
- account_2 = create_account("USD", parent_account)
-
- ba = frappe.get_doc(
- {
- "doctype": "Bank Account",
- "account_name": "_Test_B_Account",
- "account": account_1.name,
- "bank": "_Test_Bank",
- "is_company_account": 1,
- }
- ).insert(ignore_permissions=True, ignore_links=True)
+ account_1 = create_currency_account("EUR", parent_account)
+ account_2 = create_currency_account("USD", parent_account)
+
+ ba = create_bank_account(account_1.name)
brr_doc = frappe.new_doc("Bank Reconciliation Rule")
brr_doc.bank_account = ba.name
@@ -46,16 +39,3 @@ def test_validate_filters(self):
brr_doc.filters = "[]"
with self.assertRaises(NoFiltersError):
brr_doc.validate_filters()
-
-
-def create_account(currency: str, parent_account: str):
- account = frappe.new_doc("Account")
- account.update(
- {
- "account_name": f"_Test_Account_{currency}",
- "account_currency": currency,
- "parent_account": parent_account,
- }
- )
- account.insert(ignore_permissions=True, ignore_mandatory=True, ignore_links=True)
- return account
diff --git a/banking/utils.py b/banking/utils.py
index 439ad4d8..081c039e 100644
--- a/banking/utils.py
+++ b/banking/utils.py
@@ -90,3 +90,26 @@ def create_bank(iban: str) -> str:
doc.insert()
return doc.name
+
+
+def create_currency_account(currency: str, parent_account: str):
+ """Used in tests that ensure accounts have matching currencies."""
+ acc = frappe.new_doc("Account")
+ acc.account_name = f"_Test_Account_{currency}"
+ acc.account_currency = currency
+ acc.parent_account = parent_account
+ acc.insert(ignore_permissions=True, ignore_mandatory=True, ignore_links=True)
+ return acc
+
+
+def create_bank_account(account: str, bank_fee_account: str | None = None):
+ """Used in tests that ensure bank accounts have matching currencies."""
+ ba = frappe.new_doc("Bank Account")
+ ba.account_name = "_Test_B_Account"
+ ba.account = account
+ ba.bank = "_Test_Bank"
+ ba.is_company_account = 1
+ if bank_fee_account:
+ ba.bank_fee_account = bank_fee_account
+ ba.insert(ignore_permissions=True, ignore_links=True)
+ return ba
From 1639a8b5dbf359dbb62b16b19bef254c7b36f68d Mon Sep 17 00:00:00 2001
From: barredterra <14891507+barredterra@users.noreply.github.com>
Date: Tue, 27 Jan 2026 20:17:53 +0100
Subject: [PATCH 19/38] refactor: test_bank_account to make use of helpers
---
banking/overrides/bank_account.py | 6 +++-
banking/overrides/test_bank_account.py | 46 ++++++--------------------
2 files changed, 15 insertions(+), 37 deletions(-)
diff --git a/banking/overrides/bank_account.py b/banking/overrides/bank_account.py
index c65a93dd..21000595 100644
--- a/banking/overrides/bank_account.py
+++ b/banking/overrides/bank_account.py
@@ -1,6 +1,8 @@
import frappe
from frappe import _
+from banking.exceptions import CurrencyMismatchError
+
def before_validate(doc, method):
"""Remove spaces from IBAN"""
@@ -17,4 +19,6 @@ def validate_account_currencies(doc):
bank_account_currency = frappe.db.get_value("Account", doc.account, "account_currency")
bank_fee_currency = frappe.db.get_value("Account", doc.bank_fee_account, "account_currency")
if bank_account_currency != bank_fee_currency:
- frappe.throw(_("Company Account and Bank Fee Account must be in the same currency!"))
+ frappe.throw(
+ _("Company Account and Bank Fee Account must be in the same currency!"), CurrencyMismatchError
+ )
diff --git a/banking/overrides/test_bank_account.py b/banking/overrides/test_bank_account.py
index 9dc7ac79..4017f247 100644
--- a/banking/overrides/test_bank_account.py
+++ b/banking/overrides/test_bank_account.py
@@ -4,44 +4,18 @@
import frappe
from frappe.tests.utils import FrappeTestCase
+from banking.exceptions import CurrencyMismatchError
+from banking.utils import create_bank_account, create_currency_account
+
class TestBankReconciliationRule(FrappeTestCase):
def test_validate_account_currencies(self):
- account_1 = frappe.get_doc(
- {
- "doctype": "Account",
- "account_name": "_Test_Account_EUR",
- "account_currency": "EUR",
- "parent_account": frappe.get_all("Account", filters={"is_group": 1}, pluck="name")[0],
- }
- ).insert(ignore_permissions=True, ignore_mandatory=True, ignore_links=True)
-
- account_2 = frappe.get_doc(
- {
- "doctype": "Account",
- "account_name": "_Test_Account_USD",
- "account_currency": "USD",
- "parent_account": frappe.get_all("Account", filters={"is_group": 1}, pluck="name")[0],
- }
- ).insert(ignore_permissions=True, ignore_mandatory=True, ignore_links=True)
-
- ba = frappe.get_doc(
- {
- "doctype": "Bank Account",
- "account_name": "_Test_B_Account",
- "account": account_1.name,
- "bank": "_Test_Bank",
- "is_company_account": 1,
- "bank_fee_account": account_2.name,
- }
- )
+ parent_account = frappe.db.get_value("Account", {"is_group": 1})
+ account_1 = create_currency_account("EUR", parent_account)
+ account_2 = create_currency_account("USD", parent_account)
- with self.assertRaisesRegex(
- frappe.ValidationError,
- "Company Account and Bank Fee Account must be in the same currency!",
- ):
- ba.insert(ignore_permissions=True, ignore_links=True)
+ with self.assertRaises(CurrencyMismatchError):
+ create_bank_account(account_1.name, bank_fee_account=account_2.name)
- frappe.db.delete("Bank Account", ba.name)
- frappe.db.delete("Account", account_1.name)
- frappe.db.delete("Account", account_2.name)
+ account_1.delete(ignore_permissions=True, delete_permanently=True)
+ account_2.delete(ignore_permissions=True, delete_permanently=True)
From e18e89baceb048f64c3a90dfb8fd539ef01cb5ee Mon Sep 17 00:00:00 2001
From: barredterra <14891507+barredterra@users.noreply.github.com>
Date: Tue, 27 Jan 2026 20:49:24 +0100
Subject: [PATCH 20/38] refactor: test_bank_transaction to make use of helpers
---
banking/overrides/test_bank_transaction.py | 220 ++++++---------------
banking/utils.py | 4 +-
2 files changed, 61 insertions(+), 163 deletions(-)
diff --git a/banking/overrides/test_bank_transaction.py b/banking/overrides/test_bank_transaction.py
index db55881a..cd5f3312 100644
--- a/banking/overrides/test_bank_transaction.py
+++ b/banking/overrides/test_bank_transaction.py
@@ -6,6 +6,8 @@
import frappe
from frappe.tests.utils import FrappeTestCase
+from banking.utils import create_bank_account, create_currency_account
+
class TestBankReconciliationRule(FrappeTestCase):
def test_ensure_positive_deposit_withdrawal_fees(self):
@@ -53,41 +55,17 @@ def test_ensure_positive_deposit_withdrawal_fees(self):
def test_before_submit(self, mock_create_bank_fees, mock_create_auto_rules):
from banking.overrides.bank_transaction import before_submit
- account_1 = frappe.get_doc(
- {
- "doctype": "Account",
- "account_name": "_Test_Account_EUR",
- "account_currency": "EUR",
- "parent_account": frappe.get_all("Account", filters={"is_group": 1}, pluck="name")[0],
- }
- ).insert(ignore_permissions=True, ignore_mandatory=True, ignore_links=True)
-
- account_2 = frappe.get_doc(
- {
- "doctype": "Account",
- "account_name": "_Test_Account_EUR_Fee",
- "account_currency": "EUR",
- "parent_account": frappe.get_all("Account", filters={"is_group": 1}, pluck="name")[0],
- }
- ).insert(ignore_permissions=True, ignore_mandatory=True, ignore_links=True)
-
- ba = frappe.get_doc(
- {
- "doctype": "Bank Account",
- "account_name": "_Test_B_Account",
- "account": account_1.name,
- "bank": "_Test_Bank",
- "is_company_account": 1,
- "bank_fee_account": account_2.name,
- }
- ).insert(ignore_permissions=True, ignore_mandatory=True, ignore_links=True)
-
- bt = frappe.get_doc(
- {
- "doctype": "Bank Transaction",
- "company": "_Test Company",
- }
+ parent_account = frappe.db.get_value("Account", {"is_group": 1})
+ account_1 = create_currency_account("EUR", parent_account)
+ account_2 = create_currency_account(
+ "EUR",
+ parent_account,
+ account_name="_Test_Account_EUR_Fee",
)
+ ba = create_bank_account(account_1.name, bank_fee_account=account_2.name)
+
+ bt = frappe.new_doc("Bank Transaction")
+ bt.company = "_Test Company"
with self.assertRaisesRegex(
frappe.ValidationError,
@@ -119,9 +97,9 @@ def test_before_submit(self, mock_create_bank_fees, mock_create_auto_rules):
mock_create_bank_fees.assert_called_once()
mock_create_auto_rules.assert_called_once()
- frappe.db.delete("Bank Account", ba.name)
- frappe.db.delete("Account", account_1.name)
- frappe.db.delete("Account", account_2.name)
+ ba.delete(delete_permanently=True, ignore_permissions=True)
+ account_1.delete(delete_permanently=True, ignore_permissions=True)
+ account_2.delete(delete_permanently=True, ignore_permissions=True)
@patch("banking.overrides.bank_transaction.create_automatic_journal_entry")
def test_create_je_bank_fees_withdrawal(self, mock_create_je):
@@ -130,34 +108,14 @@ def test_create_je_bank_fees_withdrawal(self, mock_create_je):
mock_create_je.return_value = "JE-TEST-0001"
date = "2025-01-01"
- account_1 = frappe.get_doc(
- {
- "doctype": "Account",
- "account_name": "_Test_Account_EUR",
- "account_currency": "EUR",
- "parent_account": frappe.get_all("Account", filters={"is_group": 1}, pluck="name")[0],
- }
- ).insert(ignore_permissions=True, ignore_mandatory=True, ignore_links=True)
-
- account_2 = frappe.get_doc(
- {
- "doctype": "Account",
- "account_name": "_Test_Account_EUR_Fee",
- "account_currency": "EUR",
- "parent_account": frappe.get_all("Account", filters={"is_group": 1}, pluck="name")[0],
- }
- ).insert(ignore_permissions=True, ignore_mandatory=True, ignore_links=True)
-
- ba = frappe.get_doc(
- {
- "doctype": "Bank Account",
- "account_name": "_Test_B_Account",
- "account": account_1.name,
- "bank": "_Test_Bank",
- "is_company_account": 1,
- "bank_fee_account": account_2.name,
- }
- ).insert(ignore_permissions=True, ignore_mandatory=True, ignore_links=True)
+ parent_account = frappe.db.get_value("Account", {"is_group": 1})
+ account_1 = create_currency_account("EUR", parent_account)
+ account_2 = create_currency_account(
+ "EUR",
+ parent_account,
+ account_name="_Test_Account_EUR_Fee",
+ )
+ ba = create_bank_account(account_1.name, bank_fee_account=account_2.name)
bt = frappe.get_doc(
{
@@ -200,9 +158,9 @@ def test_create_je_bank_fees_withdrawal(self, mock_create_je):
self.assertEqual(bt.unallocated_amount, 0.0)
self.assertEqual(bt.status, "Reconciled")
- frappe.db.delete("Bank Account", ba.name)
- frappe.db.delete("Account", account_1.name)
- frappe.db.delete("Account", account_2.name)
+ ba.delete(delete_permanently=True, ignore_permissions=True)
+ account_1.delete(delete_permanently=True, ignore_permissions=True)
+ account_2.delete(delete_permanently=True, ignore_permissions=True)
@patch("banking.overrides.bank_transaction.create_automatic_journal_entry")
def test_create_je_bank_fees_deposit(self, mock_create_je):
@@ -211,34 +169,14 @@ def test_create_je_bank_fees_deposit(self, mock_create_je):
mock_create_je.return_value = "JE-TEST-0001"
date = "2025-01-01"
- account_1 = frappe.get_doc(
- {
- "doctype": "Account",
- "account_name": "_Test_Account_EUR",
- "account_currency": "EUR",
- "parent_account": frappe.get_all("Account", filters={"is_group": 1}, pluck="name")[0],
- }
- ).insert(ignore_permissions=True, ignore_mandatory=True, ignore_links=True)
-
- account_2 = frappe.get_doc(
- {
- "doctype": "Account",
- "account_name": "_Test_Account_EUR_Fee",
- "account_currency": "EUR",
- "parent_account": frappe.get_all("Account", filters={"is_group": 1}, pluck="name")[0],
- }
- ).insert(ignore_permissions=True, ignore_mandatory=True, ignore_links=True)
-
- ba = frappe.get_doc(
- {
- "doctype": "Bank Account",
- "account_name": "_Test_B_Account",
- "account": account_1.name,
- "bank": "_Test_Bank",
- "is_company_account": 1,
- "bank_fee_account": account_2.name,
- }
- ).insert(ignore_permissions=True, ignore_mandatory=True, ignore_links=True)
+ parent_account = frappe.db.get_value("Account", {"is_group": 1})
+ account_1 = create_currency_account("EUR", parent_account)
+ account_2 = create_currency_account(
+ "EUR",
+ parent_account,
+ account_name="_Test_Account_EUR_Fee",
+ )
+ ba = create_bank_account(account_1.name, bank_fee_account=account_2.name)
bt = frappe.get_doc(
{
@@ -273,9 +211,9 @@ def test_create_je_bank_fees_deposit(self, mock_create_je):
self.assertEqual(bt.unallocated_amount, 5.0)
self.assertEqual(bt.status, "Pending")
- frappe.db.delete("Bank Account", ba.name)
- frappe.db.delete("Account", account_1.name)
- frappe.db.delete("Account", account_2.name)
+ ba.delete(delete_permanently=True, ignore_permissions=True)
+ account_1.delete(delete_permanently=True, ignore_permissions=True)
+ account_2.delete(delete_permanently=True, ignore_permissions=True)
@patch("banking.overrides.bank_transaction.create_automatic_journal_entry")
def test_create_je_automatic_rules_withdrawal(self, mock_create_je):
@@ -286,34 +224,14 @@ def test_create_je_automatic_rules_withdrawal(self, mock_create_je):
mock_create_je.return_value = "JE-TEST-0001"
date = "2025-01-01"
- account_1 = frappe.get_doc(
- {
- "doctype": "Account",
- "account_name": "_Test_Account_EUR",
- "account_currency": "EUR",
- "parent_account": frappe.get_all("Account", filters={"is_group": 1}, pluck="name")[0],
- }
- ).insert(ignore_permissions=True, ignore_mandatory=True, ignore_links=True)
-
- account_2 = frappe.get_doc(
- {
- "doctype": "Account",
- "account_name": "_Test_Account_EUR_Fee",
- "account_currency": "EUR",
- "parent_account": frappe.get_all("Account", filters={"is_group": 1}, pluck="name")[0],
- }
- ).insert(ignore_permissions=True, ignore_mandatory=True, ignore_links=True)
-
- ba = frappe.get_doc(
- {
- "doctype": "Bank Account",
- "account_name": "_Test_B_Account",
- "account": account_1.name,
- "bank": "_Test_Bank",
- "is_company_account": 1,
- "bank_fee_account": account_1.name,
- }
- ).insert(ignore_permissions=True, ignore_mandatory=True, ignore_links=True)
+ parent_account = frappe.db.get_value("Account", {"is_group": 1})
+ account_1 = create_currency_account("EUR", parent_account)
+ account_2 = create_currency_account(
+ "EUR",
+ parent_account,
+ account_name="_Test_Account_EUR_Fee",
+ )
+ ba = create_bank_account(account_1.name, bank_fee_account=account_1.name)
# brr_1 => correct
brr_1 = frappe.get_doc(
@@ -384,9 +302,9 @@ def test_create_je_automatic_rules_withdrawal(self, mock_create_je):
self.assertEqual(bt.allocated_amount, 4.0)
self.assertEqual(bt.status, "Reconciled")
- frappe.db.delete("Bank Account", ba.name)
- frappe.db.delete("Account", account_1.name)
- frappe.db.delete("Account", account_2.name)
+ ba.delete(delete_permanently=True, ignore_permissions=True)
+ account_1.delete(delete_permanently=True, ignore_permissions=True)
+ account_2.delete(delete_permanently=True, ignore_permissions=True)
@patch("banking.overrides.bank_transaction.create_automatic_journal_entry")
def test_create_je_automatic_rules_deposit(self, mock_create_je):
@@ -397,34 +315,14 @@ def test_create_je_automatic_rules_deposit(self, mock_create_je):
mock_create_je.return_value = "JE-TEST-0001"
date = "2025-01-01"
- account_1 = frappe.get_doc(
- {
- "doctype": "Account",
- "account_name": "_Test_Account_EUR",
- "account_currency": "EUR",
- "parent_account": frappe.get_all("Account", filters={"is_group": 1}, pluck="name")[0],
- }
- ).insert(ignore_permissions=True, ignore_mandatory=True, ignore_links=True)
-
- account_2 = frappe.get_doc(
- {
- "doctype": "Account",
- "account_name": "_Test_Account_EUR_Fee",
- "account_currency": "EUR",
- "parent_account": frappe.get_all("Account", filters={"is_group": 1}, pluck="name")[0],
- }
- ).insert(ignore_permissions=True, ignore_mandatory=True, ignore_links=True)
-
- ba = frappe.get_doc(
- {
- "doctype": "Bank Account",
- "account_name": "_Test_B_Account",
- "account": account_1.name,
- "bank": "_Test_Bank",
- "is_company_account": 1,
- "bank_fee_account": account_1.name,
- }
- ).insert(ignore_permissions=True, ignore_mandatory=True, ignore_links=True)
+ parent_account = frappe.db.get_value("Account", {"is_group": 1})
+ account_1 = create_currency_account("EUR", parent_account)
+ account_2 = create_currency_account(
+ "EUR",
+ parent_account,
+ account_name="_Test_Account_EUR_Fee",
+ )
+ ba = create_bank_account(account_1.name, bank_fee_account=account_1.name)
# brr_1 => correct
brr_1 = frappe.get_doc(
@@ -495,6 +393,6 @@ def test_create_je_automatic_rules_deposit(self, mock_create_je):
self.assertEqual(bt.allocated_amount, 5.0)
self.assertEqual(bt.status, "Reconciled")
- frappe.db.delete("Bank Account", ba.name)
- frappe.db.delete("Account", account_1.name)
- frappe.db.delete("Account", account_2.name)
+ ba.delete(delete_permanently=True, ignore_permissions=True)
+ account_1.delete(delete_permanently=True, ignore_permissions=True)
+ account_2.delete(delete_permanently=True, ignore_permissions=True)
diff --git a/banking/utils.py b/banking/utils.py
index 081c039e..342c13e0 100644
--- a/banking/utils.py
+++ b/banking/utils.py
@@ -92,10 +92,10 @@ def create_bank(iban: str) -> str:
return doc.name
-def create_currency_account(currency: str, parent_account: str):
+def create_currency_account(currency: str, parent_account: str, account_name: str | None = None):
"""Used in tests that ensure accounts have matching currencies."""
acc = frappe.new_doc("Account")
- acc.account_name = f"_Test_Account_{currency}"
+ acc.account_name = account_name or f"_Test_Account_{currency}"
acc.account_currency = currency
acc.parent_account = parent_account
acc.insert(ignore_permissions=True, ignore_mandatory=True, ignore_links=True)
From ad17236d40446508d9198e7d878e19d40f76014d Mon Sep 17 00:00:00 2001
From: barredterra <14891507+barredterra@users.noreply.github.com>
Date: Tue, 27 Jan 2026 20:55:58 +0100
Subject: [PATCH 21/38] refactor: don't check for exact error message
---
banking/overrides/test_bank_transaction.py | 12 ++++++------
1 file changed, 6 insertions(+), 6 deletions(-)
diff --git a/banking/overrides/test_bank_transaction.py b/banking/overrides/test_bank_transaction.py
index cd5f3312..d759206d 100644
--- a/banking/overrides/test_bank_transaction.py
+++ b/banking/overrides/test_bank_transaction.py
@@ -67,27 +67,27 @@ def test_before_submit(self, mock_create_bank_fees, mock_create_auto_rules):
bt = frappe.new_doc("Bank Transaction")
bt.company = "_Test Company"
- with self.assertRaisesRegex(
+ with self.assertRaises(
frappe.ValidationError,
- "No bank account - verify data!",
+ msg="Expected ValidationError when bank account is missing.",
):
before_submit(bt, None)
bt.bank_account = ba.name
bt.deposit = -1.0
- with self.assertRaisesRegex(
+ with self.assertRaises(
frappe.ValidationError,
- "Debit or Credit is negative. Verify input data!",
+ msg="Expected ValidationError for negative deposit.",
):
before_submit(bt, None)
bt.deposit = 0.0
bt.withdrawal = -1.0
- with self.assertRaisesRegex(
+ with self.assertRaises(
frappe.ValidationError,
- "Debit or Credit is negative. Verify input data!",
+ msg="Expected ValidationError for negative withdrawal.",
):
before_submit(bt, None)
From 51a24875b1a0adadb6c09828db05329ddaed4bc4 Mon Sep 17 00:00:00 2001
From: barredterra <14891507+barredterra@users.noreply.github.com>
Date: Tue, 27 Jan 2026 21:11:44 +0100
Subject: [PATCH 22/38] refactor: move doc creation into helpers
---
banking/overrides/test_bank_transaction.py | 184 +++++++++------------
1 file changed, 81 insertions(+), 103 deletions(-)
diff --git a/banking/overrides/test_bank_transaction.py b/banking/overrides/test_bank_transaction.py
index d759206d..e6823aca 100644
--- a/banking/overrides/test_bank_transaction.py
+++ b/banking/overrides/test_bank_transaction.py
@@ -9,11 +9,40 @@
from banking.utils import create_bank_account, create_currency_account
+def create_bank_reconciliation_rule(
+ bank_account,
+ target_account,
+ filters,
+ disabled=0,
+ submit=True,
+):
+ rule = frappe.new_doc("Bank Reconciliation Rule")
+ rule.disabled = disabled
+ rule.bank_account = bank_account
+ rule.target_account = target_account
+ rule.filters = filters
+ rule.insert(ignore_permissions=True, ignore_mandatory=True)
+ if submit:
+ rule.flags.ignore_permissions = True
+ rule.flags.ignore_mandatory = True
+ rule.submit()
+ return rule
+
+
+def create_bank_transaction(insert=True, **values):
+ doc = frappe.new_doc("Bank Transaction")
+ doc.company = "_Test Company"
+ doc.update(values)
+ if insert:
+ doc.insert(ignore_permissions=True, ignore_mandatory=True, ignore_links=True)
+ return doc
+
+
class TestBankReconciliationRule(FrappeTestCase):
def test_ensure_positive_deposit_withdrawal_fees(self):
from banking.overrides.bank_transaction import ensure_positive_deposit_withdrawal_fees
- doc = frappe.new_doc("Bank Transaction")
+ doc = create_bank_transaction(insert=False)
doc.deposit = -2.0
doc.withdrawal = 0.0
doc.included_fee = -1.0
@@ -64,8 +93,7 @@ def test_before_submit(self, mock_create_bank_fees, mock_create_auto_rules):
)
ba = create_bank_account(account_1.name, bank_fee_account=account_2.name)
- bt = frappe.new_doc("Bank Transaction")
- bt.company = "_Test Company"
+ bt = create_bank_transaction(insert=False)
with self.assertRaises(
frappe.ValidationError,
@@ -117,15 +145,7 @@ def test_create_je_bank_fees_withdrawal(self, mock_create_je):
)
ba = create_bank_account(account_1.name, bank_fee_account=account_2.name)
- bt = frappe.get_doc(
- {
- "doctype": "Bank Transaction",
- "company": "_Test Company",
- "withdrawal": 5.0,
- "included_fee": 1.0,
- "bank_account": ba.name,
- }
- ).insert(ignore_permissions=True, ignore_mandatory=True, ignore_links=True)
+ bt = create_bank_transaction(withdrawal=5.0, included_fee=1.0, bank_account=ba.name)
company_doc = frappe.get_cached_doc("Company", bt.company)
@@ -178,15 +198,7 @@ def test_create_je_bank_fees_deposit(self, mock_create_je):
)
ba = create_bank_account(account_1.name, bank_fee_account=account_2.name)
- bt = frappe.get_doc(
- {
- "doctype": "Bank Transaction",
- "company": "_Test Company",
- "deposit": 5.0,
- "included_fee": 1.0,
- "bank_account": ba.name,
- }
- ).insert(ignore_permissions=True, ignore_mandatory=True, ignore_links=True)
+ bt = create_bank_transaction(deposit=5.0, included_fee=1.0, bank_account=ba.name)
company_doc = frappe.get_cached_doc("Company", bt.company)
@@ -234,51 +246,34 @@ def test_create_je_automatic_rules_withdrawal(self, mock_create_je):
ba = create_bank_account(account_1.name, bank_fee_account=account_1.name)
# brr_1 => correct
- brr_1 = frappe.get_doc(
- {
- "doctype": "Bank Reconciliation Rule",
- "disabled": 0,
- "bank_account": ba.name,
- "docstatus": 1,
- "target_account": account_2.name,
- "filters": '[["Bank Transaction","description","=","FLAG-TRUE",false]]',
- }
- ).insert(ignore_permissions=True, ignore_mandatory=True)
+ brr_1 = create_bank_reconciliation_rule(
+ ba.name,
+ account_2.name,
+ '[["Bank Transaction","description","=","FLAG-TRUE",false]]',
+ )
# brr_2 => fail
- frappe.get_doc(
- {
- "doctype": "Bank Reconciliation Rule",
- "disabled": 1,
- "bank_account": ba.name,
- "docstatus": 1,
- "target_account": account_1.name,
- "filters": '[["Bank Transaction","description","=","FLAG-TRUE",false]]',
- }
- ).insert(ignore_permissions=True, ignore_mandatory=True)
+ create_bank_reconciliation_rule(
+ ba.name,
+ account_1.name,
+ '[["Bank Transaction","description","=","FLAG-TRUE",false]]',
+ disabled=1,
+ )
# brr_3 => fail
- frappe.get_doc(
- {
- "doctype": "Bank Reconciliation Rule",
- "disabled": 0,
- "bank_account": ba.name,
- "docstatus": 0,
- "target_account": account_1.name,
- "filters": '[["Bank Transaction","description","=","FLAG-TRUE",false]]',
- }
- ).insert(ignore_permissions=True, ignore_mandatory=True)
-
- bt = frappe.get_doc(
- {
- "doctype": "Bank Transaction",
- "company": "_Test Company",
- "withdrawal": 5.0,
- "included_fee": 1.0,
- "bank_account": ba.name,
- "description": "FLAG-TRUE",
- }
- ).insert(ignore_permissions=True, ignore_mandatory=True, ignore_links=True)
+ create_bank_reconciliation_rule(
+ ba.name,
+ account_1.name,
+ '[["Bank Transaction","description","=","FLAG-TRUE",false]]',
+ submit=False,
+ )
+
+ bt = create_bank_transaction(
+ withdrawal=5.0,
+ included_fee=1.0,
+ bank_account=ba.name,
+ description="FLAG-TRUE",
+ )
company_doc = frappe.get_cached_doc("Company", bt.company)
@@ -325,51 +320,34 @@ def test_create_je_automatic_rules_deposit(self, mock_create_je):
ba = create_bank_account(account_1.name, bank_fee_account=account_1.name)
# brr_1 => correct
- brr_1 = frappe.get_doc(
- {
- "doctype": "Bank Reconciliation Rule",
- "disabled": 0,
- "bank_account": ba.name,
- "docstatus": 1,
- "target_account": account_2.name,
- "filters": '[["Bank Transaction","description","=","FLAG-TRUE",false]]',
- }
- ).insert(ignore_permissions=True, ignore_mandatory=True)
+ brr_1 = create_bank_reconciliation_rule(
+ ba.name,
+ account_2.name,
+ '[["Bank Transaction","description","=","FLAG-TRUE",false]]',
+ )
# brr_2 => fail
- frappe.get_doc(
- {
- "doctype": "Bank Reconciliation Rule",
- "disabled": 1,
- "bank_account": ba.name,
- "docstatus": 1,
- "target_account": account_1.name,
- "filters": '[["Bank Transaction","description","=","FLAG-TRUE",false]]',
- }
- ).insert(ignore_permissions=True, ignore_mandatory=True)
+ create_bank_reconciliation_rule(
+ ba.name,
+ account_1.name,
+ '[["Bank Transaction","description","=","FLAG-TRUE",false]]',
+ disabled=1,
+ )
# brr_3 => fail
- frappe.get_doc(
- {
- "doctype": "Bank Reconciliation Rule",
- "disabled": 0,
- "bank_account": ba.name,
- "docstatus": 0,
- "target_account": account_1.name,
- "filters": '[["Bank Transaction","description","=","FLAG-TRUE",false]]',
- }
- ).insert(ignore_permissions=True, ignore_mandatory=True)
-
- bt = frappe.get_doc(
- {
- "doctype": "Bank Transaction",
- "company": "_Test Company",
- "deposit": 5.0,
- "included_fee": 1.0,
- "bank_account": ba.name,
- "description": "FLAG-TRUE",
- }
- ).insert(ignore_permissions=True, ignore_mandatory=True, ignore_links=True)
+ create_bank_reconciliation_rule(
+ ba.name,
+ account_1.name,
+ '[["Bank Transaction","description","=","FLAG-TRUE",false]]',
+ submit=False,
+ )
+
+ bt = create_bank_transaction(
+ deposit=5.0,
+ included_fee=1.0,
+ bank_account=ba.name,
+ description="FLAG-TRUE",
+ )
company_doc = frappe.get_cached_doc("Company", bt.company)
From f9f00ceeb11f3c671348a84a1732ffc73c5cb5da Mon Sep 17 00:00:00 2001
From: barredterra <14891507+barredterra@users.noreply.github.com>
Date: Tue, 27 Jan 2026 22:02:45 +0100
Subject: [PATCH 23/38] fix: user-facing messages
---
.../bank_reconciliation_rule.json | 4 +-
banking/overrides/bank_transaction.py | 48 +++++++++++++------
2 files changed, 36 insertions(+), 16 deletions(-)
diff --git a/banking/klarna_kosma_integration/doctype/bank_reconciliation_rule/bank_reconciliation_rule.json b/banking/klarna_kosma_integration/doctype/bank_reconciliation_rule/bank_reconciliation_rule.json
index e8cfb809..1308022d 100644
--- a/banking/klarna_kosma_integration/doctype/bank_reconciliation_rule/bank_reconciliation_rule.json
+++ b/banking/klarna_kosma_integration/doctype/bank_reconciliation_rule/bank_reconciliation_rule.json
@@ -57,7 +57,7 @@
{
"fieldname": "filter_description",
"fieldtype": "HTML",
- "options": "Automatically create a Journal Entry if the submitted document matches these filters.
Note: if the filter is not strict enough, accounting errors will occur.
"
+ "options": "A Journal Entry is created automatically, when a submitted Bank Transaction matches these filters.
Please define set these filters as strictly as possible to avoid accounting errors.
"
},
{
"fieldname": "amended_from",
@@ -81,7 +81,7 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
- "modified": "2026-01-14 01:42:33.190842",
+ "modified": "2026-01-27 21:55:21.191776",
"modified_by": "Administrator",
"module": "Klarna Kosma Integration",
"name": "Bank Reconciliation Rule",
diff --git a/banking/overrides/bank_transaction.py b/banking/overrides/bank_transaction.py
index 52178517..bb6a724c 100644
--- a/banking/overrides/bank_transaction.py
+++ b/banking/overrides/bank_transaction.py
@@ -5,14 +5,16 @@
from frappe import _
from frappe.core.utils import find
from frappe.utils import flt, getdate
-from frappe.utils.data import evaluate_filters
+from frappe.utils.data import evaluate_filters, get_link_to_form
class CustomBankTransaction(BankTransaction):
def add_payment_entries(self, vouchers: list, reconcile_multi_party: bool = False):
"Add the vouchers with zero allocation. Save() will perform the allocations and clearance"
if self.unallocated_amount <= 0.0:
- frappe.throw(frappe._("Bank Transaction {0} is already fully reconciled").format(self.name))
+ frappe.throw(
+ _("{0} is already fully reconciled").format(get_link_to_form("Bank Transaction", self.name))
+ )
pe_length_before = len(self.payment_entries)
self.reconcile_paid_vouchers(vouchers)
@@ -62,21 +64,29 @@ def is_duplicate_reference(self, voucher_type, voucher_name):
)
-def before_validate(doc, method):
+def before_validate(doc: "CustomBankTransaction", method):
ensure_positive_deposit_withdrawal_fees(doc, method)
-def before_submit(doc, method):
+def before_submit(doc: "CustomBankTransaction", method):
date = doc.date or frappe.utils.nowdate()
if not doc.bank_account:
- frappe.throw(_("No bank account - verify data!"))
+ frappe.throw(
+ _("The field {0} is required. Please verify the input data.").format(
+ doc.meta.get_label("bank_account")
+ )
+ )
if doc.deposit == 0 and doc.withdrawal == 0:
return
if doc.deposit < 0 or doc.withdrawal < 0:
- frappe.throw(_("Debit or Credit is negative. Verify input data!"))
+ frappe.throw(
+ _("The field {0} is negative. Please verify the input data.").format(
+ doc.meta.get_label("deposit" if doc.deposit < 0 else "withdrawal")
+ )
+ )
# Generic data catching
# Get Company
@@ -101,12 +111,13 @@ def on_update_after_submit(doc, event):
for entry in doc.payment_entries:
to_allocate -= flt(entry.allocated_amount)
if round(to_allocate, 2) < 0.0:
- symbol = frappe.db.get_value("Currency", doc.currency, "symbol")
frappe.throw(
- msg=_("The Bank Transaction is over-allocated by {0} at row {1}.").format(
- frappe.bold(f"{symbol} {abs(to_allocate)!s}"), frappe.bold(entry.idx)
+ msg=_("{0} is over-allocated by {1} at row {2}.").format(
+ get_link_to_form("Bank Transaction", doc.name),
+ frappe.bold(frappe.format(abs(to_allocate), "Currency", currency=doc.currency)),
+ frappe.bold(entry.idx),
),
- title=_("Over Allocation"),
+ title=_("Over-allocation"),
)
@@ -124,7 +135,9 @@ def on_cancel(doc, method):
if doc.docstatus == 1:
doc.cancel()
except Exception as e:
- frappe.msgprint(_("Failed to cancel {0}: {1}").format(journal_entry, e))
+ frappe.msgprint(
+ _("Failed to cancel {0}: {1}").format(get_link_to_form("Journal Entry", journal_entry), e)
+ )
def create_je_bank_fees(doc, company_doc, date, account, debit, credit):
@@ -136,7 +149,11 @@ def create_je_bank_fees(doc, company_doc, date, account, debit, credit):
bank_fee_account = frappe.db.get_value("Bank Account", doc.bank_account, "bank_fee_account")
if not bank_fee_account:
- frappe.throw(_("Please set the bank fee account in the bank account."))
+ frappe.throw(
+ _("Please specify a Bank Fee Account for {0}.").format(
+ get_link_to_form("Bank Account", doc.bank_account)
+ )
+ )
je_fee_name = create_automatic_journal_entry(
doc, company_doc, date, account, bank_fee_account, None, 0, included_fee
@@ -216,8 +233,11 @@ def create_automatic_journal_entry(
journal_entry.voucher_type = "Journal Entry"
journal_entry.posting_date = date
journal_entry.company = doc.company
- rule_part = " " + _("by automatic rule {0}").format(rule) if rule else ""
- journal_entry.user_remark = _("Auto-created from BT: {0}").format(doc.name) + rule_part
+ journal_entry.user_remark = (
+ _("Auto-created from Bank Transaction {0} by Bank Reconciliation Rule {1}").format(doc.name, rule)
+ if rule
+ else _("Auto-created from Bank Transaction {0}").format(doc.name)
+ )
journal_entry.cheque_no = doc.name
journal_entry.cheque_date = date
journal_entry.multi_currency = 1
From 6c53ba12e7f8cf854ef6b790d8ae09532a2ba48d Mon Sep 17 00:00:00 2001
From: barredterra <14891507+barredterra@users.noreply.github.com>
Date: Tue, 27 Jan 2026 22:02:54 +0100
Subject: [PATCH 24/38] chore: update translations
---
banking/locale/de.po | 78 ++++++++++++++++++++++-------------------
banking/locale/main.pot | 68 ++++++++++++++++++-----------------
2 files changed, 77 insertions(+), 69 deletions(-)
diff --git a/banking/locale/de.po b/banking/locale/de.po
index 8af1724f..4559a6eb 100644
--- a/banking/locale/de.po
+++ b/banking/locale/de.po
@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: ALYF Banking VERSION\n"
"Report-Msgid-Bugs-To: hallo@alyf.de\n"
-"POT-Creation-Date: 2026-01-27 21:26+0053\n"
+"POT-Creation-Date: 2026-01-27 21:56+0053\n"
"PO-Revision-Date: 2025-01-31 19:26+0053\n"
"Last-Translator: hallo@alyf.de\n"
"Language: de\n"
@@ -33,6 +33,12 @@ msgstr "Banking"
msgid "- It stores DocType-wise mapping of fields that should be considered as 'reference number' fields in Bank Reconciliation Tool Beta
- Link and Data fields are supported
- The field must have a singular reference number for matching with Bank Transactions
"
msgstr "- DocType-weise Zuordnung von Feldern, die im Bankabgleich Beta als zusätzliche Referenznummer verwendet werden sollen
- Es werden Link- und Datenfelder unterstützt
- Das Feld sollte genau eine Referenznummer enthalten, die auch in der Banktransaktionen vorkommen kann
"
+#. Content of the 'filter_description' (HTML) field in DocType 'Bank
+#. Reconciliation Rule'
+#: banking/klarna_kosma_integration/doctype/bank_reconciliation_rule/bank_reconciliation_rule.json
+msgid "A Journal Entry is created automatically, when a submitted Bank Transaction matches these filters.
Please define set these filters as strictly as possible to avoid accounting errors.
"
+msgstr "Ein Buchungssatz wird automatisch erstellt, wenn eine gebuchte Banktransaktion diesen Filtern entspricht.
Bitte definieren Sie diese Filter so streng wie möglich, um Buchungsfehler zu vermeiden.
"
+
#: banking/klarna_kosma_integration/doctype/banking_settings/banking_settings.py:214
msgid "A new version of the Banking app is available ({0}). Please update your instance."
msgstr "Eine neue Version der Banking-App ist verfügbar ({0}). Bitte aktualisieren Sie Ihre Instanz."
@@ -98,17 +104,15 @@ msgstr "Automatischer Abgleich ..."
msgid "Auto reconcile bank transactions based on matching reference numbers?"
msgstr "Möchten Sie Banktransaktionen automatisch abgleichen, basierend auf übereinstimmenden Referenznummern?"
-#: banking/overrides/bank_transaction.py:220
-msgid "Auto-created from BT: {0}"
-msgstr "Automatisch erstellt von BT: {0}"
+#: banking/overrides/bank_transaction.py:239
+msgid "Auto-created from Bank Transaction {0}"
+msgstr "Automatisch erstellt aus Banktransaktion {0}"
-#. Content of the 'filter_description' (HTML) field in DocType 'Bank
-#. Reconciliation Rule'
-#: banking/klarna_kosma_integration/doctype/bank_reconciliation_rule/bank_reconciliation_rule.json
-msgid "Automatically create a Journal Entry if the submitted document matches these filters.
Note: if the filter is not strict enough, accounting errors will occur.
"
-msgstr "Erstellen Sie automatisch einen Buchungssatz, wenn das gebuchte Dokument diesen Filtern entspricht.
Achtung: Wenn der Filter nicht strikt genug ist, können Buchungsfehler auftreten.
"
+#: banking/overrides/bank_transaction.py:237
+msgid "Auto-created from Bank Transaction {0} by Bank Reconciliation Rule {1}"
+msgstr "Automatisch erstellt aus Banktransaktion {0} durch Bankabgleich-Regel {1}"
-#: banking/klarna_kosma_integration/doctype/bank_reconciliation_rule/bank_reconciliation_rule.py:43
+#: banking/klarna_kosma_integration/doctype/bank_reconciliation_rule/bank_reconciliation_rule.py:42
msgid "Bank Account and Target Account need to be in the same currency!"
msgstr "Bankkonto und Zielkonto müssen die gleiche Währung haben!"
@@ -133,7 +137,7 @@ msgstr "Bankabgleich"
#: banking/klarna_kosma_integration/doctype/bank_reconciliation_rule/bank_reconciliation_rule.json
#: banking/klarna_kosma_integration/workspace/alyf_banking/alyf_banking.json
msgid "Bank Reconciliation Rule"
-msgstr "Bankabgleich Regeln"
+msgstr "Bankabgleich-Regel"
#. Name of a DocType
#: banking/klarna_kosma_integration/doctype/bank_reconciliation_tool_beta/bank_reconciliation_tool_beta.json
@@ -290,10 +294,6 @@ msgstr ""
msgid "DOWNLOADED"
msgstr "HERUNTERGELADEN"
-#: banking/overrides/bank_transaction.py:79
-msgid "Debit or Credit is negative. Verify input data!"
-msgstr "Haben oder Soll sind negative. Verifiziere die eingegebenen Daten!"
-
#: banking/klarna_kosma_integration/doctype/banking_settings/banking_settings.py:87
#: banking/klarna_kosma_integration/doctype/banking_settings/banking_settings.py:152
msgid "Developer mode is enabled. Please disable it to continue auto-syncing bank transactions."
@@ -325,7 +325,7 @@ msgstr "Antwortdateien herunterladen"
msgid "Downloaded"
msgstr "Heruntergeladen"
-#: banking/overrides/bank_transaction.py:36
+#: banking/overrides/bank_transaction.py:38
msgid "Due to Period Closing, you cannot reconcile unpaid vouchers with a Bank Transaction before {0}"
msgstr "Aufgrund des Periodenschlusses können Sie keine unbezahlten Belege mit einer Banktransaktion vor dem {0} abgleichen"
@@ -464,9 +464,9 @@ msgstr "Fehler beim Abrufen von Releases"
msgid "Execution Date"
msgstr "Ausführungsdatum"
-#: banking/overrides/bank_transaction.py:127
+#: banking/overrides/bank_transaction.py:139
msgid "Failed to cancel {0}: {1}"
-msgstr "Fehler beim Absagen von {0}: {1}"
+msgstr "Stornieren von {0} ist fehlgeschlagen: {1}"
#: banking/public/js/bank_reconciliation_beta/actions_panel/create_tab.js:91
msgid "Failed to create {0} against {1}"
@@ -647,10 +647,6 @@ msgstr "Keine Daten verfügbar"
msgid "No Transactions found for the current filters."
msgstr "Keine Transaktionen gefunden für die aktuellen Filter."
-#: banking/overrides/bank_transaction.py:73
-msgid "No bank account - verify data!"
-msgstr "Kein Bankkonto - verifiziere die Daten!"
-
#: banking/ebics/doctype/ebics_request/ebics_request.py:72
msgid "No data available for download."
msgstr "Keine Daten für das Herunterladen verfügbar."
@@ -685,8 +681,8 @@ msgstr "Nur unbezahlte Belege"
msgid "Open Billing Portal"
msgstr "Abrechnungsportal öffnen"
-#: banking/overrides/bank_transaction.py:109
-msgid "Over Allocation"
+#: banking/overrides/bank_transaction.py:120
+msgid "Over-allocation"
msgstr "Überbuchung"
#: banking/klarna_kosma_integration/doctype/bank_reconciliation_tool_beta/unpaid_vouchers.py:279
@@ -748,9 +744,9 @@ msgstr "Bitte fügen Sie dem Land {0} einen zweistelligen Ländercode hinzu"
msgid "Please confirm that the following keys are identical to the ones mentioned on your bank's letter:"
msgstr "Bitte bestätigen Sie, dass die folgenden Schlüssel mit denen auf dem Schreiben Ihrer Bank übereinstimmen:"
-#: banking/klarna_kosma_integration/doctype/bank_reconciliation_rule/bank_reconciliation_rule.py:49
+#: banking/klarna_kosma_integration/doctype/bank_reconciliation_rule/bank_reconciliation_rule.py:48
msgid "Please define at least one filter!"
-msgstr "Bitte definiere mindestens einen Filter!"
+msgstr "Bitte definieren Sie mindestens einen Filter!"
#. Description of the 'section_break_qjlc' (Section Break) field in DocType
#. 'EBICS User'
@@ -779,9 +775,9 @@ msgstr "Bitte wählen Sie Belege des gleichen Typs aus, um sie abzugleichen"
msgid "Please set the 'Bank Account' filter"
msgstr "Bitte setzen Sie den 'Bankkonto'-Filter"
-#: banking/overrides/bank_transaction.py:139
-msgid "Please set the bank fee account in the bank account."
-msgstr "Bitte setze ein Bankgebührenkonto am Bankkonto."
+#: banking/overrides/bank_transaction.py:153
+msgid "Please specify a Bank Fee Account for {0}."
+msgstr "Bitte geben Sie ein Bankgebührenkonto für {0} an."
#. Label of a Password field in DocType 'Banking Settings'
#: banking/klarna_kosma_integration/doctype/banking_settings/banking_settings.json
@@ -956,10 +952,6 @@ msgstr "Lieferanten-Bankkonto wurde nicht erstellt."
msgid "Target Account"
msgstr "Zielkonto"
-#: banking/overrides/bank_transaction.py:106
-msgid "The Bank Transaction is over-allocated by {0} at row {1}."
-msgstr "Die Banktransaktion ist um {0} bei Zeile {1} überbucht."
-
#: banking/klarna_kosma_integration/doctype/bank_reconciliation_tool_beta/unpaid_vouchers.py:280
msgid "The allocated amount cannot be negative. Please adjust the selected return vouchers."
msgstr "Der zugewiesene Betrag kann nicht negativ sein. Bitte passen Sie die ausgewählten Belege an."
@@ -972,6 +964,14 @@ msgstr "Die Währung des zweiten Kontos ({0} : {1}) muss mit der des Bankkontos
msgid "The currency of the second account ({0}) must be the same as of the bank account ({1})"
msgstr "Die Währung des zweiten Kontos ({0}) muss mit der des Bankkontos ({1}) übereinstimmen."
+#: banking/overrides/bank_transaction.py:86
+msgid "The field {0} is negative. Please verify the input data."
+msgstr "Das Feld {0} ist negativ. Bitte verifizieren Sie die Eingabedaten."
+
+#: banking/overrides/bank_transaction.py:76
+msgid "The field {0} is required. Please verify the input data."
+msgstr "Das Feld {0} ist erforderlich. Bitte verifizieren Sie die Eingabedaten."
+
#: banking/klarna_kosma_integration/doctype/banking_settings/banking_settings.py:219
msgid "The scheduler is inactive. Please activate it to continue auto-syncing bank transactions."
msgstr "Der Scheduler ist inaktiv. Bitte aktivieren Sie ihn, um den automatischen Abruf von Banktransaktionen fortzusetzen."
@@ -1067,15 +1067,19 @@ msgstr "Wir versuchen, neue Transaktionen von heute via camt.052 he
msgid "You selected protocol version {0}, but the bank {1} only supports {2}."
msgstr "Sie haben Protokollversion {0} ausgewählt, aber die Bank {1} unterstützt nur {2}."
-#: banking/overrides/bank_transaction.py:219
-msgid "by automatic rule {0}"
-msgstr "durch automatische Regel {0}"
-
#: banking/klarna_kosma_integration/doctype/bank_reconciliation_tool_beta/bank_reconciliation_tool_beta.js:270
#: banking/klarna_kosma_integration/doctype/bank_reconciliation_tool_beta/bank_reconciliation_tool_beta.js:304
msgid "to import bank transactions for {0}."
msgstr "um Banktransaktionen für {0} zu importieren."
+#: banking/overrides/bank_transaction.py:16
+msgid "{0} is already fully reconciled"
+msgstr "{0} ist bereits vollständig abgeglichen"
+
+#: banking/overrides/bank_transaction.py:115
+msgid "{0} is over-allocated by {1} at row {2}."
+msgstr "{0} ist um {1} bei Zeile {2} überbucht."
+
#: banking/klarna_kosma_integration/doctype/bank_reconciliation_tool_beta/bank_reconciliation_tool_beta.py:418
#: banking/klarna_kosma_integration/doctype/bank_reconciliation_tool_beta/bank_reconciliation_tool_beta.py:427
msgid "{0} {1} {2}"
diff --git a/banking/locale/main.pot b/banking/locale/main.pot
index 242b3080..2cd9e35e 100644
--- a/banking/locale/main.pot
+++ b/banking/locale/main.pot
@@ -7,8 +7,8 @@ msgid ""
msgstr ""
"Project-Id-Version: ALYF Banking VERSION\n"
"Report-Msgid-Bugs-To: hallo@alyf.de\n"
-"POT-Creation-Date: 2026-01-27 21:26+0053\n"
-"PO-Revision-Date: 2026-01-27 21:26+0053\n"
+"POT-Creation-Date: 2026-01-27 21:56+0053\n"
+"PO-Revision-Date: 2026-01-27 21:56+0053\n"
"Last-Translator: hallo@alyf.de\n"
"Language-Team: hallo@alyf.de\n"
"MIME-Version: 1.0\n"
@@ -31,6 +31,12 @@ msgstr ""
msgid "- It stores DocType-wise mapping of fields that should be considered as 'reference number' fields in Bank Reconciliation Tool Beta
- Link and Data fields are supported
- The field must have a singular reference number for matching with Bank Transactions
"
msgstr ""
+#. Content of the 'filter_description' (HTML) field in DocType 'Bank
+#. Reconciliation Rule'
+#: banking/klarna_kosma_integration/doctype/bank_reconciliation_rule/bank_reconciliation_rule.json
+msgid "A Journal Entry is created automatically, when a submitted Bank Transaction matches these filters.
Please define set these filters as strictly as possible to avoid accounting errors.
"
+msgstr ""
+
#: banking/klarna_kosma_integration/doctype/banking_settings/banking_settings.py:214
msgid "A new version of the Banking app is available ({0}). Please update your instance."
msgstr ""
@@ -96,17 +102,15 @@ msgstr ""
msgid "Auto reconcile bank transactions based on matching reference numbers?"
msgstr ""
-#: banking/overrides/bank_transaction.py:220
-msgid "Auto-created from BT: {0}"
+#: banking/overrides/bank_transaction.py:239
+msgid "Auto-created from Bank Transaction {0}"
msgstr ""
-#. Content of the 'filter_description' (HTML) field in DocType 'Bank
-#. Reconciliation Rule'
-#: banking/klarna_kosma_integration/doctype/bank_reconciliation_rule/bank_reconciliation_rule.json
-msgid "Automatically create a Journal Entry if the submitted document matches these filters.
Note: if the filter is not strict enough, accounting errors will occur.
"
+#: banking/overrides/bank_transaction.py:237
+msgid "Auto-created from Bank Transaction {0} by Bank Reconciliation Rule {1}"
msgstr ""
-#: banking/klarna_kosma_integration/doctype/bank_reconciliation_rule/bank_reconciliation_rule.py:43
+#: banking/klarna_kosma_integration/doctype/bank_reconciliation_rule/bank_reconciliation_rule.py:42
msgid "Bank Account and Target Account need to be in the same currency!"
msgstr ""
@@ -288,10 +292,6 @@ msgstr ""
msgid "DOWNLOADED"
msgstr ""
-#: banking/overrides/bank_transaction.py:79
-msgid "Debit or Credit is negative. Verify input data!"
-msgstr ""
-
#: banking/klarna_kosma_integration/doctype/banking_settings/banking_settings.py:87
#: banking/klarna_kosma_integration/doctype/banking_settings/banking_settings.py:152
msgid "Developer mode is enabled. Please disable it to continue auto-syncing bank transactions."
@@ -323,7 +323,7 @@ msgstr ""
msgid "Downloaded"
msgstr ""
-#: banking/overrides/bank_transaction.py:36
+#: banking/overrides/bank_transaction.py:38
msgid "Due to Period Closing, you cannot reconcile unpaid vouchers with a Bank Transaction before {0}"
msgstr ""
@@ -462,7 +462,7 @@ msgstr ""
msgid "Execution Date"
msgstr ""
-#: banking/overrides/bank_transaction.py:127
+#: banking/overrides/bank_transaction.py:139
msgid "Failed to cancel {0}: {1}"
msgstr ""
@@ -645,10 +645,6 @@ msgstr ""
msgid "No Transactions found for the current filters."
msgstr ""
-#: banking/overrides/bank_transaction.py:73
-msgid "No bank account - verify data!"
-msgstr ""
-
#: banking/ebics/doctype/ebics_request/ebics_request.py:72
msgid "No data available for download."
msgstr ""
@@ -683,8 +679,8 @@ msgstr ""
msgid "Open Billing Portal"
msgstr ""
-#: banking/overrides/bank_transaction.py:109
-msgid "Over Allocation"
+#: banking/overrides/bank_transaction.py:120
+msgid "Over-allocation"
msgstr ""
#: banking/klarna_kosma_integration/doctype/bank_reconciliation_tool_beta/unpaid_vouchers.py:279
@@ -746,7 +742,7 @@ msgstr ""
msgid "Please confirm that the following keys are identical to the ones mentioned on your bank's letter:"
msgstr ""
-#: banking/klarna_kosma_integration/doctype/bank_reconciliation_rule/bank_reconciliation_rule.py:49
+#: banking/klarna_kosma_integration/doctype/bank_reconciliation_rule/bank_reconciliation_rule.py:48
msgid "Please define at least one filter!"
msgstr ""
@@ -777,8 +773,8 @@ msgstr ""
msgid "Please set the 'Bank Account' filter"
msgstr ""
-#: banking/overrides/bank_transaction.py:139
-msgid "Please set the bank fee account in the bank account."
+#: banking/overrides/bank_transaction.py:153
+msgid "Please specify a Bank Fee Account for {0}."
msgstr ""
#. Label of a Password field in DocType 'Banking Settings'
@@ -954,10 +950,6 @@ msgstr ""
msgid "Target Account"
msgstr ""
-#: banking/overrides/bank_transaction.py:106
-msgid "The Bank Transaction is over-allocated by {0} at row {1}."
-msgstr ""
-
#: banking/klarna_kosma_integration/doctype/bank_reconciliation_tool_beta/unpaid_vouchers.py:280
msgid "The allocated amount cannot be negative. Please adjust the selected return vouchers."
msgstr ""
@@ -970,6 +962,14 @@ msgstr ""
msgid "The currency of the second account ({0}) must be the same as of the bank account ({1})"
msgstr ""
+#: banking/overrides/bank_transaction.py:86
+msgid "The field {0} is negative. Please verify the input data."
+msgstr ""
+
+#: banking/overrides/bank_transaction.py:76
+msgid "The field {0} is required. Please verify the input data."
+msgstr ""
+
#: banking/klarna_kosma_integration/doctype/banking_settings/banking_settings.py:219
msgid "The scheduler is inactive. Please activate it to continue auto-syncing bank transactions."
msgstr ""
@@ -1065,15 +1065,19 @@ msgstr ""
msgid "You selected protocol version {0}, but the bank {1} only supports {2}."
msgstr ""
-#: banking/overrides/bank_transaction.py:219
-msgid "by automatic rule {0}"
-msgstr ""
-
#: banking/klarna_kosma_integration/doctype/bank_reconciliation_tool_beta/bank_reconciliation_tool_beta.js:270
#: banking/klarna_kosma_integration/doctype/bank_reconciliation_tool_beta/bank_reconciliation_tool_beta.js:304
msgid "to import bank transactions for {0}."
msgstr ""
+#: banking/overrides/bank_transaction.py:16
+msgid "{0} is already fully reconciled"
+msgstr ""
+
+#: banking/overrides/bank_transaction.py:115
+msgid "{0} is over-allocated by {1} at row {2}."
+msgstr ""
+
#: banking/klarna_kosma_integration/doctype/bank_reconciliation_tool_beta/bank_reconciliation_tool_beta.py:418
#: banking/klarna_kosma_integration/doctype/bank_reconciliation_tool_beta/bank_reconciliation_tool_beta.py:427
msgid "{0} {1} {2}"
From f9f09848a9b1006f86fb9fb1df01b0d573a94973 Mon Sep 17 00:00:00 2001
From: barredterra <14891507+barredterra@users.noreply.github.com>
Date: Tue, 27 Jan 2026 22:06:21 +0100
Subject: [PATCH 25/38] fix: translate field labels
---
banking/overrides/bank_transaction.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/banking/overrides/bank_transaction.py b/banking/overrides/bank_transaction.py
index bb6a724c..28ce313b 100644
--- a/banking/overrides/bank_transaction.py
+++ b/banking/overrides/bank_transaction.py
@@ -74,7 +74,7 @@ def before_submit(doc: "CustomBankTransaction", method):
if not doc.bank_account:
frappe.throw(
_("The field {0} is required. Please verify the input data.").format(
- doc.meta.get_label("bank_account")
+ _(doc.meta.get_label("bank_account"))
)
)
@@ -84,7 +84,7 @@ def before_submit(doc: "CustomBankTransaction", method):
if doc.deposit < 0 or doc.withdrawal < 0:
frappe.throw(
_("The field {0} is negative. Please verify the input data.").format(
- doc.meta.get_label("deposit" if doc.deposit < 0 else "withdrawal")
+ _(doc.meta.get_label("deposit" if doc.deposit < 0 else "withdrawal"))
)
)
From 836c5e0a3bc824a2453b8c81a4b55c6905386450 Mon Sep 17 00:00:00 2001
From: Raffael Meyer <14891507+barredterra@users.noreply.github.com>
Date: Mon, 2 Feb 2026 14:34:03 +0100
Subject: [PATCH 26/38] fix: mark label as translatable
---
banking/custom_fields.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/banking/custom_fields.py b/banking/custom_fields.py
index 68f4ca3c..e3c8015a 100644
--- a/banking/custom_fields.py
+++ b/banking/custom_fields.py
@@ -91,7 +91,7 @@ def get_custom_fields():
dict(
fieldname="bank_fee_account",
fieldtype="Link",
- label="Bank Fee Account",
+ label=_("Bank Fee Account"),
options="Account",
depends_on="eval:doc.is_company_account && doc.account",
insert_after="account_subtype",
From 287cd329e38b3759d4e30910c09e3f310dd7929b Mon Sep 17 00:00:00 2001
From: barredterra <14891507+barredterra@users.noreply.github.com>
Date: Mon, 2 Feb 2026 16:14:46 +0100
Subject: [PATCH 27/38] refactor(Bank Transaction): enforce positive values
---
banking/overrides/bank_transaction.py | 24 +++++++++++++++-------
banking/overrides/test_bank_transaction.py | 10 ++++-----
2 files changed, 22 insertions(+), 12 deletions(-)
diff --git a/banking/overrides/bank_transaction.py b/banking/overrides/bank_transaction.py
index 28ce313b..93b11981 100644
--- a/banking/overrides/bank_transaction.py
+++ b/banking/overrides/bank_transaction.py
@@ -63,9 +63,14 @@ def is_duplicate_reference(self, voucher_type, voucher_name):
lambda x: x.payment_document == voucher_type and x.payment_entry == voucher_name,
)
+ def convert_to_positive_value(self, fieldname: str):
+ cur_value = self.get(fieldname)
+ if cur_value is not None and flt(cur_value) < 0:
+ self.set(fieldname, abs(flt(cur_value)))
+
def before_validate(doc: "CustomBankTransaction", method):
- ensure_positive_deposit_withdrawal_fees(doc, method)
+ enforce_positive_values(doc)
def before_submit(doc: "CustomBankTransaction", method):
@@ -272,10 +277,15 @@ def create_automatic_journal_entry(
return journal_entry.name
-def ensure_positive_deposit_withdrawal_fees(doc, method):
- doc.deposit = abs(flt(doc.deposit) or 0.0)
- doc.withdrawal = abs(flt(doc.withdrawal) or 0.0)
- doc.included_fee = abs(flt(doc.included_fee) or 0.0)
- doc.excluded_fee = abs(flt(doc.excluded_fee) or 0.0)
+def enforce_positive_values(doc: "CustomBankTransaction"):
+ """Convert any negative values to positive values.
+
+ Bank Statements often contain negative values (mostly for withdrawals and
+ fees) while ERPNext expects positive values only. To avoid errors during
+ Data Import, we accept negative values but convert them to positive values.
+ """
+ for fieldname in ["deposit", "withdrawal", "included_fee", "excluded_fee"]:
+ doc.convert_to_positive_value(fieldname)
+
# Re-call this function as the original function runs before this one and values are not converted
- BankTransaction.handle_excluded_fee(doc)
+ doc.handle_excluded_fee()
diff --git a/banking/overrides/test_bank_transaction.py b/banking/overrides/test_bank_transaction.py
index e6823aca..6c359f15 100644
--- a/banking/overrides/test_bank_transaction.py
+++ b/banking/overrides/test_bank_transaction.py
@@ -39,8 +39,8 @@ def create_bank_transaction(insert=True, **values):
class TestBankReconciliationRule(FrappeTestCase):
- def test_ensure_positive_deposit_withdrawal_fees(self):
- from banking.overrides.bank_transaction import ensure_positive_deposit_withdrawal_fees
+ def test_enforce_positive_values(self):
+ from banking.overrides.bank_transaction import enforce_positive_values
doc = create_bank_transaction(insert=False)
doc.deposit = -2.0
@@ -48,7 +48,7 @@ def test_ensure_positive_deposit_withdrawal_fees(self):
doc.included_fee = -1.0
doc.excluded_fee = -1.0
- ensure_positive_deposit_withdrawal_fees(doc, None)
+ enforce_positive_values(doc)
self.assertEqual(doc.deposit, 1.0)
self.assertEqual(doc.withdrawal, 0.0)
@@ -60,7 +60,7 @@ def test_ensure_positive_deposit_withdrawal_fees(self):
doc.included_fee = -1.0
doc.excluded_fee = -1.0
- ensure_positive_deposit_withdrawal_fees(doc, None)
+ enforce_positive_values(doc)
self.assertEqual(doc.deposit, 0.0)
self.assertEqual(doc.withdrawal, 2.0)
@@ -72,7 +72,7 @@ def test_ensure_positive_deposit_withdrawal_fees(self):
doc.included_fee = None
doc.excluded_fee = None
- ensure_positive_deposit_withdrawal_fees(doc, None)
+ enforce_positive_values(doc)
self.assertEqual(doc.deposit, 0.0)
self.assertEqual(doc.withdrawal, 0.0)
From 0cf3e0cf9029b1e866f1f0780034d9e54e956285 Mon Sep 17 00:00:00 2001
From: barredterra <14891507+barredterra@users.noreply.github.com>
Date: Mon, 2 Feb 2026 16:15:11 +0100
Subject: [PATCH 28/38] refactor: pass cost center instead of company doc
---
banking/overrides/bank_transaction.py | 29 ++++++++++-----------------
1 file changed, 11 insertions(+), 18 deletions(-)
diff --git a/banking/overrides/bank_transaction.py b/banking/overrides/bank_transaction.py
index 93b11981..c3054480 100644
--- a/banking/overrides/bank_transaction.py
+++ b/banking/overrides/bank_transaction.py
@@ -93,21 +93,14 @@ def before_submit(doc: "CustomBankTransaction", method):
)
)
- # Generic data catching
- # Get Company
- company_doc = frappe.get_cached_doc("Company", doc.company)
-
- # Bank account debit/credit
- account = frappe.db.get_value("Bank Account", doc.bank_account, "account")
- # End Generic data catching
-
- # Set initial values
+ cost_center = frappe.get_cached_value("Company", doc.company, "cost_center")
+ account = frappe.get_cached_value("Bank Account", doc.bank_account, "account")
debit, credit = (doc.deposit, 0) if doc.deposit > 0 else (0, doc.withdrawal)
included_fee = doc.included_fee or 0
- create_je_bank_fees(doc, company_doc, date, account, debit, credit)
+ create_je_bank_fees(doc, cost_center, date, account, debit, credit)
credit_no_fee = max(0, credit - included_fee)
- create_je_automatic_rules(doc, company_doc, date, account, debit, credit_no_fee)
+ create_je_automatic_rules(doc, cost_center, date, account, debit, credit_no_fee)
def on_update_after_submit(doc, event):
@@ -145,7 +138,7 @@ def on_cancel(doc, method):
)
-def create_je_bank_fees(doc, company_doc, date, account, debit, credit):
+def create_je_bank_fees(doc, cost_center, date, account, debit, credit):
# First step: Create a journal entry for included bank fees
included_fee = doc.included_fee
@@ -161,7 +154,7 @@ def create_je_bank_fees(doc, company_doc, date, account, debit, credit):
)
je_fee_name = create_automatic_journal_entry(
- doc, company_doc, date, account, bank_fee_account, None, 0, included_fee
+ doc, cost_center, date, account, bank_fee_account, None, 0, included_fee
)
if credit > 0:
@@ -189,7 +182,7 @@ def create_je_bank_fees(doc, company_doc, date, account, debit, credit):
doc.status = "Reconciled"
-def create_je_automatic_rules(doc, company_doc, date, account, debit, credit):
+def create_je_automatic_rules(doc, cost_center, date, account, debit, credit):
# Second step: Automatic reconcilation based on the Bank Reconciliation Rules
bank_reconciliation_rules = frappe.db.get_list(
"Bank Reconciliation Rule",
@@ -211,7 +204,7 @@ def create_je_automatic_rules(doc, company_doc, date, account, debit, credit):
rule = br_rule[0]
target_account = br_rule[1]
je_auto_name = create_automatic_journal_entry(
- doc, company_doc, date, account, target_account, rule, debit, credit
+ doc, cost_center, date, account, target_account, rule, debit, credit
)
doc.append(
"payment_entries",
@@ -232,7 +225,7 @@ def create_je_automatic_rules(doc, company_doc, date, account, debit, credit):
def create_automatic_journal_entry(
- doc, company_doc, date, account, target_account, rule=None, debit=0, credit=0
+ doc, cost_center, date, account, target_account, rule=None, debit=0, credit=0
):
journal_entry = frappe.new_doc("Journal Entry")
journal_entry.voucher_type = "Journal Entry"
@@ -255,7 +248,7 @@ def create_automatic_journal_entry(
"bank_account": doc.bank_account,
"debit_in_account_currency": debit,
"credit_in_account_currency": credit,
- "cost_center": company_doc.cost_center,
+ "cost_center": cost_center,
},
)
@@ -267,7 +260,7 @@ def create_automatic_journal_entry(
"bank_account": "",
"debit_in_account_currency": credit,
"credit_in_account_currency": debit,
- "cost_center": company_doc.cost_center,
+ "cost_center": cost_center,
},
)
From 1a3fd1e86449f956b3a2714dcb73b622c31283be Mon Sep 17 00:00:00 2001
From: barredterra <14891507+barredterra@users.noreply.github.com>
Date: Mon, 2 Feb 2026 16:25:40 +0100
Subject: [PATCH 29/38] fix: handle case when fee is not set
---
banking/overrides/bank_transaction.py | 13 +++++++------
1 file changed, 7 insertions(+), 6 deletions(-)
diff --git a/banking/overrides/bank_transaction.py b/banking/overrides/bank_transaction.py
index c3054480..35c794ff 100644
--- a/banking/overrides/bank_transaction.py
+++ b/banking/overrides/bank_transaction.py
@@ -86,12 +86,13 @@ def before_submit(doc: "CustomBankTransaction", method):
if doc.deposit == 0 and doc.withdrawal == 0:
return
- if doc.deposit < 0 or doc.withdrawal < 0:
- frappe.throw(
- _("The field {0} is negative. Please verify the input data.").format(
- _(doc.meta.get_label("deposit" if doc.deposit < 0 else "withdrawal"))
+ for fieldname in ["deposit", "withdrawal", "included_fee"]:
+ if doc.get(fieldname) < 0:
+ frappe.throw(
+ _("The field {0} is negative. Please verify the input data.").format(
+ _(doc.meta.get_label(fieldname))
+ )
)
- )
cost_center = frappe.get_cached_value("Company", doc.company, "cost_center")
account = frappe.get_cached_value("Bank Account", doc.bank_account, "account")
@@ -142,7 +143,7 @@ def create_je_bank_fees(doc, cost_center, date, account, debit, credit):
# First step: Create a journal entry for included bank fees
included_fee = doc.included_fee
- if included_fee <= 0:
+ if included_fee is None or included_fee <= 0:
return
bank_fee_account = frappe.db.get_value("Bank Account", doc.bank_account, "bank_fee_account")
From c43341af512c3d9792d1218d8300ea50492f1e7e Mon Sep 17 00:00:00 2001
From: barredterra <14891507+barredterra@users.noreply.github.com>
Date: Mon, 2 Feb 2026 16:27:55 +0100
Subject: [PATCH 30/38] test: don't convert None
---
banking/overrides/test_bank_transaction.py | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/banking/overrides/test_bank_transaction.py b/banking/overrides/test_bank_transaction.py
index 6c359f15..f17db3bb 100644
--- a/banking/overrides/test_bank_transaction.py
+++ b/banking/overrides/test_bank_transaction.py
@@ -74,10 +74,10 @@ def test_enforce_positive_values(self):
enforce_positive_values(doc)
- self.assertEqual(doc.deposit, 0.0)
- self.assertEqual(doc.withdrawal, 0.0)
- self.assertEqual(doc.included_fee, 0.0)
- self.assertEqual(doc.excluded_fee, 0.0)
+ self.assertEqual(doc.deposit, None)
+ self.assertEqual(doc.withdrawal, None)
+ self.assertEqual(doc.included_fee, None)
+ self.assertEqual(doc.excluded_fee, None)
@patch("banking.overrides.bank_transaction.create_je_automatic_rules")
@patch("banking.overrides.bank_transaction.create_je_bank_fees")
From d28975e85a4aed9389b553910615d24376e90f6d Mon Sep 17 00:00:00 2001
From: barredterra <14891507+barredterra@users.noreply.github.com>
Date: Mon, 2 Feb 2026 16:56:00 +0100
Subject: [PATCH 31/38] test: make sure we use the same company everywhere
---
.../test_bank_reconciliation_rule.py | 4 ++--
banking/overrides/test_bank_account.py | 4 ++--
banking/overrides/test_bank_transaction.py | 14 +++++++-------
banking/utils.py | 3 +++
4 files changed, 14 insertions(+), 11 deletions(-)
diff --git a/banking/klarna_kosma_integration/doctype/bank_reconciliation_rule/test_bank_reconciliation_rule.py b/banking/klarna_kosma_integration/doctype/bank_reconciliation_rule/test_bank_reconciliation_rule.py
index 3fff8fce..71ae3b84 100644
--- a/banking/klarna_kosma_integration/doctype/bank_reconciliation_rule/test_bank_reconciliation_rule.py
+++ b/banking/klarna_kosma_integration/doctype/bank_reconciliation_rule/test_bank_reconciliation_rule.py
@@ -8,12 +8,12 @@
from banking.klarna_kosma_integration.doctype.bank_reconciliation_rule.bank_reconciliation_rule import (
NoFiltersError,
)
-from banking.utils import create_bank_account, create_currency_account
+from banking.utils import TEST_COMPANY, create_bank_account, create_currency_account
class TestBankReconciliationRule(FrappeTestCase):
def test_validate_account_currencies(self):
- parent_account = frappe.db.get_value("Account", {"is_group": 1})
+ parent_account = frappe.db.get_value("Account", {"is_group": 1, "company": TEST_COMPANY})
account_1 = create_currency_account("EUR", parent_account)
account_2 = create_currency_account("USD", parent_account)
diff --git a/banking/overrides/test_bank_account.py b/banking/overrides/test_bank_account.py
index 4017f247..647e27c9 100644
--- a/banking/overrides/test_bank_account.py
+++ b/banking/overrides/test_bank_account.py
@@ -5,12 +5,12 @@
from frappe.tests.utils import FrappeTestCase
from banking.exceptions import CurrencyMismatchError
-from banking.utils import create_bank_account, create_currency_account
+from banking.utils import TEST_COMPANY, create_bank_account, create_currency_account
class TestBankReconciliationRule(FrappeTestCase):
def test_validate_account_currencies(self):
- parent_account = frappe.db.get_value("Account", {"is_group": 1})
+ parent_account = frappe.db.get_value("Account", {"is_group": 1, "company": TEST_COMPANY})
account_1 = create_currency_account("EUR", parent_account)
account_2 = create_currency_account("USD", parent_account)
diff --git a/banking/overrides/test_bank_transaction.py b/banking/overrides/test_bank_transaction.py
index f17db3bb..76e8f329 100644
--- a/banking/overrides/test_bank_transaction.py
+++ b/banking/overrides/test_bank_transaction.py
@@ -6,7 +6,7 @@
import frappe
from frappe.tests.utils import FrappeTestCase
-from banking.utils import create_bank_account, create_currency_account
+from banking.utils import TEST_COMPANY, create_bank_account, create_currency_account
def create_bank_reconciliation_rule(
@@ -31,7 +31,7 @@ def create_bank_reconciliation_rule(
def create_bank_transaction(insert=True, **values):
doc = frappe.new_doc("Bank Transaction")
- doc.company = "_Test Company"
+ doc.company = TEST_COMPANY
doc.update(values)
if insert:
doc.insert(ignore_permissions=True, ignore_mandatory=True, ignore_links=True)
@@ -84,7 +84,7 @@ def test_enforce_positive_values(self):
def test_before_submit(self, mock_create_bank_fees, mock_create_auto_rules):
from banking.overrides.bank_transaction import before_submit
- parent_account = frappe.db.get_value("Account", {"is_group": 1})
+ parent_account = frappe.db.get_value("Account", {"is_group": 1, "company": TEST_COMPANY})
account_1 = create_currency_account("EUR", parent_account)
account_2 = create_currency_account(
"EUR",
@@ -136,7 +136,7 @@ def test_create_je_bank_fees_withdrawal(self, mock_create_je):
mock_create_je.return_value = "JE-TEST-0001"
date = "2025-01-01"
- parent_account = frappe.db.get_value("Account", {"is_group": 1})
+ parent_account = frappe.db.get_value("Account", {"is_group": 1, "company": TEST_COMPANY})
account_1 = create_currency_account("EUR", parent_account)
account_2 = create_currency_account(
"EUR",
@@ -189,7 +189,7 @@ def test_create_je_bank_fees_deposit(self, mock_create_je):
mock_create_je.return_value = "JE-TEST-0001"
date = "2025-01-01"
- parent_account = frappe.db.get_value("Account", {"is_group": 1})
+ parent_account = frappe.db.get_value("Account", {"is_group": 1, "company": TEST_COMPANY})
account_1 = create_currency_account("EUR", parent_account)
account_2 = create_currency_account(
"EUR",
@@ -236,7 +236,7 @@ def test_create_je_automatic_rules_withdrawal(self, mock_create_je):
mock_create_je.return_value = "JE-TEST-0001"
date = "2025-01-01"
- parent_account = frappe.db.get_value("Account", {"is_group": 1})
+ parent_account = frappe.db.get_value("Account", {"is_group": 1, "company": TEST_COMPANY})
account_1 = create_currency_account("EUR", parent_account)
account_2 = create_currency_account(
"EUR",
@@ -310,7 +310,7 @@ def test_create_je_automatic_rules_deposit(self, mock_create_je):
mock_create_je.return_value = "JE-TEST-0001"
date = "2025-01-01"
- parent_account = frappe.db.get_value("Account", {"is_group": 1})
+ parent_account = frappe.db.get_value("Account", {"is_group": 1, "company": TEST_COMPANY})
account_1 = create_currency_account("EUR", parent_account)
account_2 = create_currency_account(
"EUR",
diff --git a/banking/utils.py b/banking/utils.py
index 342c13e0..e7afcc2d 100644
--- a/banking/utils.py
+++ b/banking/utils.py
@@ -10,6 +10,9 @@
from erpnext.accounts.doctype.bank_account.bank_account import BankAccount
+TEST_COMPANY = "Bolt Trades"
+
+
def before_tests():
# complete setup if missing
year = now_datetime().year
From ec519927baaa8df898448fe97d4e0f5972f4d15c Mon Sep 17 00:00:00 2001
From: barredterra <14891507+barredterra@users.noreply.github.com>
Date: Mon, 2 Feb 2026 16:57:27 +0100
Subject: [PATCH 32/38] refactor: create_je_automatic_rules
- reduce nesting
- catch JSONDecodeErrors
---
banking/overrides/bank_transaction.py | 66 +++++++++++++++------------
1 file changed, 37 insertions(+), 29 deletions(-)
diff --git a/banking/overrides/bank_transaction.py b/banking/overrides/bank_transaction.py
index 35c794ff..d97c9428 100644
--- a/banking/overrides/bank_transaction.py
+++ b/banking/overrides/bank_transaction.py
@@ -185,44 +185,52 @@ def create_je_bank_fees(doc, cost_center, date, account, debit, credit):
def create_je_automatic_rules(doc, cost_center, date, account, debit, credit):
# Second step: Automatic reconcilation based on the Bank Reconciliation Rules
- bank_reconciliation_rules = frappe.db.get_list(
+ bank_reconciliation_rules = frappe.get_all(
"Bank Reconciliation Rule",
filters={
"disabled": 0,
"bank_account": doc.bank_account,
"docstatus": 1,
+ "filters": ("is", "set"),
},
fields=["name", "target_account", "filters"],
as_list=True,
)
- for br_rule in bank_reconciliation_rules:
- # Check if line matches filter
- if br_rule[2]:
- filters = json.loads(br_rule[2])
- if filters:
- condition_met = evaluate_filters(doc, filters)
- if condition_met:
- rule = br_rule[0]
- target_account = br_rule[1]
- je_auto_name = create_automatic_journal_entry(
- doc, cost_center, date, account, target_account, rule, debit, credit
- )
- doc.append(
- "payment_entries",
- {
- "payment_document": "Journal Entry",
- "payment_entry": je_auto_name,
- "allocated_amount": debit + credit,
- },
- )
- # Set manually the un-/allocated amounts, as this value is already set and needs to be updated
- doc.allocated_amount = doc.allocated_amount + debit + credit
- # Set remaining debit and credit to 0, so no cash in transit is generated
- debit = 0
- credit = 0
- doc.unallocated_amount = 0
- doc.status = "Reconciled"
- break
+
+ for br_rule_name, target_account, filters in bank_reconciliation_rules:
+ try:
+ filters = json.loads(filters)
+ except json.JSONDecodeError:
+ frappe.log_error(
+ title="Invalid Filters in Bank Reconciliation Rule",
+ message=f"The filters for the Bank Reconciliation Rule {br_rule_name} are not valid JSON: {filters}",
+ reference_doctype="Bank Reconciliation Rule",
+ reference_name=br_rule_name,
+ )
+ continue
+
+ if not evaluate_filters(doc, filters):
+ continue
+
+ je_auto_name = create_automatic_journal_entry(
+ doc, cost_center, date, account, target_account, br_rule_name, debit, credit
+ )
+ doc.append(
+ "payment_entries",
+ {
+ "payment_document": "Journal Entry",
+ "payment_entry": je_auto_name,
+ "allocated_amount": debit + credit,
+ },
+ )
+ # Set manually the un-/allocated amounts, as this value is already set and needs to be updated
+ doc.allocated_amount = doc.allocated_amount + debit + credit
+ # Set remaining debit and credit to 0, so no cash in transit is generated
+ debit = 0
+ credit = 0
+ doc.unallocated_amount = 0
+ doc.status = "Reconciled"
+ break
def create_automatic_journal_entry(
From c1e32f87b2533ca5e43430aec434cea78bd68be4 Mon Sep 17 00:00:00 2001
From: barredterra <14891507+barredterra@users.noreply.github.com>
Date: Mon, 2 Feb 2026 17:16:39 +0100
Subject: [PATCH 33/38] fix: avoid comparing with None
---
banking/overrides/bank_transaction.py | 8 ++++++--
1 file changed, 6 insertions(+), 2 deletions(-)
diff --git a/banking/overrides/bank_transaction.py b/banking/overrides/bank_transaction.py
index d97c9428..52f47def 100644
--- a/banking/overrides/bank_transaction.py
+++ b/banking/overrides/bank_transaction.py
@@ -87,7 +87,11 @@ def before_submit(doc: "CustomBankTransaction", method):
return
for fieldname in ["deposit", "withdrawal", "included_fee"]:
- if doc.get(fieldname) < 0:
+ value = doc.get(fieldname)
+ if value is None:
+ continue
+
+ if value < 0:
frappe.throw(
_("The field {0} is negative. Please verify the input data.").format(
_(doc.meta.get_label(fieldname))
@@ -96,7 +100,7 @@ def before_submit(doc: "CustomBankTransaction", method):
cost_center = frappe.get_cached_value("Company", doc.company, "cost_center")
account = frappe.get_cached_value("Bank Account", doc.bank_account, "account")
- debit, credit = (doc.deposit, 0) if doc.deposit > 0 else (0, doc.withdrawal)
+ debit, credit = (doc.deposit, 0) if doc.deposit else (0, doc.withdrawal)
included_fee = doc.included_fee or 0
create_je_bank_fees(doc, cost_center, date, account, debit, credit)
From 8cbcb4edc71b7154335495391b5bb71618d5b46a Mon Sep 17 00:00:00 2001
From: barredterra <14891507+barredterra@users.noreply.github.com>
Date: Mon, 2 Feb 2026 18:07:54 +0100
Subject: [PATCH 34/38] test: separate class for positive/negative unit test
---
banking/overrides/test_bank_transaction.py | 16 ++++++++++++----
1 file changed, 12 insertions(+), 4 deletions(-)
diff --git a/banking/overrides/test_bank_transaction.py b/banking/overrides/test_bank_transaction.py
index 76e8f329..627c6fde 100644
--- a/banking/overrides/test_bank_transaction.py
+++ b/banking/overrides/test_bank_transaction.py
@@ -6,6 +6,7 @@
import frappe
from frappe.tests.utils import FrappeTestCase
+from banking.overrides.bank_transaction import enforce_positive_values
from banking.utils import TEST_COMPANY, create_bank_account, create_currency_account
@@ -38,11 +39,10 @@ def create_bank_transaction(insert=True, **values):
return doc
-class TestBankReconciliationRule(FrappeTestCase):
- def test_enforce_positive_values(self):
- from banking.overrides.bank_transaction import enforce_positive_values
+class TestEnforcePositiveValues(FrappeTestCase):
+ def test_deposit(self):
+ doc = frappe.new_doc("Bank Transaction")
- doc = create_bank_transaction(insert=False)
doc.deposit = -2.0
doc.withdrawal = 0.0
doc.included_fee = -1.0
@@ -55,6 +55,9 @@ def test_enforce_positive_values(self):
self.assertEqual(doc.included_fee, 2.0)
self.assertEqual(doc.excluded_fee, 0.0)
+ def test_withdrawal(self):
+ doc = frappe.new_doc("Bank Transaction")
+
doc.deposit = 0.0
doc.withdrawal = -1.0
doc.included_fee = -1.0
@@ -67,6 +70,9 @@ def test_enforce_positive_values(self):
self.assertEqual(doc.included_fee, 2.0)
self.assertEqual(doc.excluded_fee, 0.0)
+ def test_none(self):
+ doc = frappe.new_doc("Bank Transaction")
+
doc.deposit = None
doc.withdrawal = None
doc.included_fee = None
@@ -79,6 +85,8 @@ def test_enforce_positive_values(self):
self.assertEqual(doc.included_fee, None)
self.assertEqual(doc.excluded_fee, None)
+
+class TestBankReconciliationRule(FrappeTestCase):
@patch("banking.overrides.bank_transaction.create_je_automatic_rules")
@patch("banking.overrides.bank_transaction.create_je_bank_fees")
def test_before_submit(self, mock_create_bank_fees, mock_create_auto_rules):
From ff920d98d1ef1e5fb0cefd984d8920618d369acb Mon Sep 17 00:00:00 2001
From: barredterra <14891507+barredterra@users.noreply.github.com>
Date: Mon, 2 Feb 2026 18:23:44 +0100
Subject: [PATCH 35/38] test: centralize setup, rely on rollback for deletion
---
banking/overrides/test_bank_account.py | 16 +--
banking/overrides/test_bank_transaction.py | 135 ++++++---------------
2 files changed, 49 insertions(+), 102 deletions(-)
diff --git a/banking/overrides/test_bank_account.py b/banking/overrides/test_bank_account.py
index 647e27c9..715f74ba 100644
--- a/banking/overrides/test_bank_account.py
+++ b/banking/overrides/test_bank_account.py
@@ -9,13 +9,15 @@
class TestBankReconciliationRule(FrappeTestCase):
- def test_validate_account_currencies(self):
+ @classmethod
+ def setUpClass(cls):
+ super().setUpClass()
+
+ # these should be cleaned up by the DB rollback of FrappeTestCase
parent_account = frappe.db.get_value("Account", {"is_group": 1, "company": TEST_COMPANY})
- account_1 = create_currency_account("EUR", parent_account)
- account_2 = create_currency_account("USD", parent_account)
+ cls.account_1 = create_currency_account("EUR", parent_account)
+ cls.account_2 = create_currency_account("USD", parent_account)
+ def test_validate_account_currencies(self):
with self.assertRaises(CurrencyMismatchError):
- create_bank_account(account_1.name, bank_fee_account=account_2.name)
-
- account_1.delete(ignore_permissions=True, delete_permanently=True)
- account_2.delete(ignore_permissions=True, delete_permanently=True)
+ create_bank_account(self.account_1.name, bank_fee_account=self.account_2.name)
diff --git a/banking/overrides/test_bank_transaction.py b/banking/overrides/test_bank_transaction.py
index 627c6fde..f5d4be4c 100644
--- a/banking/overrides/test_bank_transaction.py
+++ b/banking/overrides/test_bank_transaction.py
@@ -87,20 +87,21 @@ def test_none(self):
class TestBankReconciliationRule(FrappeTestCase):
+ @classmethod
+ def setUpClass(cls):
+ super().setUpClass()
+
+ # these should be cleaned up by the DB rollback of FrappeTestCase
+ parent_account = frappe.db.get_value("Account", {"is_group": 1, "company": TEST_COMPANY})
+ cls.account_1 = create_currency_account("EUR", parent_account)
+ cls.account_2 = create_currency_account("EUR", parent_account, account_name="_Test_Account_EUR_Fee")
+ cls.ba = create_bank_account(cls.account_1.name, bank_fee_account=cls.account_2.name)
+
@patch("banking.overrides.bank_transaction.create_je_automatic_rules")
@patch("banking.overrides.bank_transaction.create_je_bank_fees")
def test_before_submit(self, mock_create_bank_fees, mock_create_auto_rules):
from banking.overrides.bank_transaction import before_submit
- parent_account = frappe.db.get_value("Account", {"is_group": 1, "company": TEST_COMPANY})
- account_1 = create_currency_account("EUR", parent_account)
- account_2 = create_currency_account(
- "EUR",
- parent_account,
- account_name="_Test_Account_EUR_Fee",
- )
- ba = create_bank_account(account_1.name, bank_fee_account=account_2.name)
-
bt = create_bank_transaction(insert=False)
with self.assertRaises(
@@ -109,7 +110,7 @@ def test_before_submit(self, mock_create_bank_fees, mock_create_auto_rules):
):
before_submit(bt, None)
- bt.bank_account = ba.name
+ bt.bank_account = self.ba.name
bt.deposit = -1.0
with self.assertRaises(
@@ -133,10 +134,6 @@ def test_before_submit(self, mock_create_bank_fees, mock_create_auto_rules):
mock_create_bank_fees.assert_called_once()
mock_create_auto_rules.assert_called_once()
- ba.delete(delete_permanently=True, ignore_permissions=True)
- account_1.delete(delete_permanently=True, ignore_permissions=True)
- account_2.delete(delete_permanently=True, ignore_permissions=True)
-
@patch("banking.overrides.bank_transaction.create_automatic_journal_entry")
def test_create_je_bank_fees_withdrawal(self, mock_create_je):
from banking.overrides.bank_transaction import create_je_bank_fees
@@ -144,28 +141,19 @@ def test_create_je_bank_fees_withdrawal(self, mock_create_je):
mock_create_je.return_value = "JE-TEST-0001"
date = "2025-01-01"
- parent_account = frappe.db.get_value("Account", {"is_group": 1, "company": TEST_COMPANY})
- account_1 = create_currency_account("EUR", parent_account)
- account_2 = create_currency_account(
- "EUR",
- parent_account,
- account_name="_Test_Account_EUR_Fee",
- )
- ba = create_bank_account(account_1.name, bank_fee_account=account_2.name)
-
- bt = create_bank_transaction(withdrawal=5.0, included_fee=1.0, bank_account=ba.name)
+ bt = create_bank_transaction(withdrawal=5.0, included_fee=1.0, bank_account=self.ba.name)
company_doc = frappe.get_cached_doc("Company", bt.company)
# Assert regular entry
- create_je_bank_fees(bt, company_doc, date, account_1, 0, bt.withdrawal)
+ create_je_bank_fees(bt, company_doc, date, self.account_1, 0, bt.withdrawal)
mock_create_je.assert_called_once_with(
bt,
company_doc,
date,
- account_1,
- account_2.name,
+ self.account_1,
+ self.account_2.name,
None,
0,
1.0,
@@ -180,16 +168,12 @@ def test_create_je_bank_fees_withdrawal(self, mock_create_je):
# Assert fee = full amount entry
bt.withdrawal = 1.0
- create_je_bank_fees(bt, company_doc, date, account_1, 0, bt.withdrawal)
+ create_je_bank_fees(bt, company_doc, date, self.account_1, 0, bt.withdrawal)
self.assertEqual(bt.allocated_amount, 1.0)
self.assertEqual(bt.unallocated_amount, 0.0)
self.assertEqual(bt.status, "Reconciled")
- ba.delete(delete_permanently=True, ignore_permissions=True)
- account_1.delete(delete_permanently=True, ignore_permissions=True)
- account_2.delete(delete_permanently=True, ignore_permissions=True)
-
@patch("banking.overrides.bank_transaction.create_automatic_journal_entry")
def test_create_je_bank_fees_deposit(self, mock_create_je):
from banking.overrides.bank_transaction import create_je_bank_fees
@@ -197,28 +181,19 @@ def test_create_je_bank_fees_deposit(self, mock_create_je):
mock_create_je.return_value = "JE-TEST-0001"
date = "2025-01-01"
- parent_account = frappe.db.get_value("Account", {"is_group": 1, "company": TEST_COMPANY})
- account_1 = create_currency_account("EUR", parent_account)
- account_2 = create_currency_account(
- "EUR",
- parent_account,
- account_name="_Test_Account_EUR_Fee",
- )
- ba = create_bank_account(account_1.name, bank_fee_account=account_2.name)
-
- bt = create_bank_transaction(deposit=5.0, included_fee=1.0, bank_account=ba.name)
+ bt = create_bank_transaction(deposit=5.0, included_fee=1.0, bank_account=self.ba.name)
company_doc = frappe.get_cached_doc("Company", bt.company)
# Assert regular entry
- create_je_bank_fees(bt, company_doc, date, account_1, bt.deposit, 0)
+ create_je_bank_fees(bt, company_doc, date, self.account_1, bt.deposit, 0)
mock_create_je.assert_called_once_with(
bt,
company_doc,
date,
- account_1,
- account_2.name,
+ self.account_1,
+ self.account_2.name,
None,
0,
1.0,
@@ -231,10 +206,6 @@ def test_create_je_bank_fees_deposit(self, mock_create_je):
self.assertEqual(bt.unallocated_amount, 5.0)
self.assertEqual(bt.status, "Pending")
- ba.delete(delete_permanently=True, ignore_permissions=True)
- account_1.delete(delete_permanently=True, ignore_permissions=True)
- account_2.delete(delete_permanently=True, ignore_permissions=True)
-
@patch("banking.overrides.bank_transaction.create_automatic_journal_entry")
def test_create_je_automatic_rules_withdrawal(self, mock_create_je):
from banking.overrides.bank_transaction import create_je_automatic_rules
@@ -244,34 +215,25 @@ def test_create_je_automatic_rules_withdrawal(self, mock_create_je):
mock_create_je.return_value = "JE-TEST-0001"
date = "2025-01-01"
- parent_account = frappe.db.get_value("Account", {"is_group": 1, "company": TEST_COMPANY})
- account_1 = create_currency_account("EUR", parent_account)
- account_2 = create_currency_account(
- "EUR",
- parent_account,
- account_name="_Test_Account_EUR_Fee",
- )
- ba = create_bank_account(account_1.name, bank_fee_account=account_1.name)
-
# brr_1 => correct
brr_1 = create_bank_reconciliation_rule(
- ba.name,
- account_2.name,
+ self.ba.name,
+ self.account_2.name,
'[["Bank Transaction","description","=","FLAG-TRUE",false]]',
)
# brr_2 => fail
create_bank_reconciliation_rule(
- ba.name,
- account_1.name,
+ self.ba.name,
+ self.account_1.name,
'[["Bank Transaction","description","=","FLAG-TRUE",false]]',
disabled=1,
)
# brr_3 => fail
create_bank_reconciliation_rule(
- ba.name,
- account_1.name,
+ self.ba.name,
+ self.account_1.name,
'[["Bank Transaction","description","=","FLAG-TRUE",false]]',
submit=False,
)
@@ -279,21 +241,21 @@ def test_create_je_automatic_rules_withdrawal(self, mock_create_je):
bt = create_bank_transaction(
withdrawal=5.0,
included_fee=1.0,
- bank_account=ba.name,
+ bank_account=self.ba.name,
description="FLAG-TRUE",
)
company_doc = frappe.get_cached_doc("Company", bt.company)
# Assert regular entry
- create_je_automatic_rules(bt, company_doc, date, account_1, 0, bt.withdrawal - bt.included_fee)
+ create_je_automatic_rules(bt, company_doc, date, self.account_1, 0, bt.withdrawal - bt.included_fee)
mock_create_je.assert_called_once_with(
bt,
company_doc,
date,
- account_1,
- account_2.name,
+ self.account_1,
+ self.account_2.name,
brr_1.name,
0.0,
4.0,
@@ -305,10 +267,6 @@ def test_create_je_automatic_rules_withdrawal(self, mock_create_je):
self.assertEqual(bt.allocated_amount, 4.0)
self.assertEqual(bt.status, "Reconciled")
- ba.delete(delete_permanently=True, ignore_permissions=True)
- account_1.delete(delete_permanently=True, ignore_permissions=True)
- account_2.delete(delete_permanently=True, ignore_permissions=True)
-
@patch("banking.overrides.bank_transaction.create_automatic_journal_entry")
def test_create_je_automatic_rules_deposit(self, mock_create_je):
from banking.overrides.bank_transaction import create_je_automatic_rules
@@ -318,34 +276,25 @@ def test_create_je_automatic_rules_deposit(self, mock_create_je):
mock_create_je.return_value = "JE-TEST-0001"
date = "2025-01-01"
- parent_account = frappe.db.get_value("Account", {"is_group": 1, "company": TEST_COMPANY})
- account_1 = create_currency_account("EUR", parent_account)
- account_2 = create_currency_account(
- "EUR",
- parent_account,
- account_name="_Test_Account_EUR_Fee",
- )
- ba = create_bank_account(account_1.name, bank_fee_account=account_1.name)
-
# brr_1 => correct
brr_1 = create_bank_reconciliation_rule(
- ba.name,
- account_2.name,
+ self.ba.name,
+ self.account_2.name,
'[["Bank Transaction","description","=","FLAG-TRUE",false]]',
)
# brr_2 => fail
create_bank_reconciliation_rule(
- ba.name,
- account_1.name,
+ self.ba.name,
+ self.account_1.name,
'[["Bank Transaction","description","=","FLAG-TRUE",false]]',
disabled=1,
)
# brr_3 => fail
create_bank_reconciliation_rule(
- ba.name,
- account_1.name,
+ self.ba.name,
+ self.account_1.name,
'[["Bank Transaction","description","=","FLAG-TRUE",false]]',
submit=False,
)
@@ -353,21 +302,21 @@ def test_create_je_automatic_rules_deposit(self, mock_create_je):
bt = create_bank_transaction(
deposit=5.0,
included_fee=1.0,
- bank_account=ba.name,
+ bank_account=self.ba.name,
description="FLAG-TRUE",
)
company_doc = frappe.get_cached_doc("Company", bt.company)
# Assert regular entry
- create_je_automatic_rules(bt, company_doc, date, account_1, bt.deposit, 0)
+ create_je_automatic_rules(bt, company_doc, date, self.account_1, bt.deposit, 0)
mock_create_je.assert_called_once_with(
bt,
company_doc,
date,
- account_1,
- account_2.name,
+ self.account_1,
+ self.account_2.name,
brr_1.name,
5.0,
0.0,
@@ -378,7 +327,3 @@ def test_create_je_automatic_rules_deposit(self, mock_create_je):
self.assertEqual(bt.payment_entries[0].allocated_amount, 5.0)
self.assertEqual(bt.allocated_amount, 5.0)
self.assertEqual(bt.status, "Reconciled")
-
- ba.delete(delete_permanently=True, ignore_permissions=True)
- account_1.delete(delete_permanently=True, ignore_permissions=True)
- account_2.delete(delete_permanently=True, ignore_permissions=True)
From a60df17e6fea5a4c1bfb286195a797d64c5202da Mon Sep 17 00:00:00 2001
From: barredterra <14891507+barredterra@users.noreply.github.com>
Date: Mon, 2 Feb 2026 18:38:02 +0100
Subject: [PATCH 36/38] test: centralize setup, rely on rollback for deletion
(part 2)
---
.../test_bank_reconciliation_rule.py | 22 +++++++++----------
1 file changed, 11 insertions(+), 11 deletions(-)
diff --git a/banking/klarna_kosma_integration/doctype/bank_reconciliation_rule/test_bank_reconciliation_rule.py b/banking/klarna_kosma_integration/doctype/bank_reconciliation_rule/test_bank_reconciliation_rule.py
index 71ae3b84..6ff45c7a 100644
--- a/banking/klarna_kosma_integration/doctype/bank_reconciliation_rule/test_bank_reconciliation_rule.py
+++ b/banking/klarna_kosma_integration/doctype/bank_reconciliation_rule/test_bank_reconciliation_rule.py
@@ -12,24 +12,24 @@
class TestBankReconciliationRule(FrappeTestCase):
- def test_validate_account_currencies(self):
- parent_account = frappe.db.get_value("Account", {"is_group": 1, "company": TEST_COMPANY})
- account_1 = create_currency_account("EUR", parent_account)
- account_2 = create_currency_account("USD", parent_account)
+ @classmethod
+ def setUpClass(cls):
+ super().setUpClass()
- ba = create_bank_account(account_1.name)
+ # these should be cleaned up by the DB rollback of FrappeTestCase
+ parent_account = frappe.db.get_value("Account", {"is_group": 1, "company": TEST_COMPANY})
+ bank_account = create_currency_account("EUR", parent_account)
+ cls.target_account = create_currency_account("USD", parent_account)
+ cls.ba = create_bank_account(bank_account.name)
+ def test_validate_account_currencies(self):
brr_doc = frappe.new_doc("Bank Reconciliation Rule")
- brr_doc.bank_account = ba.name
- brr_doc.target_account = account_2.name
+ brr_doc.bank_account = self.ba.name
+ brr_doc.target_account = self.target_account.name
with self.assertRaises(CurrencyMismatchError):
brr_doc.validate_account_currencies()
- ba.delete(delete_permanently=True, ignore_permissions=True)
- account_1.delete(delete_permanently=True, ignore_permissions=True)
- account_2.delete(delete_permanently=True, ignore_permissions=True)
-
def test_validate_filters(self):
brr_doc = frappe.new_doc("Bank Reconciliation Rule")
brr_doc.filters = None
From 3124e4ce7df4dcca6caa92e6431f7e82330b390b Mon Sep 17 00:00:00 2001
From: barredterra <14891507+barredterra@users.noreply.github.com>
Date: Tue, 17 Feb 2026 21:28:34 +0100
Subject: [PATCH 37/38] refactor: create_automatic_journal_entry
- type hints
- optional parameters last
- pass atomic values instead of doc
---
banking/overrides/bank_transaction.py | 44 +++++++++--
banking/overrides/test_bank_transaction.py | 85 +++++++++++++---------
2 files changed, 85 insertions(+), 44 deletions(-)
diff --git a/banking/overrides/bank_transaction.py b/banking/overrides/bank_transaction.py
index 52f47def..b7a5a81d 100644
--- a/banking/overrides/bank_transaction.py
+++ b/banking/overrides/bank_transaction.py
@@ -159,7 +159,15 @@ def create_je_bank_fees(doc, cost_center, date, account, debit, credit):
)
je_fee_name = create_automatic_journal_entry(
- doc, cost_center, date, account, bank_fee_account, None, 0, included_fee
+ company=doc.company,
+ bank_account=doc.bank_account,
+ bank_transaction=doc.name,
+ cost_center=cost_center,
+ date=date,
+ account=account,
+ target_account=bank_fee_account,
+ debit=0,
+ credit=included_fee,
)
if credit > 0:
@@ -217,7 +225,16 @@ def create_je_automatic_rules(doc, cost_center, date, account, debit, credit):
continue
je_auto_name = create_automatic_journal_entry(
- doc, cost_center, date, account, target_account, br_rule_name, debit, credit
+ company=doc.company,
+ bank_account=doc.bank_account,
+ bank_transaction=doc.name,
+ cost_center=cost_center,
+ date=date,
+ account=account,
+ target_account=target_account,
+ debit=debit,
+ credit=credit,
+ rule=br_rule_name,
)
doc.append(
"payment_entries",
@@ -238,18 +255,29 @@ def create_je_automatic_rules(doc, cost_center, date, account, debit, credit):
def create_automatic_journal_entry(
- doc, cost_center, date, account, target_account, rule=None, debit=0, credit=0
+ company: str,
+ bank_account: str,
+ bank_transaction: str,
+ cost_center: str,
+ date: str,
+ account: str,
+ target_account: str,
+ debit: float = 0,
+ credit: float = 0,
+ rule: str | None = None,
):
journal_entry = frappe.new_doc("Journal Entry")
journal_entry.voucher_type = "Journal Entry"
journal_entry.posting_date = date
- journal_entry.company = doc.company
+ journal_entry.company = company
journal_entry.user_remark = (
- _("Auto-created from Bank Transaction {0} by Bank Reconciliation Rule {1}").format(doc.name, rule)
+ _("Auto-created from Bank Transaction {0} by Bank Reconciliation Rule {1}").format(
+ bank_transaction, rule
+ )
if rule
- else _("Auto-created from Bank Transaction {0}").format(doc.name)
+ else _("Auto-created from Bank Transaction {0}").format(bank_transaction)
)
- journal_entry.cheque_no = doc.name
+ journal_entry.cheque_no = bank_transaction
journal_entry.cheque_date = date
journal_entry.multi_currency = 1
@@ -258,7 +286,7 @@ def create_automatic_journal_entry(
"accounts",
{
"account": account,
- "bank_account": doc.bank_account,
+ "bank_account": bank_account,
"debit_in_account_currency": debit,
"credit_in_account_currency": credit,
"cost_center": cost_center,
diff --git a/banking/overrides/test_bank_transaction.py b/banking/overrides/test_bank_transaction.py
index f5d4be4c..b19fdbc0 100644
--- a/banking/overrides/test_bank_transaction.py
+++ b/banking/overrides/test_bank_transaction.py
@@ -146,17 +146,18 @@ def test_create_je_bank_fees_withdrawal(self, mock_create_je):
company_doc = frappe.get_cached_doc("Company", bt.company)
# Assert regular entry
- create_je_bank_fees(bt, company_doc, date, self.account_1, 0, bt.withdrawal)
+ create_je_bank_fees(bt, company_doc.cost_center, date, self.account_1, 0, bt.withdrawal)
mock_create_je.assert_called_once_with(
- bt,
- company_doc,
- date,
- self.account_1,
- self.account_2.name,
- None,
- 0,
- 1.0,
+ company=bt.company,
+ bank_account=bt.bank_account,
+ bank_transaction=bt.name,
+ cost_center=company_doc.cost_center,
+ date=date,
+ account=self.account_1,
+ target_account=self.account_2.name,
+ debit=0,
+ credit=1.0,
)
self.assertEqual(len(bt.payment_entries), 1)
@@ -168,7 +169,7 @@ def test_create_je_bank_fees_withdrawal(self, mock_create_je):
# Assert fee = full amount entry
bt.withdrawal = 1.0
- create_je_bank_fees(bt, company_doc, date, self.account_1, 0, bt.withdrawal)
+ create_je_bank_fees(bt, company_doc.cost_center, date, self.account_1, 0, bt.withdrawal)
self.assertEqual(bt.allocated_amount, 1.0)
self.assertEqual(bt.unallocated_amount, 0.0)
@@ -186,17 +187,18 @@ def test_create_je_bank_fees_deposit(self, mock_create_je):
company_doc = frappe.get_cached_doc("Company", bt.company)
# Assert regular entry
- create_je_bank_fees(bt, company_doc, date, self.account_1, bt.deposit, 0)
+ create_je_bank_fees(bt, company_doc.cost_center, date, self.account_1, bt.deposit, 0)
mock_create_je.assert_called_once_with(
- bt,
- company_doc,
- date,
- self.account_1,
- self.account_2.name,
- None,
- 0,
- 1.0,
+ company=bt.company,
+ bank_account=bt.bank_account,
+ bank_transaction=bt.name,
+ cost_center=company_doc.cost_center,
+ date=date,
+ account=self.account_1,
+ target_account=self.account_2.name,
+ debit=0,
+ credit=1.0,
)
self.assertEqual(len(bt.payment_entries), 1)
@@ -248,17 +250,26 @@ def test_create_je_automatic_rules_withdrawal(self, mock_create_je):
company_doc = frappe.get_cached_doc("Company", bt.company)
# Assert regular entry
- create_je_automatic_rules(bt, company_doc, date, self.account_1, 0, bt.withdrawal - bt.included_fee)
-
- mock_create_je.assert_called_once_with(
+ create_je_automatic_rules(
bt,
- company_doc,
+ company_doc.cost_center,
date,
self.account_1,
- self.account_2.name,
- brr_1.name,
- 0.0,
- 4.0,
+ 0,
+ bt.withdrawal - bt.included_fee,
+ )
+
+ mock_create_je.assert_called_once_with(
+ company=bt.company,
+ bank_account=bt.bank_account,
+ bank_transaction=bt.name,
+ cost_center=company_doc.cost_center,
+ date=date,
+ account=self.account_1,
+ target_account=self.account_2.name,
+ debit=0.0,
+ credit=4.0,
+ rule=brr_1.name,
)
self.assertEqual(len(bt.payment_entries), 1)
@@ -309,17 +320,19 @@ def test_create_je_automatic_rules_deposit(self, mock_create_je):
company_doc = frappe.get_cached_doc("Company", bt.company)
# Assert regular entry
- create_je_automatic_rules(bt, company_doc, date, self.account_1, bt.deposit, 0)
+ create_je_automatic_rules(bt, company_doc.cost_center, date, self.account_1, bt.deposit, 0)
mock_create_je.assert_called_once_with(
- bt,
- company_doc,
- date,
- self.account_1,
- self.account_2.name,
- brr_1.name,
- 5.0,
- 0.0,
+ company=bt.company,
+ bank_account=bt.bank_account,
+ bank_transaction=bt.name,
+ cost_center=company_doc.cost_center,
+ date=date,
+ account=self.account_1,
+ target_account=self.account_2.name,
+ debit=5.0,
+ credit=0.0,
+ rule=brr_1.name,
)
self.assertEqual(len(bt.payment_entries), 1)
From 5189c344592b710dd401f8c0c029276163ff28bb Mon Sep 17 00:00:00 2001
From: barredterra <14891507+barredterra@users.noreply.github.com>
Date: Tue, 17 Feb 2026 22:18:41 +0100
Subject: [PATCH 38/38] fix: make bank fee entry optional
Otherwise existing sites will break because they don't have a Bank Fee Account specified yet.
---
.../banking_settings/banking_settings.json | 10 ++-
.../banking_settings/banking_settings.py | 1 +
banking/overrides/bank_transaction.py | 17 ++--
banking/overrides/test_bank_transaction.py | 77 +++++++++++++++++++
4 files changed, 97 insertions(+), 8 deletions(-)
diff --git a/banking/klarna_kosma_integration/doctype/banking_settings/banking_settings.json b/banking/klarna_kosma_integration/doctype/banking_settings/banking_settings.json
index 102ab36a..fb0c3735 100644
--- a/banking/klarna_kosma_integration/doctype/banking_settings/banking_settings.json
+++ b/banking/klarna_kosma_integration/doctype/banking_settings/banking_settings.json
@@ -19,6 +19,7 @@
"fintech_licensee_name",
"fintech_license_key",
"bank_reconciliation_tab",
+ "enable_automatic_journal_entries_for_bank_fees",
"advanced_section",
"reference_fields",
"voucher_matching_defaults"
@@ -115,13 +116,20 @@
"fieldtype": "Table MultiSelect",
"label": "Voucher Matching Defaults",
"options": "Voucher Matching Default"
+ },
+ {
+ "default": "0",
+ "description": "When a Bank Transaction with Included Fee is submitted, we'll automatically create a Journal Entry for the fee amount. Please specify the Bank Fee Account in your Bank Account.",
+ "fieldname": "enable_automatic_journal_entries_for_bank_fees",
+ "fieldtype": "Check",
+ "label": "Enable Automatic Journal Entries for Bank Fees"
}
],
"grid_page_length": 50,
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
- "modified": "2025-10-07 18:44:07.181312",
+ "modified": "2026-02-17 22:00:58.508344",
"modified_by": "Administrator",
"module": "Klarna Kosma Integration",
"name": "Banking Settings",
diff --git a/banking/klarna_kosma_integration/doctype/banking_settings/banking_settings.py b/banking/klarna_kosma_integration/doctype/banking_settings/banking_settings.py
index 2ed7c120..6ae16fbc 100644
--- a/banking/klarna_kosma_integration/doctype/banking_settings/banking_settings.py
+++ b/banking/klarna_kosma_integration/doctype/banking_settings/banking_settings.py
@@ -33,6 +33,7 @@ class BankingSettings(Document):
admin_endpoint: DF.Data | None
api_token: DF.Password | None
customer_id: DF.Data | None
+ enable_automatic_journal_entries_for_bank_fees: DF.Check
enable_ebics: DF.Check
enabled: DF.Check
fintech_license_key: DF.Password | None
diff --git a/banking/overrides/bank_transaction.py b/banking/overrides/bank_transaction.py
index b7a5a81d..852cd457 100644
--- a/banking/overrides/bank_transaction.py
+++ b/banking/overrides/bank_transaction.py
@@ -103,8 +103,13 @@ def before_submit(doc: "CustomBankTransaction", method):
debit, credit = (doc.deposit, 0) if doc.deposit else (0, doc.withdrawal)
included_fee = doc.included_fee or 0
- create_je_bank_fees(doc, cost_center, date, account, debit, credit)
+ if frappe.db.get_single_value("Banking Settings", "enable_automatic_journal_entries_for_bank_fees"):
+ create_je_bank_fees(doc, cost_center, date, account, debit, credit)
+
+ # For withdrawals, `included_fee` must be posted separately (auto or manual).
+ # So rules should only reconcile the net amount.
credit_no_fee = max(0, credit - included_fee)
+
create_je_automatic_rules(doc, cost_center, date, account, debit, credit_no_fee)
@@ -245,12 +250,10 @@ def create_je_automatic_rules(doc, cost_center, date, account, debit, credit):
},
)
# Set manually the un-/allocated amounts, as this value is already set and needs to be updated
- doc.allocated_amount = doc.allocated_amount + debit + credit
- # Set remaining debit and credit to 0, so no cash in transit is generated
- debit = 0
- credit = 0
- doc.unallocated_amount = 0
- doc.status = "Reconciled"
+ doc.allocated_amount = flt(doc.allocated_amount) + debit + credit
+ total_amount = abs(flt(doc.withdrawal) - flt(doc.deposit))
+ doc.unallocated_amount = max(0, total_amount - flt(doc.allocated_amount))
+ doc.status = "Reconciled" if doc.unallocated_amount == 0 else "Pending"
break
diff --git a/banking/overrides/test_bank_transaction.py b/banking/overrides/test_bank_transaction.py
index b19fdbc0..30d49d01 100644
--- a/banking/overrides/test_bank_transaction.py
+++ b/banking/overrides/test_bank_transaction.py
@@ -129,11 +129,36 @@ def test_before_submit(self, mock_create_bank_fees, mock_create_auto_rules):
before_submit(bt, None)
bt.withdrawal = 1.0
+ frappe.db.set_single_value("Banking Settings", "enable_automatic_journal_entries_for_bank_fees", 1)
before_submit(bt, None)
mock_create_bank_fees.assert_called_once()
mock_create_auto_rules.assert_called_once()
+ @patch("banking.overrides.bank_transaction.create_je_automatic_rules")
+ @patch("banking.overrides.bank_transaction.create_je_bank_fees")
+ def test_before_submit_without_automatic_bank_fee_entries(
+ self, mock_create_bank_fees, mock_create_auto_rules
+ ):
+ from banking.overrides.bank_transaction import before_submit
+
+ frappe.db.set_single_value("Banking Settings", "enable_automatic_journal_entries_for_bank_fees", 0)
+ bt = create_bank_transaction(
+ insert=False,
+ bank_account=self.ba.name,
+ withdrawal=10.0,
+ included_fee=1.0,
+ date="2025-01-01",
+ )
+
+ before_submit(bt, None)
+
+ mock_create_bank_fees.assert_not_called()
+ mock_create_auto_rules.assert_called_once()
+ self.assertEqual(mock_create_auto_rules.call_args.args[0], bt)
+ self.assertEqual(mock_create_auto_rules.call_args.args[4], 0)
+ self.assertEqual(mock_create_auto_rules.call_args.args[5], 9.0)
+
@patch("banking.overrides.bank_transaction.create_automatic_journal_entry")
def test_create_je_bank_fees_withdrawal(self, mock_create_je):
from banking.overrides.bank_transaction import create_je_bank_fees
@@ -276,6 +301,58 @@ def test_create_je_automatic_rules_withdrawal(self, mock_create_je):
self.assertEqual(bt.payment_entries[0].payment_entry, "JE-TEST-0001")
self.assertEqual(bt.payment_entries[0].allocated_amount, 4.0)
self.assertEqual(bt.allocated_amount, 4.0)
+ self.assertEqual(bt.unallocated_amount, 1.0)
+ self.assertEqual(bt.status, "Pending")
+
+ @patch("banking.overrides.bank_transaction.create_automatic_journal_entry")
+ def test_create_je_automatic_rules_withdrawal_with_preallocated_fee(self, mock_create_je):
+ from banking.overrides.bank_transaction import create_je_automatic_rules
+
+ frappe.db.delete("Bank Reconciliation Rule")
+
+ mock_create_je.return_value = "JE-TEST-0001"
+ date = "2025-01-01"
+
+ brr_1 = create_bank_reconciliation_rule(
+ self.ba.name,
+ self.account_2.name,
+ '[["Bank Transaction","description","=","FLAG-TRUE",false]]',
+ )
+
+ bt = create_bank_transaction(
+ withdrawal=5.0,
+ included_fee=1.0,
+ bank_account=self.ba.name,
+ description="FLAG-TRUE",
+ )
+ bt.allocated_amount = 1.0
+ bt.unallocated_amount = 4.0
+
+ company_doc = frappe.get_cached_doc("Company", bt.company)
+
+ create_je_automatic_rules(
+ bt,
+ company_doc.cost_center,
+ date,
+ self.account_1,
+ 0,
+ bt.withdrawal - bt.included_fee,
+ )
+
+ mock_create_je.assert_called_once_with(
+ company=bt.company,
+ bank_account=bt.bank_account,
+ bank_transaction=bt.name,
+ cost_center=company_doc.cost_center,
+ date=date,
+ account=self.account_1,
+ target_account=self.account_2.name,
+ debit=0.0,
+ credit=4.0,
+ rule=brr_1.name,
+ )
+ self.assertEqual(bt.allocated_amount, 5.0)
+ self.assertEqual(bt.unallocated_amount, 0.0)
self.assertEqual(bt.status, "Reconciled")
@patch("banking.overrides.bank_transaction.create_automatic_journal_entry")