From 896b123fb116d48f3632ca9463ee9ecc4f46f945 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Sun, 20 Aug 2023 21:17:27 +0530 Subject: [PATCH 1/3] fix: broken advance field in Accounts Receivable summary rpt --- erpnext/accounts/party.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py index 0d67752ba74..a295f6315a9 100644 --- a/erpnext/accounts/party.py +++ b/erpnext/accounts/party.py @@ -950,7 +950,7 @@ def get_partywise_advanced_payment_amount( if party: query = query.where(gle.party == party) - data = query.run(as_dict=True) + data = query.run() if data: return frappe._dict(data) From 0dc5e5c4303a64c52df5d9858c0de8fee576268e Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 21 Aug 2023 08:46:55 +0530 Subject: [PATCH 2/3] refactor: use payment ledger to fetch advance amount --- erpnext/accounts/party.py | 29 +++++++++---------- .../accounts_receivable_summary.py | 1 - 2 files changed, 14 insertions(+), 16 deletions(-) diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py index a295f6315a9..8bd7b5a3fee 100644 --- a/erpnext/accounts/party.py +++ b/erpnext/accounts/party.py @@ -14,7 +14,7 @@ from frappe.contacts.doctype.address.address import ( from frappe.contacts.doctype.contact.contact import get_contact_details from frappe.core.doctype.user_permission.user_permission import get_permitted_documents from frappe.model.utils import get_fetch_values -from frappe.query_builder.functions import Date, Sum +from frappe.query_builder.functions import Abs, Date, Sum from frappe.utils import ( add_days, add_months, @@ -922,33 +922,32 @@ def get_party_shipping_address(doctype: str, name: str) -> Optional[str]: def get_partywise_advanced_payment_amount( - party_type, posting_date=None, future_payment=0, company=None, party=None, account_type=None + party_type, posting_date=None, future_payment=0, company=None, party=None ): - gle = frappe.qb.DocType("GL Entry") + ple = frappe.qb.DocType("Payment Ledger Entry") query = ( - frappe.qb.from_(gle) - .select(gle.party) + frappe.qb.from_(ple) + .select(ple.party, Abs(Sum(ple.amount).as_("amount"))) .where( - (gle.party_type.isin(party_type)) & (gle.against_voucher.isnull()) & (gle.is_cancelled == 0) + (ple.party_type.isin(party_type)) + & (ple.amount < 0) + & (ple.against_voucher_no == ple.voucher_no) + & (ple.delinked == 0) ) - .groupby(gle.party) + .groupby(ple.party) ) - if account_type == "Receivable": - query = query.select(Sum(gle.credit).as_("amount")) - else: - query = query.select(Sum(gle.debit).as_("amount")) if posting_date: if future_payment: - query = query.where((gle.posting_date <= posting_date) | (Date(gle.creation) <= posting_date)) + query = query.where((ple.posting_date <= posting_date) | (Date(ple.creation) <= posting_date)) else: - query = query.where(gle.posting_date <= posting_date) + query = query.where(ple.posting_date <= posting_date) if company: - query = query.where(gle.company == company) + query = query.where(ple.company == company) if party: - query = query.where(gle.party == party) + query = query.where(ple.party == party) data = query.run() if data: diff --git a/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py b/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py index da4c9dabbf6..3675e80b54a 100644 --- a/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py +++ b/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py @@ -50,7 +50,6 @@ class AccountsReceivableSummary(ReceivablePayableReport): self.filters.show_future_payments, self.filters.company, party=party, - account_type=self.account_type, ) or {} ) From af52f21ecefa35a1ab7cdec4f7d64156933d91eb Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 21 Aug 2023 14:54:03 +0530 Subject: [PATCH 3/3] test: add test for receivable summary report --- .../test_accounts_receivable_summary.py | 203 ++++++++++++++++++ erpnext/accounts/test/accounts_mixin.py | 13 ++ 2 files changed, 216 insertions(+) create mode 100644 erpnext/accounts/report/accounts_receivable_summary/test_accounts_receivable_summary.py diff --git a/erpnext/accounts/report/accounts_receivable_summary/test_accounts_receivable_summary.py b/erpnext/accounts/report/accounts_receivable_summary/test_accounts_receivable_summary.py new file mode 100644 index 00000000000..3ee35a114d1 --- /dev/null +++ b/erpnext/accounts/report/accounts_receivable_summary/test_accounts_receivable_summary.py @@ -0,0 +1,203 @@ +import unittest + +import frappe +from frappe.tests.utils import FrappeTestCase, change_settings +from frappe.utils import today + +from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry +from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice +from erpnext.accounts.report.accounts_receivable_summary.accounts_receivable_summary import execute +from erpnext.accounts.test.accounts_mixin import AccountsTestMixin + + +class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase): + def setUp(self): + self.maxDiff = None + self.create_company() + self.create_customer() + self.create_item() + self.clear_old_entries() + + def tearDown(self): + frappe.db.rollback() + + def test_01_receivable_summary_output(self): + """ + Test for Invoices, Paid, Advance and Outstanding + """ + filters = { + "company": self.company, + "customer": self.customer, + "posting_date": today(), + "range1": 30, + "range2": 60, + "range3": 90, + "range4": 120, + } + + si = create_sales_invoice( + item=self.item, + company=self.company, + customer=self.customer, + debit_to=self.debit_to, + posting_date=today(), + parent_cost_center=self.cost_center, + cost_center=self.cost_center, + rate=200, + price_list_rate=200, + ) + + customer_group, customer_territory = frappe.db.get_all( + "Customer", + filters={"name": self.customer}, + fields=["customer_group", "territory"], + as_list=True, + )[0] + + report = execute(filters) + rpt_output = report[1] + expected_data = { + "party_type": "Customer", + "advance": 0, + "party": self.customer, + "invoiced": 200.0, + "paid": 0.0, + "credit_note": 0.0, + "outstanding": 200.0, + "range1": 200.0, + "range2": 0.0, + "range3": 0.0, + "range4": 0.0, + "range5": 0.0, + "total_due": 200.0, + "future_amount": 0.0, + "sales_person": [], + "currency": si.currency, + "territory": customer_territory, + "customer_group": customer_group, + } + + self.assertEqual(len(rpt_output), 1) + self.assertDictEqual(rpt_output[0], expected_data) + + # simulate advance payment + pe = get_payment_entry(si.doctype, si.name) + pe.paid_amount = 50 + pe.references[0].allocated_amount = 0 # this essitially removes the reference + pe.save().submit() + + # update expected data with advance + expected_data.update( + { + "advance": 50.0, + "outstanding": 150.0, + "range1": 150.0, + "total_due": 150.0, + } + ) + + report = execute(filters) + rpt_output = report[1] + self.assertEqual(len(rpt_output), 1) + self.assertDictEqual(rpt_output[0], expected_data) + + # make partial payment + pe = get_payment_entry(si.doctype, si.name) + pe.paid_amount = 125 + pe.references[0].allocated_amount = 125 + pe.save().submit() + + # update expected data after advance and partial payment + expected_data.update( + {"advance": 50.0, "paid": 125.0, "outstanding": 25.0, "range1": 25.0, "total_due": 25.0} + ) + + report = execute(filters) + rpt_output = report[1] + self.assertEqual(len(rpt_output), 1) + self.assertDictEqual(rpt_output[0], expected_data) + + @change_settings("Selling Settings", {"cust_master_name": "Naming Series"}) + def test_02_various_filters_and_output(self): + filters = { + "company": self.company, + "customer": self.customer, + "posting_date": today(), + "range1": 30, + "range2": 60, + "range3": 90, + "range4": 120, + } + + si = create_sales_invoice( + item=self.item, + company=self.company, + customer=self.customer, + debit_to=self.debit_to, + posting_date=today(), + parent_cost_center=self.cost_center, + cost_center=self.cost_center, + rate=200, + price_list_rate=200, + ) + # make partial payment + pe = get_payment_entry(si.doctype, si.name) + pe.paid_amount = 150 + pe.references[0].allocated_amount = 150 + pe.save().submit() + + customer_group, customer_territory = frappe.db.get_all( + "Customer", + filters={"name": self.customer}, + fields=["customer_group", "territory"], + as_list=True, + )[0] + + report = execute(filters) + rpt_output = report[1] + expected_data = { + "party_type": "Customer", + "advance": 0, + "party": self.customer, + "party_name": self.customer, + "invoiced": 200.0, + "paid": 150.0, + "credit_note": 0.0, + "outstanding": 50.0, + "range1": 50.0, + "range2": 0.0, + "range3": 0.0, + "range4": 0.0, + "range5": 0.0, + "total_due": 50.0, + "future_amount": 0.0, + "sales_person": [], + "currency": si.currency, + "territory": customer_territory, + "customer_group": customer_group, + } + + self.assertEqual(len(rpt_output), 1) + self.assertDictEqual(rpt_output[0], expected_data) + + # with gl balance filter + filters.update({"show_gl_balance": True}) + expected_data.update({"gl_balance": 50.0, "diff": 0.0}) + report = execute(filters) + rpt_output = report[1] + self.assertEqual(len(rpt_output), 1) + self.assertDictEqual(rpt_output[0], expected_data) + + # with gl balance and future payments filter + filters.update({"show_future_payments": True}) + expected_data.update({"remaining_balance": 50.0}) + report = execute(filters) + rpt_output = report[1] + self.assertEqual(len(rpt_output), 1) + self.assertDictEqual(rpt_output[0], expected_data) + + # invoice fully paid + pe = get_payment_entry(si.doctype, si.name).save().submit() + report = execute(filters) + rpt_output = report[1] + self.assertEqual(len(rpt_output), 0) diff --git a/erpnext/accounts/test/accounts_mixin.py b/erpnext/accounts/test/accounts_mixin.py index 70bbf7e694d..debfffdcbb3 100644 --- a/erpnext/accounts/test/accounts_mixin.py +++ b/erpnext/accounts/test/accounts_mixin.py @@ -1,4 +1,5 @@ import frappe +from frappe import qb from erpnext.stock.doctype.item.test_item import create_item @@ -103,3 +104,15 @@ class AccountsTestMixin: ) new_acc.save() setattr(self, acc.attribute_name, new_acc.name) + + def clear_old_entries(self): + doctype_list = [ + "GL Entry", + "Payment Ledger Entry", + "Sales Invoice", + "Purchase Invoice", + "Payment Entry", + "Journal Entry", + ] + for doctype in doctype_list: + qb.from_(qb.DocType(doctype)).delete().where(qb.DocType(doctype).company == self.company).run()