diff --git a/erpnext/accounts/report/purchase_register/purchase_register.py b/erpnext/accounts/report/purchase_register/purchase_register.py index c3599593103..a73c72c6d82 100644 --- a/erpnext/accounts/report/purchase_register/purchase_register.py +++ b/erpnext/accounts/report/purchase_register/purchase_register.py @@ -124,11 +124,10 @@ def get_columns(invoice_list, additional_table_columns): _("Purchase Receipt") + ":Link/Purchase Receipt:100", {"fieldname": "currency", "label": _("Currency"), "fieldtype": "Data", "width": 80}, ] - expense_accounts = ( - tax_accounts - ) = ( - expense_columns - ) = tax_columns = unrealized_profit_loss_accounts = unrealized_profit_loss_account_columns = [] + + expense_accounts = [] + tax_accounts = [] + unrealized_profit_loss_accounts = [] if invoice_list: expense_accounts = frappe.db.sql_list( @@ -163,10 +162,11 @@ def get_columns(invoice_list, additional_table_columns): unrealized_profit_loss_account_columns = [ (account + ":Currency/currency:120") for account in unrealized_profit_loss_accounts ] - - for account in tax_accounts: - if account not in expense_accounts: - tax_columns.append(account + ":Currency/currency:120") + tax_columns = [ + (account + ":Currency/currency:120") + for account in tax_accounts + if account not in expense_accounts + ] columns = ( columns diff --git a/erpnext/selling/doctype/customer/customer.py b/erpnext/selling/doctype/customer/customer.py index 2e5cbb80cb6..8889a5f939a 100644 --- a/erpnext/selling/doctype/customer/customer.py +++ b/erpnext/selling/doctype/customer/customer.py @@ -100,7 +100,8 @@ class Customer(TransactionBase): @frappe.whitelist() def get_customer_group_details(self): doc = frappe.get_doc("Customer Group", self.customer_group) - self.accounts = self.credit_limits = [] + self.accounts = [] + self.credit_limits = [] self.payment_terms = self.default_price_list = "" tables = [["accounts", "account"], ["credit_limits", "credit_limit"]] diff --git a/erpnext/selling/doctype/customer/test_customer.py b/erpnext/selling/doctype/customer/test_customer.py index 4979b8f976a..f631a6ef568 100644 --- a/erpnext/selling/doctype/customer/test_customer.py +++ b/erpnext/selling/doctype/customer/test_customer.py @@ -47,7 +47,8 @@ class TestCustomer(FrappeTestCase): c_doc.customer_name = "Testing Customer" c_doc.customer_group = "_Testing Customer Group" c_doc.payment_terms = c_doc.default_price_list = "" - c_doc.accounts = c_doc.credit_limits = [] + c_doc.accounts = [] + c_doc.credit_limits = [] c_doc.insert() c_doc.get_customer_group_details() self.assertEqual(c_doc.payment_terms, "_Test Payment Term Template 3") diff --git a/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.js b/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.js index 0e36b3fe3d2..c068ae3b5a4 100644 --- a/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.js +++ b/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.js @@ -27,28 +27,55 @@ function get_filters() { "default": frappe.datetime.get_today() }, { - "fieldname":"sales_order", - "label": __("Sales Order"), - "fieldtype": "MultiSelectList", + "fieldname":"customer_group", + "label": __("Customer Group"), + "fieldtype": "Link", "width": 100, - "options": "Sales Order", - "get_data": function(txt) { - return frappe.db.get_link_options("Sales Order", txt, this.filters()); - }, - "filters": () => { - return { - docstatus: 1, - payment_terms_template: ['not in', ['']], - company: frappe.query_report.get_filter_value("company"), - transaction_date: ['between', [frappe.query_report.get_filter_value("period_start_date"), frappe.query_report.get_filter_value("period_end_date")]] + "options": "Customer Group", + }, + { + "fieldname":"customer", + "label": __("Customer"), + "fieldtype": "Link", + "width": 100, + "options": "Customer", + "get_query": () => { + var customer_group = frappe.query_report.get_filter_value('customer_group'); + return{ + "query": "erpnext.selling.report.payment_terms_status_for_sales_order.payment_terms_status_for_sales_order.get_customers_or_items", + "filters": [ + ['Customer', 'disabled', '=', '0'], + ['Customer Group','name', '=', customer_group] + ] + } + } + }, + { + "fieldname":"item_group", + "label": __("Item Group"), + "fieldtype": "Link", + "width": 100, + "options": "Item Group", + + }, + { + "fieldname":"item", + "label": __("Item"), + "fieldtype": "Link", + "width": 100, + "options": "Item", + "get_query": () => { + var item_group = frappe.query_report.get_filter_value('item_group'); + return{ + "query": "erpnext.selling.report.payment_terms_status_for_sales_order.payment_terms_status_for_sales_order.get_customers_or_items", + "filters": [ + ['Item', 'disabled', '=', '0'], + ['Item Group','name', '=', item_group] + ] } - }, - on_change: function(){ - frappe.query_report.refresh(); } } ] - return filters; } diff --git a/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py b/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py index 7f797f67eee..cb22fb6a80f 100644 --- a/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py +++ b/erpnext/selling/report/payment_terms_status_for_sales_order/payment_terms_status_for_sales_order.py @@ -3,7 +3,7 @@ import frappe from frappe import _, qb, query_builder -from frappe.query_builder import functions +from frappe.query_builder import Criterion, functions def get_columns(): @@ -14,6 +14,12 @@ def get_columns(): "fieldtype": "Link", "options": "Sales Order", }, + { + "label": _("Customer"), + "fieldname": "customer", + "fieldtype": "Link", + "options": "Customer", + }, { "label": _("Posting Date"), "fieldname": "submitted", @@ -67,6 +73,55 @@ def get_columns(): return columns +def get_descendants_of(doctype, group_name): + group_doc = qb.DocType(doctype) + # get lft and rgt of group node + lft, rgt = ( + qb.from_(group_doc).select(group_doc.lft, group_doc.rgt).where(group_doc.name == group_name) + ).run()[0] + + # get all children of group node + query = ( + qb.from_(group_doc).select(group_doc.name).where((group_doc.lft >= lft) & (group_doc.rgt <= rgt)) + ) + + child_nodes = [] + for x in query.run(): + child_nodes.append(x[0]) + + return child_nodes + + +@frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs +def get_customers_or_items(doctype, txt, searchfield, start, page_len, filters): + filter_list = [] + if isinstance(filters, list): + for item in filters: + if item[0] == doctype: + filter_list.append(item) + elif item[0] == "Customer Group": + if item[3] != "": + filter_list.append( + [doctype, "customer_group", "in", get_descendants_of("Customer Group", item[3])] + ) + elif item[0] == "Item Group": + if item[3] != "": + filter_list.append([doctype, "item_group", "in", get_descendants_of("Item Group", item[3])]) + + if searchfield and txt: + filter_list.append([doctype, searchfield, "like", "%%%s%%" % txt]) + + return frappe.desk.reportview.execute( + doctype, + filters=filter_list, + fields=["name", "customer_group"] if doctype == "Customer" else ["name", "item_group"], + limit_start=start, + limit_page_length=page_len, + as_list=True, + ) + + def get_conditions(filters): """ Convert filter options to conditions used in query @@ -79,11 +134,37 @@ def get_conditions(filters): conditions.start_date = filters.period_start_date or frappe.utils.add_months( conditions.end_date, -1 ) - conditions.sales_order = filters.sales_order or [] return conditions +def build_filter_criterions(filters): + filters = frappe._dict(filters) if filters else frappe._dict({}) + qb_criterions = [] + + if filters.customer_group: + qb_criterions.append( + qb.DocType("Sales Order").customer_group.isin( + get_descendants_of("Customer Group", filters.customer_group) + ) + ) + + if filters.customer: + qb_criterions.append(qb.DocType("Sales Order").customer == filters.customer) + + if filters.item_group: + qb_criterions.append( + qb.DocType("Sales Order Item").item_group.isin( + get_descendants_of("Item Group", filters.item_group) + ) + ) + + if filters.item: + qb_criterions.append(qb.DocType("Sales Order Item").item_code == filters.item) + + return qb_criterions + + def get_so_with_invoices(filters): """ Get Sales Order with payment terms template with their associated Invoices @@ -92,16 +173,23 @@ def get_so_with_invoices(filters): so = qb.DocType("Sales Order") ps = qb.DocType("Payment Schedule") + soi = qb.DocType("Sales Order Item") + + conditions = get_conditions(filters) + filter_criterions = build_filter_criterions(filters) + datediff = query_builder.CustomFunction("DATEDIFF", ["cur_date", "due_date"]) ifelse = query_builder.CustomFunction("IF", ["condition", "then", "else"]) - conditions = get_conditions(filters) query_so = ( qb.from_(so) + .join(soi) + .on(soi.parent == so.name) .join(ps) .on(ps.parent == so.name) .select( so.name, + so.customer, so.transaction_date.as_("submitted"), ifelse(datediff(ps.due_date, functions.CurDate()) < 0, "Overdue", "Unpaid").as_("status"), ps.payment_term, @@ -117,12 +205,10 @@ def get_so_with_invoices(filters): & (so.company == conditions.company) & (so.transaction_date[conditions.start_date : conditions.end_date]) ) + .where(Criterion.all(filter_criterions)) .orderby(so.name, so.transaction_date, ps.due_date) ) - if conditions.sales_order != []: - query_so = query_so.where(so.name.isin(conditions.sales_order)) - sorders = query_so.run(as_dict=True) invoices = [] diff --git a/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py b/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py index 89940a6e872..9d542f5079c 100644 --- a/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py +++ b/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py @@ -11,10 +11,13 @@ from erpnext.selling.report.payment_terms_status_for_sales_order.payment_terms_s ) from erpnext.stock.doctype.item.test_item import create_item -test_dependencies = ["Sales Order", "Item", "Sales Invoice", "Payment Terms Template"] +test_dependencies = ["Sales Order", "Item", "Sales Invoice", "Payment Terms Template", "Customer"] class TestPaymentTermsStatusForSalesOrder(FrappeTestCase): + def tearDown(self): + frappe.db.rollback() + def create_payment_terms_template(self): # create template for 50-50 payments template = None @@ -48,9 +51,9 @@ class TestPaymentTermsStatusForSalesOrder(FrappeTestCase): template.insert() self.template = template - def test_payment_terms_status(self): + def test_01_payment_terms_status(self): self.create_payment_terms_template() - item = create_item(item_code="_Test Excavator", is_stock_item=0) + item = create_item(item_code="_Test Excavator 1", is_stock_item=0) so = make_sales_order( transaction_date="2021-06-15", delivery_date=add_days("2021-06-15", -30), @@ -78,13 +81,14 @@ class TestPaymentTermsStatusForSalesOrder(FrappeTestCase): "company": "_Test Company", "period_start_date": "2021-06-01", "period_end_date": "2021-06-30", - "sales_order": [so.name], + "item": item.item_code, } ) expected_value = [ { "name": so.name, + "customer": so.customer, "submitted": datetime.date(2021, 6, 15), "status": "Completed", "payment_term": None, @@ -98,6 +102,7 @@ class TestPaymentTermsStatusForSalesOrder(FrappeTestCase): }, { "name": so.name, + "customer": so.customer, "submitted": datetime.date(2021, 6, 15), "status": "Partly Paid", "payment_term": None, @@ -132,11 +137,11 @@ class TestPaymentTermsStatusForSalesOrder(FrappeTestCase): ) doc.insert() - def test_alternate_currency(self): + def test_02_alternate_currency(self): transaction_date = "2021-06-15" self.create_payment_terms_template() self.create_exchange_rate(transaction_date) - item = create_item(item_code="_Test Excavator", is_stock_item=0) + item = create_item(item_code="_Test Excavator 2", is_stock_item=0) so = make_sales_order( transaction_date=transaction_date, currency="USD", @@ -166,7 +171,7 @@ class TestPaymentTermsStatusForSalesOrder(FrappeTestCase): "company": "_Test Company", "period_start_date": "2021-06-01", "period_end_date": "2021-06-30", - "sales_order": [so.name], + "item": item.item_code, } ) @@ -174,6 +179,7 @@ class TestPaymentTermsStatusForSalesOrder(FrappeTestCase): expected_value = [ { "name": so.name, + "customer": so.customer, "submitted": datetime.date(2021, 6, 15), "status": "Completed", "payment_term": None, @@ -187,6 +193,7 @@ class TestPaymentTermsStatusForSalesOrder(FrappeTestCase): }, { "name": so.name, + "customer": so.customer, "submitted": datetime.date(2021, 6, 15), "status": "Partly Paid", "payment_term": None, @@ -200,3 +207,134 @@ class TestPaymentTermsStatusForSalesOrder(FrappeTestCase): }, ] self.assertEqual(data, expected_value) + + def test_03_group_filters(self): + transaction_date = "2021-06-15" + self.create_payment_terms_template() + item1 = create_item(item_code="_Test Excavator 1", is_stock_item=0) + item1.item_group = "Products" + item1.save() + + so1 = make_sales_order( + transaction_date=transaction_date, + delivery_date=add_days(transaction_date, -30), + item=item1.item_code, + qty=1, + rate=1000000, + do_not_save=True, + ) + so1.po_no = "" + so1.taxes_and_charges = "" + so1.taxes = "" + so1.payment_terms_template = self.template.name + so1.save() + so1.submit() + + item2 = create_item(item_code="_Test Steel", is_stock_item=0) + item2.item_group = "Raw Material" + item2.save() + + so2 = make_sales_order( + customer="_Test Customer 1", + transaction_date=transaction_date, + delivery_date=add_days(transaction_date, -30), + item=item2.item_code, + qty=100, + rate=1000, + do_not_save=True, + ) + so2.po_no = "" + so2.taxes_and_charges = "" + so2.taxes = "" + so2.payment_terms_template = self.template.name + so2.save() + so2.submit() + + base_filters = { + "company": "_Test Company", + "period_start_date": "2021-06-01", + "period_end_date": "2021-06-30", + } + + expected_value_so1 = [ + { + "name": so1.name, + "customer": so1.customer, + "submitted": datetime.date(2021, 6, 15), + "status": "Overdue", + "payment_term": None, + "description": "_Test 50-50", + "due_date": datetime.date(2021, 6, 30), + "invoice_portion": 50.0, + "currency": "INR", + "base_payment_amount": 500000.0, + "paid_amount": 0.0, + "invoices": "", + }, + { + "name": so1.name, + "customer": so1.customer, + "submitted": datetime.date(2021, 6, 15), + "status": "Overdue", + "payment_term": None, + "description": "_Test 50-50", + "due_date": datetime.date(2021, 7, 15), + "invoice_portion": 50.0, + "currency": "INR", + "base_payment_amount": 500000.0, + "paid_amount": 0.0, + "invoices": "", + }, + ] + + expected_value_so2 = [ + { + "name": so2.name, + "customer": so2.customer, + "submitted": datetime.date(2021, 6, 15), + "status": "Overdue", + "payment_term": None, + "description": "_Test 50-50", + "due_date": datetime.date(2021, 6, 30), + "invoice_portion": 50.0, + "currency": "INR", + "base_payment_amount": 50000.0, + "paid_amount": 0.0, + "invoices": "", + }, + { + "name": so2.name, + "customer": so2.customer, + "submitted": datetime.date(2021, 6, 15), + "status": "Overdue", + "payment_term": None, + "description": "_Test 50-50", + "due_date": datetime.date(2021, 7, 15), + "invoice_portion": 50.0, + "currency": "INR", + "base_payment_amount": 50000.0, + "paid_amount": 0.0, + "invoices": "", + }, + ] + + group_filters = [ + {"customer_group": "All Customer Groups"}, + {"item_group": "All Item Groups"}, + {"item_group": "Products"}, + {"item_group": "Raw Material"}, + ] + + expected_values_for_group_filters = [ + expected_value_so1 + expected_value_so2, + expected_value_so1 + expected_value_so2, + expected_value_so1, + expected_value_so2, + ] + + for idx, g in enumerate(group_filters, 0): + # build filter + filters = frappe._dict({}).update(base_filters).update(g) + with self.subTest(filters=filters): + columns, data, message, chart = execute(filters) + self.assertEqual(data, expected_values_for_group_filters[idx])