From 14b2c5f3c9fb45e9f3b749eeda8263f9da68a6d5 Mon Sep 17 00:00:00 2001 From: Mohammed Yusuf Shaikh Date: Mon, 26 Jul 2021 15:58:44 +0530 Subject: [PATCH 01/28] feat: Sales Pipeline Analytics Report --- .../sales_pipeline_analytics/__init__.py | 0 .../sales_pipeline_analytics.js | 74 ++++ .../sales_pipeline_analytics.json | 29 ++ .../sales_pipeline_analytics.py | 409 ++++++++++++++++++ 4 files changed, 512 insertions(+) create mode 100644 erpnext/crm/report/sales_pipeline_analytics/__init__.py create mode 100644 erpnext/crm/report/sales_pipeline_analytics/sales_pipeline_analytics.js create mode 100644 erpnext/crm/report/sales_pipeline_analytics/sales_pipeline_analytics.json create mode 100644 erpnext/crm/report/sales_pipeline_analytics/sales_pipeline_analytics.py diff --git a/erpnext/crm/report/sales_pipeline_analytics/__init__.py b/erpnext/crm/report/sales_pipeline_analytics/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/erpnext/crm/report/sales_pipeline_analytics/sales_pipeline_analytics.js b/erpnext/crm/report/sales_pipeline_analytics/sales_pipeline_analytics.js new file mode 100644 index 000000000000..7490bf938390 --- /dev/null +++ b/erpnext/crm/report/sales_pipeline_analytics/sales_pipeline_analytics.js @@ -0,0 +1,74 @@ +// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt +/* eslint-disable */ + +frappe.query_reports["Sales Pipeline Analytics"] = { + "filters": [ + { + fieldname: "pipeline_by", + label: __("Pipeline By"), + fieldtype: "Select", + options: "Owner\nSales Stage", + default: "Owner" + }, + { + fieldname:"from_date", + label: __("From Date"), + fieldtype: "Date", + + }, + { + fieldname:"to_date", + label: __("To Date"), + fieldtype: "Date", + }, + { + fieldname: "range", + label: __("Range"), + fieldtype: "Select", + options: "Monthly\nQuaterly", + default: "Monthly" + }, + { + fieldname: "assigned_to", + label: __("Assigned To"), + fieldtype: "Link", + options: "User" + }, + { + fieldname: "status", + label: __("Status"), + fieldtype: "Select", + options: "Open\nQuotation\nConverted\nReplied", + default: "Open" + }, + { + fieldname: "based_on", + label: __("Based On"), + fieldtype: "Select", + options: "Number\nAmount", + default: "Number" + }, + { + fieldname: "company", + label: __("Company"), + fieldtype: "Link", + options: "Company", + default: frappe.defaults.get_user_default("Company") + }, + { + fieldname: "opportunity_source", + label: __("Opportunity Source"), + fieldtype: "Link", + options: "Lead Source" + }, + { + fieldname: "opportunity_type", + label: __("Opportunity Type"), + fieldtype: "Link", + options: "Opportunity Type", + default: "Sales" + }, + + ] +}; diff --git a/erpnext/crm/report/sales_pipeline_analytics/sales_pipeline_analytics.json b/erpnext/crm/report/sales_pipeline_analytics/sales_pipeline_analytics.json new file mode 100644 index 000000000000..cffdddfd23fc --- /dev/null +++ b/erpnext/crm/report/sales_pipeline_analytics/sales_pipeline_analytics.json @@ -0,0 +1,29 @@ +{ + "add_total_row": 0, + "columns": [], + "creation": "2021-07-01 17:29:09.530787", + "disable_prepared_report": 0, + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "filters": [], + "idx": 0, + "is_standard": "Yes", + "modified": "2021-07-01 17:45:17.612861", + "modified_by": "Administrator", + "module": "CRM", + "name": "Sales Pipeline Analytics", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "Opportunity", + "report_name": "Sales Pipeline Analytics", + "report_type": "Script Report", + "roles": [ + { + "role": "Sales User" + }, + { + "role": "Sales Manager" + } + ] +} \ No newline at end of file diff --git a/erpnext/crm/report/sales_pipeline_analytics/sales_pipeline_analytics.py b/erpnext/crm/report/sales_pipeline_analytics/sales_pipeline_analytics.py new file mode 100644 index 000000000000..cdc3db49b4c9 --- /dev/null +++ b/erpnext/crm/report/sales_pipeline_analytics/sales_pipeline_analytics.py @@ -0,0 +1,409 @@ +# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +import json +import frappe +from datetime import datetime +from dateutil.relativedelta import relativedelta +from six import iteritems + +def execute(filters=None): + return SalesPipelineAnalytics(filters).run() + +class SalesPipelineAnalytics(object): + + def __init__(self, filters=None): + self.filters = frappe._dict(filters or {}) + + def run(self): + self.get_columns() + self.get_data() + self.get_chart_data() + + return self.columns, self.data, None, self.chart + + def get_columns(self): + self.columns = [] + if self.filters['range'] == "Monthly": + + current_date = datetime.date(datetime.now()) + current_month_number = int(current_date.strftime("%m")) + + for i in range(current_month_number,13): + self.columns.append( + { + 'fieldname': current_date.strftime("%B"), + 'label': current_date.strftime("%B"), + 'width': 200 + } + ) + current_date = current_date + relativedelta(months=1) + + elif self.filters['range'] == "Quaterly": + + for quarter in range(1,5): + self.columns.append( + { + 'fieldname': f"Q{quarter}", + 'label': f"Q{quarter}", + 'width': 200 + } + ) + + if self.filters.get("pipeline_by") == "Owner": + self.columns.insert(0,{ + 'fieldname': "opportunity_owner", + 'label': "Opportunity Owner", + 'width':200 + }) + elif self.filters.get("pipeline_by") == "Sales Stage": + self.columns.insert(0,{ + 'fieldname':"sales_stage", + 'label':"Sales Stage", + 'width':200 + }) + return self.columns + + def get_data(self): + self.data = [] + if self.filters.get("range") == "Monthly": + data = self.get_monthly_data() + + if self.filters.get("range") == "Quaterly": + data = self.get_quaterly_data() + + return data + + def get_monthly_data(self): + + if self.filters.get("pipeline_by") == "Owner": + select = '_assign as opportunity_owner' + group_by = '_assign' + + if self.filters.get("pipeline_by") == "Sales Stage": + select = 'sales_stage' + group_by = 'sales_stage' + + if self.filters.get("based_on") == "Number": + self.query_result = frappe.db.sql("""SELECT COUNT(name) as count,{select},monthname(expected_closing) as month from tabOpportunity + where {conditions} + GROUP BY {group_by},month(expected_closing) ORDER BY month(expected_closing)""".format(conditions=self.get_conditions(),select=select,group_by=group_by) + ,self.filters,as_dict=1) + + if self.filters.get("pipeline_by") == "Owner": + self.get_periodic_data() + for customer,period_data in iteritems(self.periodic_data): + row = {'opportunity_owner': customer} + for info in self.query_result: + period = info.get('month') + count = period_data.get(period,0.0) + row[period] = count + self.data.append(row) + + + if self.filters.get("pipeline_by") == "Sales Stage": + self.get_periodic_data() + for sales_stage,period_data in iteritems(self.periodic_data): + row = {'sales_stage': sales_stage} + for info in self.query_result: + period = info.get('month') + count = period_data.get(period,0.0) + row[period] = count + self.data.append(row) + + return self.data + + + if self.filters.get("based_on") == "Amount": + self.query_result = frappe.db.sql("""SELECT sum(opportunity_amount) as amount,{select},monthname(expected_closing) as month from tabOpportunity + where {conditions} + GROUP BY {group_by},month(expected_closing) ORDER BY month(expected_closing)""".format(conditions=self.get_conditions(),select=select,group_by=group_by,) + ,self.filters,as_dict=1) + + if self.filters.get("pipeline_by") == "Owner": + self.get_periodic_data() + for user,period_data in iteritems(self.periodic_data): + row = {'opportunity_owner': user} + for info in self.query_result: + period = info.get('month') + count = period_data.get(period,0.0) + row[period] = count + + self.data.append(row) + + if self.filters.get("pipeline_by") == "Sales Stage": + self.get_periodic_data() + for sales_stage,period_data in iteritems(self.periodic_data): + row = {'sales_stage': sales_stage} + for info in self.query_result: + period = info.get('month') + count = period_data.get(period,0.0) + row[period] = count + + self.data.append(row) + + return self.data + + def get_quaterly_data(self): + if self.filters.get("pipeline_by") == "Owner": + select = '_assign as opportunity_owner' + group_by = '_assign' + if self.filters.get("pipeline_by") == "Sales Stage": + select = 'sales_stage' + group_by = 'sales_stage' + + if self.filters.get("based_on") == "Number": + self.query_result = frappe.db.sql("""select count(name) as count,{select},QUARTER(expected_closing) as quarter from tabOpportunity + where {conditions} + group by {group_by},QUARTER(expected_closing) order by QUARTER(expected_closing) + """.format(conditions=self.get_conditions(),select=select,group_by=group_by),self.filters,as_dict=1) + + if self.filters.get("pipeline_by") == "Owner": + self.get_periodic_data() + for customer,period_data in iteritems(self.periodic_data): + row = {'opportunity_owner': customer} + for info in self.query_result: + period = "Q" + str(info.get('quarter')) + count = period_data.get(period,0.0) + row[period] = count + self.data.append(row) + + return self.data + + if self.filters.get("pipeline_by") == "Sales Stage": + self.get_periodic_data() + for customer,period_data in iteritems(self.periodic_data): + row = {'sales_stage': customer} + for info in self.query_result: + period = "Q" + str(info.get('quarter')) + count = period_data.get(period,0.0) + row[period] = count + self.data.append(row) + + return self.data + + if self.filters.get("based_on") == "Amount": + self.query_result = frappe.db.sql("""select sum(opportunity_amount) as amount,{select},QUARTER(expected_closing) as quarter from tabOpportunity + where {conditions} + group by {group_by},QUARTER(expected_closing) order by QUARTER(expected_closing) + """.format(conditions=self.get_conditions(),select=select,group_by=group_by),self.filters,as_dict=1) + + if self.filters.get("pipeline_by") == "Owner": + self.get_periodic_data() + for customer,period_data in iteritems(self.periodic_data): + row = {'opportunity_owner': customer} + for info in self.query_result: + period = "Q" + str(info.get('quarter')) + count = period_data.get(period,0.0) + row[period] = count + self.data.append(row) + + if self.filters.get("pipeline_by") == "Sales Stage": + self.get_periodic_data() + for sales_stage,period_data in iteritems(self.periodic_data): + row = {'sales_stage': sales_stage} + for info in self.query_result: + period = "Q" + str(info.get('quarter')) + count = period_data.get(period,0.0) + row[period] = count + self.data.append(row) + + return self.data + + def get_conditions(self): + current_date = datetime.date(datetime.now()) + conditions = [] + if self.filters.get("opportunity_source"): + conditions.append('source=%(opportunity_source)s') + if self.filters.get("opportunity_type"): + conditions.append('opportunity_type=%(opportunity_type)s') + if self.filters.get("status"): + conditions.append('status=%(status)s') + if self.filters.get("company"): + conditions.append('company=%(company)s') + if self.filters.get("from_date"): + conditions.append('expected_closing>=%(from_date)s') + if self.filters.get("to_date"): + conditions.append('expected_closing<=%(to_date)s') + + if not self.filters.get("from_date") and not self.filters.get("to_date") and self.filters.get("Monthly"): + conditions.append('expected_closing between {cd} and {dd}'.format(cd = current_date + ,dd= current_date + relativedelta(months=1) + relativedelta(months=1))) + + return "{}".format(" and ".join(conditions)) + + def get_chart_data(self): + labels = [] + values = [] + quarter_list = [1,2,3,4] + count = [0,0,0,0] + count_month = [0,0,0,0,0,0,0,0,0,0,0,0] + datasets = [] + month_list = [] + current_date = datetime.date(datetime.now()) + current_month_number = int(current_date.strftime("%m")) + + for month in range(current_month_number,13): + month_list.append(current_date.strftime("%B")) + current_date = current_date + relativedelta(months=1) + + if self.filters.get("range") == "Quaterly" and self.filters.get("based_on") == "Amount": + for info in self.query_result: + for q in range(0,len(quarter_list)): + if info['quarter'] == quarter_list[q]: + count[q] = count[q] + info['amount'] + values = count + datasets.append({'name':'Amount','values':values}) + + + if self.filters.get("range") == "Quaterly" and self.filters.get("based_on") == "Number": + for info in self.query_result: + for q in range(0,len(quarter_list)): + if info['quarter'] == quarter_list[q]: + count[q] = count[q] + info['count'] + values = count + datasets.append({'name':'Number','values':values}) + + if self.filters.get("range") == "Monthly" and self.filters.get("based_on") == "Amount": + for info in self.query_result: + for m in range(0,len(month_list)): + if info['month'] == month_list[m]: + count_month[m] = count_month[m] + info['amount'] + values = count_month + datasets.append({'name':'Amount','values':values}) + + + if self.filters.get("range") == "Monthly" and self.filters.get("based_on") == "Number": + for info in self.query_result: + for m in range(0,len(month_list)): + if info['month'] == month_list[m]: + count_month[m] = count_month[m] + info['count'] + values = count_month + datasets.append({'name':'Count','values':values}) + + + for c in self.columns: + if c['fieldname'] == "opportunity_owner" or c['fieldname'] == "sales_stage": + pass + else: + labels.append(c['fieldname']) + + self.chart = { + "data":{ + 'labels': labels, + 'datasets': datasets + + }, + "type":"line" + } + + return self.chart + + def get_periodic_data(self): + self.periodic_data = frappe._dict() + + for info in self.query_result: + if self.filters.get("range") == "Monthly" and self.filters.get("based_on") == "Number" and self.filters.get("pipeline_by") == "Owner": + period = info.get('month') + value = info.get('opportunity_owner') + count = info.get('count') + temp = json.loads(value) + + if self.filters.get("assigned_to"): + for data in json.loads(info.get('opportunity_owner')): + if data == self.filters.get("assigned_to"): + self.helper(period,data,count,temp) + else: + self.helper(period,value,count,temp) + + + if self.filters.get("range") == "Monthly" and self.filters.get("based_on") == "Number" and self.filters.get("pipeline_by") == "Sales Stage": + period = info.get('month') + value = info.get('sales_stage') + count = info.get('count') + self.helper(period,value,count,None) + + + if self.filters.get("range") == "Monthly" and self.filters.get("based_on") == "Amount" and self.filters.get("pipeline_by") == "Sales Stage": + period = info.get('month') + value = info.get('sales_stage') + amount = info.get('amount') + self.helper(period,value,amount,None) + + + if self.filters.get("range") == "Monthly" and self.filters.get("based_on") == "Amount" and self.filters.get("pipeline_by") == "Owner": + period = info.get('month') + value = info.get('opportunity_owner') + amount = info.get('amount') + temp = json.loads(value) + + if self.filters.get("assigned_to"): + for data in json.loads(info.get('opportunity_owner')): + if data == self.filters.get("assigned_to"): + self.helper(period,data,amount,temp) + else: + self.helper(period,value,amount,temp) + + if self.filters.get("range") == "Quaterly" and self.filters.get("based_on") == "Number" and self.filters.get("pipeline_by") == "Owner": + period = "Q" + str(info.get('quarter')) + value = info.get('opportunity_owner') + count = info.get('count') + temp = json.loads(value) + + if self.filters.get("assigned_to"): + for data in json.loads(info.get('opportunity_owner')): + if data == self.filters.get("assigned_to"): + self.helper(period,data,count,temp) + else: + self.helper(period,value,count,temp) + + if self.filters.get("range") == "Quaterly" and self.filters.get("based_on") == "Number" and self.filters.get("pipeline_by") == "Sales Stage": + period = "Q" + str(info.get('quarter')) + value = info.get('sales_stage') + count = info.get('count') + self.helper(period,value,count,None) + + + if self.filters.get("range") == "Quaterly" and self.filters.get("based_on") == "Amount" and self.filters.get("pipeline_by") == "Owner": + period = "Q" + str(info.get('quarter')) + value = info.get('opportunity_owner') + amount = info.get('amount') + temp = json.loads(value) + + if self.filters.get("assigned_to"): + for data in json.loads(info.get('opportunity_owner')): + if data == self.filters.get("assigned_to"): + self.helper(period,data,amount,temp) + else: + self.helper(period,value,amount,temp) + + if self.filters.get("range") == "Quaterly" and self.filters.get("based_on") == "Amount" and self.filters.get("pipeline_by") == "Sales Stage": + period = "Q" + str(info.get('quarter')) + value = info.get('sales_stage') + amount = info.get('amount') + self.helper(period,value,amount,None) + + def helper(self,period,value,val,temp): + + if temp: + if len(temp) > 1: + if self.filters.get("assigned_to"): + print(temp) + for user in temp: + if self.filters.get("assigned_to") == user: + value = user + self.periodic_data.setdefault(value,frappe._dict()).setdefault(period,0.0) + self.periodic_data[value][period]= val + else: + for user in temp: + value = user + self.periodic_data.setdefault(value,frappe._dict()).setdefault(period,0.0) + self.periodic_data[value][period]= val + else: + value = temp[0] + self.periodic_data.setdefault(value,frappe._dict()).setdefault(period,0.0) + self.periodic_data[value][period]= val + + else: + self.periodic_data.setdefault(value,frappe._dict()).setdefault(period,0.0) + self.periodic_data[value][period]= val \ No newline at end of file From 49986581c183f7168a34d9b42912f4b3c9148f11 Mon Sep 17 00:00:00 2001 From: Mohammed Yusuf Shaikh Date: Wed, 28 Jul 2021 13:30:23 +0530 Subject: [PATCH 02/28] fix: sider Issues and added tests --- .../sales_pipeline_analytics.py | 22 +- .../test_sales_pipeline_analytics.py | 256 ++++++++++++++++++ 2 files changed, 267 insertions(+), 11 deletions(-) create mode 100644 erpnext/crm/report/sales_pipeline_analytics/test_sales_pipeline_analytics.py diff --git a/erpnext/crm/report/sales_pipeline_analytics/sales_pipeline_analytics.py b/erpnext/crm/report/sales_pipeline_analytics/sales_pipeline_analytics.py index cdc3db49b4c9..a8933cb54974 100644 --- a/erpnext/crm/report/sales_pipeline_analytics/sales_pipeline_analytics.py +++ b/erpnext/crm/report/sales_pipeline_analytics/sales_pipeline_analytics.py @@ -55,7 +55,8 @@ def get_columns(self): 'fieldname': "opportunity_owner", 'label': "Opportunity Owner", 'width':200 - }) + }) + elif self.filters.get("pipeline_by") == "Sales Stage": self.columns.insert(0,{ 'fieldname':"sales_stage", @@ -96,7 +97,7 @@ def get_monthly_data(self): row = {'opportunity_owner': customer} for info in self.query_result: period = info.get('month') - count = period_data.get(period,0.0) + count = period_data.get(period,0.0) row[period] = count self.data.append(row) @@ -107,7 +108,7 @@ def get_monthly_data(self): row = {'sales_stage': sales_stage} for info in self.query_result: period = info.get('month') - count = period_data.get(period,0.0) + count = period_data.get(period,0.0) row[period] = count self.data.append(row) @@ -126,7 +127,7 @@ def get_monthly_data(self): row = {'opportunity_owner': user} for info in self.query_result: period = info.get('month') - count = period_data.get(period,0.0) + count = period_data.get(period,0.0) row[period] = count self.data.append(row) @@ -137,7 +138,7 @@ def get_monthly_data(self): row = {'sales_stage': sales_stage} for info in self.query_result: period = info.get('month') - count = period_data.get(period,0.0) + count = period_data.get(period,0.0) row[period] = count self.data.append(row) @@ -164,7 +165,7 @@ def get_quaterly_data(self): row = {'opportunity_owner': customer} for info in self.query_result: period = "Q" + str(info.get('quarter')) - count = period_data.get(period,0.0) + count = period_data.get(period,0.0) row[period] = count self.data.append(row) @@ -176,7 +177,7 @@ def get_quaterly_data(self): row = {'sales_stage': customer} for info in self.query_result: period = "Q" + str(info.get('quarter')) - count = period_data.get(period,0.0) + count = period_data.get(period,0.0) row[period] = count self.data.append(row) @@ -194,7 +195,7 @@ def get_quaterly_data(self): row = {'opportunity_owner': customer} for info in self.query_result: period = "Q" + str(info.get('quarter')) - count = period_data.get(period,0.0) + count = period_data.get(period,0.0) row[period] = count self.data.append(row) @@ -204,14 +205,14 @@ def get_quaterly_data(self): row = {'sales_stage': sales_stage} for info in self.query_result: period = "Q" + str(info.get('quarter')) - count = period_data.get(period,0.0) + count = period_data.get(period,0.0) row[period] = count self.data.append(row) return self.data def get_conditions(self): - current_date = datetime.date(datetime.now()) + current_date = datetime.date(datetime.now()) conditions = [] if self.filters.get("opportunity_source"): conditions.append('source=%(opportunity_source)s') @@ -388,7 +389,6 @@ def helper(self,period,value,val,temp): if temp: if len(temp) > 1: if self.filters.get("assigned_to"): - print(temp) for user in temp: if self.filters.get("assigned_to") == user: value = user diff --git a/erpnext/crm/report/sales_pipeline_analytics/test_sales_pipeline_analytics.py b/erpnext/crm/report/sales_pipeline_analytics/test_sales_pipeline_analytics.py new file mode 100644 index 000000000000..f1f9381570f1 --- /dev/null +++ b/erpnext/crm/report/sales_pipeline_analytics/test_sales_pipeline_analytics.py @@ -0,0 +1,256 @@ +import unittest +import frappe +from erpnext.crm.report.sales_pipeline_analytics.sales_pipeline_analytics import execute + +class TestSalesPipelineAnalytics(unittest.TestCase): + + @classmethod + def setUpClass(self): + create_company() + create_customer() + create_lead() + create_opportunity() + + + def test_sales_pipeline_analytics(self): + self.check_for_monthly_and_number() + self.check_for_monthly_and_amount() + self.check_for_quarterly_and_number() + self.check_for_quarterly_and_amount() + self.check_for_all_filters() + + + def check_for_monthly_and_number(self): + filters = { + 'pipeline_by':"Owner", + 'range':"Monthly", + 'based_on':"Number", + 'status':"Open", + 'opportunity_type':"Sales", + 'company':"__Test Company" + } + + report = execute(filters) + + expected_data = [ + { + 'opportunity_owner':'[]', + 'August':1 + } + ] + + self.assertEqual(expected_data,report[1]) + + filters = { + 'pipeline_by':"Sales Stage", + 'range':"Monthly", + 'based_on':"Number", + 'status':"Open", + 'opportunity_type':"Sales", + 'company':"__Test Company" + } + + report = execute(filters) + + expected_data = [ + { + 'sales_stage':'Prospecting', + 'August':1 + } + ] + + self.assertEqual(expected_data,report[1]) + + + def check_for_monthly_and_amount(self): + filters = { + 'pipeline_by':"Owner", + 'range':"Monthly", + 'based_on':"Amount", + 'status':"Open", + 'opportunity_type':"Sales", + 'company':"__Test Company" + } + + report = execute(filters) + + expected_data = [ + { + 'opportunity_owner':'[]', + 'August':150000 + } + ] + + self.assertEqual(expected_data,report[1]) + + filters = { + 'pipeline_by':"Sales Stage", + 'range':"Monthly", + 'based_on':"Amount", + 'status':"Open", + 'opportunity_type':"Sales", + 'company':"__Test Company" + } + + report = execute(filters) + + expected_data = [ + { + 'sales_stage':'Prospecting', + 'August':150000 + } + ] + + self.assertEqual(expected_data,report[1]) + + + def check_for_quarterly_and_number(self): + filters = { + 'pipeline_by':"Owner", + 'range':"Quaterly", + 'based_on':"Number", + 'status':"Open", + 'opportunity_type':"Sales", + 'company':"__Test Company" + } + + report = execute(filters) + + expected_data = [ + { + 'opportunity_owner':'[]', + 'Q3':1 + } + ] + + self.assertEqual(expected_data,report[1]) + + filters = { + 'pipeline_by':"Sales Stage", + 'range':"Quaterly", + 'based_on':"Number", + 'status':"Open", + 'opportunity_type':"Sales", + 'company':"__Test Company" + } + + report = execute(filters) + + expected_data = [ + { + 'sales_stage':'Prospecting', + 'Q3':1 + } + ] + + self.assertEqual(expected_data,report[1]) + + + def check_for_quarterly_and_amount(self): + filters = { + 'pipeline_by':"Owner", + 'range':"Quaterly", + 'based_on':"Amount", + 'status':"Open", + 'opportunity_type':"Sales", + 'company':"__Test Company" + } + + report = execute(filters) + + expected_data = [ + { + 'opportunity_owner':'[]', + 'Q3':150000 + } + ] + + self.assertEqual(expected_data,report[1]) + + filters = { + 'pipeline_by':"Sales Stage", + 'range':"Quaterly", + 'based_on':"Amount", + 'status':"Open", + 'opportunity_type':"Sales", + 'company':"__Test Company" + } + + report = execute(filters) + + expected_data = [ + { + 'sales_stage':'Prospecting', + 'Q3':150000 + } + ] + + self.assertEqual(expected_data,report[1]) + + + def check_for_all_filters(self): + filters = { + 'pipeline_by':"Owner", + 'range':"Monthly", + 'based_on':"Number", + 'status':"Open", + 'opportunity_type':"Sales", + 'company':"__Test Company", + 'opportunity_source':'Cold Calling', + 'opportunity_type':"Sales", + 'from_date': '2021-08-01', + 'to_date':'2021-08-31' + } + + report = execute(filters) + + expected_data = [ + { + 'opportunity_owner':'[]', + 'August': 1 + } + ] + + self.assertEqual(expected_data,report[1]) + + +def create_company(): + doc = frappe.db.exists('Company','__Test Company') + if not doc: + doc = frappe.new_doc('Company') + doc.company_name = '__Test Company' + doc.abbr = "_TC" + doc.default_currency = "INR" + doc.insert() + +def create_customer(): + doc = frappe.db.exists("Customer","_Test Customer") + if not doc: + doc = frappe.new_doc("Customer") + doc.customer_name = '_Test Customer' + doc.insert() + +def create_lead(): + doc = frappe.db.exists("Lead","_Test Lead") + if not doc: + doc = frappe.new_doc("Lead") + doc.lead_name = '_Test Lead' + doc.company_name = 'Client Company' + doc.company = "__Test Company" + doc.insert() + +def create_opportunity(): + doc = frappe.db.exists({ + "doctype":"Opportunity", + "title":"Client Company" + }) + if not doc: + doc = frappe.new_doc("Opportunity") + doc.opportunity_from = "Lead" + lead_name = frappe.db.get_value("Lead",{"company":'__Test Company'},['name']) + doc.party_name = lead_name + doc.opportunity_amount = 150000 + doc.source = "Cold Calling" + doc.expected_closing = "2021-08-31" + doc.company = "__Test Company" + doc.insert() \ No newline at end of file From 6521acd2f218104a7aec2e957b2801de0db3b0aa Mon Sep 17 00:00:00 2001 From: Mohammed Yusuf Shaikh Date: Wed, 28 Jul 2021 13:37:51 +0530 Subject: [PATCH 03/28] fix: Semgrep Issue --- .../sales_pipeline_analytics/test_sales_pipeline_analytics.py | 1 - 1 file changed, 1 deletion(-) diff --git a/erpnext/crm/report/sales_pipeline_analytics/test_sales_pipeline_analytics.py b/erpnext/crm/report/sales_pipeline_analytics/test_sales_pipeline_analytics.py index f1f9381570f1..6762392e47c8 100644 --- a/erpnext/crm/report/sales_pipeline_analytics/test_sales_pipeline_analytics.py +++ b/erpnext/crm/report/sales_pipeline_analytics/test_sales_pipeline_analytics.py @@ -197,7 +197,6 @@ def check_for_all_filters(self): 'opportunity_type':"Sales", 'company':"__Test Company", 'opportunity_source':'Cold Calling', - 'opportunity_type':"Sales", 'from_date': '2021-08-01', 'to_date':'2021-08-31' } From 3c22097315d518380430838786a9e5c9a7f596df Mon Sep 17 00:00:00 2001 From: Mohammed Yusuf Shaikh Date: Tue, 3 Aug 2021 12:05:15 +0530 Subject: [PATCH 04/28] feat: Opportunity Summary by Sales Stage Report --- .../__init__.py | 0 .../opportunity_summary_by_sales_stage.js | 72 +++++ .../opportunity_summary_by_sales_stage.json | 29 ++ .../opportunity_summary_by_sales_stage.py | 259 ++++++++++++++++++ .../test_sales_pipeline_analytics.py | 5 +- erpnext/crm/workspace/crm/crm.json | 21 +- 6 files changed, 381 insertions(+), 5 deletions(-) create mode 100644 erpnext/crm/report/opportunity_summary_by_sales_stage/__init__.py create mode 100644 erpnext/crm/report/opportunity_summary_by_sales_stage/opportunity_summary_by_sales_stage.js create mode 100644 erpnext/crm/report/opportunity_summary_by_sales_stage/opportunity_summary_by_sales_stage.json create mode 100644 erpnext/crm/report/opportunity_summary_by_sales_stage/opportunity_summary_by_sales_stage.py diff --git a/erpnext/crm/report/opportunity_summary_by_sales_stage/__init__.py b/erpnext/crm/report/opportunity_summary_by_sales_stage/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/erpnext/crm/report/opportunity_summary_by_sales_stage/opportunity_summary_by_sales_stage.js b/erpnext/crm/report/opportunity_summary_by_sales_stage/opportunity_summary_by_sales_stage.js new file mode 100644 index 000000000000..c14bb4fc159b --- /dev/null +++ b/erpnext/crm/report/opportunity_summary_by_sales_stage/opportunity_summary_by_sales_stage.js @@ -0,0 +1,72 @@ +// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt +/* eslint-disable */ + +frappe.query_reports["Opportunity Summary by Sales Stage"] = { + "filters": [ + { + fieldname:"based_on", + label: __("Based On"), + fieldtype:"Select", + options:"Opportunity Owner\nSource\nOpportunity Type", + default:"Source" + } + , + { + fieldname:"data_based_on", + label: __("Data Based On"), + fieldtype:"Select", + options:"Number\nAmount", + default:"Number" + } + , + { + fieldname:"from_date", + label: __("From Date"), + fieldtype: "Date", + + } + , + { + fieldname:"to_date", + label: __("To Date"), + fieldtype: "Date", + } + , + { + fieldname:"status", + label: __("Status"), + fieldtype:"MultiSelectList", + get_data:function(){ + return [{value:"Open",description:"Status"}, + {value:"Converted",description:"Status"}, + {value:"Quotation",description:"Status"}, + {value:"Replied",description:"Status"} + ] + } + } + , + { + fieldname:"opportunity_source", + label: __("Oppoturnity Source"), + fieldtype:"Link", + options:"Lead Source", + } + , + { + fieldname:"opportunity_type", + label: __("Opportunity Type"), + fieldtype:"Link", + options:"Opportunity Type", + }, + { + fieldname:"company", + label: __("Company"), + fieldtype:"Link", + options:"Company", + default:frappe.defaults.get_user_default("Company") + } + + + ] +}; \ No newline at end of file diff --git a/erpnext/crm/report/opportunity_summary_by_sales_stage/opportunity_summary_by_sales_stage.json b/erpnext/crm/report/opportunity_summary_by_sales_stage/opportunity_summary_by_sales_stage.json new file mode 100644 index 000000000000..3605aecacd9b --- /dev/null +++ b/erpnext/crm/report/opportunity_summary_by_sales_stage/opportunity_summary_by_sales_stage.json @@ -0,0 +1,29 @@ +{ + "add_total_row": 0, + "columns": [], + "creation": "2021-07-28 12:18:24.028737", + "disable_prepared_report": 0, + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "filters": [], + "idx": 0, + "is_standard": "Yes", + "modified": "2021-07-28 12:18:24.028737", + "modified_by": "Administrator", + "module": "CRM", + "name": "Opportunity Summary by Sales Stage", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "Opportunity", + "report_name": "Opportunity Summary by Sales Stage ", + "report_type": "Script Report", + "roles": [ + { + "role": "Sales User" + }, + { + "role": "Sales Manager" + } + ] +} \ No newline at end of file diff --git a/erpnext/crm/report/opportunity_summary_by_sales_stage/opportunity_summary_by_sales_stage.py b/erpnext/crm/report/opportunity_summary_by_sales_stage/opportunity_summary_by_sales_stage.py new file mode 100644 index 000000000000..65f0a12d0da7 --- /dev/null +++ b/erpnext/crm/report/opportunity_summary_by_sales_stage/opportunity_summary_by_sales_stage.py @@ -0,0 +1,259 @@ +# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +import frappe +from frappe import _ +from six import iteritems +import json + + +def execute(filters=None): + return OpportunitySummaryBySalesStage(filters).run() + +class OpportunitySummaryBySalesStage(object): + + def __init__(self,filters=None): + self.filters = frappe._dict(filters or {}) + + def run(self): + self.get_columns() + self.get_data() + self.get_chart_data() + + return self.columns,self.data,None,self.chart + + def get_columns(self): + self.columns = [] + + if self.filters.get('based_on') == "Opportunity Owner": + self.columns.append({ + 'label': _('Opportunity Owner'), + 'fieldname': 'opportunity_owner', + 'width':200 + }) + if self.filters.get('based_on') == "Source": + self.columns.append({ + 'label': _('Source'), + 'fieldname': 'source', + 'fieldtype': 'Link', + 'options':'Lead Source', + 'width': 200 + }) + if self.filters.get('based_on') == "Opportunity Type": + self.columns.append({ + 'label': _('Opportunity Type'), + 'fieldname': 'opportunity_type', + 'width': 200 + }) + + + self.sales_stage_list = frappe.db.get_list("Sales Stage",pluck="name") + for sales_stage in self.sales_stage_list: + self.columns.append({ + 'label': _(sales_stage), + 'fieldname': sales_stage, + 'width':150 + }) + + return self.columns + + def get_data(self): + self.data = [] + if self.filters.get('based_on') == "Opportunity Owner": + sql = "_assign" + self.get_data_query(sql) + if self.filters.get('based_on') == "Source": + sql = "source" + self.get_data_query(sql) + if self.filters.get('based_on') == "Opportunity Type": + sql = "opportunity_type" + self.get_data_query(sql) + + self.get_rows() + + def get_data_query(self,sql): + filter_data = self.filters.get('status') + + if self.filters.get("data_based_on") == "Number": + self.filters.update({'status':tuple(filter_data)}) + self.query_result = frappe.db.sql("""select sales_stage,count(name) as count,{sql} from tabOpportunity + where {conditions} + group by sales_stage,{sql}""".format(conditions=self.get_conditions(),sql=sql),self.filters,as_dict=1) + + if self.filters.get("data_based_on") == "Amount": + self.filters.update({'status':tuple(filter_data)}) + self.query_result = frappe.db.sql("""select sales_stage,sum(opportunity_amount) as amount,{sql} from tabOpportunity + where {conditions} + group by sales_stage,{sql}""".format(conditions=self.get_conditions(),sql=sql),self.filters,as_dict=1) + + def get_rows(self): + self.data = [] + self.get_formatted_data() + currency_symbol = self.get_currency() + + for based_on,data in iteritems(self.formatted_data): + if self.filters.get("based_on") == "Opportunity Owner": + row = {'opportunity_owner': based_on} + + if self.filters.get("data_based_on") == "Number": + for d in self.query_result: + sales_stage = d.get('sales_stage') + count = data.get(sales_stage) + row[sales_stage] = count + + if self.filters.get("data_based_on") == "Amount": + for d in self.query_result: + sales_stage = d.get('sales_stage') + amount = data.get(sales_stage) + if amount: + row[sales_stage] = str(amount) + currency_symbol + + if self.filters.get("based_on") == "Source": + row = {'source': based_on} + + if self.filters.get("data_based_on") == "Number": + for d in self.query_result: + sales_stage = d.get('sales_stage') + count = data.get(sales_stage) + row[sales_stage] = count + + if self.filters.get("data_based_on") == "Amount": + for d in self.query_result: + sales_stage = d.get('sales_stage') + amount = data.get(sales_stage) + if amount: + row[sales_stage] = str(amount) + currency_symbol + + if self.filters.get("based_on") == "Opportunity Type": + row = {'opportunity_type': based_on} + + if self.filters.get("data_based_on") == "Number": + for d in self.query_result: + sales_stage = d.get('sales_stage') + count = data.get(sales_stage) + row[sales_stage] = count + + if self.filters.get("data_based_on") == "Amount": + for d in self.query_result: + sales_stage = d.get('sales_stage') + amount = data.get(sales_stage) + if amount: + row[sales_stage] = str(amount) + currency_symbol + + self.data.append(row) + + def get_formatted_data(self): + self.formatted_data = frappe._dict() + + for d in self.query_result: + if self.filters.get("based_on") == "Opportunity Owner": + if self.filters.get("data_based_on") == "Number": + temp = json.loads(d.get("_assign")) + if len(temp) > 1: + sales_stage = d.get('sales_stage') + count = d.get('count') + for owner in temp: + self.helper(owner,sales_stage,count) + + else: + owner = temp[0] + sales_stage = d.get('sales_stage') + count = d.get('count') + self.helper(owner,sales_stage,count) + + if self.filters.get("data_based_on") == "Amount": + + temp = json.loads(d.get("_assign")) + if len(temp) > 1: + sales_stage = d.get('sales_stage') + amount = d.get('amount') + for owner in temp: + self.helper(owner,sales_stage,amount) + + else: + owner = temp[0] + sales_stage = d.get('sales_stage') + amount = d.get('amount') + self.helper(owner,sales_stage,amount) + + + if self.filters.get("based_on") == "Source": + if self.filters.get("data_based_on") == "Number": + source = d.get('source') + sales_stage = d.get('sales_stage') + count = d.get('count') + self.helper(source,sales_stage,count) + + if self.filters.get("data_based_on") == "Amount": + source = d.get('source') + sales_stage = d.get('sales_stage') + amount = d.get('amount') + self.helper(source,sales_stage,amount) + + if self.filters.get("based_on") == "Opportunity Type": + if self.filters.get("data_based_on") == "Number": + opportunity_type = d.get('opportunity_type') + sales_stage = d.get('sales_stage') + count = d.get('count') + self.helper(opportunity_type,sales_stage,count) + + if self.filters.get("data_based_on") == "Amount": + opportunity_type = d.get('opportunity_type') + sales_stage = d.get('sales_stage') + amount = d.get('amount') + self.helper(opportunity_type,sales_stage,amount) + + def helper(self,based_on,sales_stage,data): + self.formatted_data.setdefault(based_on,frappe._dict()).setdefault(sales_stage,0) + self.formatted_data[based_on][sales_stage] += data + + def get_conditions(self): + conditions = [] + if self.filters.get("opportunity_source"): + conditions.append('source=%(opportunity_source)s') + if self.filters.get("opportunity_type"): + conditions.append('opportunity_type=%(opportunity_type)s') + if self.filters.get("status"): + conditions.append('status in %(status)s') + if self.filters.get("company"): + conditions.append('company=%(company)s') + if self.filters.get("from_date"): + conditions.append('transaction_date>=%(from_date)s') + if self.filters.get("to_date"): + conditions.append('transaction_date<=%(to_date)s') + + return "{}".format(" and ".join(conditions)) + + def get_chart_data(self): + labels = [] + datasets = [] + values = [0,0,0,0,0,0,0,0] + for sales_stage in self.sales_stage_list: + labels.append(sales_stage) + + if self.filters.get("data_based_on") == "Number": + for data in self.query_result: + for count in range(0,8): + if data['sales_stage'] == labels[count]: + values[count] = values[count] + data['count'] + datasets.append({"name":'Count','values':values}) + + if self.filters.get("data_based_on") == "Amount": + for data in self.query_result: + for count in range(0,8): + if data['sales_stage'] == labels[count]: + values[count] = values[count] + data['amount'] + datasets.append({"name":'Amount','values':values}) + + self.chart = { + "data":{ + 'labels': labels, + 'datasets': datasets + }, + "type":"line" + } + + def get_currency(self): + company = self.filters.get('company') + default_currency = frappe.db.get_value('Company',company,['default_currency']) + return frappe.db.get_value('Currency',default_currency,['symbol']) \ No newline at end of file diff --git a/erpnext/crm/report/sales_pipeline_analytics/test_sales_pipeline_analytics.py b/erpnext/crm/report/sales_pipeline_analytics/test_sales_pipeline_analytics.py index 6762392e47c8..df0da2d69f2d 100644 --- a/erpnext/crm/report/sales_pipeline_analytics/test_sales_pipeline_analytics.py +++ b/erpnext/crm/report/sales_pipeline_analytics/test_sales_pipeline_analytics.py @@ -239,10 +239,7 @@ def create_lead(): doc.insert() def create_opportunity(): - doc = frappe.db.exists({ - "doctype":"Opportunity", - "title":"Client Company" - }) + doc = frappe.db.exists({"doctype":"Opportunity","title":"Client Company"}) if not doc: doc = frappe.new_doc("Opportunity") doc.opportunity_from = "Lead" diff --git a/erpnext/crm/workspace/crm/crm.json b/erpnext/crm/workspace/crm/crm.json index b4fb7d8abe9c..11a247dc57f6 100644 --- a/erpnext/crm/workspace/crm/crm.json +++ b/erpnext/crm/workspace/crm/crm.json @@ -14,6 +14,7 @@ "hide_custom": 0, "icon": "crm", "idx": 0, + "is_default": 0, "is_standard": 1, "label": "CRM", "links": [ @@ -131,6 +132,24 @@ "onboard": 1, "type": "Link" }, + { + "hidden": 0, + "is_query_report": 1, + "label": "Sales Pipeline Analytics", + "link_to": "Sales Pipeline Analytics", + "link_type": "Report", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 1, + "label": "Opportunity Summary by Sales Stage", + "link_to": "Opportunity Summary by Sales Stage", + "link_type": "Report", + "onboard": 0, + "type": "Link" + }, { "dependencies": "", "hidden": 0, @@ -363,7 +382,7 @@ "type": "Link" } ], - "modified": "2020-12-01 13:38:36.871352", + "modified": "2021-08-03 11:56:09.894546", "modified_by": "Administrator", "module": "CRM", "name": "CRM", From 6748dfcf41360c37184cdfd75fd49cfbcf3e7787 Mon Sep 17 00:00:00 2001 From: Mohammed Yusuf Shaikh Date: Tue, 3 Aug 2021 15:19:30 +0530 Subject: [PATCH 05/28] fix: add some checks and tests --- .../opportunity_summary_by_sales_stage.py | 43 ++++++--- ...test_opportunity_summary_by_sales_stage.py | 87 +++++++++++++++++++ 2 files changed, 116 insertions(+), 14 deletions(-) create mode 100644 erpnext/crm/report/opportunity_summary_by_sales_stage/test_opportunity_summary_by_sales_stage.py diff --git a/erpnext/crm/report/opportunity_summary_by_sales_stage/opportunity_summary_by_sales_stage.py b/erpnext/crm/report/opportunity_summary_by_sales_stage/opportunity_summary_by_sales_stage.py index 65f0a12d0da7..5a48ace11ffa 100644 --- a/erpnext/crm/report/opportunity_summary_by_sales_stage/opportunity_summary_by_sales_stage.py +++ b/erpnext/crm/report/opportunity_summary_by_sales_stage/opportunity_summary_by_sales_stage.py @@ -75,13 +75,15 @@ def get_data_query(self,sql): filter_data = self.filters.get('status') if self.filters.get("data_based_on") == "Number": - self.filters.update({'status':tuple(filter_data)}) + if self.filters.get('status'): + self.filters.update({'status':tuple(filter_data)}) self.query_result = frappe.db.sql("""select sales_stage,count(name) as count,{sql} from tabOpportunity where {conditions} group by sales_stage,{sql}""".format(conditions=self.get_conditions(),sql=sql),self.filters,as_dict=1) if self.filters.get("data_based_on") == "Amount": - self.filters.update({'status':tuple(filter_data)}) + if self.filters.get('status'): + self.filters.update({'status':tuple(filter_data)}) self.query_result = frappe.db.sql("""select sales_stage,sum(opportunity_amount) as amount,{sql} from tabOpportunity where {conditions} group by sales_stage,{sql}""".format(conditions=self.get_conditions(),sql=sql),self.filters,as_dict=1) @@ -149,29 +151,42 @@ def get_formatted_data(self): if self.filters.get("based_on") == "Opportunity Owner": if self.filters.get("data_based_on") == "Number": temp = json.loads(d.get("_assign")) - if len(temp) > 1: - sales_stage = d.get('sales_stage') - count = d.get('count') - for owner in temp: + if temp: + if len(temp) > 1: + sales_stage = d.get('sales_stage') + count = d.get('count') + for owner in temp: + self.helper(owner,sales_stage,count) + + else: + owner = temp[0] + sales_stage = d.get('sales_stage') + count = d.get('count') self.helper(owner,sales_stage,count) - else: - owner = temp[0] + owner = "Not Assigned" sales_stage = d.get('sales_stage') count = d.get('count') self.helper(owner,sales_stage,count) + if self.filters.get("data_based_on") == "Amount": temp = json.loads(d.get("_assign")) - if len(temp) > 1: - sales_stage = d.get('sales_stage') - amount = d.get('amount') - for owner in temp: + if temp: + if len(temp) > 1: + sales_stage = d.get('sales_stage') + amount = d.get('amount') + for owner in temp: + self.helper(owner,sales_stage,amount) + + else: + owner = temp[0] + sales_stage = d.get('sales_stage') + amount = d.get('amount') self.helper(owner,sales_stage,amount) - else: - owner = temp[0] + owner = "Not Assigned" sales_stage = d.get('sales_stage') amount = d.get('amount') self.helper(owner,sales_stage,amount) diff --git a/erpnext/crm/report/opportunity_summary_by_sales_stage/test_opportunity_summary_by_sales_stage.py b/erpnext/crm/report/opportunity_summary_by_sales_stage/test_opportunity_summary_by_sales_stage.py new file mode 100644 index 000000000000..b30cd7d2a7bc --- /dev/null +++ b/erpnext/crm/report/opportunity_summary_by_sales_stage/test_opportunity_summary_by_sales_stage.py @@ -0,0 +1,87 @@ +import re +import unittest +import frappe +from erpnext.crm.report.opportunity_summary_by_sales_stage.opportunity_summary_by_sales_stage import execute +from erpnext.crm.report.sales_pipeline_analytics.test_sales_pipeline_analytics import create_company,create_customer,create_lead,create_opportunity + +class TestOpportunitySummaryBySalesStage(unittest.TestCase): + + @classmethod + def setUpClass(self): + create_company() + create_customer() + create_lead() + create_opportunity() + + def test_opportunity_summary_by_sales_stage(self): + self.check_for_opportunity_owner() + self.check_for_source() + self.check_for_opportunity_type() + self.check_all_filters() + + def check_for_opportunity_owner(self): + filters = { + 'based_on': "Opportunity Owner", + 'data_based_on': "Number", + 'company': "__Test Company" + } + + report = execute(filters) + + expected_data = [{ + 'opportunity_owner': "Not Assigned", + 'Prospecting': 1 + }] + + self.assertEqual(expected_data,report[1]) + + def check_for_source(self): + filters = { + 'based_on': "Source", + 'data_based_on': "Number", + 'company': "__Test Company" + } + + report = execute(filters) + + expected_data = [{ + 'source': 'Cold Calling', + 'Prospecting': 1 + }] + + self.assertEqual(expected_data,report[1]) + + def check_for_opportunity_type(self): + filters = { + 'based_on': "Opportunity Type", + 'data_based_on': "Number", + 'company': "__Test Company" + } + + report = execute(filters) + + expected_data = [{ + 'opportunity_type': 'Sales', + 'Prospecting': 1 + }] + + self.assertEqual(expected_data,report[1]) + + def check_all_filters(self): + filters = { + 'based_on': "Opportunity Type", + 'data_based_on': "Number", + 'company': "__Test Company", + 'opportunity_source': "Cold Calling", + 'opportunity_type': "Sales", + 'status': ["Open"] + } + + report = execute(filters) + + expected_data = [{ + 'opportunity_type': 'Sales', + 'Prospecting': 1 + }] + + self.assertEqual(expected_data,report[1]) \ No newline at end of file From 8d948a27719b579d1c6f7c495c7966dc66201668 Mon Sep 17 00:00:00 2001 From: Mohammed Yusuf Shaikh Date: Tue, 3 Aug 2021 16:59:53 +0530 Subject: [PATCH 06/28] fix: sider issues and test --- .../test_opportunity_summary_by_sales_stage.py | 2 -- .../sales_pipeline_analytics/test_sales_pipeline_analytics.py | 1 - 2 files changed, 3 deletions(-) diff --git a/erpnext/crm/report/opportunity_summary_by_sales_stage/test_opportunity_summary_by_sales_stage.py b/erpnext/crm/report/opportunity_summary_by_sales_stage/test_opportunity_summary_by_sales_stage.py index b30cd7d2a7bc..811e0bc2358c 100644 --- a/erpnext/crm/report/opportunity_summary_by_sales_stage/test_opportunity_summary_by_sales_stage.py +++ b/erpnext/crm/report/opportunity_summary_by_sales_stage/test_opportunity_summary_by_sales_stage.py @@ -1,6 +1,4 @@ -import re import unittest -import frappe from erpnext.crm.report.opportunity_summary_by_sales_stage.opportunity_summary_by_sales_stage import execute from erpnext.crm.report.sales_pipeline_analytics.test_sales_pipeline_analytics import create_company,create_customer,create_lead,create_opportunity diff --git a/erpnext/crm/report/sales_pipeline_analytics/test_sales_pipeline_analytics.py b/erpnext/crm/report/sales_pipeline_analytics/test_sales_pipeline_analytics.py index df0da2d69f2d..cd1214b9df37 100644 --- a/erpnext/crm/report/sales_pipeline_analytics/test_sales_pipeline_analytics.py +++ b/erpnext/crm/report/sales_pipeline_analytics/test_sales_pipeline_analytics.py @@ -218,7 +218,6 @@ def create_company(): if not doc: doc = frappe.new_doc('Company') doc.company_name = '__Test Company' - doc.abbr = "_TC" doc.default_currency = "INR" doc.insert() From b9053f816c99d9bf8ff1bb9248f1dac912a569ed Mon Sep 17 00:00:00 2001 From: Mohammed Yusuf Shaikh Date: Tue, 3 Aug 2021 20:06:41 +0530 Subject: [PATCH 07/28] fix: additional checks for error handling and minor changes --- .../sales_pipeline_analytics.js | 1 - .../sales_pipeline_analytics.py | 25 +++++++++++++------ 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/erpnext/crm/report/sales_pipeline_analytics/sales_pipeline_analytics.js b/erpnext/crm/report/sales_pipeline_analytics/sales_pipeline_analytics.js index 7490bf938390..c3ba31cd3222 100644 --- a/erpnext/crm/report/sales_pipeline_analytics/sales_pipeline_analytics.js +++ b/erpnext/crm/report/sales_pipeline_analytics/sales_pipeline_analytics.js @@ -40,7 +40,6 @@ frappe.query_reports["Sales Pipeline Analytics"] = { label: __("Status"), fieldtype: "Select", options: "Open\nQuotation\nConverted\nReplied", - default: "Open" }, { fieldname: "based_on", diff --git a/erpnext/crm/report/sales_pipeline_analytics/sales_pipeline_analytics.py b/erpnext/crm/report/sales_pipeline_analytics/sales_pipeline_analytics.py index a8933cb54974..00e28fc83e44 100644 --- a/erpnext/crm/report/sales_pipeline_analytics/sales_pipeline_analytics.py +++ b/erpnext/crm/report/sales_pipeline_analytics/sales_pipeline_analytics.py @@ -308,7 +308,8 @@ def get_periodic_data(self): period = info.get('month') value = info.get('opportunity_owner') count = info.get('count') - temp = json.loads(value) + if value: + temp = json.loads(value) if self.filters.get("assigned_to"): for data in json.loads(info.get('opportunity_owner')): @@ -336,7 +337,8 @@ def get_periodic_data(self): period = info.get('month') value = info.get('opportunity_owner') amount = info.get('amount') - temp = json.loads(value) + if value: + temp = json.loads(value) if self.filters.get("assigned_to"): for data in json.loads(info.get('opportunity_owner')): @@ -349,7 +351,8 @@ def get_periodic_data(self): period = "Q" + str(info.get('quarter')) value = info.get('opportunity_owner') count = info.get('count') - temp = json.loads(value) + if value: + temp = json.loads(value) if self.filters.get("assigned_to"): for data in json.loads(info.get('opportunity_owner')): @@ -369,7 +372,8 @@ def get_periodic_data(self): period = "Q" + str(info.get('quarter')) value = info.get('opportunity_owner') amount = info.get('amount') - temp = json.loads(value) + if value: + temp = json.loads(value) if self.filters.get("assigned_to"): for data in json.loads(info.get('opportunity_owner')): @@ -393,17 +397,22 @@ def helper(self,period,value,val,temp): if self.filters.get("assigned_to") == user: value = user self.periodic_data.setdefault(value,frappe._dict()).setdefault(period,0.0) - self.periodic_data[value][period]= val + self.periodic_data[value][period] += val else: for user in temp: value = user self.periodic_data.setdefault(value,frappe._dict()).setdefault(period,0.0) - self.periodic_data[value][period]= val + self.periodic_data[value][period] += val else: value = temp[0] self.periodic_data.setdefault(value,frappe._dict()).setdefault(period,0.0) - self.periodic_data[value][period]= val + self.periodic_data[value][period] += val + + elif not temp: + value = value + self.periodic_data.setdefault(value,frappe._dict()).setdefault(period,0.0) + self.periodic_data[value][period] += val else: self.periodic_data.setdefault(value,frappe._dict()).setdefault(period,0.0) - self.periodic_data[value][period]= val \ No newline at end of file + self.periodic_data[value][period] += val \ No newline at end of file From ea8324dee551588cc3daad029a212cf44c83d522 Mon Sep 17 00:00:00 2001 From: Mohammed Yusuf Shaikh Date: Wed, 4 Aug 2021 12:45:32 +0530 Subject: [PATCH 08/28] fix: remove unused conditions --- .../sales_pipeline_analytics.py | 56 +++++++++---------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/erpnext/crm/report/sales_pipeline_analytics/sales_pipeline_analytics.py b/erpnext/crm/report/sales_pipeline_analytics/sales_pipeline_analytics.py index 00e28fc83e44..25926a590e77 100644 --- a/erpnext/crm/report/sales_pipeline_analytics/sales_pipeline_analytics.py +++ b/erpnext/crm/report/sales_pipeline_analytics/sales_pipeline_analytics.py @@ -311,12 +311,12 @@ def get_periodic_data(self): if value: temp = json.loads(value) - if self.filters.get("assigned_to"): - for data in json.loads(info.get('opportunity_owner')): - if data == self.filters.get("assigned_to"): - self.helper(period,data,count,temp) - else: - self.helper(period,value,count,temp) + if self.filters.get("assigned_to"): + for data in json.loads(info.get('opportunity_owner')): + if data == self.filters.get("assigned_to"): + self.helper(period,data,count,temp) + else: + self.helper(period,value,count,temp) if self.filters.get("range") == "Monthly" and self.filters.get("based_on") == "Number" and self.filters.get("pipeline_by") == "Sales Stage": @@ -340,12 +340,12 @@ def get_periodic_data(self): if value: temp = json.loads(value) - if self.filters.get("assigned_to"): - for data in json.loads(info.get('opportunity_owner')): - if data == self.filters.get("assigned_to"): - self.helper(period,data,amount,temp) - else: - self.helper(period,value,amount,temp) + if self.filters.get("assigned_to"): + for data in json.loads(info.get('opportunity_owner')): + if data == self.filters.get("assigned_to"): + self.helper(period,data,amount,temp) + else: + self.helper(period,value,amount,temp) if self.filters.get("range") == "Quaterly" and self.filters.get("based_on") == "Number" and self.filters.get("pipeline_by") == "Owner": period = "Q" + str(info.get('quarter')) @@ -354,12 +354,12 @@ def get_periodic_data(self): if value: temp = json.loads(value) - if self.filters.get("assigned_to"): - for data in json.loads(info.get('opportunity_owner')): - if data == self.filters.get("assigned_to"): - self.helper(period,data,count,temp) - else: - self.helper(period,value,count,temp) + if self.filters.get("assigned_to"): + for data in json.loads(info.get('opportunity_owner')): + if data == self.filters.get("assigned_to"): + self.helper(period,data,count,temp) + else: + self.helper(period,value,count,temp) if self.filters.get("range") == "Quaterly" and self.filters.get("based_on") == "Number" and self.filters.get("pipeline_by") == "Sales Stage": period = "Q" + str(info.get('quarter')) @@ -375,12 +375,12 @@ def get_periodic_data(self): if value: temp = json.loads(value) - if self.filters.get("assigned_to"): - for data in json.loads(info.get('opportunity_owner')): - if data == self.filters.get("assigned_to"): - self.helper(period,data,amount,temp) - else: - self.helper(period,value,amount,temp) + if self.filters.get("assigned_to"): + for data in json.loads(info.get('opportunity_owner')): + if data == self.filters.get("assigned_to"): + self.helper(period,data,amount,temp) + else: + self.helper(period,value,amount,temp) if self.filters.get("range") == "Quaterly" and self.filters.get("based_on") == "Amount" and self.filters.get("pipeline_by") == "Sales Stage": period = "Q" + str(info.get('quarter')) @@ -408,11 +408,11 @@ def helper(self,period,value,val,temp): self.periodic_data.setdefault(value,frappe._dict()).setdefault(period,0.0) self.periodic_data[value][period] += val - elif not temp: + else: value = value self.periodic_data.setdefault(value,frappe._dict()).setdefault(period,0.0) self.periodic_data[value][period] += val - else: - self.periodic_data.setdefault(value,frappe._dict()).setdefault(period,0.0) - self.periodic_data[value][period] += val \ No newline at end of file + # else: + # self.periodic_data.setdefault(value,frappe._dict()).setdefault(period,0.0) + # self.periodic_data[value][period] += val \ No newline at end of file From c47c6ff401a294d1d76b6fa773f785d00c8e69cb Mon Sep 17 00:00:00 2001 From: Mohammed Yusuf Shaikh Date: Tue, 10 Aug 2021 17:23:33 +0530 Subject: [PATCH 09/28] fix: Changes mentioned on PR --- .../opportunity_summary_by_sales_stage.js | 66 ++++++++----------- .../opportunity_summary_by_sales_stage.py | 60 ++++++++++------- 2 files changed, 66 insertions(+), 60 deletions(-) diff --git a/erpnext/crm/report/opportunity_summary_by_sales_stage/opportunity_summary_by_sales_stage.js b/erpnext/crm/report/opportunity_summary_by_sales_stage/opportunity_summary_by_sales_stage.js index c14bb4fc159b..8e2b7ce5b685 100644 --- a/erpnext/crm/report/opportunity_summary_by_sales_stage/opportunity_summary_by_sales_stage.js +++ b/erpnext/crm/report/opportunity_summary_by_sales_stage/opportunity_summary_by_sales_stage.js @@ -5,68 +5,60 @@ frappe.query_reports["Opportunity Summary by Sales Stage"] = { "filters": [ { - fieldname:"based_on", + fieldname: "based_on", label: __("Based On"), - fieldtype:"Select", - options:"Opportunity Owner\nSource\nOpportunity Type", - default:"Source" - } - , + fieldtype: "Select", + options: "Opportunity Owner\nSource\nOpportunity Type", + default: "Source" + }, { - fieldname:"data_based_on", + fieldname: "data_based_on", label: __("Data Based On"), - fieldtype:"Select", - options:"Number\nAmount", - default:"Number" - } - , + fieldtype: "Select", + options: "Number\nAmount", + default: "Number" + }, { - fieldname:"from_date", + fieldname: "from_date", label: __("From Date"), fieldtype: "Date", - } - , + }, { - fieldname:"to_date", + fieldname: "to_date", label: __("To Date"), fieldtype: "Date", - } - , + }, { - fieldname:"status", + fieldname: "status", label: __("Status"), - fieldtype:"MultiSelectList", - get_data:function(){ + fieldtype: "MultiSelectList", + get_data: function(){ return [{value:"Open",description:"Status"}, {value:"Converted",description:"Status"}, {value:"Quotation",description:"Status"}, {value:"Replied",description:"Status"} ] } - } - , + }, { - fieldname:"opportunity_source", + fieldname: "opportunity_source", label: __("Oppoturnity Source"), - fieldtype:"Link", - options:"Lead Source", - } - , + fieldtype: "Link", + options: "Lead Source", + }, { - fieldname:"opportunity_type", + fieldname: "opportunity_type", label: __("Opportunity Type"), - fieldtype:"Link", - options:"Opportunity Type", + fieldtype: "Link", + options: "Opportunity Type", }, { - fieldname:"company", + fieldname: "company", label: __("Company"), - fieldtype:"Link", - options:"Company", - default:frappe.defaults.get_user_default("Company") + fieldtype: "Link", + options: "Company", + default: frappe.defaults.get_user_default("Company") } - - ] }; \ No newline at end of file diff --git a/erpnext/crm/report/opportunity_summary_by_sales_stage/opportunity_summary_by_sales_stage.py b/erpnext/crm/report/opportunity_summary_by_sales_stage/opportunity_summary_by_sales_stage.py index 5a48ace11ffa..965ed22f4c15 100644 --- a/erpnext/crm/report/opportunity_summary_by_sales_stage/opportunity_summary_by_sales_stage.py +++ b/erpnext/crm/report/opportunity_summary_by_sales_stage/opportunity_summary_by_sales_stage.py @@ -20,7 +20,7 @@ def run(self): self.get_data() self.get_chart_data() - return self.columns,self.data,None,self.chart + return self.columns, self.data, None, self.chart def get_columns(self): self.columns = [] @@ -49,25 +49,32 @@ def get_columns(self): self.sales_stage_list = frappe.db.get_list("Sales Stage",pluck="name") for sales_stage in self.sales_stage_list: - self.columns.append({ - 'label': _(sales_stage), - 'fieldname': sales_stage, - 'width':150 - }) + if self.filters.get('data_based_on') == 'Number': + self.columns.append({ + 'label': _(sales_stage), + 'fieldname': sales_stage, + 'width':150 + }) + if self.filters.get('data_based_on') == 'Amount': + self.columns.append({ + 'label': _(sales_stage), + 'fieldname': sales_stage, + 'fieldtype': 'Currency', + 'width':150 + }) return self.columns def get_data(self): self.data = [] - if self.filters.get('based_on') == "Opportunity Owner": - sql = "_assign" - self.get_data_query(sql) - if self.filters.get('based_on') == "Source": - sql = "source" - self.get_data_query(sql) - if self.filters.get('based_on') == "Opportunity Type": - sql = "opportunity_type" - self.get_data_query(sql) + + sql = { + 'Opportunity Owner': "_assign", + 'Source': "source", + 'Opportunity Source': "opportunity_type" + }[self.filters.get('based_on')] + + self.get_data_query(sql) self.get_rows() @@ -91,7 +98,6 @@ def get_data_query(self,sql): def get_rows(self): self.data = [] self.get_formatted_data() - currency_symbol = self.get_currency() for based_on,data in iteritems(self.formatted_data): if self.filters.get("based_on") == "Opportunity Owner": @@ -108,7 +114,7 @@ def get_rows(self): sales_stage = d.get('sales_stage') amount = data.get(sales_stage) if amount: - row[sales_stage] = str(amount) + currency_symbol + row[sales_stage] = amount if self.filters.get("based_on") == "Source": row = {'source': based_on} @@ -124,7 +130,7 @@ def get_rows(self): sales_stage = d.get('sales_stage') amount = data.get(sales_stage) if amount: - row[sales_stage] = str(amount) + currency_symbol + row[sales_stage] = amount if self.filters.get("based_on") == "Opportunity Type": row = {'opportunity_type': based_on} @@ -140,7 +146,7 @@ def get_rows(self): sales_stage = d.get('sales_stage') amount = data.get(sales_stage) if amount: - row[sales_stage] = str(amount) + currency_symbol + row[sales_stage] = amount self.data.append(row) @@ -268,7 +274,15 @@ def get_chart_data(self): "type":"line" } - def get_currency(self): - company = self.filters.get('company') - default_currency = frappe.db.get_value('Company',company,['default_currency']) - return frappe.db.get_value('Currency',default_currency,['symbol']) \ No newline at end of file + def append_chart_data(self,data,values,labels,datasets): + for data in self.query_result: + for count in range(0,8): + if data['sales_stage'] == labels[count]: + values[count] = values[count] + data['count'] + datasets.append({"name":'Count','values':values}) + + + # def get_currency(self): + # company = self.filters.get('company') + # default_currency = frappe.db.get_value('Company',company,['default_currency']) + # return frappe.db.get_value('Currency',default_currency,['symbol']) \ No newline at end of file From 2edf3d8b0789906eeec41264e6b24e57022246d1 Mon Sep 17 00:00:00 2001 From: Mohammed Yusuf Shaikh Date: Thu, 19 Aug 2021 20:06:49 +0530 Subject: [PATCH 10/28] fix: currency conversions and other changes --- .../opportunity_summary_by_sales_stage.js | 2 +- .../opportunity_summary_by_sales_stage.py | 268 ++++------ .../sales_pipeline_analytics.py | 494 +++++++----------- .../test_sales_pipeline_analytics.py | 10 +- erpnext/crm/workspace/crm/crm.json | 2 +- 5 files changed, 312 insertions(+), 464 deletions(-) diff --git a/erpnext/crm/report/opportunity_summary_by_sales_stage/opportunity_summary_by_sales_stage.js b/erpnext/crm/report/opportunity_summary_by_sales_stage/opportunity_summary_by_sales_stage.js index 8e2b7ce5b685..d7e758e566cd 100644 --- a/erpnext/crm/report/opportunity_summary_by_sales_stage/opportunity_summary_by_sales_stage.js +++ b/erpnext/crm/report/opportunity_summary_by_sales_stage/opportunity_summary_by_sales_stage.js @@ -9,7 +9,7 @@ frappe.query_reports["Opportunity Summary by Sales Stage"] = { label: __("Based On"), fieldtype: "Select", options: "Opportunity Owner\nSource\nOpportunity Type", - default: "Source" + default: "Opportunity Owner" }, { fieldname: "data_based_on", diff --git a/erpnext/crm/report/opportunity_summary_by_sales_stage/opportunity_summary_by_sales_stage.py b/erpnext/crm/report/opportunity_summary_by_sales_stage/opportunity_summary_by_sales_stage.py index 965ed22f4c15..4029b51abe2e 100644 --- a/erpnext/crm/report/opportunity_summary_by_sales_stage/opportunity_summary_by_sales_stage.py +++ b/erpnext/crm/report/opportunity_summary_by_sales_stage/opportunity_summary_by_sales_stage.py @@ -1,10 +1,15 @@ # Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt +from math import inf +import re import frappe +import pandas from frappe import _ from six import iteritems import json +from frappe.utils import flt +from erpnext.setup.utils import get_exchange_rate def execute(filters=None): @@ -18,8 +23,8 @@ def __init__(self,filters=None): def run(self): self.get_columns() self.get_data() + self.chart = {} self.get_chart_data() - return self.columns, self.data, None, self.chart def get_columns(self): @@ -45,7 +50,6 @@ def get_columns(self): 'fieldname': 'opportunity_type', 'width': 200 }) - self.sales_stage_list = frappe.db.get_list("Sales Stage",pluck="name") for sales_stage in self.sales_stage_list: @@ -53,6 +57,7 @@ def get_columns(self): self.columns.append({ 'label': _(sales_stage), 'fieldname': sales_stage, + 'fieldtype': 'Int', 'width':150 }) if self.filters.get('data_based_on') == 'Amount': @@ -68,163 +73,105 @@ def get_columns(self): def get_data(self): self.data = [] - sql = { + based_on = { 'Opportunity Owner': "_assign", 'Source': "source", - 'Opportunity Source': "opportunity_type" + 'Opportunity Type': "opportunity_type" }[self.filters.get('based_on')] - self.get_data_query(sql) + data_based_on = { + 'Number': "count(name) as count", + 'Amount': "opportunity_amount as amount", + }[self.filters.get('data_based_on')] + + self.get_data_query(based_on,data_based_on) self.get_rows() - def get_data_query(self,sql): + def get_data_query(self,based_on,data_based_on): filter_data = self.filters.get('status') - - if self.filters.get("data_based_on") == "Number": - if self.filters.get('status'): - self.filters.update({'status':tuple(filter_data)}) - self.query_result = frappe.db.sql("""select sales_stage,count(name) as count,{sql} from tabOpportunity - where {conditions} - group by sales_stage,{sql}""".format(conditions=self.get_conditions(),sql=sql),self.filters,as_dict=1) - - if self.filters.get("data_based_on") == "Amount": - if self.filters.get('status'): - self.filters.update({'status':tuple(filter_data)}) - self.query_result = frappe.db.sql("""select sales_stage,sum(opportunity_amount) as amount,{sql} from tabOpportunity - where {conditions} - group by sales_stage,{sql}""".format(conditions=self.get_conditions(),sql=sql),self.filters,as_dict=1) - + + if filter_data: + self.filters.update({'status':tuple(filter_data)}) + + if self.filters.get('data_based_on') == 'Number': + self.query_result = frappe.db.sql("""select sales_stage,{select},{sql},currency from tabOpportunity + where {conditions} + group by sales_stage,{sql}""".format(conditions=self.get_conditions(),sql=based_on,select=data_based_on),self.filters,as_dict=1) + + if self.filters.get('data_based_on') == 'Amount': + self.query_result = frappe.db.sql("""select sales_stage,{based_on},currency,{data_based_on} from tabOpportunity + where {conditions} """.format(conditions=self.get_conditions(),based_on=based_on,data_based_on=data_based_on),self.filters,as_dict=1) + + self.convert_to_base_currency() + dataframe = pandas.DataFrame.from_records(self.query_result) + result = dataframe.groupby(['sales_stage',based_on],as_index=False)['amount'].sum() + self.grouped_data = [] + + for i in range(len(result['amount'])): + self.grouped_data.append({'sales_stage': result['sales_stage'][i], based_on : result[based_on][i], 'amount': result['amount'][i]}) + + self.query_result = self.grouped_data + def get_rows(self): self.data = [] self.get_formatted_data() for based_on,data in iteritems(self.formatted_data): - if self.filters.get("based_on") == "Opportunity Owner": - row = {'opportunity_owner': based_on} - - if self.filters.get("data_based_on") == "Number": - for d in self.query_result: - sales_stage = d.get('sales_stage') - count = data.get(sales_stage) - row[sales_stage] = count - - if self.filters.get("data_based_on") == "Amount": - for d in self.query_result: - sales_stage = d.get('sales_stage') - amount = data.get(sales_stage) - if amount: - row[sales_stage] = amount - - if self.filters.get("based_on") == "Source": - row = {'source': based_on} - - if self.filters.get("data_based_on") == "Number": - for d in self.query_result: - sales_stage = d.get('sales_stage') - count = data.get(sales_stage) - row[sales_stage] = count - - if self.filters.get("data_based_on") == "Amount": - for d in self.query_result: - sales_stage = d.get('sales_stage') - amount = data.get(sales_stage) - if amount: - row[sales_stage] = amount - - if self.filters.get("based_on") == "Opportunity Type": - row = {'opportunity_type': based_on} - - if self.filters.get("data_based_on") == "Number": - for d in self.query_result: - sales_stage = d.get('sales_stage') - count = data.get(sales_stage) - row[sales_stage] = count - - if self.filters.get("data_based_on") == "Amount": - for d in self.query_result: - sales_stage = d.get('sales_stage') - amount = data.get(sales_stage) - if amount: - row[sales_stage] = amount + row_based_on={ + 'Opportunity Owner': 'opportunity_owner', + 'Source': 'source', + 'Opportunity Type': 'opportunity_type' + }[self.filters.get('based_on')] + + row = {row_based_on: based_on} + + for d in self.query_result: + sales_stage = d.get('sales_stage') + row[sales_stage] = data.get(sales_stage) + self.data.append(row) def get_formatted_data(self): self.formatted_data = frappe._dict() for d in self.query_result: + data_based_on ={ + 'Number': 'count', + 'Amount': 'amount' + }[self.filters.get('data_based_on')] + + based_on ={ + 'Opportunity Owner': '_assign', + 'Source': 'source', + 'Opportunity Type': 'opportunity_type' + }[self.filters.get('based_on')] + if self.filters.get("based_on") == "Opportunity Owner": - if self.filters.get("data_based_on") == "Number": - temp = json.loads(d.get("_assign")) - if temp: - if len(temp) > 1: - sales_stage = d.get('sales_stage') - count = d.get('count') - for owner in temp: - self.helper(owner,sales_stage,count) - - else: - owner = temp[0] - sales_stage = d.get('sales_stage') - count = d.get('count') - self.helper(owner,sales_stage,count) - else: - owner = "Not Assigned" - sales_stage = d.get('sales_stage') - count = d.get('count') - self.helper(owner,sales_stage,count) - - - if self.filters.get("data_based_on") == "Amount": - - temp = json.loads(d.get("_assign")) - if temp: - if len(temp) > 1: - sales_stage = d.get('sales_stage') - amount = d.get('amount') - for owner in temp: - self.helper(owner,sales_stage,amount) - - else: - owner = temp[0] - sales_stage = d.get('sales_stage') - amount = d.get('amount') - self.helper(owner,sales_stage,amount) + temp = json.loads(d.get(based_on)) + sales_stage = d.get('sales_stage') + count = d.get(data_based_on) + if temp: + if len(temp) > 1: + for value in temp: + self.insert_formatted_data(value,sales_stage,count) + else: - owner = "Not Assigned" - sales_stage = d.get('sales_stage') - amount = d.get('amount') - self.helper(owner,sales_stage,amount) + value = temp[0] + self.insert_formatted_data(value,sales_stage,count) + + else: + value = "Not Assigned" + self.insert_formatted_data(value,sales_stage,count) + else: + value = d.get(based_on) + sales_stage = d.get('sales_stage') + count = d.get(data_based_on) + self.insert_formatted_data(value,sales_stage,count) - if self.filters.get("based_on") == "Source": - if self.filters.get("data_based_on") == "Number": - source = d.get('source') - sales_stage = d.get('sales_stage') - count = d.get('count') - self.helper(source,sales_stage,count) - - if self.filters.get("data_based_on") == "Amount": - source = d.get('source') - sales_stage = d.get('sales_stage') - amount = d.get('amount') - self.helper(source,sales_stage,amount) - - if self.filters.get("based_on") == "Opportunity Type": - if self.filters.get("data_based_on") == "Number": - opportunity_type = d.get('opportunity_type') - sales_stage = d.get('sales_stage') - count = d.get('count') - self.helper(opportunity_type,sales_stage,count) - - if self.filters.get("data_based_on") == "Amount": - opportunity_type = d.get('opportunity_type') - sales_stage = d.get('sales_stage') - amount = d.get('amount') - self.helper(opportunity_type,sales_stage,amount) - - def helper(self,based_on,sales_stage,data): + def insert_formatted_data(self,based_on,sales_stage,data): self.formatted_data.setdefault(based_on,frappe._dict()).setdefault(sales_stage,0) self.formatted_data[based_on][sales_stage] += data @@ -252,19 +199,16 @@ def get_chart_data(self): for sales_stage in self.sales_stage_list: labels.append(sales_stage) - if self.filters.get("data_based_on") == "Number": - for data in self.query_result: - for count in range(0,8): - if data['sales_stage'] == labels[count]: - values[count] = values[count] + data['count'] - datasets.append({"name":'Count','values':values}) + options = { + 'Number': 'count', + 'Amount': 'amount' + }[self.filters.get('data_based_on')] - if self.filters.get("data_based_on") == "Amount": - for data in self.query_result: - for count in range(0,8): - if data['sales_stage'] == labels[count]: - values[count] = values[count] + data['amount'] - datasets.append({"name":'Amount','values':values}) + for data in self.query_result: + for count in range(len(values)): + if data['sales_stage'] == labels[count]: + values[count] = values[count] + data[options] + datasets.append({"name":options,'values':values}) self.chart = { "data":{ @@ -274,15 +218,25 @@ def get_chart_data(self): "type":"line" } - def append_chart_data(self,data,values,labels,datasets): - for data in self.query_result: - for count in range(0,8): - if data['sales_stage'] == labels[count]: - values[count] = values[count] + data['count'] - datasets.append({"name":'Count','values':values}) + def currency_conversion(self,from_currency,to_currency): + cacheobj = frappe.cache() + if cacheobj.get(from_currency): + return flt(str(cacheobj.get(from_currency),'UTF-8')) - # def get_currency(self): - # company = self.filters.get('company') - # default_currency = frappe.db.get_value('Company',company,['default_currency']) - # return frappe.db.get_value('Currency',default_currency,['symbol']) \ No newline at end of file + else: + value = get_exchange_rate(from_currency,to_currency) + cacheobj.set(from_currency,value) + return flt(str(cacheobj.get(from_currency),'UTF-8')) + + def get_default_currency(self): + company = self.filters.get('company') + return frappe.db.get_value('Company',company,['default_currency']) + + def convert_to_base_currency(self): + default_currency = self.get_default_currency() + for data in self.query_result: + if data.get('currency') != default_currency: + opportunity_currency = data.get('currency') + value = self.currency_conversion(opportunity_currency,default_currency) + data['amount'] = data['amount'] * value \ No newline at end of file diff --git a/erpnext/crm/report/sales_pipeline_analytics/sales_pipeline_analytics.py b/erpnext/crm/report/sales_pipeline_analytics/sales_pipeline_analytics.py index 25926a590e77..2ca3b71d958d 100644 --- a/erpnext/crm/report/sales_pipeline_analytics/sales_pipeline_analytics.py +++ b/erpnext/crm/report/sales_pipeline_analytics/sales_pipeline_analytics.py @@ -3,9 +3,12 @@ import json import frappe -from datetime import datetime +from datetime import date +import pandas from dateutil.relativedelta import relativedelta from six import iteritems +from frappe.utils import flt +from erpnext.setup.utils import get_exchange_rate def execute(filters=None): return SalesPipelineAnalytics(filters).run() @@ -24,27 +27,31 @@ def run(self): def get_columns(self): self.columns = [] - if self.filters['range'] == "Monthly": - - current_date = datetime.date(datetime.now()) - current_month_number = int(current_date.strftime("%m")) - - for i in range(current_month_number,13): + + based_on = { + 'Number' : "Int", + 'Amount' : "Currency" + }[self.filters.get('based_on')] + + if self.filters.get('range') == "Monthly": + month_list = self.get_month_list() + + for month in month_list: self.columns.append( { - 'fieldname': current_date.strftime("%B"), - 'label': current_date.strftime("%B"), + 'fieldname': month, + 'fieldtype': based_on, + 'label': month, 'width': 200 } ) - current_date = current_date + relativedelta(months=1) - - elif self.filters['range'] == "Quaterly": + elif self.filters.get('range') == "Quaterly": for quarter in range(1,5): self.columns.append( { 'fieldname': f"Q{quarter}", + 'fieldtype': based_on, 'label': f"Q{quarter}", 'width': 200 } @@ -66,234 +73,92 @@ def get_columns(self): return self.columns def get_data(self): - self.data = [] - if self.filters.get("range") == "Monthly": - data = self.get_monthly_data() - if self.filters.get("range") == "Quaterly": - data = self.get_quaterly_data() - - return data - - def get_monthly_data(self): - - if self.filters.get("pipeline_by") == "Owner": - select = '_assign as opportunity_owner' - group_by = '_assign' - - if self.filters.get("pipeline_by") == "Sales Stage": - select = 'sales_stage' - group_by = 'sales_stage' - - if self.filters.get("based_on") == "Number": - self.query_result = frappe.db.sql("""SELECT COUNT(name) as count,{select},monthname(expected_closing) as month from tabOpportunity - where {conditions} - GROUP BY {group_by},month(expected_closing) ORDER BY month(expected_closing)""".format(conditions=self.get_conditions(),select=select,group_by=group_by) - ,self.filters,as_dict=1) - - if self.filters.get("pipeline_by") == "Owner": - self.get_periodic_data() - for customer,period_data in iteritems(self.periodic_data): - row = {'opportunity_owner': customer} - for info in self.query_result: - period = info.get('month') - count = period_data.get(period,0.0) - row[period] = count - self.data.append(row) - - - if self.filters.get("pipeline_by") == "Sales Stage": - self.get_periodic_data() - for sales_stage,period_data in iteritems(self.periodic_data): - row = {'sales_stage': sales_stage} - for info in self.query_result: - period = info.get('month') - count = period_data.get(period,0.0) - row[period] = count - self.data.append(row) - - return self.data - - - if self.filters.get("based_on") == "Amount": - self.query_result = frappe.db.sql("""SELECT sum(opportunity_amount) as amount,{select},monthname(expected_closing) as month from tabOpportunity - where {conditions} - GROUP BY {group_by},month(expected_closing) ORDER BY month(expected_closing)""".format(conditions=self.get_conditions(),select=select,group_by=group_by,) - ,self.filters,as_dict=1) - - if self.filters.get("pipeline_by") == "Owner": - self.get_periodic_data() - for user,period_data in iteritems(self.periodic_data): - row = {'opportunity_owner': user} - for info in self.query_result: - period = info.get('month') - count = period_data.get(period,0.0) - row[period] = count - - self.data.append(row) - - if self.filters.get("pipeline_by") == "Sales Stage": - self.get_periodic_data() - for sales_stage,period_data in iteritems(self.periodic_data): - row = {'sales_stage': sales_stage} - for info in self.query_result: - period = info.get('month') - count = period_data.get(period,0.0) - row[period] = count - - self.data.append(row) - - return self.data - - def get_quaterly_data(self): - if self.filters.get("pipeline_by") == "Owner": - select = '_assign as opportunity_owner' - group_by = '_assign' - if self.filters.get("pipeline_by") == "Sales Stage": - select = 'sales_stage' - group_by = 'sales_stage' - - if self.filters.get("based_on") == "Number": - self.query_result = frappe.db.sql("""select count(name) as count,{select},QUARTER(expected_closing) as quarter from tabOpportunity - where {conditions} - group by {group_by},QUARTER(expected_closing) order by QUARTER(expected_closing) - """.format(conditions=self.get_conditions(),select=select,group_by=group_by),self.filters,as_dict=1) - - if self.filters.get("pipeline_by") == "Owner": - self.get_periodic_data() - for customer,period_data in iteritems(self.periodic_data): - row = {'opportunity_owner': customer} - for info in self.query_result: - period = "Q" + str(info.get('quarter')) - count = period_data.get(period,0.0) - row[period] = count - self.data.append(row) - - return self.data - - if self.filters.get("pipeline_by") == "Sales Stage": - self.get_periodic_data() - for customer,period_data in iteritems(self.periodic_data): - row = {'sales_stage': customer} - for info in self.query_result: - period = "Q" + str(info.get('quarter')) - count = period_data.get(period,0.0) - row[period] = count - self.data.append(row) - - return self.data - - if self.filters.get("based_on") == "Amount": - self.query_result = frappe.db.sql("""select sum(opportunity_amount) as amount,{select},QUARTER(expected_closing) as quarter from tabOpportunity - where {conditions} - group by {group_by},QUARTER(expected_closing) order by QUARTER(expected_closing) - """.format(conditions=self.get_conditions(),select=select,group_by=group_by),self.filters,as_dict=1) - - if self.filters.get("pipeline_by") == "Owner": - self.get_periodic_data() - for customer,period_data in iteritems(self.periodic_data): - row = {'opportunity_owner': customer} - for info in self.query_result: - period = "Q" + str(info.get('quarter')) - count = period_data.get(period,0.0) - row[period] = count - self.data.append(row) - - if self.filters.get("pipeline_by") == "Sales Stage": - self.get_periodic_data() - for sales_stage,period_data in iteritems(self.periodic_data): - row = {'sales_stage': sales_stage} - for info in self.query_result: - period = "Q" + str(info.get('quarter')) - count = period_data.get(period,0.0) - row[period] = count - self.data.append(row) - - return self.data - + select_1 ={ + 'Owner': '_assign as opportunity_owner', + 'Sales Stage': 'sales_stage' + }[self.filters.get('pipeline_by')] + + select_2 ={ + 'Number': 'count(name) as count', + 'Amount': 'opportunity_amount as amount' + }[self.filters.get('based_on')] + + group_by_1 = { + 'Owner': '_assign', + 'Sales Stage': 'sales_stage' + }[self.filters.get('pipeline_by')] + + group_by_2 = { + 'Monthly': 'month(expected_closing)', + 'Quaterly': 'QUARTER(expected_closing)' + }[self.filters.get('range')] + + + pipeline_by = { + 'Owner': 'opportunity_owner', + 'Sales Stage': 'sales_stage' + }[self.filters.get('pipeline_by')] + + duration = { + 'Monthly': 'monthname(expected_closing) as month', + 'Quaterly': 'QUARTER(expected_closing) as quarter' + }[self.filters.get('range')] + + period_by = { + 'Monthly': 'month', + 'Quaterly': 'quarter' + }[self.filters.get('range')] + + if self.filters.get('based_on') == 'Number': + self.query_result = frappe.db.get_list('Opportunity',filters=self.get_conditions(),fields=[select_1,select_2,duration] + ,group_by="{},{}".format(group_by_1,group_by_2),order_by=group_by_2) + + if self.filters.get('based_on') == 'Amount': + self.query_result = frappe.db.get_list('Opportunity',filters=self.get_conditions(),fields=[select_1,select_2,duration,'currency']) + self.convert_to_base_currency() + dataframe = pandas.DataFrame.from_records(self.query_result) + result = dataframe.groupby([pipeline_by,period_by],as_index=False)['amount'].sum() + self.grouped_data = [] + + for i in range(len(result['amount'])): + self.grouped_data.append({pipeline_by : result[pipeline_by][i], period_by : result[period_by][i], 'amount': result['amount'][i]}) + self.query_result = self.grouped_data + + self.get_periodic_data() + self.append_data(pipeline_by,period_by) + def get_conditions(self): - current_date = datetime.date(datetime.now()) + conditions = [] if self.filters.get("opportunity_source"): - conditions.append('source=%(opportunity_source)s') + conditions.append({"source": self.filters.get('opportunity_source')}) if self.filters.get("opportunity_type"): - conditions.append('opportunity_type=%(opportunity_type)s') + conditions.append({'opportunity_type': self.filters.get('opportunity_type')}) if self.filters.get("status"): - conditions.append('status=%(status)s') + conditions.append({'status': self.filters.get('status')}) if self.filters.get("company"): - conditions.append('company=%(company)s') - if self.filters.get("from_date"): - conditions.append('expected_closing>=%(from_date)s') - if self.filters.get("to_date"): - conditions.append('expected_closing<=%(to_date)s') - - if not self.filters.get("from_date") and not self.filters.get("to_date") and self.filters.get("Monthly"): - conditions.append('expected_closing between {cd} and {dd}'.format(cd = current_date - ,dd= current_date + relativedelta(months=1) + relativedelta(months=1))) + conditions.append({'company': self.filters.get('company')}) + if self.filters.get("from_date") and self.filters.get("to_date"): + conditions.append(['expected_closing','between',[self.filters.get("from_date"),self.filters.get("to_date")]]) - return "{}".format(" and ".join(conditions)) + return conditions def get_chart_data(self): - labels = [] - values = [] - quarter_list = [1,2,3,4] - count = [0,0,0,0] - count_month = [0,0,0,0,0,0,0,0,0,0,0,0] + + labels = values = [] datasets = [] - month_list = [] - current_date = datetime.date(datetime.now()) - current_month_number = int(current_date.strftime("%m")) - - for month in range(current_month_number,13): - month_list.append(current_date.strftime("%B")) - current_date = current_date + relativedelta(months=1) - - if self.filters.get("range") == "Quaterly" and self.filters.get("based_on") == "Amount": - for info in self.query_result: - for q in range(0,len(quarter_list)): - if info['quarter'] == quarter_list[q]: - count[q] = count[q] + info['amount'] - values = count - datasets.append({'name':'Amount','values':values}) - - - if self.filters.get("range") == "Quaterly" and self.filters.get("based_on") == "Number": - for info in self.query_result: - for q in range(0,len(quarter_list)): - if info['quarter'] == quarter_list[q]: - count[q] = count[q] + info['count'] - values = count - datasets.append({'name':'Number','values':values}) - - if self.filters.get("range") == "Monthly" and self.filters.get("based_on") == "Amount": - for info in self.query_result: - for m in range(0,len(month_list)): - if info['month'] == month_list[m]: - count_month[m] = count_month[m] + info['amount'] - values = count_month - datasets.append({'name':'Amount','values':values}) - - if self.filters.get("range") == "Monthly" and self.filters.get("based_on") == "Number": - for info in self.query_result: - for m in range(0,len(month_list)): - if info['month'] == month_list[m]: - count_month[m] = count_month[m] + info['count'] - values = count_month - datasets.append({'name':'Count','values':values}) + self.append_to_dataset(values,datasets) - for c in self.columns: - if c['fieldname'] == "opportunity_owner" or c['fieldname'] == "sales_stage": - pass - else: + if c['fieldname'] != "opportunity_owner" and c['fieldname'] != "sales_stage": labels.append(c['fieldname']) self.chart = { "data":{ 'labels': labels, 'datasets': datasets - }, "type":"line" } @@ -303,92 +168,41 @@ def get_chart_data(self): def get_periodic_data(self): self.periodic_data = frappe._dict() - for info in self.query_result: - if self.filters.get("range") == "Monthly" and self.filters.get("based_on") == "Number" and self.filters.get("pipeline_by") == "Owner": - period = info.get('month') - value = info.get('opportunity_owner') - count = info.get('count') - if value: - temp = json.loads(value) + based_on = { + 'Number': 'count', + 'Amount': 'amount' + }[self.filters.get('based_on')] - if self.filters.get("assigned_to"): - for data in json.loads(info.get('opportunity_owner')): - if data == self.filters.get("assigned_to"): - self.helper(period,data,count,temp) - else: - self.helper(period,value,count,temp) - - - if self.filters.get("range") == "Monthly" and self.filters.get("based_on") == "Number" and self.filters.get("pipeline_by") == "Sales Stage": - period = info.get('month') - value = info.get('sales_stage') - count = info.get('count') - self.helper(period,value,count,None) + pipeline_by = { + 'Owner': 'opportunity_owner', + 'Sales Stage': 'sales_stage' + }[self.filters.get('pipeline_by')] + range ={ + 'Monthly': 'month', + 'Quaterly': 'quarter' + }[self.filters.get('range')] - if self.filters.get("range") == "Monthly" and self.filters.get("based_on") == "Amount" and self.filters.get("pipeline_by") == "Sales Stage": - period = info.get('month') - value = info.get('sales_stage') - amount = info.get('amount') - self.helper(period,value,amount,None) - - - if self.filters.get("range") == "Monthly" and self.filters.get("based_on") == "Amount" and self.filters.get("pipeline_by") == "Owner": - period = info.get('month') - value = info.get('opportunity_owner') - amount = info.get('amount') - if value: - temp = json.loads(value) - - if self.filters.get("assigned_to"): - for data in json.loads(info.get('opportunity_owner')): - if data == self.filters.get("assigned_to"): - self.helper(period,data,amount,temp) - else: - self.helper(period,value,amount,temp) - - if self.filters.get("range") == "Quaterly" and self.filters.get("based_on") == "Number" and self.filters.get("pipeline_by") == "Owner": - period = "Q" + str(info.get('quarter')) - value = info.get('opportunity_owner') - count = info.get('count') - if value: - temp = json.loads(value) - - if self.filters.get("assigned_to"): - for data in json.loads(info.get('opportunity_owner')): - if data == self.filters.get("assigned_to"): - self.helper(period,data,count,temp) - else: - self.helper(period,value,count,temp) - - if self.filters.get("range") == "Quaterly" and self.filters.get("based_on") == "Number" and self.filters.get("pipeline_by") == "Sales Stage": + for info in self.query_result: + if self.filters.get('range') == 'Monthly': + period = info.get(range) + if self.filters.get('range') == 'Quaterly': period = "Q" + str(info.get('quarter')) - value = info.get('sales_stage') - count = info.get('count') - self.helper(period,value,count,None) + value = info.get(pipeline_by) + count = info.get(based_on) - if self.filters.get("range") == "Quaterly" and self.filters.get("based_on") == "Amount" and self.filters.get("pipeline_by") == "Owner": - period = "Q" + str(info.get('quarter')) - value = info.get('opportunity_owner') - amount = info.get('amount') - if value: + if self.filters.get('pipeline_by') == 'Owner': + if value == '[]': + temp = ["Not Assigned"] + else: temp = json.loads(value) + self.check_for_assigned_to(period,value,count,temp,info) - if self.filters.get("assigned_to"): - for data in json.loads(info.get('opportunity_owner')): - if data == self.filters.get("assigned_to"): - self.helper(period,data,amount,temp) - else: - self.helper(period,value,amount,temp) - - if self.filters.get("range") == "Quaterly" and self.filters.get("based_on") == "Amount" and self.filters.get("pipeline_by") == "Sales Stage": - period = "Q" + str(info.get('quarter')) - value = info.get('sales_stage') - amount = info.get('amount') - self.helper(period,value,amount,None) + else: + self.insert_formatted_data(period,value,count,None) - def helper(self,period,value,val,temp): + def insert_formatted_data(self,period,value,val,temp): if temp: if len(temp) > 1: @@ -413,6 +227,86 @@ def helper(self,period,value,val,temp): self.periodic_data.setdefault(value,frappe._dict()).setdefault(period,0.0) self.periodic_data[value][period] += val - # else: - # self.periodic_data.setdefault(value,frappe._dict()).setdefault(period,0.0) - # self.periodic_data[value][period] += val \ No newline at end of file + def check_for_assigned_to(self,period,value,count,temp,info): + if self.filters.get("assigned_to"): + for data in json.loads(info.get('opportunity_owner')): + if data == self.filters.get("assigned_to"): + self.insert_formatted_data(period,data,count,temp) + else: + self.insert_formatted_data(period,value,count,temp) + + def get_month_list(self): + month_list= [] + current_date = date.today() + month_number = date.today().month + + for month in range(month_number,13): + month_list.append(current_date.strftime("%B")) + current_date = current_date + relativedelta(months=1) + + return month_list + + def append_to_dataset(self,values,datasets): + + range_by = { + 'Monthly': 'month', + 'Quaterly': 'quarter' + }[self.filters.get('range')] + + based_on = { + 'Amount': 'amount', + 'Number': 'count' + }[self.filters.get('based_on')] + + if self.filters.get("range") == "Quaterly": + list = [1,2,3,4] + count = [0,0,0,0] + + if self.filters.get("range") == "Monthly": + list = self.get_month_list() + count = [0,0,0,0,0,0,0,0,0,0,0,0] + + for info in self.query_result: + for i in range(len(list)): + if info[range_by] == list[i]: + count[i] = count[i] + info[based_on] + values = count + datasets.append({'name': based_on,'values':values}) + + def append_data(self,pipeline_by,period_by): + self.data = [] + for pipeline,period_data in iteritems(self.periodic_data): + row = {pipeline_by : pipeline} + for info in self.query_result: + if self.filters.get('range') == 'Monthly': + period = info.get(period_by) + if self.filters.get('range') == 'Quaterly': + period = "Q" + str(info.get(period_by)) + count = period_data.get(period,0.0) + row[period] = count + self.data.append(row) + + return self.data + + def get_default_currency(self): + company = self.filters.get('company') + return frappe.db.get_value('Company',company,['default_currency']) + + def get_currency_rate(self,from_currency,to_currency): + cacheobj = frappe.cache() + + if cacheobj.get(from_currency): + return flt(str(cacheobj.get(from_currency),'UTF-8')) + + else: + value = get_exchange_rate(from_currency,to_currency) + cacheobj.set(from_currency,value) + return flt(str(cacheobj.get(from_currency),'UTF-8')) + + def convert_to_base_currency(self): + default_currency = self.get_default_currency() + for data in self.query_result: + if data.get('currency') != default_currency: + opportunity_currency = data.get('currency') + value = self.get_currency_rate(opportunity_currency,default_currency) + data['amount'] = data['amount'] * value \ No newline at end of file diff --git a/erpnext/crm/report/sales_pipeline_analytics/test_sales_pipeline_analytics.py b/erpnext/crm/report/sales_pipeline_analytics/test_sales_pipeline_analytics.py index cd1214b9df37..e22f77434e6d 100644 --- a/erpnext/crm/report/sales_pipeline_analytics/test_sales_pipeline_analytics.py +++ b/erpnext/crm/report/sales_pipeline_analytics/test_sales_pipeline_analytics.py @@ -34,7 +34,7 @@ def check_for_monthly_and_number(self): expected_data = [ { - 'opportunity_owner':'[]', + 'opportunity_owner':'Not Assigned', 'August':1 } ] @@ -76,7 +76,7 @@ def check_for_monthly_and_amount(self): expected_data = [ { - 'opportunity_owner':'[]', + 'opportunity_owner':'Not Assigned', 'August':150000 } ] @@ -118,7 +118,7 @@ def check_for_quarterly_and_number(self): expected_data = [ { - 'opportunity_owner':'[]', + 'opportunity_owner':'Not Assigned', 'Q3':1 } ] @@ -160,7 +160,7 @@ def check_for_quarterly_and_amount(self): expected_data = [ { - 'opportunity_owner':'[]', + 'opportunity_owner':'Not Assigned', 'Q3':150000 } ] @@ -205,7 +205,7 @@ def check_for_all_filters(self): expected_data = [ { - 'opportunity_owner':'[]', + 'opportunity_owner':'Not Assigned', 'August': 1 } ] diff --git a/erpnext/crm/workspace/crm/crm.json b/erpnext/crm/workspace/crm/crm.json index 11a247dc57f6..e7ee47d265f2 100644 --- a/erpnext/crm/workspace/crm/crm.json +++ b/erpnext/crm/workspace/crm/crm.json @@ -382,7 +382,7 @@ "type": "Link" } ], - "modified": "2021-08-03 11:56:09.894546", + "modified": "2021-08-19 19:08:08.728876", "modified_by": "Administrator", "module": "CRM", "name": "CRM", From d31b164bf3a2f88c56c4e577914d049d2eb1b3d1 Mon Sep 17 00:00:00 2001 From: Mohammed Yusuf Shaikh Date: Mon, 23 Aug 2021 12:38:25 +0530 Subject: [PATCH 11/28] fix: remove unused imports --- .../opportunity_summary_by_sales_stage.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/erpnext/crm/report/opportunity_summary_by_sales_stage/opportunity_summary_by_sales_stage.py b/erpnext/crm/report/opportunity_summary_by_sales_stage/opportunity_summary_by_sales_stage.py index 4029b51abe2e..14f0a9745cf0 100644 --- a/erpnext/crm/report/opportunity_summary_by_sales_stage/opportunity_summary_by_sales_stage.py +++ b/erpnext/crm/report/opportunity_summary_by_sales_stage/opportunity_summary_by_sales_stage.py @@ -1,8 +1,6 @@ # Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt -from math import inf -import re import frappe import pandas from frappe import _ From 8ab5f3364383e6ed2672e88d1ca6b5865baf3043 Mon Sep 17 00:00:00 2001 From: Mohammed Yusuf Shaikh Date: Mon, 23 Aug 2021 18:03:30 +0530 Subject: [PATCH 12/28] fix: correction for failing test case --- .../opportunity_summary_by_sales_stage.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/erpnext/crm/report/opportunity_summary_by_sales_stage/opportunity_summary_by_sales_stage.py b/erpnext/crm/report/opportunity_summary_by_sales_stage/opportunity_summary_by_sales_stage.py index 14f0a9745cf0..e130bc991af3 100644 --- a/erpnext/crm/report/opportunity_summary_by_sales_stage/opportunity_summary_by_sales_stage.py +++ b/erpnext/crm/report/opportunity_summary_by_sales_stage/opportunity_summary_by_sales_stage.py @@ -147,22 +147,20 @@ def get_formatted_data(self): }[self.filters.get('based_on')] if self.filters.get("based_on") == "Opportunity Owner": - temp = json.loads(d.get(based_on)) + if d.get(based_on) == '[]': + temp = ["Not Assigned"] + else: + temp = json.loads(d.get(based_on)) + sales_stage = d.get('sales_stage') count = d.get(data_based_on) if temp: if len(temp) > 1: for value in temp: self.insert_formatted_data(value,sales_stage,count) - else: value = temp[0] - self.insert_formatted_data(value,sales_stage,count) - - else: - value = "Not Assigned" - self.insert_formatted_data(value,sales_stage,count) - + self.insert_formatted_data(value,sales_stage,count) else: value = d.get(based_on) sales_stage = d.get('sales_stage') From 171640e77d57e1fd220d3748e210945016778ebf Mon Sep 17 00:00:00 2001 From: Mohammed Yusuf Shaikh Date: Tue, 24 Aug 2021 16:26:11 +0530 Subject: [PATCH 13/28] fix: recorrected failing test case --- .../opportunity_summary_by_sales_stage.py | 3 ++- .../sales_pipeline_analytics/sales_pipeline_analytics.js | 1 - .../sales_pipeline_analytics/sales_pipeline_analytics.py | 7 ++++--- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/erpnext/crm/report/opportunity_summary_by_sales_stage/opportunity_summary_by_sales_stage.py b/erpnext/crm/report/opportunity_summary_by_sales_stage/opportunity_summary_by_sales_stage.py index e130bc991af3..3852d1e4d3f6 100644 --- a/erpnext/crm/report/opportunity_summary_by_sales_stage/opportunity_summary_by_sales_stage.py +++ b/erpnext/crm/report/opportunity_summary_by_sales_stage/opportunity_summary_by_sales_stage.py @@ -147,7 +147,8 @@ def get_formatted_data(self): }[self.filters.get('based_on')] if self.filters.get("based_on") == "Opportunity Owner": - if d.get(based_on) == '[]': + + if d.get(based_on) == '[]' or d.get(based_on) == None: temp = ["Not Assigned"] else: temp = json.loads(d.get(based_on)) diff --git a/erpnext/crm/report/sales_pipeline_analytics/sales_pipeline_analytics.js b/erpnext/crm/report/sales_pipeline_analytics/sales_pipeline_analytics.js index c3ba31cd3222..fa127f66d5a3 100644 --- a/erpnext/crm/report/sales_pipeline_analytics/sales_pipeline_analytics.js +++ b/erpnext/crm/report/sales_pipeline_analytics/sales_pipeline_analytics.js @@ -66,7 +66,6 @@ frappe.query_reports["Sales Pipeline Analytics"] = { label: __("Opportunity Type"), fieldtype: "Link", options: "Opportunity Type", - default: "Sales" }, ] diff --git a/erpnext/crm/report/sales_pipeline_analytics/sales_pipeline_analytics.py b/erpnext/crm/report/sales_pipeline_analytics/sales_pipeline_analytics.py index 2ca3b71d958d..5c96d7c3cab4 100644 --- a/erpnext/crm/report/sales_pipeline_analytics/sales_pipeline_analytics.py +++ b/erpnext/crm/report/sales_pipeline_analytics/sales_pipeline_analytics.py @@ -193,10 +193,11 @@ def get_periodic_data(self): count = info.get(based_on) if self.filters.get('pipeline_by') == 'Owner': - if value == '[]': - temp = ["Not Assigned"] + + if value == None or value == '[]': + temp = ["Not Assgined"] else: - temp = json.loads(value) + temp = json.loads(value) self.check_for_assigned_to(period,value,count,temp,info) else: From 7f5ff699d69e8c5c183351e0b11f478485246ce3 Mon Sep 17 00:00:00 2001 From: Mohammed Yusuf Shaikh Date: Tue, 24 Aug 2021 18:04:00 +0530 Subject: [PATCH 14/28] fix: sider issues and resolve test case errors --- .../opportunity_summary_by_sales_stage.py | 2 +- ...test_opportunity_summary_by_sales_stage.py | 3 +-- .../sales_pipeline_analytics.py | 15 ++++++++------- .../test_sales_pipeline_analytics.py | 19 +------------------ 4 files changed, 11 insertions(+), 28 deletions(-) diff --git a/erpnext/crm/report/opportunity_summary_by_sales_stage/opportunity_summary_by_sales_stage.py b/erpnext/crm/report/opportunity_summary_by_sales_stage/opportunity_summary_by_sales_stage.py index 3852d1e4d3f6..eb6739f6b315 100644 --- a/erpnext/crm/report/opportunity_summary_by_sales_stage/opportunity_summary_by_sales_stage.py +++ b/erpnext/crm/report/opportunity_summary_by_sales_stage/opportunity_summary_by_sales_stage.py @@ -148,7 +148,7 @@ def get_formatted_data(self): if self.filters.get("based_on") == "Opportunity Owner": - if d.get(based_on) == '[]' or d.get(based_on) == None: + if d.get(based_on) == '[]' or d.get(based_on) is None: temp = ["Not Assigned"] else: temp = json.loads(d.get(based_on)) diff --git a/erpnext/crm/report/opportunity_summary_by_sales_stage/test_opportunity_summary_by_sales_stage.py b/erpnext/crm/report/opportunity_summary_by_sales_stage/test_opportunity_summary_by_sales_stage.py index 811e0bc2358c..c5b70464b448 100644 --- a/erpnext/crm/report/opportunity_summary_by_sales_stage/test_opportunity_summary_by_sales_stage.py +++ b/erpnext/crm/report/opportunity_summary_by_sales_stage/test_opportunity_summary_by_sales_stage.py @@ -1,6 +1,6 @@ import unittest from erpnext.crm.report.opportunity_summary_by_sales_stage.opportunity_summary_by_sales_stage import execute -from erpnext.crm.report.sales_pipeline_analytics.test_sales_pipeline_analytics import create_company,create_customer,create_lead,create_opportunity +from erpnext.crm.report.sales_pipeline_analytics.test_sales_pipeline_analytics import create_company,create_customer,create_opportunity class TestOpportunitySummaryBySalesStage(unittest.TestCase): @@ -8,7 +8,6 @@ class TestOpportunitySummaryBySalesStage(unittest.TestCase): def setUpClass(self): create_company() create_customer() - create_lead() create_opportunity() def test_opportunity_summary_by_sales_stage(self): diff --git a/erpnext/crm/report/sales_pipeline_analytics/sales_pipeline_analytics.py b/erpnext/crm/report/sales_pipeline_analytics/sales_pipeline_analytics.py index 5c96d7c3cab4..7d2113952be6 100644 --- a/erpnext/crm/report/sales_pipeline_analytics/sales_pipeline_analytics.py +++ b/erpnext/crm/report/sales_pipeline_analytics/sales_pipeline_analytics.py @@ -194,10 +194,10 @@ def get_periodic_data(self): if self.filters.get('pipeline_by') == 'Owner': - if value == None or value == '[]': - temp = ["Not Assgined"] + if value is None or value == '[]': + temp = ["Not Assigned"] else: - temp = json.loads(value) + temp = json.loads(value) self.check_for_assigned_to(period,value,count,temp,info) else: @@ -211,21 +211,21 @@ def insert_formatted_data(self,period,value,val,temp): for user in temp: if self.filters.get("assigned_to") == user: value = user - self.periodic_data.setdefault(value,frappe._dict()).setdefault(period,0.0) + self.periodic_data.setdefault(value,frappe._dict()).setdefault(period,0) self.periodic_data[value][period] += val else: for user in temp: value = user - self.periodic_data.setdefault(value,frappe._dict()).setdefault(period,0.0) + self.periodic_data.setdefault(value,frappe._dict()).setdefault(period,0) self.periodic_data[value][period] += val else: value = temp[0] - self.periodic_data.setdefault(value,frappe._dict()).setdefault(period,0.0) + self.periodic_data.setdefault(value,frappe._dict()).setdefault(period,0) self.periodic_data[value][period] += val else: value = value - self.periodic_data.setdefault(value,frappe._dict()).setdefault(period,0.0) + self.periodic_data.setdefault(value,frappe._dict()).setdefault(period,0) self.periodic_data[value][period] += val def check_for_assigned_to(self,period,value,count,temp,info): @@ -283,6 +283,7 @@ def append_data(self,pipeline_by,period_by): period = info.get(period_by) if self.filters.get('range') == 'Quaterly': period = "Q" + str(info.get(period_by)) + count = period_data.get(period,0.0) row[period] = count self.data.append(row) diff --git a/erpnext/crm/report/sales_pipeline_analytics/test_sales_pipeline_analytics.py b/erpnext/crm/report/sales_pipeline_analytics/test_sales_pipeline_analytics.py index e22f77434e6d..d1a85b1a2848 100644 --- a/erpnext/crm/report/sales_pipeline_analytics/test_sales_pipeline_analytics.py +++ b/erpnext/crm/report/sales_pipeline_analytics/test_sales_pipeline_analytics.py @@ -8,18 +8,15 @@ class TestSalesPipelineAnalytics(unittest.TestCase): def setUpClass(self): create_company() create_customer() - create_lead() create_opportunity() - def test_sales_pipeline_analytics(self): self.check_for_monthly_and_number() self.check_for_monthly_and_amount() self.check_for_quarterly_and_number() self.check_for_quarterly_and_amount() self.check_for_all_filters() - - + def check_for_monthly_and_number(self): filters = { 'pipeline_by':"Owner", @@ -61,7 +58,6 @@ def check_for_monthly_and_number(self): self.assertEqual(expected_data,report[1]) - def check_for_monthly_and_amount(self): filters = { 'pipeline_by':"Owner", @@ -103,7 +99,6 @@ def check_for_monthly_and_amount(self): self.assertEqual(expected_data,report[1]) - def check_for_quarterly_and_number(self): filters = { 'pipeline_by':"Owner", @@ -145,7 +140,6 @@ def check_for_quarterly_and_number(self): self.assertEqual(expected_data,report[1]) - def check_for_quarterly_and_amount(self): filters = { 'pipeline_by':"Owner", @@ -187,7 +181,6 @@ def check_for_quarterly_and_amount(self): self.assertEqual(expected_data,report[1]) - def check_for_all_filters(self): filters = { 'pipeline_by':"Owner", @@ -212,7 +205,6 @@ def check_for_all_filters(self): self.assertEqual(expected_data,report[1]) - def create_company(): doc = frappe.db.exists('Company','__Test Company') if not doc: @@ -228,15 +220,6 @@ def create_customer(): doc.customer_name = '_Test Customer' doc.insert() -def create_lead(): - doc = frappe.db.exists("Lead","_Test Lead") - if not doc: - doc = frappe.new_doc("Lead") - doc.lead_name = '_Test Lead' - doc.company_name = 'Client Company' - doc.company = "__Test Company" - doc.insert() - def create_opportunity(): doc = frappe.db.exists({"doctype":"Opportunity","title":"Client Company"}) if not doc: From 65c717bc27cf2d80fd72f3a48f79a017722531a6 Mon Sep 17 00:00:00 2001 From: Mohammed Yusuf Shaikh Date: Thu, 26 Aug 2021 12:34:15 +0530 Subject: [PATCH 15/28] fix: rewrite query using query builder --- .../opportunity_summary_by_sales_stage.py | 41 ++++++++----------- 1 file changed, 16 insertions(+), 25 deletions(-) diff --git a/erpnext/crm/report/opportunity_summary_by_sales_stage/opportunity_summary_by_sales_stage.py b/erpnext/crm/report/opportunity_summary_by_sales_stage/opportunity_summary_by_sales_stage.py index eb6739f6b315..3102b168fbe7 100644 --- a/erpnext/crm/report/opportunity_summary_by_sales_stage/opportunity_summary_by_sales_stage.py +++ b/erpnext/crm/report/opportunity_summary_by_sales_stage/opportunity_summary_by_sales_stage.py @@ -21,7 +21,6 @@ def __init__(self,filters=None): def run(self): self.get_columns() self.get_data() - self.chart = {} self.get_chart_data() return self.columns, self.data, None, self.chart @@ -87,19 +86,13 @@ def get_data(self): self.get_rows() def get_data_query(self,based_on,data_based_on): - filter_data = self.filters.get('status') - - if filter_data: - self.filters.update({'status':tuple(filter_data)}) if self.filters.get('data_based_on') == 'Number': - self.query_result = frappe.db.sql("""select sales_stage,{select},{sql},currency from tabOpportunity - where {conditions} - group by sales_stage,{sql}""".format(conditions=self.get_conditions(),sql=based_on,select=data_based_on),self.filters,as_dict=1) + group_by = "{},{}".format('sales_stage',based_on) + self.query_result = frappe.db.get_list("Opportunity",filters=self.get_conditions(),fields=['sales_stage',data_based_on,based_on],group_by=group_by) if self.filters.get('data_based_on') == 'Amount': - self.query_result = frappe.db.sql("""select sales_stage,{based_on},currency,{data_based_on} from tabOpportunity - where {conditions} """.format(conditions=self.get_conditions(),based_on=based_on,data_based_on=data_based_on),self.filters,as_dict=1) + self.query_result = frappe.db.get_list("Opportunity",filters=self.get_conditions(),fields=['sales_stage',based_on,data_based_on,'currency']) self.convert_to_base_currency() dataframe = pandas.DataFrame.from_records(self.query_result) @@ -173,21 +166,19 @@ def insert_formatted_data(self,based_on,sales_stage,data): self.formatted_data[based_on][sales_stage] += data def get_conditions(self): - conditions = [] - if self.filters.get("opportunity_source"): - conditions.append('source=%(opportunity_source)s') - if self.filters.get("opportunity_type"): - conditions.append('opportunity_type=%(opportunity_type)s') - if self.filters.get("status"): - conditions.append('status in %(status)s') - if self.filters.get("company"): - conditions.append('company=%(company)s') - if self.filters.get("from_date"): - conditions.append('transaction_date>=%(from_date)s') - if self.filters.get("to_date"): - conditions.append('transaction_date<=%(to_date)s') - - return "{}".format(" and ".join(conditions)) + filters = [] + if self.filters.get('company'): + filters.append({'company': self.filters.get('company')}) + if self.filters.get('opportunity_type'): + filters.append({'opportunity_type': self.filters.get('opportunity_type')}) + if self.filters.get('opportunity_source'): + filters.append({'source': self.filters.get('opportunity_source')}) + if self.filters.get('status'): + filters.append({'status':("in",self.filters.get('status'))}) + if self.filters.get('from_date') and self.filters.get('to_date'): + filters.append(['transaction_date', 'between' , [self.filters.get('from_date'),self.filters.get('to_date')]]) + + return filters def get_chart_data(self): labels = [] From 0e8d48e138841c7f90d7a73553ef34d478f68e27 Mon Sep 17 00:00:00 2001 From: Mohammed Yusuf Shaikh Date: Thu, 26 Aug 2021 16:30:30 +0530 Subject: [PATCH 16/28] fix: test case changes --- .../opportunity_summary_by_sales_stage.py | 3 ++- .../sales_pipeline_analytics/sales_pipeline_analytics.py | 6 +++++- .../test_sales_pipeline_analytics.py | 8 ++++---- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/erpnext/crm/report/opportunity_summary_by_sales_stage/opportunity_summary_by_sales_stage.py b/erpnext/crm/report/opportunity_summary_by_sales_stage/opportunity_summary_by_sales_stage.py index 3102b168fbe7..aa2b807b65a0 100644 --- a/erpnext/crm/report/opportunity_summary_by_sales_stage/opportunity_summary_by_sales_stage.py +++ b/erpnext/crm/report/opportunity_summary_by_sales_stage/opportunity_summary_by_sales_stage.py @@ -96,6 +96,7 @@ def get_data_query(self,based_on,data_based_on): self.convert_to_base_currency() dataframe = pandas.DataFrame.from_records(self.query_result) + dataframe.replace(to_replace=[None], value="Not Assigned", inplace=True) result = dataframe.groupby(['sales_stage',based_on],as_index=False)['amount'].sum() self.grouped_data = [] @@ -141,7 +142,7 @@ def get_formatted_data(self): if self.filters.get("based_on") == "Opportunity Owner": - if d.get(based_on) == '[]' or d.get(based_on) is None: + if d.get(based_on) == '[]' or d.get(based_on) is None or d.get(based_on) == "Not Assigned": temp = ["Not Assigned"] else: temp = json.loads(d.get(based_on)) diff --git a/erpnext/crm/report/sales_pipeline_analytics/sales_pipeline_analytics.py b/erpnext/crm/report/sales_pipeline_analytics/sales_pipeline_analytics.py index 7d2113952be6..1db6f29a7e02 100644 --- a/erpnext/crm/report/sales_pipeline_analytics/sales_pipeline_analytics.py +++ b/erpnext/crm/report/sales_pipeline_analytics/sales_pipeline_analytics.py @@ -3,6 +3,7 @@ import json import frappe +import numpy as np from datetime import date import pandas from dateutil.relativedelta import relativedelta @@ -118,6 +119,7 @@ def get_data(self): self.query_result = frappe.db.get_list('Opportunity',filters=self.get_conditions(),fields=[select_1,select_2,duration,'currency']) self.convert_to_base_currency() dataframe = pandas.DataFrame.from_records(self.query_result) + dataframe.replace(to_replace= [None],value="Not Assigned",inplace=True) result = dataframe.groupby([pipeline_by,period_by],as_index=False)['amount'].sum() self.grouped_data = [] @@ -128,6 +130,8 @@ def get_data(self): self.get_periodic_data() self.append_data(pipeline_by,period_by) + return self.data + def get_conditions(self): conditions = [] @@ -194,7 +198,7 @@ def get_periodic_data(self): if self.filters.get('pipeline_by') == 'Owner': - if value is None or value == '[]': + if value == "Not Assigned" or value == '[]' or value is None: temp = ["Not Assigned"] else: temp = json.loads(value) diff --git a/erpnext/crm/report/sales_pipeline_analytics/test_sales_pipeline_analytics.py b/erpnext/crm/report/sales_pipeline_analytics/test_sales_pipeline_analytics.py index d1a85b1a2848..5abc2cd2d5ca 100644 --- a/erpnext/crm/report/sales_pipeline_analytics/test_sales_pipeline_analytics.py +++ b/erpnext/crm/report/sales_pipeline_analytics/test_sales_pipeline_analytics.py @@ -221,12 +221,12 @@ def create_customer(): doc.insert() def create_opportunity(): - doc = frappe.db.exists({"doctype":"Opportunity","title":"Client Company"}) + doc = frappe.db.exists({"doctype":"Opportunity","party_name":"_Test Customer"}) if not doc: doc = frappe.new_doc("Opportunity") - doc.opportunity_from = "Lead" - lead_name = frappe.db.get_value("Lead",{"company":'__Test Company'},['name']) - doc.party_name = lead_name + doc.opportunity_from = "Customer" + customer_name = frappe.db.get_value("Customer",{"customer_name":'_Test Customer'},['customer_name']) + doc.party_name = customer_name doc.opportunity_amount = 150000 doc.source = "Cold Calling" doc.expected_closing = "2021-08-31" From 30423021efa17a1e1c1e04502c3767aa516520c9 Mon Sep 17 00:00:00 2001 From: Mohammed Yusuf Shaikh Date: Thu, 26 Aug 2021 18:25:48 +0530 Subject: [PATCH 17/28] fix: sider fixes and other changes --- ...test_opportunity_summary_by_sales_stage.py | 8 ++--- .../sales_pipeline_analytics.py | 4 +-- .../test_sales_pipeline_analytics.py | 32 +++++++++---------- 3 files changed, 22 insertions(+), 22 deletions(-) diff --git a/erpnext/crm/report/opportunity_summary_by_sales_stage/test_opportunity_summary_by_sales_stage.py b/erpnext/crm/report/opportunity_summary_by_sales_stage/test_opportunity_summary_by_sales_stage.py index c5b70464b448..dbe652ce65e2 100644 --- a/erpnext/crm/report/opportunity_summary_by_sales_stage/test_opportunity_summary_by_sales_stage.py +++ b/erpnext/crm/report/opportunity_summary_by_sales_stage/test_opportunity_summary_by_sales_stage.py @@ -20,7 +20,7 @@ def check_for_opportunity_owner(self): filters = { 'based_on': "Opportunity Owner", 'data_based_on': "Number", - 'company': "__Test Company" + 'company': "Best Test" } report = execute(filters) @@ -36,7 +36,7 @@ def check_for_source(self): filters = { 'based_on': "Source", 'data_based_on': "Number", - 'company': "__Test Company" + 'company': "Best Test" } report = execute(filters) @@ -52,7 +52,7 @@ def check_for_opportunity_type(self): filters = { 'based_on': "Opportunity Type", 'data_based_on': "Number", - 'company': "__Test Company" + 'company': "Best Test" } report = execute(filters) @@ -68,7 +68,7 @@ def check_all_filters(self): filters = { 'based_on': "Opportunity Type", 'data_based_on': "Number", - 'company': "__Test Company", + 'company': "Best Test", 'opportunity_source': "Cold Calling", 'opportunity_type': "Sales", 'status': ["Open"] diff --git a/erpnext/crm/report/sales_pipeline_analytics/sales_pipeline_analytics.py b/erpnext/crm/report/sales_pipeline_analytics/sales_pipeline_analytics.py index 1db6f29a7e02..2b95a1c87204 100644 --- a/erpnext/crm/report/sales_pipeline_analytics/sales_pipeline_analytics.py +++ b/erpnext/crm/report/sales_pipeline_analytics/sales_pipeline_analytics.py @@ -3,7 +3,6 @@ import json import frappe -import numpy as np from datetime import date import pandas from dateutil.relativedelta import relativedelta @@ -208,7 +207,7 @@ def get_periodic_data(self): self.insert_formatted_data(period,value,count,None) def insert_formatted_data(self,period,value,val,temp): - + if temp: if len(temp) > 1: if self.filters.get("assigned_to"): @@ -233,6 +232,7 @@ def insert_formatted_data(self,period,value,val,temp): self.periodic_data[value][period] += val def check_for_assigned_to(self,period,value,count,temp,info): + if self.filters.get("assigned_to"): for data in json.loads(info.get('opportunity_owner')): if data == self.filters.get("assigned_to"): diff --git a/erpnext/crm/report/sales_pipeline_analytics/test_sales_pipeline_analytics.py b/erpnext/crm/report/sales_pipeline_analytics/test_sales_pipeline_analytics.py index 5abc2cd2d5ca..02285dfd8ab0 100644 --- a/erpnext/crm/report/sales_pipeline_analytics/test_sales_pipeline_analytics.py +++ b/erpnext/crm/report/sales_pipeline_analytics/test_sales_pipeline_analytics.py @@ -24,7 +24,7 @@ def check_for_monthly_and_number(self): 'based_on':"Number", 'status':"Open", 'opportunity_type':"Sales", - 'company':"__Test Company" + 'company':"Best Test" } report = execute(filters) @@ -44,7 +44,7 @@ def check_for_monthly_and_number(self): 'based_on':"Number", 'status':"Open", 'opportunity_type':"Sales", - 'company':"__Test Company" + 'company':"Best Test" } report = execute(filters) @@ -65,7 +65,7 @@ def check_for_monthly_and_amount(self): 'based_on':"Amount", 'status':"Open", 'opportunity_type':"Sales", - 'company':"__Test Company" + 'company':"Best Test" } report = execute(filters) @@ -85,7 +85,7 @@ def check_for_monthly_and_amount(self): 'based_on':"Amount", 'status':"Open", 'opportunity_type':"Sales", - 'company':"__Test Company" + 'company':"Best Test" } report = execute(filters) @@ -106,7 +106,7 @@ def check_for_quarterly_and_number(self): 'based_on':"Number", 'status':"Open", 'opportunity_type':"Sales", - 'company':"__Test Company" + 'company':"Best Test" } report = execute(filters) @@ -126,7 +126,7 @@ def check_for_quarterly_and_number(self): 'based_on':"Number", 'status':"Open", 'opportunity_type':"Sales", - 'company':"__Test Company" + 'company':"Best Test" } report = execute(filters) @@ -147,7 +147,7 @@ def check_for_quarterly_and_amount(self): 'based_on':"Amount", 'status':"Open", 'opportunity_type':"Sales", - 'company':"__Test Company" + 'company':"Best Test" } report = execute(filters) @@ -167,7 +167,7 @@ def check_for_quarterly_and_amount(self): 'based_on':"Amount", 'status':"Open", 'opportunity_type':"Sales", - 'company':"__Test Company" + 'company':"Best Test" } report = execute(filters) @@ -188,7 +188,7 @@ def check_for_all_filters(self): 'based_on':"Number", 'status':"Open", 'opportunity_type':"Sales", - 'company':"__Test Company", + 'company':"Best Test", 'opportunity_source':'Cold Calling', 'from_date': '2021-08-01', 'to_date':'2021-08-31' @@ -206,29 +206,29 @@ def check_for_all_filters(self): self.assertEqual(expected_data,report[1]) def create_company(): - doc = frappe.db.exists('Company','__Test Company') + doc = frappe.db.exists('Company','Best Test') if not doc: doc = frappe.new_doc('Company') - doc.company_name = '__Test Company' + doc.company_name = 'Best Test' doc.default_currency = "INR" doc.insert() def create_customer(): - doc = frappe.db.exists("Customer","_Test Customer") + doc = frappe.db.exists("Customer","_Test NC") if not doc: doc = frappe.new_doc("Customer") - doc.customer_name = '_Test Customer' + doc.customer_name = '_Test NC' doc.insert() def create_opportunity(): - doc = frappe.db.exists({"doctype":"Opportunity","party_name":"_Test Customer"}) + doc = frappe.db.exists({"doctype":"Opportunity","party_name":"_Test NC"}) if not doc: doc = frappe.new_doc("Opportunity") doc.opportunity_from = "Customer" - customer_name = frappe.db.get_value("Customer",{"customer_name":'_Test Customer'},['customer_name']) + customer_name = frappe.db.get_value("Customer",{"customer_name":'_Test NC'},['customer_name']) doc.party_name = customer_name doc.opportunity_amount = 150000 doc.source = "Cold Calling" doc.expected_closing = "2021-08-31" - doc.company = "__Test Company" + doc.company = 'Best Test' doc.insert() \ No newline at end of file From d31c7786fcd27dc8d0a6964ab776e13b0fdb904a Mon Sep 17 00:00:00 2001 From: Mohammed Yusuf Shaikh Date: Fri, 27 Aug 2021 13:06:53 +0530 Subject: [PATCH 18/28] fix: clear data before running test --- .../test_opportunity_summary_by_sales_stage.py | 2 ++ .../sales_pipeline_analytics/test_sales_pipeline_analytics.py | 1 + 2 files changed, 3 insertions(+) diff --git a/erpnext/crm/report/opportunity_summary_by_sales_stage/test_opportunity_summary_by_sales_stage.py b/erpnext/crm/report/opportunity_summary_by_sales_stage/test_opportunity_summary_by_sales_stage.py index dbe652ce65e2..b42ebfbec3ef 100644 --- a/erpnext/crm/report/opportunity_summary_by_sales_stage/test_opportunity_summary_by_sales_stage.py +++ b/erpnext/crm/report/opportunity_summary_by_sales_stage/test_opportunity_summary_by_sales_stage.py @@ -1,4 +1,5 @@ import unittest +import frappe from erpnext.crm.report.opportunity_summary_by_sales_stage.opportunity_summary_by_sales_stage import execute from erpnext.crm.report.sales_pipeline_analytics.test_sales_pipeline_analytics import create_company,create_customer,create_opportunity @@ -6,6 +7,7 @@ class TestOpportunitySummaryBySalesStage(unittest.TestCase): @classmethod def setUpClass(self): + frappe.db.delete("Opportunity") create_company() create_customer() create_opportunity() diff --git a/erpnext/crm/report/sales_pipeline_analytics/test_sales_pipeline_analytics.py b/erpnext/crm/report/sales_pipeline_analytics/test_sales_pipeline_analytics.py index 02285dfd8ab0..128e45eb752e 100644 --- a/erpnext/crm/report/sales_pipeline_analytics/test_sales_pipeline_analytics.py +++ b/erpnext/crm/report/sales_pipeline_analytics/test_sales_pipeline_analytics.py @@ -6,6 +6,7 @@ class TestSalesPipelineAnalytics(unittest.TestCase): @classmethod def setUpClass(self): + frappe.db.delete("Opportunity") create_company() create_customer() create_opportunity() From 0f25145ea18b0e3c39729ad5d57b61a45213df62 Mon Sep 17 00:00:00 2001 From: Mohammed Yusuf Shaikh Date: Fri, 27 Aug 2021 20:09:31 +0530 Subject: [PATCH 19/28] fix: test case fixed --- .../sales_pipeline_analytics/test_sales_pipeline_analytics.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/crm/report/sales_pipeline_analytics/test_sales_pipeline_analytics.py b/erpnext/crm/report/sales_pipeline_analytics/test_sales_pipeline_analytics.py index 128e45eb752e..fe0f53e76603 100644 --- a/erpnext/crm/report/sales_pipeline_analytics/test_sales_pipeline_analytics.py +++ b/erpnext/crm/report/sales_pipeline_analytics/test_sales_pipeline_analytics.py @@ -230,6 +230,7 @@ def create_opportunity(): doc.party_name = customer_name doc.opportunity_amount = 150000 doc.source = "Cold Calling" + doc.currency = "INR" doc.expected_closing = "2021-08-31" doc.company = 'Best Test' doc.insert() \ No newline at end of file From 0017bc5efc58f806d207230c022fc9a4fc3a3d54 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Tue, 31 Aug 2021 18:20:30 +0530 Subject: [PATCH 20/28] refactor: code formatting - smaller functions - variable and function naming --- .../opportunity_summary_by_sales_stage.js | 15 +- .../opportunity_summary_by_sales_stage.py | 134 +++++++----- .../sales_pipeline_analytics.py | 201 +++++++++--------- 3 files changed, 190 insertions(+), 160 deletions(-) diff --git a/erpnext/crm/report/opportunity_summary_by_sales_stage/opportunity_summary_by_sales_stage.js b/erpnext/crm/report/opportunity_summary_by_sales_stage/opportunity_summary_by_sales_stage.js index d7e758e566cd..116db2f5a27b 100644 --- a/erpnext/crm/report/opportunity_summary_by_sales_stage/opportunity_summary_by_sales_stage.js +++ b/erpnext/crm/report/opportunity_summary_by_sales_stage/opportunity_summary_by_sales_stage.js @@ -27,18 +27,19 @@ frappe.query_reports["Opportunity Summary by Sales Stage"] = { { fieldname: "to_date", label: __("To Date"), - fieldtype: "Date", + fieldtype: "Date", }, { fieldname: "status", label: __("Status"), fieldtype: "MultiSelectList", - get_data: function(){ - return [{value:"Open",description:"Status"}, - {value:"Converted",description:"Status"}, - {value:"Quotation",description:"Status"}, - {value:"Replied",description:"Status"} - ] + get_data: function() { + return [ + {value: "Open", description: "Status"}, + {value: "Converted", description: "Status"}, + {value: "Quotation", description: "Status"}, + {value: "Replied", description: "Status"} + ] } }, { diff --git a/erpnext/crm/report/opportunity_summary_by_sales_stage/opportunity_summary_by_sales_stage.py b/erpnext/crm/report/opportunity_summary_by_sales_stage/opportunity_summary_by_sales_stage.py index aa2b807b65a0..04bfb2a8601f 100644 --- a/erpnext/crm/report/opportunity_summary_by_sales_stage/opportunity_summary_by_sales_stage.py +++ b/erpnext/crm/report/opportunity_summary_by_sales_stage/opportunity_summary_by_sales_stage.py @@ -14,7 +14,6 @@ def execute(filters=None): return OpportunitySummaryBySalesStage(filters).run() class OpportunitySummaryBySalesStage(object): - def __init__(self,filters=None): self.filters = frappe._dict(filters or {}) @@ -27,90 +26,106 @@ def run(self): def get_columns(self): self.columns = [] - if self.filters.get('based_on') == "Opportunity Owner": + if self.filters.get('based_on') == 'Opportunity Owner': self.columns.append({ 'label': _('Opportunity Owner'), 'fieldname': 'opportunity_owner', - 'width':200 + 'width': 200 }) - if self.filters.get('based_on') == "Source": + + if self.filters.get('based_on') == 'Source': self.columns.append({ 'label': _('Source'), 'fieldname': 'source', 'fieldtype': 'Link', - 'options':'Lead Source', + 'options': 'Lead Source', 'width': 200 }) - if self.filters.get('based_on') == "Opportunity Type": + + if self.filters.get('based_on') == 'Opportunity Type': self.columns.append({ 'label': _('Opportunity Type'), 'fieldname': 'opportunity_type', 'width': 200 }) - - self.sales_stage_list = frappe.db.get_list("Sales Stage",pluck="name") + + self.set_sales_stage_columns() + + def set_sales_stage_columns(self): + self.sales_stage_list = frappe.db.get_list('Sales Stage', pluck='name') + for sales_stage in self.sales_stage_list: if self.filters.get('data_based_on') == 'Number': self.columns.append({ 'label': _(sales_stage), 'fieldname': sales_stage, 'fieldtype': 'Int', - 'width':150 + 'width': 150 }) - if self.filters.get('data_based_on') == 'Amount': + + elif self.filters.get('data_based_on') == 'Amount': self.columns.append({ 'label': _(sales_stage), 'fieldname': sales_stage, 'fieldtype': 'Currency', - 'width':150 + 'width': 150 }) - return self.columns - def get_data(self): self.data = [] based_on = { - 'Opportunity Owner': "_assign", - 'Source': "source", - 'Opportunity Type': "opportunity_type" + 'Opportunity Owner': '_assign', + 'Source': 'source', + 'Opportunity Type': 'opportunity_type' }[self.filters.get('based_on')] data_based_on = { - 'Number': "count(name) as count", - 'Amount': "opportunity_amount as amount", + 'Number': 'count(name) as count', + 'Amount': 'opportunity_amount as amount', }[self.filters.get('data_based_on')] - self.get_data_query(based_on,data_based_on) + self.get_data_query(based_on, data_based_on) self.get_rows() - - def get_data_query(self,based_on,data_based_on): + def get_data_query(self, based_on, data_based_on): if self.filters.get('data_based_on') == 'Number': - group_by = "{},{}".format('sales_stage',based_on) - self.query_result = frappe.db.get_list("Opportunity",filters=self.get_conditions(),fields=['sales_stage',data_based_on,based_on],group_by=group_by) - - if self.filters.get('data_based_on') == 'Amount': - self.query_result = frappe.db.get_list("Opportunity",filters=self.get_conditions(),fields=['sales_stage',based_on,data_based_on,'currency']) + group_by = '{},{}'.format('sales_stage', based_on) + self.query_result = frappe.db.get_list('Opportunity', + filters=self.get_conditions(), + fields=['sales_stage', data_based_on, based_on], + group_by=group_by + ) + + elif self.filters.get('data_based_on') == 'Amount': + self.query_result = frappe.db.get_list('Opportunity', + filters=self.get_conditions(), + fields=['sales_stage', based_on, data_based_on, 'currency'] + ) self.convert_to_base_currency() + dataframe = pandas.DataFrame.from_records(self.query_result) - dataframe.replace(to_replace=[None], value="Not Assigned", inplace=True) - result = dataframe.groupby(['sales_stage',based_on],as_index=False)['amount'].sum() + dataframe.replace(to_replace=[None], value='Not Assigned', inplace=True) + result = dataframe.groupby(['sales_stage', based_on], as_index=False)['amount'].sum() + self.grouped_data = [] for i in range(len(result['amount'])): - self.grouped_data.append({'sales_stage': result['sales_stage'][i], based_on : result[based_on][i], 'amount': result['amount'][i]}) - + self.grouped_data.append({ + 'sales_stage': result['sales_stage'][i], + based_on : result[based_on][i], + 'amount': result['amount'][i] + }) + self.query_result = self.grouped_data def get_rows(self): self.data = [] self.get_formatted_data() - - for based_on,data in iteritems(self.formatted_data): + for based_on,data in iteritems(self.formatted_data): row_based_on={ 'Opportunity Owner': 'opportunity_owner', 'Source': 'source', @@ -122,7 +137,7 @@ def get_rows(self): for d in self.query_result: sales_stage = d.get('sales_stage') row[sales_stage] = data.get(sales_stage) - + self.data.append(row) def get_formatted_data(self): @@ -140,51 +155,57 @@ def get_formatted_data(self): 'Opportunity Type': 'opportunity_type' }[self.filters.get('based_on')] - if self.filters.get("based_on") == "Opportunity Owner": - - if d.get(based_on) == '[]' or d.get(based_on) is None or d.get(based_on) == "Not Assigned": - temp = ["Not Assigned"] + if self.filters.get('based_on') == 'Opportunity Owner': + if d.get(based_on) == '[]' or d.get(based_on) is None or d.get(based_on) == 'Not Assigned': + assignments = ['Not Assigned'] else: - temp = json.loads(d.get(based_on)) + assignments = json.loads(d.get(based_on)) sales_stage = d.get('sales_stage') count = d.get(data_based_on) - if temp: - if len(temp) > 1: - for value in temp: - self.insert_formatted_data(value,sales_stage,count) + + if assignments: + if len(assignments) > 1: + for assigned_to in assignments: + self.set_formatted_data_based_on_sales_stage(assigned_to, sales_stage, count) else: - value = temp[0] - self.insert_formatted_data(value,sales_stage,count) + assigned_to = assignments[0] + self.set_formatted_data_based_on_sales_stage(assigned_to, sales_stage, count) else: value = d.get(based_on) sales_stage = d.get('sales_stage') count = d.get(data_based_on) - self.insert_formatted_data(value,sales_stage,count) + self.set_formatted_data_based_on_sales_stage(value, sales_stage, count) + + def set_formatted_data_based_on_sales_stage(self, based_on, sales_stage, count): + self.formatted_data.setdefault(based_on, frappe._dict()).setdefault(sales_stage, 0) + self.formatted_data[based_on][sales_stage] += count - def insert_formatted_data(self,based_on,sales_stage,data): - self.formatted_data.setdefault(based_on,frappe._dict()).setdefault(sales_stage,0) - self.formatted_data[based_on][sales_stage] += data - def get_conditions(self): filters = [] + if self.filters.get('company'): filters.append({'company': self.filters.get('company')}) + if self.filters.get('opportunity_type'): filters.append({'opportunity_type': self.filters.get('opportunity_type')}) + if self.filters.get('opportunity_source'): filters.append({'source': self.filters.get('opportunity_source')}) + if self.filters.get('status'): - filters.append({'status':("in",self.filters.get('status'))}) + filters.append({'status': ('in',self.filters.get('status'))}) + if self.filters.get('from_date') and self.filters.get('to_date'): - filters.append(['transaction_date', 'between' , [self.filters.get('from_date'),self.filters.get('to_date')]]) + filters.append(['transaction_date', 'between', [self.filters.get('from_date'), self.filters.get('to_date')]]) return filters def get_chart_data(self): labels = [] datasets = [] - values = [0,0,0,0,0,0,0,0] + values = [0] * 8 + for sales_stage in self.sales_stage_list: labels.append(sales_stage) @@ -197,14 +218,15 @@ def get_chart_data(self): for count in range(len(values)): if data['sales_stage'] == labels[count]: values[count] = values[count] + data[options] - datasets.append({"name":options,'values':values}) + + datasets.append({'name':options, 'values':values}) self.chart = { - "data":{ + 'data':{ 'labels': labels, 'datasets': datasets }, - "type":"line" + 'type':'line' } def currency_conversion(self,from_currency,to_currency): @@ -220,7 +242,7 @@ def currency_conversion(self,from_currency,to_currency): def get_default_currency(self): company = self.filters.get('company') - return frappe.db.get_value('Company',company,['default_currency']) + return frappe.db.get_value('Company', company, 'default_currency') def convert_to_base_currency(self): default_currency = self.get_default_currency() diff --git a/erpnext/crm/report/sales_pipeline_analytics/sales_pipeline_analytics.py b/erpnext/crm/report/sales_pipeline_analytics/sales_pipeline_analytics.py index 2b95a1c87204..61d0a46ea696 100644 --- a/erpnext/crm/report/sales_pipeline_analytics/sales_pipeline_analytics.py +++ b/erpnext/crm/report/sales_pipeline_analytics/sales_pipeline_analytics.py @@ -14,7 +14,6 @@ def execute(filters=None): return SalesPipelineAnalytics(filters).run() class SalesPipelineAnalytics(object): - def __init__(self, filters=None): self.filters = frappe._dict(filters or {}) @@ -28,52 +27,51 @@ def run(self): def get_columns(self): self.columns = [] + self.set_range_columns() + self.set_pipeline_based_on_column() + + def set_range_columns(self): based_on = { - 'Number' : "Int", - 'Amount' : "Currency" + 'Number': 'Int', + 'Amount': 'Currency' }[self.filters.get('based_on')] - if self.filters.get('range') == "Monthly": + if self.filters.get('range') == 'Monthly': month_list = self.get_month_list() - for month in month_list: - self.columns.append( - { - 'fieldname': month, - 'fieldtype': based_on, - 'label': month, - 'width': 200 - } - ) - - elif self.filters.get('range') == "Quaterly": - for quarter in range(1,5): - self.columns.append( - { - 'fieldname': f"Q{quarter}", - 'fieldtype': based_on, - 'label': f"Q{quarter}", - 'width': 200 - } - ) - - if self.filters.get("pipeline_by") == "Owner": - self.columns.insert(0,{ - 'fieldname': "opportunity_owner", - 'label': "Opportunity Owner", - 'width':200 + for month in month_list: + self.columns.append({ + 'fieldname': month, + 'fieldtype': based_on, + 'label': month, + 'width': 200 + }) + + elif self.filters.get('range') == 'Quaterly': + for quarter in range(1, 5): + self.columns.append({ + 'fieldname': f'Q{quarter}', + 'fieldtype': based_on, + 'label': f'Q{quarter}', + 'width': 200 + }) + + def set_pipeline_based_on_column(self): + if self.filters.get('pipeline_by') == 'Owner': + self.columns.insert(0, { + 'fieldname': 'opportunity_owner', + 'label': 'Opportunity Owner', + 'width': 200 }) - elif self.filters.get("pipeline_by") == "Sales Stage": - self.columns.insert(0,{ - 'fieldname':"sales_stage", - 'label':"Sales Stage", - 'width':200 - }) - return self.columns + elif self.filters.get('pipeline_by') == 'Sales Stage': + self.columns.insert(0, { + 'fieldname': 'sales_stage', + 'label': 'Sales Stage', + 'width': 200 + }) def get_data(self): - select_1 ={ 'Owner': '_assign as opportunity_owner', 'Sales Stage': 'sales_stage' @@ -81,7 +79,7 @@ def get_data(self): select_2 ={ 'Number': 'count(name) as count', - 'Amount': 'opportunity_amount as amount' + 'Amount': 'opportunity_amount as amount' }[self.filters.get('based_on')] group_by_1 = { @@ -111,15 +109,25 @@ def get_data(self): }[self.filters.get('range')] if self.filters.get('based_on') == 'Number': - self.query_result = frappe.db.get_list('Opportunity',filters=self.get_conditions(),fields=[select_1,select_2,duration] - ,group_by="{},{}".format(group_by_1,group_by_2),order_by=group_by_2) + self.query_result = frappe.db.get_list('Opportunity', + filters=self.get_conditions(), + fields=[select_1, select_2, duration], + group_by='{},{}'.format(group_by_1, group_by_2), + order_by=group_by_2 + ) if self.filters.get('based_on') == 'Amount': - self.query_result = frappe.db.get_list('Opportunity',filters=self.get_conditions(),fields=[select_1,select_2,duration,'currency']) + self.query_result = frappe.db.get_list('Opportunity', + filters=self.get_conditions(), + fields=[select_1, select_2, duration, 'currency'] + ) + self.convert_to_base_currency() + dataframe = pandas.DataFrame.from_records(self.query_result) - dataframe.replace(to_replace= [None],value="Not Assigned",inplace=True) - result = dataframe.groupby([pipeline_by,period_by],as_index=False)['amount'].sum() + dataframe.replace(to_replace=[None], value='Not Assigned', inplace=True) + result = dataframe.groupby([pipeline_by, period_by], as_index=False)['amount'].sum() + self.grouped_data = [] for i in range(len(result['amount'])): @@ -127,43 +135,45 @@ def get_data(self): self.query_result = self.grouped_data self.get_periodic_data() - self.append_data(pipeline_by,period_by) - - return self.data + self.append_data(pipeline_by, period_by) def get_conditions(self): - conditions = [] - if self.filters.get("opportunity_source"): - conditions.append({"source": self.filters.get('opportunity_source')}) - if self.filters.get("opportunity_type"): + + if self.filters.get('opportunity_source'): + conditions.append({'source': self.filters.get('opportunity_source')}) + + if self.filters.get('opportunity_type'): conditions.append({'opportunity_type': self.filters.get('opportunity_type')}) - if self.filters.get("status"): + + if self.filters.get('status'): conditions.append({'status': self.filters.get('status')}) - if self.filters.get("company"): + + if self.filters.get('company'): conditions.append({'company': self.filters.get('company')}) - if self.filters.get("from_date") and self.filters.get("to_date"): - conditions.append(['expected_closing','between',[self.filters.get("from_date"),self.filters.get("to_date")]]) + + if self.filters.get('from_date') and self.filters.get('to_date'): + conditions.append(['expected_closing', 'between', + [self.filters.get('from_date'), self.filters.get('to_date')]]) return conditions def get_chart_data(self): - - labels = values = [] + labels = [] datasets = [] - self.append_to_dataset(values,datasets) - - for c in self.columns: - if c['fieldname'] != "opportunity_owner" and c['fieldname'] != "sales_stage": - labels.append(c['fieldname']) + self.append_to_dataset(datasets) + + for column in self.columns: + if column['fieldname'] != 'opportunity_owner' and column['fieldname'] != 'sales_stage': + labels.append(column['fieldname']) self.chart = { - "data":{ + 'data':{ 'labels': labels, 'datasets': datasets }, - "type":"line" + 'type':'line' } return self.chart @@ -181,38 +191,37 @@ def get_periodic_data(self): 'Sales Stage': 'sales_stage' }[self.filters.get('pipeline_by')] - range ={ + frequency = { 'Monthly': 'month', 'Quaterly': 'quarter' }[self.filters.get('range')] for info in self.query_result: if self.filters.get('range') == 'Monthly': - period = info.get(range) + period = info.get(frequency) if self.filters.get('range') == 'Quaterly': - period = "Q" + str(info.get('quarter')) + period = f'Q{info.get("quarter")}' value = info.get(pipeline_by) count = info.get(based_on) if self.filters.get('pipeline_by') == 'Owner': - if value == "Not Assigned" or value == '[]' or value is None: - temp = ["Not Assigned"] + if value == 'Not Assigned' or value == '[]' or value is None: + temp = ['Not Assigned'] else: temp = json.loads(value) self.check_for_assigned_to(period,value,count,temp,info) else: - self.insert_formatted_data(period,value,count,None) + self.set_formatted_data(period,value,count,None) - def insert_formatted_data(self,period,value,val,temp): - + def set_formatted_data(self, period, value, val, temp): if temp: if len(temp) > 1: - if self.filters.get("assigned_to"): + if self.filters.get('assigned_to'): for user in temp: - if self.filters.get("assigned_to") == user: + if self.filters.get('assigned_to') == user: value = user self.periodic_data.setdefault(value,frappe._dict()).setdefault(period,0) self.periodic_data[value][period] += val @@ -232,13 +241,12 @@ def insert_formatted_data(self,period,value,val,temp): self.periodic_data[value][period] += val def check_for_assigned_to(self,period,value,count,temp,info): - - if self.filters.get("assigned_to"): + if self.filters.get('assigned_to'): for data in json.loads(info.get('opportunity_owner')): - if data == self.filters.get("assigned_to"): - self.insert_formatted_data(period,data,count,temp) + if data == self.filters.get('assigned_to'): + self.set_formatted_data(period, data, count, temp) else: - self.insert_formatted_data(period,value,count,temp) + self.set_formatted_data(period, value, count, temp) def get_month_list(self): month_list= [] @@ -246,13 +254,12 @@ def get_month_list(self): month_number = date.today().month for month in range(month_number,13): - month_list.append(current_date.strftime("%B")) + month_list.append(current_date.strftime('%B')) current_date = current_date + relativedelta(months=1) - - return month_list - def append_to_dataset(self,values,datasets): + return month_list + def append_to_dataset(self, datasets): range_by = { 'Monthly': 'month', 'Quaterly': 'quarter' @@ -263,36 +270,36 @@ def append_to_dataset(self,values,datasets): 'Number': 'count' }[self.filters.get('based_on')] - if self.filters.get("range") == "Quaterly": - list = [1,2,3,4] - count = [0,0,0,0] + if self.filters.get('range') == 'Quaterly': + frequency_list = [1,2,3,4] + count = [0] * 4 - if self.filters.get("range") == "Monthly": - list = self.get_month_list() - count = [0,0,0,0,0,0,0,0,0,0,0,0] + if self.filters.get('range') == 'Monthly': + frequency_list = self.get_month_list() + count = [0] * 12 for info in self.query_result: - for i in range(len(list)): - if info[range_by] == list[i]: + for i in range(len(frequency_list)): + if info[range_by] == frequency_list[i]: count[i] = count[i] + info[based_on] - values = count - datasets.append({'name': based_on,'values':values}) - def append_data(self,pipeline_by,period_by): + datasets.append({'name': based_on, 'values': count}) + + def append_data(self, pipeline_by, period_by): self.data = [] for pipeline,period_data in iteritems(self.periodic_data): row = {pipeline_by : pipeline} for info in self.query_result: if self.filters.get('range') == 'Monthly': period = info.get(period_by) + if self.filters.get('range') == 'Quaterly': - period = "Q" + str(info.get(period_by)) + period = f'Q{info.get(period_by)}' count = period_data.get(period,0.0) row[period] = count + self.data.append(row) - - return self.data def get_default_currency(self): company = self.filters.get('company') From 67beec8d53836d3b9b4d3357444343823924303f Mon Sep 17 00:00:00 2001 From: Mohammed Yusuf Shaikh Date: Wed, 1 Sep 2021 12:34:23 +0530 Subject: [PATCH 21/28] refactor: improve code formatting --- .../opportunity_summary_by_sales_stage.py | 2 + .../sales_pipeline_analytics.py | 51 +++++++++++-------- 2 files changed, 33 insertions(+), 20 deletions(-) diff --git a/erpnext/crm/report/opportunity_summary_by_sales_stage/opportunity_summary_by_sales_stage.py b/erpnext/crm/report/opportunity_summary_by_sales_stage/opportunity_summary_by_sales_stage.py index 04bfb2a8601f..790567e947ad 100644 --- a/erpnext/crm/report/opportunity_summary_by_sales_stage/opportunity_summary_by_sales_stage.py +++ b/erpnext/crm/report/opportunity_summary_by_sales_stage/opportunity_summary_by_sales_stage.py @@ -90,6 +90,7 @@ def get_data(self): self.get_rows() def get_data_query(self, based_on, data_based_on): + if self.filters.get('data_based_on') == 'Number': group_by = '{},{}'.format('sales_stage', based_on) self.query_result = frappe.db.get_list('Opportunity', @@ -122,6 +123,7 @@ def get_data_query(self, based_on, data_based_on): self.query_result = self.grouped_data def get_rows(self): + self.data = [] self.get_formatted_data() diff --git a/erpnext/crm/report/sales_pipeline_analytics/sales_pipeline_analytics.py b/erpnext/crm/report/sales_pipeline_analytics/sales_pipeline_analytics.py index 61d0a46ea696..d8403fb5e703 100644 --- a/erpnext/crm/report/sales_pipeline_analytics/sales_pipeline_analytics.py +++ b/erpnext/crm/report/sales_pipeline_analytics/sales_pipeline_analytics.py @@ -31,6 +31,7 @@ def get_columns(self): self.set_pipeline_based_on_column() def set_range_columns(self): + based_on = { 'Number': 'Int', 'Amount': 'Currency' @@ -57,6 +58,7 @@ def set_range_columns(self): }) def set_pipeline_based_on_column(self): + if self.filters.get('pipeline_by') == 'Owner': self.columns.insert(0, { 'fieldname': 'opportunity_owner', @@ -71,71 +73,80 @@ def set_pipeline_based_on_column(self): 'width': 200 }) - def get_data(self): - select_1 ={ + def get_fields(self): + + self.based_on ={ 'Owner': '_assign as opportunity_owner', 'Sales Stage': 'sales_stage' }[self.filters.get('pipeline_by')] - select_2 ={ + self.data_based_on ={ 'Number': 'count(name) as count', 'Amount': 'opportunity_amount as amount' }[self.filters.get('based_on')] - group_by_1 = { + self.group_by_based_on = { 'Owner': '_assign', 'Sales Stage': 'sales_stage' }[self.filters.get('pipeline_by')] - group_by_2 = { + self.group_by_period = { 'Monthly': 'month(expected_closing)', 'Quaterly': 'QUARTER(expected_closing)' }[self.filters.get('range')] - - pipeline_by = { + self.pipeline_by = { 'Owner': 'opportunity_owner', 'Sales Stage': 'sales_stage' }[self.filters.get('pipeline_by')] - duration = { + self.duration = { 'Monthly': 'monthname(expected_closing) as month', 'Quaterly': 'QUARTER(expected_closing) as quarter' }[self.filters.get('range')] - period_by = { + self.period_by = { 'Monthly': 'month', 'Quaterly': 'quarter' }[self.filters.get('range')] + def get_data(self): + + self.get_fields() + if self.filters.get('based_on') == 'Number': self.query_result = frappe.db.get_list('Opportunity', filters=self.get_conditions(), - fields=[select_1, select_2, duration], - group_by='{},{}'.format(group_by_1, group_by_2), - order_by=group_by_2 + fields=[self.based_on, self.data_based_on, self.duration], + group_by='{},{}'.format(self.group_by_based_on, self.group_by_period), + order_by=self.group_by_period ) if self.filters.get('based_on') == 'Amount': self.query_result = frappe.db.get_list('Opportunity', filters=self.get_conditions(), - fields=[select_1, select_2, duration, 'currency'] + fields=[self.based_on, self.data_based_on, self.duration, 'currency'] ) self.convert_to_base_currency() dataframe = pandas.DataFrame.from_records(self.query_result) dataframe.replace(to_replace=[None], value='Not Assigned', inplace=True) - result = dataframe.groupby([pipeline_by, period_by], as_index=False)['amount'].sum() + result = dataframe.groupby([self.pipeline_by, self.period_by], as_index=False)['amount'].sum() self.grouped_data = [] for i in range(len(result['amount'])): - self.grouped_data.append({pipeline_by : result[pipeline_by][i], period_by : result[period_by][i], 'amount': result['amount'][i]}) + self.grouped_data.append({ + self.pipeline_by : result[self.pipeline_by][i], + self.period_by : result[self.period_by][i], + 'amount': result['amount'][i] + }) + self.query_result = self.grouped_data self.get_periodic_data() - self.append_data(pipeline_by, period_by) + self.append_data(self.pipeline_by, self.period_by) def get_conditions(self): conditions = [] @@ -240,7 +251,7 @@ def set_formatted_data(self, period, value, val, temp): self.periodic_data.setdefault(value,frappe._dict()).setdefault(period,0) self.periodic_data[value][period] += val - def check_for_assigned_to(self,period,value,count,temp,info): + def check_for_assigned_to(self, period, value, count, temp, info): if self.filters.get('assigned_to'): for data in json.loads(info.get('opportunity_owner')): if data == self.filters.get('assigned_to'): @@ -305,15 +316,15 @@ def get_default_currency(self): company = self.filters.get('company') return frappe.db.get_value('Company',company,['default_currency']) - def get_currency_rate(self,from_currency,to_currency): + def get_currency_rate(self, from_currency, to_currency): cacheobj = frappe.cache() if cacheobj.get(from_currency): return flt(str(cacheobj.get(from_currency),'UTF-8')) else: - value = get_exchange_rate(from_currency,to_currency) - cacheobj.set(from_currency,value) + value = get_exchange_rate(from_currency, to_currency) + cacheobj.set(from_currency, value) return flt(str(cacheobj.get(from_currency),'UTF-8')) def convert_to_base_currency(self): From 86429a49f23fe1f4df35d0fbda66a227827cb1b7 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Mon, 6 Sep 2021 13:03:39 +0530 Subject: [PATCH 22/28] fix: linter issues --- .../opportunity_summary_by_sales_stage.py | 2 -- .../sales_pipeline_analytics.py | 13 +++++-------- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/erpnext/crm/report/opportunity_summary_by_sales_stage/opportunity_summary_by_sales_stage.py b/erpnext/crm/report/opportunity_summary_by_sales_stage/opportunity_summary_by_sales_stage.py index 790567e947ad..04bfb2a8601f 100644 --- a/erpnext/crm/report/opportunity_summary_by_sales_stage/opportunity_summary_by_sales_stage.py +++ b/erpnext/crm/report/opportunity_summary_by_sales_stage/opportunity_summary_by_sales_stage.py @@ -90,7 +90,6 @@ def get_data(self): self.get_rows() def get_data_query(self, based_on, data_based_on): - if self.filters.get('data_based_on') == 'Number': group_by = '{},{}'.format('sales_stage', based_on) self.query_result = frappe.db.get_list('Opportunity', @@ -123,7 +122,6 @@ def get_data_query(self, based_on, data_based_on): self.query_result = self.grouped_data def get_rows(self): - self.data = [] self.get_formatted_data() diff --git a/erpnext/crm/report/sales_pipeline_analytics/sales_pipeline_analytics.py b/erpnext/crm/report/sales_pipeline_analytics/sales_pipeline_analytics.py index d8403fb5e703..729a82d686cb 100644 --- a/erpnext/crm/report/sales_pipeline_analytics/sales_pipeline_analytics.py +++ b/erpnext/crm/report/sales_pipeline_analytics/sales_pipeline_analytics.py @@ -7,6 +7,7 @@ import pandas from dateutil.relativedelta import relativedelta from six import iteritems +from frappe import _ from frappe.utils import flt from erpnext.setup.utils import get_exchange_rate @@ -31,7 +32,6 @@ def get_columns(self): self.set_pipeline_based_on_column() def set_range_columns(self): - based_on = { 'Number': 'Int', 'Amount': 'Currency' @@ -58,23 +58,21 @@ def set_range_columns(self): }) def set_pipeline_based_on_column(self): - if self.filters.get('pipeline_by') == 'Owner': self.columns.insert(0, { 'fieldname': 'opportunity_owner', - 'label': 'Opportunity Owner', + 'label': _('Opportunity Owner'), 'width': 200 }) elif self.filters.get('pipeline_by') == 'Sales Stage': self.columns.insert(0, { 'fieldname': 'sales_stage', - 'label': 'Sales Stage', + 'label': _('Sales Stage'), 'width': 200 }) def get_fields(self): - self.based_on ={ 'Owner': '_assign as opportunity_owner', 'Sales Stage': 'sales_stage' @@ -111,7 +109,6 @@ def get_fields(self): }[self.filters.get('range')] def get_data(self): - self.get_fields() if self.filters.get('based_on') == 'Number': @@ -138,8 +135,8 @@ def get_data(self): for i in range(len(result['amount'])): self.grouped_data.append({ - self.pipeline_by : result[self.pipeline_by][i], - self.period_by : result[self.period_by][i], + self.pipeline_by : result[self.pipeline_by][i], + self.period_by : result[self.period_by][i], 'amount': result['amount'][i] }) From cfe06ffd0b6952cb7602f077ac437c0b444da33c Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Mon, 6 Sep 2021 13:16:27 +0530 Subject: [PATCH 23/28] fix: linter issues --- .../opportunity_summary_by_sales_stage.py | 5 +++-- ...test_opportunity_summary_by_sales_stage.py | 21 ++++++++++++------- .../sales_pipeline_analytics.js | 18 +++++++--------- .../sales_pipeline_analytics.py | 7 +++++-- .../test_sales_pipeline_analytics.py | 8 ++++--- 5 files changed, 34 insertions(+), 25 deletions(-) diff --git a/erpnext/crm/report/opportunity_summary_by_sales_stage/opportunity_summary_by_sales_stage.py b/erpnext/crm/report/opportunity_summary_by_sales_stage/opportunity_summary_by_sales_stage.py index 04bfb2a8601f..4cff13f2321f 100644 --- a/erpnext/crm/report/opportunity_summary_by_sales_stage/opportunity_summary_by_sales_stage.py +++ b/erpnext/crm/report/opportunity_summary_by_sales_stage/opportunity_summary_by_sales_stage.py @@ -1,12 +1,13 @@ # Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt +import json import frappe import pandas from frappe import _ -from six import iteritems -import json from frappe.utils import flt +from six import iteritems + from erpnext.setup.utils import get_exchange_rate diff --git a/erpnext/crm/report/opportunity_summary_by_sales_stage/test_opportunity_summary_by_sales_stage.py b/erpnext/crm/report/opportunity_summary_by_sales_stage/test_opportunity_summary_by_sales_stage.py index b42ebfbec3ef..4ec30f153076 100644 --- a/erpnext/crm/report/opportunity_summary_by_sales_stage/test_opportunity_summary_by_sales_stage.py +++ b/erpnext/crm/report/opportunity_summary_by_sales_stage/test_opportunity_summary_by_sales_stage.py @@ -1,10 +1,15 @@ import unittest import frappe -from erpnext.crm.report.opportunity_summary_by_sales_stage.opportunity_summary_by_sales_stage import execute -from erpnext.crm.report.sales_pipeline_analytics.test_sales_pipeline_analytics import create_company,create_customer,create_opportunity +from erpnext.crm.report.opportunity_summary_by_sales_stage.opportunity_summary_by_sales_stage import ( + execute +) +from erpnext.crm.report.sales_pipeline_analytics.test_sales_pipeline_analytics import ( + create_company, + create_customer, + create_opportunity +) class TestOpportunitySummaryBySalesStage(unittest.TestCase): - @classmethod def setUpClass(self): frappe.db.delete("Opportunity") @@ -32,7 +37,7 @@ def check_for_opportunity_owner(self): 'Prospecting': 1 }] - self.assertEqual(expected_data,report[1]) + self.assertEqual(expected_data, report[1]) def check_for_source(self): filters = { @@ -48,7 +53,7 @@ def check_for_source(self): 'Prospecting': 1 }] - self.assertEqual(expected_data,report[1]) + self.assertEqual(expected_data, report[1]) def check_for_opportunity_type(self): filters = { @@ -64,8 +69,8 @@ def check_for_opportunity_type(self): 'Prospecting': 1 }] - self.assertEqual(expected_data,report[1]) - + self.assertEqual(expected_data, report[1]) + def check_all_filters(self): filters = { 'based_on': "Opportunity Type", @@ -83,4 +88,4 @@ def check_all_filters(self): 'Prospecting': 1 }] - self.assertEqual(expected_data,report[1]) \ No newline at end of file + self.assertEqual(expected_data, report[1]) \ No newline at end of file diff --git a/erpnext/crm/report/sales_pipeline_analytics/sales_pipeline_analytics.js b/erpnext/crm/report/sales_pipeline_analytics/sales_pipeline_analytics.js index fa127f66d5a3..3454fe686111 100644 --- a/erpnext/crm/report/sales_pipeline_analytics/sales_pipeline_analytics.js +++ b/erpnext/crm/report/sales_pipeline_analytics/sales_pipeline_analytics.js @@ -12,15 +12,14 @@ frappe.query_reports["Sales Pipeline Analytics"] = { default: "Owner" }, { - fieldname:"from_date", + fieldname: "from_date", label: __("From Date"), - fieldtype: "Date", - + fieldtype: "Date" }, { - fieldname:"to_date", + fieldname: "to_date", label: __("To Date"), - fieldtype: "Date", + fieldtype: "Date" }, { fieldname: "range", @@ -33,13 +32,13 @@ frappe.query_reports["Sales Pipeline Analytics"] = { fieldname: "assigned_to", label: __("Assigned To"), fieldtype: "Link", - options: "User" + options: "User" }, { fieldname: "status", label: __("Status"), fieldtype: "Select", - options: "Open\nQuotation\nConverted\nReplied", + options: "Open\nQuotation\nConverted\nReplied" }, { fieldname: "based_on", @@ -59,14 +58,13 @@ frappe.query_reports["Sales Pipeline Analytics"] = { fieldname: "opportunity_source", label: __("Opportunity Source"), fieldtype: "Link", - options: "Lead Source" + options: "Lead Source" }, { fieldname: "opportunity_type", label: __("Opportunity Type"), fieldtype: "Link", - options: "Opportunity Type", + options: "Opportunity Type" }, - ] }; diff --git a/erpnext/crm/report/sales_pipeline_analytics/sales_pipeline_analytics.py b/erpnext/crm/report/sales_pipeline_analytics/sales_pipeline_analytics.py index 729a82d686cb..6394cefce83d 100644 --- a/erpnext/crm/report/sales_pipeline_analytics/sales_pipeline_analytics.py +++ b/erpnext/crm/report/sales_pipeline_analytics/sales_pipeline_analytics.py @@ -2,15 +2,18 @@ # For license information, please see license.txt import json -import frappe from datetime import date + +import frappe import pandas from dateutil.relativedelta import relativedelta -from six import iteritems from frappe import _ from frappe.utils import flt +from six import iteritems + from erpnext.setup.utils import get_exchange_rate + def execute(filters=None): return SalesPipelineAnalytics(filters).run() diff --git a/erpnext/crm/report/sales_pipeline_analytics/test_sales_pipeline_analytics.py b/erpnext/crm/report/sales_pipeline_analytics/test_sales_pipeline_analytics.py index fe0f53e76603..255331c199ed 100644 --- a/erpnext/crm/report/sales_pipeline_analytics/test_sales_pipeline_analytics.py +++ b/erpnext/crm/report/sales_pipeline_analytics/test_sales_pipeline_analytics.py @@ -1,23 +1,25 @@ import unittest + import frappe + from erpnext.crm.report.sales_pipeline_analytics.sales_pipeline_analytics import execute + class TestSalesPipelineAnalytics(unittest.TestCase): - @classmethod def setUpClass(self): frappe.db.delete("Opportunity") create_company() create_customer() create_opportunity() - + def test_sales_pipeline_analytics(self): self.check_for_monthly_and_number() self.check_for_monthly_and_amount() self.check_for_quarterly_and_number() self.check_for_quarterly_and_amount() self.check_for_all_filters() - + def check_for_monthly_and_number(self): filters = { 'pipeline_by':"Owner", From 8234f7d44b7d504d671c7af3c7d32b377663b5f1 Mon Sep 17 00:00:00 2001 From: Mohammed Yusuf Shaikh Date: Mon, 6 Sep 2021 17:45:43 +0530 Subject: [PATCH 24/28] fix: change indentation to tabs --- ...test_opportunity_summary_by_sales_stage.py | 166 +++---- .../test_sales_pipeline_analytics.py | 448 +++++++++--------- 2 files changed, 307 insertions(+), 307 deletions(-) diff --git a/erpnext/crm/report/opportunity_summary_by_sales_stage/test_opportunity_summary_by_sales_stage.py b/erpnext/crm/report/opportunity_summary_by_sales_stage/test_opportunity_summary_by_sales_stage.py index 4ec30f153076..1102a30cf82f 100644 --- a/erpnext/crm/report/opportunity_summary_by_sales_stage/test_opportunity_summary_by_sales_stage.py +++ b/erpnext/crm/report/opportunity_summary_by_sales_stage/test_opportunity_summary_by_sales_stage.py @@ -1,91 +1,91 @@ import unittest import frappe from erpnext.crm.report.opportunity_summary_by_sales_stage.opportunity_summary_by_sales_stage import ( - execute + execute ) from erpnext.crm.report.sales_pipeline_analytics.test_sales_pipeline_analytics import ( - create_company, - create_customer, - create_opportunity + create_company, + create_customer, + create_opportunity ) class TestOpportunitySummaryBySalesStage(unittest.TestCase): - @classmethod - def setUpClass(self): - frappe.db.delete("Opportunity") - create_company() - create_customer() - create_opportunity() - - def test_opportunity_summary_by_sales_stage(self): - self.check_for_opportunity_owner() - self.check_for_source() - self.check_for_opportunity_type() - self.check_all_filters() - - def check_for_opportunity_owner(self): - filters = { - 'based_on': "Opportunity Owner", - 'data_based_on': "Number", - 'company': "Best Test" - } - - report = execute(filters) - - expected_data = [{ - 'opportunity_owner': "Not Assigned", - 'Prospecting': 1 - }] - - self.assertEqual(expected_data, report[1]) - - def check_for_source(self): - filters = { - 'based_on': "Source", - 'data_based_on': "Number", - 'company': "Best Test" - } - - report = execute(filters) - - expected_data = [{ - 'source': 'Cold Calling', - 'Prospecting': 1 - }] - - self.assertEqual(expected_data, report[1]) - - def check_for_opportunity_type(self): - filters = { - 'based_on': "Opportunity Type", - 'data_based_on': "Number", - 'company': "Best Test" - } - - report = execute(filters) - - expected_data = [{ - 'opportunity_type': 'Sales', - 'Prospecting': 1 - }] - - self.assertEqual(expected_data, report[1]) - - def check_all_filters(self): - filters = { - 'based_on': "Opportunity Type", - 'data_based_on': "Number", - 'company': "Best Test", - 'opportunity_source': "Cold Calling", - 'opportunity_type': "Sales", - 'status': ["Open"] - } - - report = execute(filters) - - expected_data = [{ - 'opportunity_type': 'Sales', - 'Prospecting': 1 - }] - - self.assertEqual(expected_data, report[1]) \ No newline at end of file + @classmethod + def setUpClass(self): + frappe.db.delete("Opportunity") + create_company() + create_customer() + create_opportunity() + + def test_opportunity_summary_by_sales_stage(self): + self.check_for_opportunity_owner() + self.check_for_source() + self.check_for_opportunity_type() + self.check_all_filters() + + def check_for_opportunity_owner(self): + filters = { + 'based_on': "Opportunity Owner", + 'data_based_on': "Number", + 'company': "Best Test" + } + + report = execute(filters) + + expected_data = [{ + 'opportunity_owner': "Not Assigned", + 'Prospecting': 1 + }] + + self.assertEqual(expected_data, report[1]) + + def check_for_source(self): + filters = { + 'based_on': "Source", + 'data_based_on': "Number", + 'company': "Best Test" + } + + report = execute(filters) + + expected_data = [{ + 'source': 'Cold Calling', + 'Prospecting': 1 + }] + + self.assertEqual(expected_data, report[1]) + + def check_for_opportunity_type(self): + filters = { + 'based_on': "Opportunity Type", + 'data_based_on': "Number", + 'company': "Best Test" + } + + report = execute(filters) + + expected_data = [{ + 'opportunity_type': 'Sales', + 'Prospecting': 1 + }] + + self.assertEqual(expected_data, report[1]) + + def check_all_filters(self): + filters = { + 'based_on': "Opportunity Type", + 'data_based_on': "Number", + 'company': "Best Test", + 'opportunity_source': "Cold Calling", + 'opportunity_type': "Sales", + 'status': ["Open"] + } + + report = execute(filters) + + expected_data = [{ + 'opportunity_type': 'Sales', + 'Prospecting': 1 + }] + + self.assertEqual(expected_data, report[1]) \ No newline at end of file diff --git a/erpnext/crm/report/sales_pipeline_analytics/test_sales_pipeline_analytics.py b/erpnext/crm/report/sales_pipeline_analytics/test_sales_pipeline_analytics.py index 255331c199ed..bc32a2eff6f1 100644 --- a/erpnext/crm/report/sales_pipeline_analytics/test_sales_pipeline_analytics.py +++ b/erpnext/crm/report/sales_pipeline_analytics/test_sales_pipeline_analytics.py @@ -6,233 +6,233 @@ class TestSalesPipelineAnalytics(unittest.TestCase): - @classmethod - def setUpClass(self): - frappe.db.delete("Opportunity") - create_company() - create_customer() - create_opportunity() - - def test_sales_pipeline_analytics(self): - self.check_for_monthly_and_number() - self.check_for_monthly_and_amount() - self.check_for_quarterly_and_number() - self.check_for_quarterly_and_amount() - self.check_for_all_filters() - - def check_for_monthly_and_number(self): - filters = { - 'pipeline_by':"Owner", - 'range':"Monthly", - 'based_on':"Number", - 'status':"Open", - 'opportunity_type':"Sales", - 'company':"Best Test" - } - - report = execute(filters) - - expected_data = [ - { - 'opportunity_owner':'Not Assigned', - 'August':1 - } - ] - - self.assertEqual(expected_data,report[1]) - - filters = { - 'pipeline_by':"Sales Stage", - 'range':"Monthly", - 'based_on':"Number", - 'status':"Open", - 'opportunity_type':"Sales", - 'company':"Best Test" - } - - report = execute(filters) - - expected_data = [ - { - 'sales_stage':'Prospecting', - 'August':1 - } - ] - - self.assertEqual(expected_data,report[1]) - - def check_for_monthly_and_amount(self): - filters = { - 'pipeline_by':"Owner", - 'range':"Monthly", - 'based_on':"Amount", - 'status':"Open", - 'opportunity_type':"Sales", - 'company':"Best Test" - } - - report = execute(filters) - - expected_data = [ - { - 'opportunity_owner':'Not Assigned', - 'August':150000 - } - ] - - self.assertEqual(expected_data,report[1]) - - filters = { - 'pipeline_by':"Sales Stage", - 'range':"Monthly", - 'based_on':"Amount", - 'status':"Open", - 'opportunity_type':"Sales", - 'company':"Best Test" - } - - report = execute(filters) - - expected_data = [ - { - 'sales_stage':'Prospecting', - 'August':150000 - } - ] - - self.assertEqual(expected_data,report[1]) - - def check_for_quarterly_and_number(self): - filters = { - 'pipeline_by':"Owner", - 'range':"Quaterly", - 'based_on':"Number", - 'status':"Open", - 'opportunity_type':"Sales", - 'company':"Best Test" - } - - report = execute(filters) - - expected_data = [ - { - 'opportunity_owner':'Not Assigned', - 'Q3':1 - } - ] - - self.assertEqual(expected_data,report[1]) - - filters = { - 'pipeline_by':"Sales Stage", - 'range':"Quaterly", - 'based_on':"Number", - 'status':"Open", - 'opportunity_type':"Sales", - 'company':"Best Test" - } - - report = execute(filters) - - expected_data = [ - { - 'sales_stage':'Prospecting', - 'Q3':1 - } - ] - - self.assertEqual(expected_data,report[1]) - - def check_for_quarterly_and_amount(self): - filters = { - 'pipeline_by':"Owner", - 'range':"Quaterly", - 'based_on':"Amount", - 'status':"Open", - 'opportunity_type':"Sales", - 'company':"Best Test" - } - - report = execute(filters) - - expected_data = [ - { - 'opportunity_owner':'Not Assigned', - 'Q3':150000 - } - ] - - self.assertEqual(expected_data,report[1]) - - filters = { - 'pipeline_by':"Sales Stage", - 'range':"Quaterly", - 'based_on':"Amount", - 'status':"Open", - 'opportunity_type':"Sales", - 'company':"Best Test" - } - - report = execute(filters) - - expected_data = [ - { - 'sales_stage':'Prospecting', - 'Q3':150000 - } - ] - - self.assertEqual(expected_data,report[1]) - - def check_for_all_filters(self): - filters = { - 'pipeline_by':"Owner", - 'range':"Monthly", - 'based_on':"Number", - 'status':"Open", - 'opportunity_type':"Sales", - 'company':"Best Test", - 'opportunity_source':'Cold Calling', - 'from_date': '2021-08-01', - 'to_date':'2021-08-31' - } - - report = execute(filters) - - expected_data = [ - { - 'opportunity_owner':'Not Assigned', - 'August': 1 - } - ] - - self.assertEqual(expected_data,report[1]) + @classmethod + def setUpClass(self): + frappe.db.delete("Opportunity") + create_company() + create_customer() + create_opportunity() + + def test_sales_pipeline_analytics(self): + self.check_for_monthly_and_number() + self.check_for_monthly_and_amount() + self.check_for_quarterly_and_number() + self.check_for_quarterly_and_amount() + self.check_for_all_filters() + + def check_for_monthly_and_number(self): + filters = { + 'pipeline_by':"Owner", + 'range':"Monthly", + 'based_on':"Number", + 'status':"Open", + 'opportunity_type':"Sales", + 'company':"Best Test" + } + + report = execute(filters) + + expected_data = [ + { + 'opportunity_owner':'Not Assigned', + 'August':1 + } + ] + + self.assertEqual(expected_data,report[1]) + + filters = { + 'pipeline_by':"Sales Stage", + 'range':"Monthly", + 'based_on':"Number", + 'status':"Open", + 'opportunity_type':"Sales", + 'company':"Best Test" + } + + report = execute(filters) + + expected_data = [ + { + 'sales_stage':'Prospecting', + 'August':1 + } + ] + + self.assertEqual(expected_data,report[1]) + + def check_for_monthly_and_amount(self): + filters = { + 'pipeline_by':"Owner", + 'range':"Monthly", + 'based_on':"Amount", + 'status':"Open", + 'opportunity_type':"Sales", + 'company':"Best Test" + } + + report = execute(filters) + + expected_data = [ + { + 'opportunity_owner':'Not Assigned', + 'August':150000 + } + ] + + self.assertEqual(expected_data,report[1]) + + filters = { + 'pipeline_by':"Sales Stage", + 'range':"Monthly", + 'based_on':"Amount", + 'status':"Open", + 'opportunity_type':"Sales", + 'company':"Best Test" + } + + report = execute(filters) + + expected_data = [ + { + 'sales_stage':'Prospecting', + 'August':150000 + } + ] + + self.assertEqual(expected_data,report[1]) + + def check_for_quarterly_and_number(self): + filters = { + 'pipeline_by':"Owner", + 'range':"Quaterly", + 'based_on':"Number", + 'status':"Open", + 'opportunity_type':"Sales", + 'company':"Best Test" + } + + report = execute(filters) + + expected_data = [ + { + 'opportunity_owner':'Not Assigned', + 'Q3':1 + } + ] + + self.assertEqual(expected_data,report[1]) + + filters = { + 'pipeline_by':"Sales Stage", + 'range':"Quaterly", + 'based_on':"Number", + 'status':"Open", + 'opportunity_type':"Sales", + 'company':"Best Test" + } + + report = execute(filters) + + expected_data = [ + { + 'sales_stage':'Prospecting', + 'Q3':1 + } + ] + + self.assertEqual(expected_data,report[1]) + + def check_for_quarterly_and_amount(self): + filters = { + 'pipeline_by':"Owner", + 'range':"Quaterly", + 'based_on':"Amount", + 'status':"Open", + 'opportunity_type':"Sales", + 'company':"Best Test" + } + + report = execute(filters) + + expected_data = [ + { + 'opportunity_owner':'Not Assigned', + 'Q3':150000 + } + ] + + self.assertEqual(expected_data,report[1]) + + filters = { + 'pipeline_by':"Sales Stage", + 'range':"Quaterly", + 'based_on':"Amount", + 'status':"Open", + 'opportunity_type':"Sales", + 'company':"Best Test" + } + + report = execute(filters) + + expected_data = [ + { + 'sales_stage':'Prospecting', + 'Q3':150000 + } + ] + + self.assertEqual(expected_data,report[1]) + + def check_for_all_filters(self): + filters = { + 'pipeline_by':"Owner", + 'range':"Monthly", + 'based_on':"Number", + 'status':"Open", + 'opportunity_type':"Sales", + 'company':"Best Test", + 'opportunity_source':'Cold Calling', + 'from_date': '2021-08-01', + 'to_date':'2021-08-31' + } + + report = execute(filters) + + expected_data = [ + { + 'opportunity_owner':'Not Assigned', + 'August': 1 + } + ] + + self.assertEqual(expected_data,report[1]) def create_company(): - doc = frappe.db.exists('Company','Best Test') - if not doc: - doc = frappe.new_doc('Company') - doc.company_name = 'Best Test' - doc.default_currency = "INR" - doc.insert() + doc = frappe.db.exists('Company','Best Test') + if not doc: + doc = frappe.new_doc('Company') + doc.company_name = 'Best Test' + doc.default_currency = "INR" + doc.insert() def create_customer(): - doc = frappe.db.exists("Customer","_Test NC") - if not doc: - doc = frappe.new_doc("Customer") - doc.customer_name = '_Test NC' - doc.insert() + doc = frappe.db.exists("Customer","_Test NC") + if not doc: + doc = frappe.new_doc("Customer") + doc.customer_name = '_Test NC' + doc.insert() def create_opportunity(): - doc = frappe.db.exists({"doctype":"Opportunity","party_name":"_Test NC"}) - if not doc: - doc = frappe.new_doc("Opportunity") - doc.opportunity_from = "Customer" - customer_name = frappe.db.get_value("Customer",{"customer_name":'_Test NC'},['customer_name']) - doc.party_name = customer_name - doc.opportunity_amount = 150000 - doc.source = "Cold Calling" - doc.currency = "INR" - doc.expected_closing = "2021-08-31" - doc.company = 'Best Test' - doc.insert() \ No newline at end of file + doc = frappe.db.exists({"doctype":"Opportunity","party_name":"_Test NC"}) + if not doc: + doc = frappe.new_doc("Opportunity") + doc.opportunity_from = "Customer" + customer_name = frappe.db.get_value("Customer",{"customer_name":'_Test NC'},['customer_name']) + doc.party_name = customer_name + doc.opportunity_amount = 150000 + doc.source = "Cold Calling" + doc.currency = "INR" + doc.expected_closing = "2021-08-31" + doc.company = 'Best Test' + doc.insert() \ No newline at end of file From 19dff7185b499405a36dbf5d2cec0b264fc7736e Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Mon, 6 Sep 2021 21:31:31 +0530 Subject: [PATCH 25/28] fix: linter issues --- .../test_opportunity_summary_by_sales_stage.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/erpnext/crm/report/opportunity_summary_by_sales_stage/test_opportunity_summary_by_sales_stage.py b/erpnext/crm/report/opportunity_summary_by_sales_stage/test_opportunity_summary_by_sales_stage.py index 1102a30cf82f..13859d9e0b41 100644 --- a/erpnext/crm/report/opportunity_summary_by_sales_stage/test_opportunity_summary_by_sales_stage.py +++ b/erpnext/crm/report/opportunity_summary_by_sales_stage/test_opportunity_summary_by_sales_stage.py @@ -1,14 +1,17 @@ import unittest + import frappe + from erpnext.crm.report.opportunity_summary_by_sales_stage.opportunity_summary_by_sales_stage import ( - execute + execute, ) from erpnext.crm.report.sales_pipeline_analytics.test_sales_pipeline_analytics import ( create_company, create_customer, - create_opportunity + create_opportunity, ) + class TestOpportunitySummaryBySalesStage(unittest.TestCase): @classmethod def setUpClass(self): From efe425e9ca534ccfc87a21e2e0e7a6041dbdb27f Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Mon, 6 Sep 2021 22:15:16 +0530 Subject: [PATCH 26/28] fix: naming, code formatting --- .../sales_pipeline_analytics.py | 27 +++++++++---------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/erpnext/crm/report/sales_pipeline_analytics/sales_pipeline_analytics.py b/erpnext/crm/report/sales_pipeline_analytics/sales_pipeline_analytics.py index 6394cefce83d..ce3e9d95d832 100644 --- a/erpnext/crm/report/sales_pipeline_analytics/sales_pipeline_analytics.py +++ b/erpnext/crm/report/sales_pipeline_analytics/sales_pipeline_analytics.py @@ -217,32 +217,31 @@ def get_periodic_data(self): count = info.get(based_on) if self.filters.get('pipeline_by') == 'Owner': - if value == 'Not Assigned' or value == '[]' or value is None: - temp = ['Not Assigned'] + assigned_to = ['Not Assigned'] else: - temp = json.loads(value) - self.check_for_assigned_to(period,value,count,temp,info) + assigned_to = json.loads(value) + self.check_for_assigned_to(period, value, count, assigned_to, info) else: - self.set_formatted_data(period,value,count,None) + self.set_formatted_data(period, value, count, None) - def set_formatted_data(self, period, value, val, temp): - if temp: - if len(temp) > 1: + def set_formatted_data(self, period, value, val, assigned_to): + if assigned_to: + if len(assigned_to) > 1: if self.filters.get('assigned_to'): - for user in temp: + for user in assigned_to: if self.filters.get('assigned_to') == user: value = user self.periodic_data.setdefault(value,frappe._dict()).setdefault(period,0) self.periodic_data[value][period] += val else: - for user in temp: + for user in assigned_to: value = user self.periodic_data.setdefault(value,frappe._dict()).setdefault(period,0) self.periodic_data[value][period] += val else: - value = temp[0] + value = assigned_to[0] self.periodic_data.setdefault(value,frappe._dict()).setdefault(period,0) self.periodic_data[value][period] += val @@ -251,13 +250,13 @@ def set_formatted_data(self, period, value, val, temp): self.periodic_data.setdefault(value,frappe._dict()).setdefault(period,0) self.periodic_data[value][period] += val - def check_for_assigned_to(self, period, value, count, temp, info): + def check_for_assigned_to(self, period, value, count, assigned_to, info): if self.filters.get('assigned_to'): for data in json.loads(info.get('opportunity_owner')): if data == self.filters.get('assigned_to'): - self.set_formatted_data(period, data, count, temp) + self.set_formatted_data(period, data, count, assigned_to) else: - self.set_formatted_data(period, value, count, temp) + self.set_formatted_data(period, value, count, assigned_to) def get_month_list(self): month_list= [] From 057b28f4d0735e7a5f83e48c99a17e179ea5c123 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Tue, 7 Sep 2021 10:04:06 +0530 Subject: [PATCH 27/28] fix: quarterly values not showing up in Sales Pipeline Analytics --- .../sales_pipeline_analytics.js | 2 +- .../sales_pipeline_analytics.py | 56 +++++++++---------- 2 files changed, 28 insertions(+), 30 deletions(-) diff --git a/erpnext/crm/report/sales_pipeline_analytics/sales_pipeline_analytics.js b/erpnext/crm/report/sales_pipeline_analytics/sales_pipeline_analytics.js index 3454fe686111..1426f4b6fd2a 100644 --- a/erpnext/crm/report/sales_pipeline_analytics/sales_pipeline_analytics.js +++ b/erpnext/crm/report/sales_pipeline_analytics/sales_pipeline_analytics.js @@ -25,7 +25,7 @@ frappe.query_reports["Sales Pipeline Analytics"] = { fieldname: "range", label: __("Range"), fieldtype: "Select", - options: "Monthly\nQuaterly", + options: "Monthly\nQuarterly", default: "Monthly" }, { diff --git a/erpnext/crm/report/sales_pipeline_analytics/sales_pipeline_analytics.py b/erpnext/crm/report/sales_pipeline_analytics/sales_pipeline_analytics.py index ce3e9d95d832..7466982d9244 100644 --- a/erpnext/crm/report/sales_pipeline_analytics/sales_pipeline_analytics.py +++ b/erpnext/crm/report/sales_pipeline_analytics/sales_pipeline_analytics.py @@ -8,7 +8,7 @@ import pandas from dateutil.relativedelta import relativedelta from frappe import _ -from frappe.utils import flt +from frappe.utils import cint, flt from six import iteritems from erpnext.setup.utils import get_exchange_rate @@ -51,7 +51,7 @@ def set_range_columns(self): 'width': 200 }) - elif self.filters.get('range') == 'Quaterly': + elif self.filters.get('range') == 'Quarterly': for quarter in range(1, 5): self.columns.append({ 'fieldname': f'Q{quarter}', @@ -93,7 +93,7 @@ def get_fields(self): self.group_by_period = { 'Monthly': 'month(expected_closing)', - 'Quaterly': 'QUARTER(expected_closing)' + 'Quarterly': 'QUARTER(expected_closing)' }[self.filters.get('range')] self.pipeline_by = { @@ -103,12 +103,12 @@ def get_fields(self): self.duration = { 'Monthly': 'monthname(expected_closing) as month', - 'Quaterly': 'QUARTER(expected_closing) as quarter' + 'Quarterly': 'QUARTER(expected_closing) as quarter' }[self.filters.get('range')] self.period_by = { 'Monthly': 'month', - 'Quaterly': 'quarter' + 'Quarterly': 'quarter' }[self.filters.get('range')] def get_data(self): @@ -204,59 +204,58 @@ def get_periodic_data(self): frequency = { 'Monthly': 'month', - 'Quaterly': 'quarter' + 'Quarterly': 'quarter' }[self.filters.get('range')] for info in self.query_result: if self.filters.get('range') == 'Monthly': period = info.get(frequency) - if self.filters.get('range') == 'Quaterly': - period = f'Q{info.get("quarter")}' + if self.filters.get('range') == 'Quarterly': + period = f'Q{cint(info.get("quarter"))}' value = info.get(pipeline_by) - count = info.get(based_on) + count_or_amount = info.get(based_on) if self.filters.get('pipeline_by') == 'Owner': if value == 'Not Assigned' or value == '[]' or value is None: assigned_to = ['Not Assigned'] else: assigned_to = json.loads(value) - self.check_for_assigned_to(period, value, count, assigned_to, info) + self.check_for_assigned_to(period, value, count_or_amount, assigned_to, info) else: - self.set_formatted_data(period, value, count, None) + self.set_formatted_data(period, value, count_or_amount, None) - def set_formatted_data(self, period, value, val, assigned_to): + def set_formatted_data(self, period, value, count_or_amount, assigned_to): if assigned_to: if len(assigned_to) > 1: if self.filters.get('assigned_to'): for user in assigned_to: if self.filters.get('assigned_to') == user: value = user - self.periodic_data.setdefault(value,frappe._dict()).setdefault(period,0) - self.periodic_data[value][period] += val + self.periodic_data.setdefault(value, frappe._dict()).setdefault(period, 0) + self.periodic_data[value][period] += count_or_amount else: for user in assigned_to: value = user - self.periodic_data.setdefault(value,frappe._dict()).setdefault(period,0) - self.periodic_data[value][period] += val + self.periodic_data.setdefault(value, frappe._dict()).setdefault(period, 0) + self.periodic_data[value][period] += count_or_amount else: value = assigned_to[0] - self.periodic_data.setdefault(value,frappe._dict()).setdefault(period,0) - self.periodic_data[value][period] += val + self.periodic_data.setdefault(value, frappe._dict()).setdefault(period, 0) + self.periodic_data[value][period] += count_or_amount else: - value = value - self.periodic_data.setdefault(value,frappe._dict()).setdefault(period,0) - self.periodic_data[value][period] += val + self.periodic_data.setdefault(value, frappe._dict()).setdefault(period, 0) + self.periodic_data[value][period] += count_or_amount - def check_for_assigned_to(self, period, value, count, assigned_to, info): + def check_for_assigned_to(self, period, value, count_or_amount, assigned_to, info): if self.filters.get('assigned_to'): for data in json.loads(info.get('opportunity_owner')): if data == self.filters.get('assigned_to'): - self.set_formatted_data(period, data, count, assigned_to) + self.set_formatted_data(period, data, count_or_amount, assigned_to) else: - self.set_formatted_data(period, value, count, assigned_to) + self.set_formatted_data(period, value, count_or_amount, assigned_to) def get_month_list(self): month_list= [] @@ -272,7 +271,7 @@ def get_month_list(self): def append_to_dataset(self, datasets): range_by = { 'Monthly': 'month', - 'Quaterly': 'quarter' + 'Quarterly': 'quarter' }[self.filters.get('range')] based_on = { @@ -280,7 +279,7 @@ def append_to_dataset(self, datasets): 'Number': 'count' }[self.filters.get('based_on')] - if self.filters.get('range') == 'Quaterly': + if self.filters.get('range') == 'Quarterly': frequency_list = [1,2,3,4] count = [0] * 4 @@ -292,7 +291,6 @@ def append_to_dataset(self, datasets): for i in range(len(frequency_list)): if info[range_by] == frequency_list[i]: count[i] = count[i] + info[based_on] - datasets.append({'name': based_on, 'values': count}) def append_data(self, pipeline_by, period_by): @@ -303,8 +301,8 @@ def append_data(self, pipeline_by, period_by): if self.filters.get('range') == 'Monthly': period = info.get(period_by) - if self.filters.get('range') == 'Quaterly': - period = f'Q{info.get(period_by)}' + if self.filters.get('range') == 'Quarterly': + period = f'Q{cint(info.get(period_by))}' count = period_data.get(period,0.0) row[period] = count From 6a1ae7e5d540c7bab7ec1e579bf48f4e51744100 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Tue, 7 Sep 2021 10:29:44 +0530 Subject: [PATCH 28/28] fix: typo in tests --- .../test_sales_pipeline_analytics.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/erpnext/crm/report/sales_pipeline_analytics/test_sales_pipeline_analytics.py b/erpnext/crm/report/sales_pipeline_analytics/test_sales_pipeline_analytics.py index bc32a2eff6f1..24c3839d2d9b 100644 --- a/erpnext/crm/report/sales_pipeline_analytics/test_sales_pipeline_analytics.py +++ b/erpnext/crm/report/sales_pipeline_analytics/test_sales_pipeline_analytics.py @@ -105,7 +105,7 @@ def check_for_monthly_and_amount(self): def check_for_quarterly_and_number(self): filters = { 'pipeline_by':"Owner", - 'range':"Quaterly", + 'range':"Quarterly", 'based_on':"Number", 'status':"Open", 'opportunity_type':"Sales", @@ -125,7 +125,7 @@ def check_for_quarterly_and_number(self): filters = { 'pipeline_by':"Sales Stage", - 'range':"Quaterly", + 'range':"Quarterly", 'based_on':"Number", 'status':"Open", 'opportunity_type':"Sales", @@ -146,7 +146,7 @@ def check_for_quarterly_and_number(self): def check_for_quarterly_and_amount(self): filters = { 'pipeline_by':"Owner", - 'range':"Quaterly", + 'range':"Quarterly", 'based_on':"Amount", 'status':"Open", 'opportunity_type':"Sales", @@ -166,7 +166,7 @@ def check_for_quarterly_and_amount(self): filters = { 'pipeline_by':"Sales Stage", - 'range':"Quaterly", + 'range':"Quarterly", 'based_on':"Amount", 'status':"Open", 'opportunity_type':"Sales",