mirror of
https://github.com/frappe/erpnext.git
synced 2026-09-09 15:29:30 +00:00
* fix(bank reconciliation): match Payment Entries on the bank-side amount (#57740)
* fix(bank reconciliation): match Payment Entries on the bank-side amount
get_pe_matching_query() ranked and filtered on pe.paid_amount while the
match card displayed pe.base_paid_amount_after_tax, so the amount used for
the exact match never matched the amount shown.
Both now use the amount that actually hits the bank account, in that
account's currency: received_amount_after_tax when the bank account is
paid_to (deposit) and paid_amount_after_tax when it is paid_from
(withdrawal). This is the same convention as the Bank Reconciliation
Statement report and matches the bank GL entry that reconciliation
allocates against.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* test(bank reconciliation): cover bank-side amount matching
Two cases the previous behaviour got wrong or could regress on:
- A deposit from an internal transfer where the paid and received sides
differ by a charge. The match must show, and compare against, the
amount that reached this bank account.
- A withdrawal, which still matches on the paid side.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 154c6fb943)
# Conflicts:
# erpnext/accounts/doctype/bank_reconciliation_tool/test_bank_reconciliation_tool.py
* fix: conflicts
* fix: add missing import
* chore: linting
---------
Co-authored-by: Hussain Nagaria <34810212+NagariaHussain@users.noreply.github.com>
Co-authored-by: Nikhil Kothari <nik.kothari22@live.com>
This commit is contained in:
@@ -1340,9 +1340,11 @@ def get_pe_matching_query(
|
||||
ref_condition = pe.reference_no == transaction.reference_number
|
||||
ref_rank = frappe.qb.terms.Case().when(ref_condition, 1).else_(0)
|
||||
|
||||
amount_equality = pe.paid_amount == transaction.unallocated_amount
|
||||
amount_field = pe.received_amount_after_tax if account_from_to == "paid_to" else pe.paid_amount_after_tax
|
||||
|
||||
amount_equality = amount_field == transaction.unallocated_amount
|
||||
amount_rank = frappe.qb.terms.Case().when(amount_equality, 1).else_(0)
|
||||
amount_condition = amount_equality if exact_match else pe.paid_amount > 0.0
|
||||
amount_condition = amount_equality if exact_match else amount_field > 0.0
|
||||
|
||||
party_condition = (
|
||||
(pe.party_type == transaction.party_type) & (pe.party == transaction.party) & pe.party.isnotnull()
|
||||
@@ -1359,7 +1361,7 @@ def get_pe_matching_query(
|
||||
(ref_rank + amount_rank + party_rank + 1).as_("rank"),
|
||||
ConstantColumn("Payment Entry").as_("doctype"),
|
||||
pe.name,
|
||||
pe.base_paid_amount_after_tax.as_("paid_amount"),
|
||||
amount_field.as_("paid_amount"),
|
||||
pe.reference_no,
|
||||
pe.reference_date,
|
||||
pe.party,
|
||||
|
||||
@@ -8,7 +8,9 @@ from frappe.utils import add_days, today
|
||||
|
||||
from erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool import (
|
||||
auto_reconcile_vouchers,
|
||||
get_auto_reconcile_message,
|
||||
get_bank_transactions,
|
||||
get_linked_payments,
|
||||
)
|
||||
from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_entry
|
||||
from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
|
||||
@@ -97,3 +99,103 @@ class TestBankReconciliationTool(ERPNextTestSuite, AccountsTestMixin):
|
||||
# assert API output post reconciliation
|
||||
transactions = get_bank_transactions(self.bank_account, from_date, to_date)
|
||||
self.assertEqual(len(transactions), 0)
|
||||
|
||||
def make_bank_transaction(self, date, deposit=100, withdrawal=0):
|
||||
return (
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "Bank Transaction",
|
||||
"date": date,
|
||||
"deposit": deposit,
|
||||
"withdrawal": withdrawal,
|
||||
"bank_account": self.bank_account,
|
||||
"currency": "INR",
|
||||
}
|
||||
)
|
||||
.save()
|
||||
.submit()
|
||||
)
|
||||
|
||||
def get_matching_payment_entries(self, bank_transaction, exact_match=False):
|
||||
document_types = ["payment_entry", "exact_match"] if exact_match else ["payment_entry"]
|
||||
vouchers = get_linked_payments(
|
||||
bank_transaction,
|
||||
document_types,
|
||||
from_date=add_days(today(), -1),
|
||||
to_date=today(),
|
||||
)
|
||||
return [v for v in vouchers if v.get("doctype") == "Payment Entry"]
|
||||
|
||||
def test_get_bank_transactions_excludes_dates_after_to_date(self):
|
||||
self.make_bank_transaction(date=today())
|
||||
names = [t.name for t in get_bank_transactions(self.bank_account, to_date=add_days(today(), -1))]
|
||||
self.assertEqual(names, [])
|
||||
|
||||
def test_deposit_matches_amount_received_in_bank_account(self):
|
||||
# money leaves another bank account and lands here minus a charge, so the two sides differ
|
||||
payment = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Payment Entry",
|
||||
"payment_type": "Internal Transfer",
|
||||
"company": self.company,
|
||||
"posting_date": today(),
|
||||
"paid_from": "_Test Bank - _TC",
|
||||
"paid_to": self.bank,
|
||||
"paid_amount": 3537.64,
|
||||
"received_amount": 3460.52,
|
||||
"reference_no": "TRF-001",
|
||||
"reference_date": today(),
|
||||
}
|
||||
)
|
||||
payment.set_missing_values()
|
||||
payment.set_exchange_rate()
|
||||
payment.set_amounts()
|
||||
payment.deductions[-1].account = "_Test Exchange Gain/Loss - _TC"
|
||||
payment.deductions[-1].cost_center = "_Test Cost Center - _TC"
|
||||
payment = payment.save().submit()
|
||||
|
||||
transaction = self.make_bank_transaction(date=today(), deposit=3460.52)
|
||||
|
||||
# the received side is what reached this bank account, so that is what is shown
|
||||
matches = self.get_matching_payment_entries(transaction.name)
|
||||
self.assertEqual([m["name"] for m in matches], [payment.name])
|
||||
self.assertEqual(matches[0]["paid_amount"], 3460.52)
|
||||
|
||||
# and what the exact match compares against
|
||||
exact_matches = self.get_matching_payment_entries(transaction.name, exact_match=True)
|
||||
self.assertEqual([m["name"] for m in exact_matches], [payment.name])
|
||||
|
||||
def test_withdrawal_matches_amount_paid_from_bank_account(self):
|
||||
payment = create_payment_entry(
|
||||
company=self.company,
|
||||
payment_type="Pay",
|
||||
party_type="Supplier",
|
||||
party="_Test Supplier",
|
||||
paid_from=self.bank,
|
||||
paid_to="Creditors - _TC",
|
||||
paid_amount=1250,
|
||||
)
|
||||
payment = payment.save().submit()
|
||||
|
||||
transaction = self.make_bank_transaction(date=today(), deposit=0, withdrawal=1250)
|
||||
|
||||
exact_matches = self.get_matching_payment_entries(transaction.name, exact_match=True)
|
||||
self.assertEqual([m["name"] for m in exact_matches], [payment.name])
|
||||
self.assertEqual(exact_matches[0]["paid_amount"], 1250)
|
||||
|
||||
def test_auto_reconcile_message_for_no_matches(self):
|
||||
message, indicator = get_auto_reconcile_message([], [])
|
||||
self.assertEqual(indicator, "blue")
|
||||
self.assertIn("No matches", message)
|
||||
|
||||
def test_auto_reconcile_message_counts_and_pluralizes(self):
|
||||
# reconciled count is reported and the indicator turns green
|
||||
message, indicator = get_auto_reconcile_message([], ["t1", "t2"])
|
||||
self.assertEqual(indicator, "green")
|
||||
self.assertIn("2 Transaction(s) Reconciled", message)
|
||||
|
||||
# partially-reconciled label is singular for one, plural for many
|
||||
singular, _ = get_auto_reconcile_message(["p1"], [])
|
||||
self.assertIn("1 Transaction Partially Reconciled", singular)
|
||||
plural, _ = get_auto_reconcile_message(["p1", "p2"], [])
|
||||
self.assertIn("2 Transactions Partially Reconciled", plural)
|
||||
|
||||
Reference in New Issue
Block a user