Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(CRM): Sales Pipeline Analytics Report and Opportunity Summary by Sales Stage Report #26639

Merged
merged 33 commits into from
Sep 7, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
14b2c5f
feat: Sales Pipeline Analytics Report
mohammedyusufshaikh Jul 26, 2021
4998658
fix: sider Issues and added tests
mohammedyusufshaikh Jul 28, 2021
6521acd
fix: Semgrep Issue
mohammedyusufshaikh Jul 28, 2021
3c22097
feat: Opportunity Summary by Sales Stage Report
mohammedyusufshaikh Aug 3, 2021
6748dfc
fix: add some checks and tests
mohammedyusufshaikh Aug 3, 2021
8d948a2
fix: sider issues and test
mohammedyusufshaikh Aug 3, 2021
b9053f8
fix: additional checks for error handling and minor changes
mohammedyusufshaikh Aug 3, 2021
ea8324d
fix: remove unused conditions
mohammedyusufshaikh Aug 4, 2021
c47c6ff
fix: Changes mentioned on PR
mohammedyusufshaikh Aug 10, 2021
2edf3d8
fix: currency conversions and other changes
mohammedyusufshaikh Aug 19, 2021
d31b164
fix: remove unused imports
mohammedyusufshaikh Aug 23, 2021
8ab5f33
fix: correction for failing test case
mohammedyusufshaikh Aug 23, 2021
171640e
fix: recorrected failing test case
mohammedyusufshaikh Aug 24, 2021
7f5ff69
fix: sider issues and resolve test case errors
mohammedyusufshaikh Aug 24, 2021
65c717b
fix: rewrite query using query builder
mohammedyusufshaikh Aug 26, 2021
0e8d48e
fix: test case changes
mohammedyusufshaikh Aug 26, 2021
3042302
fix: sider fixes and other changes
mohammedyusufshaikh Aug 26, 2021
d31c778
fix: clear data before running test
mohammedyusufshaikh Aug 27, 2021
0f25145
fix: test case fixed
mohammedyusufshaikh Aug 27, 2021
6ecc2bf
Merge branch 'develop' into sales_report
mohammedyusufshaikh Aug 27, 2021
0017bc5
refactor: code formatting
ruchamahabal Aug 31, 2021
741cbf0
Merge branch 'develop' into sales_report
ruchamahabal Aug 31, 2021
67beec8
refactor: improve code formatting
mohammedyusufshaikh Sep 1, 2021
ce82701
Merge branch 'develop' into sales_report
ruchamahabal Sep 6, 2021
86429a4
fix: linter issues
ruchamahabal Sep 6, 2021
cfe06ff
fix: linter issues
ruchamahabal Sep 6, 2021
8234f7d
fix: change indentation to tabs
mohammedyusufshaikh Sep 6, 2021
3ce6523
Merge branch 'develop' into sales_report
ruchamahabal Sep 6, 2021
19dff71
fix: linter issues
ruchamahabal Sep 6, 2021
efe425e
fix: naming, code formatting
ruchamahabal Sep 6, 2021
057b28f
fix: quarterly values not showing up in Sales Pipeline Analytics
ruchamahabal Sep 7, 2021
440d504
Merge branch 'develop' into sales_report
ruchamahabal Sep 7, 2021
6a1ae7e
fix: typo in tests
ruchamahabal Sep 7, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// 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: "Opportunity Owner"
},
{
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")
}
]
};
Original file line number Diff line number Diff line change
@@ -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"
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,254 @@
# 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 frappe.utils import flt
from six import iteritems

from erpnext.setup.utils import get_exchange_rate


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.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
})

elif self.filters.get('data_based_on') == 'Amount':
self.columns.append({
'label': _(sales_stage),
'fieldname': sales_stage,
'fieldtype': 'Currency',
'width': 150
})

def get_data(self):
self.data = []

based_on = {
'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',
}[self.filters.get('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):
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
)

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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mohammedyusufshaikh @ruchamahabal lets stop using this module.

It's a crutch to use pandas (millions of lines of dependency) for something that can be done in 5-10 line for loop 😅

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.query_result = self.grouped_data

def get_rows(self):
self.data = []
self.get_formatted_data()

for based_on,data in iteritems(self.formatted_data):
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 d.get(based_on) == '[]' or d.get(based_on) is None or d.get(based_on) == 'Not Assigned':
assignments = ['Not Assigned']
else:
assignments = json.loads(d.get(based_on))

sales_stage = d.get('sales_stage')
count = d.get(data_based_on)

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:
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.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 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'))})

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 = []
datasets = []
values = [0] * 8

for sales_stage in self.sales_stage_list:
labels.append(sales_stage)

options = {
'Number': 'count',
'Amount': 'amount'
}[self.filters.get('data_based_on')]

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':{
'labels': labels,
'datasets': datasets
},
'type':'line'
}

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'))

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
Loading