diff --git a/erpnext/accounts/report/accounts_payable/test_accounts_payable.py b/erpnext/accounts/report/accounts_payable/test_accounts_payable.py index 8971dc3d37b..0c104f6f96e 100644 --- a/erpnext/accounts/report/accounts_payable/test_accounts_payable.py +++ b/erpnext/accounts/report/accounts_payable/test_accounts_payable.py @@ -1,6 +1,6 @@ import frappe from frappe.tests.utils import FrappeTestCase -from frappe.utils import today +from frappe.utils import add_days, today from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice from erpnext.accounts.report.accounts_payable.accounts_payable import execute @@ -57,3 +57,66 @@ class TestAccountsPayable(AccountsTestMixin, FrappeTestCase): if not do_not_submit: pi = pi.submit() return pi + + def test_payment_terms_template_filters(self): + from erpnext.controllers.accounts_controller import get_payment_terms + + payment_term1 = frappe.get_doc( + {"doctype": "Payment Term", "payment_term_name": "_Test 50% on 15 Days"} + ).insert() + payment_term2 = frappe.get_doc( + {"doctype": "Payment Term", "payment_term_name": "_Test 50% on 30 Days"} + ).insert() + + template = frappe.get_doc( + { + "doctype": "Payment Terms Template", + "template_name": "_Test 50-50", + "terms": [ + { + "doctype": "Payment Terms Template Detail", + "due_date_based_on": "Day(s) after invoice date", + "payment_term": payment_term1.name, + "description": "_Test 50-50", + "invoice_portion": 50, + "credit_days": 15, + }, + { + "doctype": "Payment Terms Template Detail", + "due_date_based_on": "Day(s) after invoice date", + "payment_term": payment_term2.name, + "description": "_Test 50-50", + "invoice_portion": 50, + "credit_days": 30, + }, + ], + } + ) + template.insert() + + filters = { + "company": self.company, + "report_date": today(), + "range": "30, 60, 90, 120", + "based_on_payment_terms": 1, + "payment_terms_template": template.name, + "ageing_based_on": "Posting Date", + } + + pi = self.create_purchase_invoice(do_not_submit=True) + pi.payment_terms_template = template.name + schedule = get_payment_terms(template.name) + pi.set("payment_schedule", []) + + for row in schedule: + row["due_date"] = add_days(pi.posting_date, row.get("credit_days", 0)) + pi.append("payment_schedule", row) + + pi.save() + pi.submit() + + report = execute(filters) + row = report[1][0] + + self.assertEqual(len(report[1]), 2) + self.assertEqual([pi.name, payment_term1.payment_term_name], [row.voucher_no, row.payment_term]) diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py index 9ca930bd829..831873055f1 100644 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py @@ -1029,9 +1029,8 @@ class ReceivablePayableReport: self, ): self.customer = qb.DocType("Customer") - if self.filters.get("customer_group"): - groups = get_customer_group_with_children(self.filters.customer_group) + groups = get_party_group_with_children("Customer", self.filters.customer_group) customers = ( qb.from_(self.customer) .select(self.customer.name) @@ -1043,14 +1042,18 @@ class ReceivablePayableReport: self.get_hierarchical_filters("Territory", "territory") if self.filters.get("payment_terms_template"): - self.qb_selection_filter.append( - self.ple.party.isin( - qb.from_(self.customer) - .select(self.customer.name) - .where(self.customer.payment_terms == self.filters.get("payment_terms_template")) - ) + customer_ptt = self.ple.party.isin( + qb.from_(self.customer) + .select(self.customer.name) + .where(self.customer.payment_terms == self.filters.get("payment_terms_template")) ) + si_ptt = self.add_payment_term_template_filters("Sales Invoice") + + sales_ptt = self.ple.against_voucher_no.isin(si_ptt) + + self.qb_selection_filter.append(Criterion.any([customer_ptt, sales_ptt])) + if self.filters.get("sales_partner"): self.qb_selection_filter.append( self.ple.party.isin( @@ -1075,14 +1078,53 @@ class ReceivablePayableReport: ) if self.filters.get("payment_terms_template"): - self.qb_selection_filter.append( - self.ple.party.isin( - qb.from_(supplier) - .select(supplier.name) - .where(supplier.payment_terms == self.filters.get("supplier_group")) - ) + supplier_ptt = self.ple.party.isin( + qb.from_(supplier) + .select(supplier.name) + .where(supplier.payment_terms == self.filters.get("payment_terms_template")) ) + pi_ptt = self.add_payment_term_template_filters("Purchase Invoice") + + purchase_ptt = self.ple.against_voucher_no.isin(pi_ptt) + + self.qb_selection_filter.append(Criterion.any([supplier_ptt, purchase_ptt])) + + def add_payment_term_template_filters(self, dtype): + voucher_type = qb.DocType(dtype) + + ptt = ( + qb.from_(voucher_type) + .select(voucher_type.name) + .where(voucher_type.payment_terms_template == self.filters.get("payment_terms_template")) + .where(voucher_type.company == self.filters.company) + ) + + if dtype == "Purchase Invoice": + party = "Supplier" + party_group_type = "supplier_group" + acc_type = "credit_to" + else: + party = "Customer" + party_group_type = "customer_group" + acc_type = "debit_to" + + if self.filters.get(party_group_type): + party_groups = get_party_group_with_children(party, self.filters.get(party_group_type)) + ptt = ptt.where((voucher_type[party_group_type]).isin(party_groups)) + + if self.filters.party: + ptt = ptt.where((voucher_type[party.lower()]).isin(self.filters.party)) + + if self.filters.cost_center: + cost_centers = get_cost_centers_with_children(self.filters.cost_center) + ptt = ptt.where(voucher_type.cost_center.isin(cost_centers)) + + if self.filters.party_account: + ptt = ptt.where(voucher_type[acc_type] == self.filters.party_account) + + return ptt + def get_hierarchical_filters(self, doctype, key): lft, rgt = frappe.db.get_value(doctype, self.filters.get(key), ["lft", "rgt"]) @@ -1320,20 +1362,26 @@ class ReceivablePayableReport: self.err_journals = [x[0] for x in results] if results else [] -def get_customer_group_with_children(customer_groups): - if not isinstance(customer_groups, list): - customer_groups = [d.strip() for d in customer_groups.strip().split(",") if d] +def get_party_group_with_children(party, party_groups): + if party not in ("Customer", "Supplier"): + return [] - all_customer_groups = [] - for d in customer_groups: - if frappe.db.exists("Customer Group", d): - lft, rgt = frappe.db.get_value("Customer Group", d, ["lft", "rgt"]) - children = frappe.get_all("Customer Group", filters={"lft": [">=", lft], "rgt": ["<=", rgt]}) - all_customer_groups += [c.name for c in children] + group_dtype = f"{party} Group" + if not isinstance(party_groups, list): + party_groups = [d.strip() for d in party_groups.strip().split(",") if d] + + all_party_groups = [] + for d in party_groups: + if frappe.db.exists(group_dtype, d): + lft, rgt = frappe.db.get_value(group_dtype, d, ["lft", "rgt"]) + children = frappe.get_all( + group_dtype, filters={"lft": [">=", lft], "rgt": ["<=", rgt]}, pluck="name" + ) + all_party_groups += children else: - frappe.throw(_("Customer Group: {0} does not exist").format(d)) + frappe.throw(_("{0}: {1} does not exist").format(group_dtype, d)) - return list(set(all_customer_groups)) + return list(set(all_party_groups)) class InitSQLProceduresForAR: diff --git a/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py index 19f51dc7a03..4dfcd3bf259 100644 --- a/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py @@ -1139,3 +1139,66 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase): self.assertEqual(len(report[1]), 1) row = report[1][0] self.assertEqual(expected_data_after_payment, [row.voucher_no, row.cost_center, row.outstanding]) + + def test_payment_terms_template_filters(self): + from erpnext.controllers.accounts_controller import get_payment_terms + + payment_term1 = frappe.get_doc( + {"doctype": "Payment Term", "payment_term_name": "_Test 50% on 15 Days"} + ).insert() + payment_term2 = frappe.get_doc( + {"doctype": "Payment Term", "payment_term_name": "_Test 50% on 30 Days"} + ).insert() + + template = frappe.get_doc( + { + "doctype": "Payment Terms Template", + "template_name": "_Test 50-50", + "terms": [ + { + "doctype": "Payment Terms Template Detail", + "due_date_based_on": "Day(s) after invoice date", + "payment_term": payment_term1.name, + "description": "_Test 50-50", + "invoice_portion": 50, + "credit_days": 15, + }, + { + "doctype": "Payment Terms Template Detail", + "due_date_based_on": "Day(s) after invoice date", + "payment_term": payment_term2.name, + "description": "_Test 50-50", + "invoice_portion": 50, + "credit_days": 30, + }, + ], + } + ) + template.insert() + + filters = { + "company": self.company, + "report_date": today(), + "range": "30, 60, 90, 120", + "based_on_payment_terms": 1, + "payment_terms_template": template.name, + "ageing_based_on": "Posting Date", + } + + si = self.create_sales_invoice(no_payment_schedule=True, do_not_submit=True) + si.payment_terms_template = template.name + schedule = get_payment_terms(template.name) + si.set("payment_schedule", []) + + for row in schedule: + row["due_date"] = add_days(si.posting_date, row.get("credit_days", 0)) + si.append("payment_schedule", row) + + si.save() + si.submit() + + report = execute(filters) + row = report[1][0] + + self.assertEqual(len(report[1]), 2) + self.assertEqual([si.name, payment_term1.payment_term_name], [row.voucher_no, row.payment_term])