Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
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
33 changes: 33 additions & 0 deletions banking/custom/bank_account.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// Copyright (c) 2025, ALYF GmbH and contributors
// For license information, please see license.txt

frappe.ui.form.on("Bank Account", {
refresh(frm) {
frm.trigger("set_fee_account_query");
},

account(frm) {
frm.trigger("set_fee_account_query");
},

async set_fee_account_query(frm) {
if (!frm.doc.account) {
return;
}

const {
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: {
root_type: "Expense",
account_currency: currency,
company: company,
},
}));
},
});
10 changes: 10 additions & 0 deletions banking/custom_fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -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="eval:doc.is_company_account && doc.account",
insert_after="account_subtype",
),
],
}
3 changes: 3 additions & 0 deletions banking/hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"}
Expand Down Expand Up @@ -108,9 +109,11 @@
doc_events = {
"Bank Transaction": {
"on_update_after_submit": "banking.overrides.bank_transaction.on_update_after_submit",
"before_submit": "banking.overrides.bank_transaction.before_submit",
},
"Bank Account": {
"before_validate": "banking.overrides.bank_account.before_validate",
"validate": "banking.overrides.bank_account.validate",
},
"Employee": {
"validate": "banking.custom.employee.validate",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from frappe.query_builder.functions import Cast, Coalesce, Sum
from frappe.utils import cint, flt, sbool
from pypika import Order
from pypika.terms import ExistsCriterion

from banking.klarna_kosma_integration.doctype.bank_reconciliation_tool_beta.utils import (
amount_rank_condition,
Expand Down Expand Up @@ -981,10 +982,30 @@ def get_je_matching_query(
)

if bank_transaction_name:
# This filter ensures that Journal Entries that have been created
# automatically for the Bank Transaction (e.g. to Cash In Transit) via
# other apps are not offered as matches.
subquery = subquery.where(je.cheque_no != bank_transaction_name)
je_reference = frappe.qb.DocType("Journal Entry Account")
has_bank_transaction_reference = ExistsCriterion(
frappe.qb.from_(je_reference)
.select(je_reference.name)
.where(
(je_reference.parent == je.name)
& (je_reference.reference_type == "Bank Transaction")
& (je_reference.reference_name == bank_transaction_name)
)
)
is_auto_fee_journal_entry = (je.is_system_generated == 1) & has_bank_transaction_reference
# Journal Entries that other apps create for the Bank Transaction
# (e.g. to Cash In Transit) are often linked via cheque_no and should
# not be offered as matches. Standard withdrawal fee JEs are excluded
# from this bucket because they should reappear after unreconciliation.
is_cheque_linked_custom_journal_entry = (
je.cheque_no == bank_transaction_name
) & ~is_auto_fee_journal_entry
subquery = subquery.where(~is_cheque_linked_custom_journal_entry)

if common_filters.payment_type == "Receive":
# Standard deposit fee JEs from the built-in bank-fee logic must stay
# hidden even if they are not identified by cheque_no.
subquery = subquery.where(~is_auto_fee_journal_entry)

if frappe.flags.auto_reconcile_vouchers:
subquery = subquery.where(je.cheque_no == common_filters.reference_no)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -683,6 +683,171 @@ def test_split_jv_match_against_transaction(self):
self.assertEqual(first_match["name"], journal_entry.name)
self.assertEqual(first_match["paid_amount"], 200.0)

def test_auto_generated_fee_journal_entry_is_excluded_for_deposit_matches(self):
"""Deposit fee JEs must stay hidden even if they are otherwise query-eligible."""
frappe.db.set_single_value("Banking Settings", "enable_automatic_journal_entries_for_bank_fees", 1)

bank = create_bank("Citi Bank Fee Filter", swift_number="CITIUS35")
gl_account = create_bank_gl_account("_Test Fee Filter Bank - _TC")
fee_account = create_bank_gl_account("_Test Fee Filter Offset - _TC")
bank_account = frappe.get_doc(
{
"doctype": "Bank Account",
"account_name": "Fee Filter Account",
"bank": bank.name,
"account": gl_account,
"bank_fee_account": fee_account,
"company": "_Test Company",
"is_company_account": 1,
}
).insert()

bt = frappe.get_doc(
{
"doctype": "Bank Transaction",
"company": "_Test Company",
"description": "Deposit with auto-generated fee entry",
"date": getdate(),
"deposit": 5.0,
"included_fee": 1.0,
"currency": "INR",
"bank_account": bank_account.name,
"reference_number": "FEE-FILTER-001",
}
).insert()
bt.submit()
bt.reload()

journal_entries = frappe.get_all(
"Journal Entry Account",
filters={
"reference_type": "Bank Transaction",
"reference_name": bt.name,
},
pluck="parent",
)
self.assertEqual(len(journal_entries), 1)

fee_journal_entry = frappe.get_doc("Journal Entry", journal_entries[0])
self.assertTrue(fee_journal_entry.is_system_generated)
self.assertEqual(fee_journal_entry.cheque_no, bt.name)

# Make the JE query-eligible and prove deposit exclusion does not depend on cheque_no.
frappe.db.set_value("Journal Entry", fee_journal_entry.name, "clearance_date", None)
frappe.db.set_value("Journal Entry", fee_journal_entry.name, "cheque_no", "UNRELATED-CHEQUE-NO")

matched_vouchers = get_linked_payments(
bank_transaction_name=bt.name,
document_types=["journal_entry"],
from_date=add_days(getdate(), -1),
to_date=add_days(getdate(), 1),
)
matched_names = [voucher["name"] for voucher in matched_vouchers]

self.assertEqual(matched_names, [])
self.assertNotIn(fee_journal_entry.name, matched_names)

def test_auto_generated_fee_journal_entry_is_offered_again_after_unreconcile(self):
"""Withdrawal fee JEs must be offered again once unreconciled from the BT."""
frappe.db.set_single_value("Banking Settings", "enable_automatic_journal_entries_for_bank_fees", 1)

bank = create_bank("Citi Bank Fee Reoffer", swift_number="CITIUS36")
gl_account = create_bank_gl_account("_Test Fee Reoffer Bank - _TC")
fee_account = create_bank_gl_account("_Test Fee Reoffer Offset - _TC")
bank_account = frappe.get_doc(
{
"doctype": "Bank Account",
"account_name": "Fee Reoffer Account",
"bank": bank.name,
"account": gl_account,
"bank_fee_account": fee_account,
"company": "_Test Company",
"is_company_account": 1,
}
).insert()

bt = frappe.get_doc(
{
"doctype": "Bank Transaction",
"company": "_Test Company",
"description": "Withdrawal with auto-generated fee entry",
"date": getdate(),
"withdrawal": 5.0,
"included_fee": 1.0,
"currency": "INR",
"bank_account": bank_account.name,
"reference_number": "FEE-FILTER-002",
}
).insert()
bt.submit()
bt.reload()

self.assertEqual(len(bt.payment_entries), 1)
fee_journal_entry = frappe.get_doc("Journal Entry", bt.payment_entries[0].payment_entry)
self.assertEqual(str(fee_journal_entry.clearance_date), str(bt.date))

bt.remove_payment_entries()
bt.reload()
fee_journal_entry.reload()

self.assertEqual(bt.status, "Unreconciled")
self.assertFalse(bt.payment_entries)
self.assertIsNone(fee_journal_entry.clearance_date)

matched_vouchers = get_linked_payments(
bank_transaction_name=bt.name,
document_types=["journal_entry"],
from_date=add_days(getdate(), -1),
to_date=add_days(getdate(), 1),
)
matched_names = [voucher["name"] for voucher in matched_vouchers]

self.assertIn(fee_journal_entry.name, matched_names)
self.assertEqual(matched_vouchers[0]["paid_amount"], 1.0)

def test_cheque_number_linked_journal_entry_is_excluded_from_matches(self):
"""Cheque-number-linked custom JEs must stay hidden."""
bt = create_bank_transaction(
date=getdate(),
withdrawal=200,
bank_account=self.bank_account,
reference_no="CUSTOM-JE-001",
)
journal_entry = create_journal_entry_bts(
bank_transaction_name=bt.name,
party_type="Customer",
party=self.customer,
posting_date=bt.date,
reference_number=bt.name,
reference_date=bt.date,
entry_type="Bank Entry",
second_account=frappe.db.get_value("Company", bt.company, "default_receivable_account"),
allow_edit=True,
)
journal_entry.submit()

self.assertFalse(journal_entry.is_system_generated)
self.assertFalse(
frappe.db.exists(
"Journal Entry Account",
{
"parent": journal_entry.name,
"reference_type": "Bank Transaction",
"reference_name": bt.name,
},
)
)

matched_vouchers = get_linked_payments(
bank_transaction_name=bt.name,
document_types=["journal_entry"],
from_date=add_days(getdate(), -1),
to_date=add_days(getdate(), 1),
)
matched_names = [voucher["name"] for voucher in matched_vouchers]

self.assertNotIn(journal_entry.name, matched_names)

def test_usd_purchase_invoice_paid_in_usd(self):
"""Reconcile a USD Purchase Invoice via a USD bank account.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -115,13 +116,20 @@
"fieldtype": "Table MultiSelect",
"label": "Voucher Matching Defaults",
"options": "Voucher Matching Default"
},
{
"default": "0",
"description": "When a <b>Bank Transaction</b> with <i>Included Fee</i> is submitted, we'll automatically create a <b>Journal Entry</b> for the fee amount. Please specify the <i>Bank Fee Account</i> in your <a href=\"/app/bank-account?is_company_account=1\"><b>Bank Account</b></a>.",
"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 23:56:13.629502",
"modified_by": "Administrator",
"module": "Klarna Kosma Integration",
"name": "Banking Settings",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from semantic_version import Version

from banking.klarna_kosma_integration.admin import Admin
from banking.overrides.bank_account import get_company_bank_accounts_without_fee_account


class BankingSettings(Document):
Expand All @@ -33,6 +34,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
Expand All @@ -41,9 +43,27 @@ class BankingSettings(Document):
voucher_matching_defaults: DF.TableMultiSelect[VoucherMatchingDefault]

# end: auto-generated types
def validate(self):
self.validate_automatic_fee_entry_configuration()

def before_validate(self):
self.update_fintech_license()

def validate_automatic_fee_entry_configuration(self):
if not self.enable_automatic_journal_entries_for_bank_fees:
return

if not self.has_value_changed("enable_automatic_journal_entries_for_bank_fees"):
return

missing_fee_accounts = get_company_bank_accounts_without_fee_account()
if missing_fee_accounts:
frappe.throw(
_(
"Automatic journal entries for bank fees can only be enabled when all company Bank Accounts have a Bank Fee Account."
)
)

def update_fintech_license(self):
if not self.enabled:
return self.reset_fintech_license()
Expand Down
Loading
Loading