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/hooks.py b/banking/hooks.py
index 72d299c1..7a2addad 100644
--- a/banking/hooks.py
+++ b/banking/hooks.py
@@ -108,6 +108,8 @@
doc_events = {
"Bank Transaction": {
"on_update_after_submit": "banking.overrides.bank_transaction.on_update_after_submit",
+ "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",
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..2d96ca91
--- /dev/null
+++ b/banking/klarna_kosma_integration/doctype/bank_reconciliation_rule/bank_reconciliation_rule.js
@@ -0,0 +1,79 @@
+// Copyright (c) 2025, ALYF GmbH and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on("Bank Reconciliation Rule", {
+ onload: function (frm) {
+ frm.trigger("render_bt_filters");
+ },
+ refresh(frm) {
+ frm.trigger("set_target_account_query");
+ },
+ bank_account(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"
+ );
+
+ if (!account) {
+ frappe.throw(
+ __("In {0} {1} the field {2} is not set.", [
+ frappe.bold(__("Bank Account")),
+ frm.doc.bank_account,
+ __("Company 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,
+ },
+ }));
+ },
+ 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);
+
+ 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);
+ }
+ });
+ },
+});
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..aa6e6b69
--- /dev/null
+++ b/banking/klarna_kosma_integration/doctype/bank_reconciliation_rule/bank_reconciliation_rule.json
@@ -0,0 +1,149 @@
+{
+ "actions": [],
+ "creation": "2025-10-23 13:00:49.226504",
+ "doctype": "DocType",
+ "engine": "InnoDB",
+ "field_order": [
+ "account_settings_section",
+ "bank_account",
+ "target_account",
+ "column_break_dvyd",
+ "priority",
+ "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],[\"Bank Account\",\"account\",\"is\",\"set\"]]",
+ "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": "A Journal Entry is created automatically, when a submitted Bank Transaction matches these filters.
Please define these filters as strictly as possible to avoid accounting errors.
"
+ },
+ {
+ "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"
+ },
+ {
+ "fieldname": "column_break_dvyd",
+ "fieldtype": "Column Break"
+ },
+ {
+ "description": "If multiple rules match a Bank Transaction, only the one with the highest priority is executed.",
+ "fieldname": "priority",
+ "fieldtype": "Int",
+ "in_list_view": 1,
+ "label": "Priority",
+ "non_negative": 1
+ }
+ ],
+ "grid_page_length": 50,
+ "index_web_pages_for_search": 1,
+ "is_submittable": 1,
+ "links": [],
+ "modified": "2026-03-28 02:28:06.918430",
+ "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..a2a3a6c7
--- /dev/null
+++ b/banking/klarna_kosma_integration/doctype/bank_reconciliation_rule/bank_reconciliation_rule.py
@@ -0,0 +1,51 @@
+# Copyright (c) 2025, ALYF GmbH and contributors
+# For license information, please see license.txt
+
+import json
+
+import frappe
+from frappe import _
+from frappe.exceptions import ValidationError
+from frappe.model.document import Document
+
+from banking.exceptions import CurrencyMismatchError
+
+
+class NoFiltersError(ValidationError):
+ pass
+
+
+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
+ priority: DF.Int
+ target_account: DF.Link
+
+ # end: auto-generated types
+ 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!"), CurrencyMismatchError
+ )
+
+ def validate_filters(self):
+ if not self.filters or not json.loads(self.filters):
+ frappe.throw(_("Please define at least one filter!"), NoFiltersError)
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..8d8652a5
--- /dev/null
+++ b/banking/klarna_kosma_integration/doctype/bank_reconciliation_rule/test_bank_reconciliation_rule.py
@@ -0,0 +1,40 @@
+# Copyright (c) 2025, ALYF GmbH and Contributors
+# See license.txt
+
+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 (
+ NoFiltersError,
+)
+from banking.testing_utils import TEST_COMPANY, create_bank_account, create_currency_account
+
+
+class TestBankReconciliationRule(FrappeTestCase):
+ @classmethod
+ def setUpClass(cls):
+ super().setUpClass()
+
+ parent_account = frappe.db.get_value("Account", {"is_group": 1, "company": TEST_COMPANY})
+ bank_account = create_currency_account("EUR", parent_account, "_Test_Account_EUR")
+ cls.target_account = create_currency_account("USD", parent_account, "_Test_Account_USD")
+ 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 = self.ba.name
+ brr_doc.target_account = self.target_account.name
+
+ with self.assertRaises(CurrencyMismatchError):
+ brr_doc.validate_account_currencies()
+
+ def test_validate_filters(self):
+ brr_doc = frappe.new_doc("Bank Reconciliation Rule")
+ brr_doc.filters = None
+ with self.assertRaises(NoFiltersError):
+ brr_doc.validate_filters()
+
+ brr_doc.filters = "[]"
+ with self.assertRaises(NoFiltersError):
+ brr_doc.validate_filters()
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..056310fa 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-02-17 23:56:35.890446",
"modified_by": "Administrator",
"module": "Klarna Kosma Integration",
"name": "ALYF Banking",
diff --git a/banking/overrides/bank_transaction.py b/banking/overrides/bank_transaction.py
index 0aa765f7..6fc1bf2d 100644
--- a/banking/overrides/bank_transaction.py
+++ b/banking/overrides/bank_transaction.py
@@ -1,8 +1,11 @@
+import json
+
import frappe
from erpnext.accounts.doctype.bank_transaction.bank_transaction import BankTransaction
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):
@@ -105,3 +108,163 @@ def on_update_after_submit(doc, event):
),
title=_("Over Allocation"),
)
+
+
+def before_submit(doc: "CustomBankTransaction", method):
+ date = doc.date or frappe.utils.nowdate()
+
+ if not doc.bank_account:
+ 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
+
+ for fieldname in ["deposit", "withdrawal"]:
+ 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))
+ )
+ )
+
+ 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 else (0, doc.withdrawal)
+
+ create_je_automatic_rules(doc, cost_center, date, account, debit, credit)
+
+
+def on_cancel(doc, method):
+ """Cancel the automatically created Journal Entries for this Bank Transaction."""
+ for journal_entry in frappe.get_all(
+ "Journal Entry",
+ filters=[
+ ["Journal Entry", "docstatus", "=", 1],
+ ["Journal Entry", "is_system_generated", "=", 1],
+ ["Journal Entry Account", "reference_type", "=", "Bank Transaction"],
+ ["Journal Entry Account", "reference_name", "=", doc.name],
+ ],
+ pluck="name",
+ distinct=True,
+ ):
+ frappe.get_doc("Journal Entry", journal_entry).cancel()
+
+
+def create_je_automatic_rules(doc, cost_center, date, account, debit, credit):
+ 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,
+ order_by="priority DESC, creation ASC",
+ )
+
+ 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(
+ 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",
+ {
+ "payment_document": "Journal Entry",
+ "payment_entry": je_auto_name,
+ "allocated_amount": debit + credit,
+ },
+ )
+ doc.allocated_amount = (doc.allocated_amount or 0) + debit + credit
+ doc.unallocated_amount = 0
+ doc.status = "Reconciled"
+ break
+
+
+def create_automatic_journal_entry(
+ 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 = company
+ journal_entry.user_remark = (
+ _("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(bank_transaction)
+ )
+ journal_entry.cheque_no = bank_transaction
+ journal_entry.cheque_date = date
+ journal_entry.multi_currency = 1
+ journal_entry.is_system_generated = 1
+
+ journal_entry.append(
+ "accounts",
+ {
+ "account": account,
+ "bank_account": bank_account,
+ "debit_in_account_currency": debit,
+ "credit_in_account_currency": credit,
+ "cost_center": cost_center,
+ "reference_type": "Bank Transaction",
+ "reference_name": bank_transaction,
+ },
+ )
+
+ journal_entry.append(
+ "accounts",
+ {
+ "account": target_account,
+ "bank_account": "",
+ "debit_in_account_currency": credit,
+ "credit_in_account_currency": debit,
+ "cost_center": cost_center,
+ },
+ )
+
+ journal_entry.submit()
+ frappe.db.set_value("Journal Entry", journal_entry.name, "clearance_date", date)
+
+ return journal_entry.name
diff --git a/banking/overrides/test_bank_transaction_reconciliation_rule.py b/banking/overrides/test_bank_transaction_reconciliation_rule.py
new file mode 100644
index 00000000..4d8a1473
--- /dev/null
+++ b/banking/overrides/test_bank_transaction_reconciliation_rule.py
@@ -0,0 +1,151 @@
+# Copyright (c) 2025, ALYF GmbH and Contributors
+# See license.txt
+
+from unittest.mock import patch
+
+import frappe
+from frappe.tests.utils import FrappeTestCase
+
+from banking.testing_utils import TEST_COMPANY, 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 TestBankTransactionReconciliationRule(FrappeTestCase):
+ @classmethod
+ def setUpClass(cls):
+ super().setUpClass()
+
+ parent_account = frappe.db.get_value("Account", {"is_group": 1, "company": TEST_COMPANY})
+ cls.account_main = create_currency_account("EUR", parent_account, "_Test_Account_EUR")
+ cls.account_target = create_currency_account("EUR", parent_account, "_Test_Account_EUR_Target")
+ cls.bank_account = create_bank_account(cls.account_main.name)
+
+ @patch("banking.overrides.bank_transaction.create_je_automatic_rules")
+ def test_before_submit_invokes_rule_engine(self, mock_create_auto_rules):
+ from banking.overrides.bank_transaction import before_submit
+
+ bt = create_bank_transaction(insert=False)
+
+ with self.assertRaises(frappe.ValidationError):
+ before_submit(bt, None)
+
+ bt.bank_account = self.bank_account.name
+ bt.deposit = 1.0
+ before_submit(bt, None)
+
+ mock_create_auto_rules.assert_called_once()
+
+ @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", {"bank_account": self.bank_account.name})
+ mock_create_je.return_value = "JE-TEST-0001"
+ date = "2025-01-01"
+
+ brr_1 = create_bank_reconciliation_rule(
+ self.bank_account.name,
+ self.account_target.name,
+ '[["Bank Transaction","description","=","FLAG-TRUE",false]]',
+ )
+
+ create_bank_reconciliation_rule(
+ self.bank_account.name,
+ self.account_main.name,
+ '[["Bank Transaction","description","=","FLAG-TRUE",false]]',
+ disabled=1,
+ )
+ create_bank_reconciliation_rule(
+ self.bank_account.name,
+ self.account_main.name,
+ '[["Bank Transaction","description","=","FLAG-TRUE",false]]',
+ submit=False,
+ )
+
+ bt = create_bank_transaction(
+ withdrawal=5.0,
+ bank_account=self.bank_account.name,
+ description="FLAG-TRUE",
+ )
+ cost_center = frappe.get_cached_value("Company", bt.company, "cost_center")
+
+ create_je_automatic_rules(bt, cost_center, date, self.account_main.name, 0, bt.withdrawal)
+
+ mock_create_je.assert_called_once_with(
+ company=bt.company,
+ bank_account=bt.bank_account,
+ bank_transaction=bt.name,
+ cost_center=cost_center,
+ date=date,
+ account=self.account_main.name,
+ target_account=self.account_target.name,
+ debit=0.0,
+ credit=5.0,
+ rule=brr_1.name,
+ )
+ 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")
+
+ @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", {"bank_account": self.bank_account.name})
+ mock_create_je.return_value = "JE-TEST-0001"
+ date = "2025-01-01"
+
+ brr_1 = create_bank_reconciliation_rule(
+ self.bank_account.name,
+ self.account_target.name,
+ '[["Bank Transaction","description","=","FLAG-TRUE",false]]',
+ )
+
+ bt = create_bank_transaction(
+ deposit=5.0,
+ bank_account=self.bank_account.name,
+ description="FLAG-TRUE",
+ )
+ cost_center = frappe.get_cached_value("Company", bt.company, "cost_center")
+
+ create_je_automatic_rules(bt, cost_center, date, self.account_main.name, bt.deposit, 0)
+
+ mock_create_je.assert_called_once_with(
+ company=bt.company,
+ bank_account=bt.bank_account,
+ bank_transaction=bt.name,
+ cost_center=cost_center,
+ date=date,
+ account=self.account_main.name,
+ target_account=self.account_target.name,
+ debit=5.0,
+ credit=0.0,
+ rule=brr_1.name,
+ )
+ 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")
diff --git a/banking/testing_utils.py b/banking/testing_utils.py
new file mode 100644
index 00000000..d3a4cef2
--- /dev/null
+++ b/banking/testing_utils.py
@@ -0,0 +1,25 @@
+# Copyright (c) 2025, ALYF GmbH and Contributors
+# See license.txt
+
+import frappe
+
+TEST_COMPANY = "Bolt Trades"
+
+
+def create_currency_account(currency: str, parent_account: str, account_name: str):
+ acc = frappe.new_doc("Account")
+ acc.account_name = account_name
+ 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):
+ ba = frappe.new_doc("Bank Account")
+ ba.account_name = "_Test_B_Account"
+ ba.account = account
+ ba.bank = "_Test_Bank"
+ ba.is_company_account = 1
+ ba.insert(ignore_permissions=True, ignore_links=True)
+ return ba