From 8d2aa69e61ff407b663de96b6cff5b702b834d70 Mon Sep 17 00:00:00 2001 From: Jatin3128 <140256508+Jatin3128@users.noreply.github.com> Date: Wed, 26 Aug 2026 13:16:24 +0530 Subject: [PATCH] feat(accounts): split bank charges from exchange gain/loss on multi-currency transfers (#58071) In a multi-currency Internal Transfer, the paid-vs-received difference was booked entirely to Exchange Gain/Loss, so a bank charge entered as a deduction pushed the Difference Amount non-zero and blocked submission. The exchange gain/loss row now absorbs only the residual after user-entered deductions, letting a Bank Charges row and the Exchange Gain/Loss row coexist and net to zero. --- .../doctype/payment_entry/payment_entry.js | 20 ++++++- .../doctype/payment_entry/payment_entry.py | 8 ++- .../payment_entry/test_payment_entry.py | 58 +++++++++++++++++++ 3 files changed, 82 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js index ec282ff0c99..2989414ead1 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.js +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js @@ -1279,8 +1279,14 @@ frappe.ui.form.on("Payment Entry", { await frappe.after_ajax(); const base_paid_amount = frm.doc.base_paid_amount || 0; const base_received_amount = frm.doc.base_received_amount || 0; + let other_deductions = 0; + if (frm.doc.payment_type === "Internal Transfer") { + other_deductions = (frm.doc.deductions || []) + .filter((row) => !row.is_exchange_gain_loss) + .reduce((sum, row) => sum + flt(row.amount), 0); + } const exchange_gain_loss = flt( - base_paid_amount - base_received_amount, + base_paid_amount - base_received_amount - other_deductions, get_deduction_amount_precision() ); @@ -1857,11 +1863,19 @@ frappe.ui.form.on("Payment Entry Deduction", { }, amount: function (frm) { - frm.events.set_unallocated_amount(frm); + if (frm.doc.payment_type === "Internal Transfer") { + frm.events.set_exchange_gain_loss_deduction(frm); + } else { + frm.events.set_unallocated_amount(frm); + } }, deductions_remove: function (frm) { - frm.events.set_unallocated_amount(frm); + if (frm.doc.payment_type === "Internal Transfer") { + frm.events.set_exchange_gain_loss_deduction(frm); + } else { + frm.events.set_unallocated_amount(frm); + } }, }); diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 247a9f9fdd3..010b229e762 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -1118,8 +1118,14 @@ class PaymentEntry(AccountsController): ) def set_exchange_gain_loss(self): + other_deductions = 0 + if self.payment_type == "Internal Transfer": + other_deductions = sum( + flt(row.amount) for row in self.get("deductions") if not row.is_exchange_gain_loss + ) + exchange_gain_loss = flt( - self.base_paid_amount - self.base_received_amount, + self.base_paid_amount - self.base_received_amount - other_deductions, self.precision("amount", "deductions"), ) diff --git a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py index d621ad2f690..c9e5405e90f 100644 --- a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py @@ -870,6 +870,64 @@ class TestPaymentEntry(ERPNextTestSuite): self.validate_gl_entries(pe.name, expected_gle) + def test_cross_currency_transfer_splits_bank_charge_and_exchange_gain_loss(self): + exchange_gain_loss_account = frappe.db.get_value( + "Company", "_Test Company", "exchange_gain_loss_account" + ) + bank_charges_account = create_account( + parent_account="Indirect Expenses - _TC", + account_name="_Test Bank Charges", + company="_Test Company", + ) + + pe = frappe.new_doc("Payment Entry") + pe.payment_type = "Internal Transfer" + pe.company = "_Test Company" + pe.paid_from = "_Test Bank USD - _TC" + pe.paid_to = "_Test Bank - _TC" + pe.paid_amount = 100 + pe.source_exchange_rate = 50 + pe.received_amount = 4500 + pe.reference_no = "6" + pe.reference_date = nowdate() + pe.append( + "deductions", + { + "account": bank_charges_account, + "cost_center": "_Test Cost Center - _TC", + "amount": 100, + }, + ) + + pe.setup_party_account_field() + pe.set_missing_values() + pe.set_exchange_rate() + pe.set_amounts() + + deductions = {d.account: d for d in pe.deductions} + self.assertEqual(deductions[bank_charges_account].amount, 100) + self.assertEqual(deductions[exchange_gain_loss_account].amount, 400) + self.assertTrue(deductions[exchange_gain_loss_account].is_exchange_gain_loss) + self.assertEqual(pe.difference_amount, 0) + + for d in pe.deductions: + d.cost_center = "_Test Cost Center - _TC" + + pe.insert() + pe.submit() + + expected_gle = dict( + (d[0], d) + for d in [ + ["_Test Bank USD - _TC", 0, 5000, None], + ["_Test Bank - _TC", 4500, 0, None], + [exchange_gain_loss_account, 400.0, 0, None], + [bank_charges_account, 100.0, 0, None], + ] + ) + + self.validate_gl_entries(pe.name, expected_gle) + def test_payment_against_negative_sales_invoice(self): si1 = create_sales_invoice()