Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions banking/exceptions.py
Original file line number Diff line number Diff line change
@@ -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
2 changes: 2 additions & 0 deletions banking/hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
});
},
});
Original file line number Diff line number Diff line change
@@ -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 <b>Journal Entry</b> is created automatically, when a submitted <b>Bank Transaction</b> matches these filters.<br>Please define these filters as strictly as possible to avoid accounting errors.<br><br>"
},
{
"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 <b>Bank Transaction</b>, 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": []
}
Original file line number Diff line number Diff line change
@@ -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)
Original file line number Diff line number Diff line change
@@ -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"];
}
},
};
Original file line number Diff line number Diff line change
@@ -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()
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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",
Expand Down
Loading
Loading