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 "" 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 "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 "" 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")