feat(CRM): Sales Pipeline Analytics Report and Opportunity Summary by Sales Stage Report (#26639)
* feat: Sales Pipeline Analytics Report * fix: sider Issues and added tests * fix: Semgrep Issue * feat: Opportunity Summary by Sales Stage Report * fix: add some checks and tests * fix: sider issues and test * fix: additional checks for error handling and minor changes * fix: remove unused conditions * fix: Changes mentioned on PR * fix: currency conversions and other changes * fix: remove unused imports * fix: correction for failing test case * fix: recorrected failing test case * fix: sider issues and resolve test case errors * fix: rewrite query using query builder * fix: test case changes * fix: sider fixes and other changes * fix: clear data before running test * fix: test case fixed * refactor: code formatting - smaller functions - variable and function naming * refactor: improve code formatting * fix: linter issues * fix: linter issues * fix: change indentation to tabs * fix: linter issues * fix: naming, code formatting * fix: quarterly values not showing up in Sales Pipeline Analytics * fix: typo in tests Co-authored-by: Rucha Mahabal <ruchamahabal2@gmail.com>
This commit is contained in:
parent
88c9fe35bd
commit
d795e55694
@ -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")
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
@ -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)
|
||||||
|
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
|
@ -0,0 +1,94 @@
|
|||||||
|
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,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
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])
|
@ -0,0 +1,70 @@
|
|||||||
|
// 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\nQuarterly",
|
||||||
|
default: "Monthly"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldname: "assigned_to",
|
||||||
|
label: __("Assigned To"),
|
||||||
|
fieldtype: "Link",
|
||||||
|
options: "User"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldname: "status",
|
||||||
|
label: __("Status"),
|
||||||
|
fieldtype: "Select",
|
||||||
|
options: "Open\nQuotation\nConverted\nReplied"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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"
|
||||||
|
},
|
||||||
|
]
|
||||||
|
};
|
@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
@ -0,0 +1,333 @@
|
|||||||
|
# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
|
# For license information, please see license.txt
|
||||||
|
|
||||||
|
import json
|
||||||
|
from datetime import date
|
||||||
|
|
||||||
|
import frappe
|
||||||
|
import pandas
|
||||||
|
from dateutil.relativedelta import relativedelta
|
||||||
|
from frappe import _
|
||||||
|
from frappe.utils import cint, flt
|
||||||
|
from six import iteritems
|
||||||
|
|
||||||
|
from erpnext.setup.utils import get_exchange_rate
|
||||||
|
|
||||||
|
|
||||||
|
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 = []
|
||||||
|
|
||||||
|
self.set_range_columns()
|
||||||
|
self.set_pipeline_based_on_column()
|
||||||
|
|
||||||
|
def set_range_columns(self):
|
||||||
|
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': month,
|
||||||
|
'fieldtype': based_on,
|
||||||
|
'label': month,
|
||||||
|
'width': 200
|
||||||
|
})
|
||||||
|
|
||||||
|
elif self.filters.get('range') == 'Quarterly':
|
||||||
|
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
|
||||||
|
})
|
||||||
|
|
||||||
|
def get_fields(self):
|
||||||
|
self.based_on ={
|
||||||
|
'Owner': '_assign as opportunity_owner',
|
||||||
|
'Sales Stage': 'sales_stage'
|
||||||
|
}[self.filters.get('pipeline_by')]
|
||||||
|
|
||||||
|
self.data_based_on ={
|
||||||
|
'Number': 'count(name) as count',
|
||||||
|
'Amount': 'opportunity_amount as amount'
|
||||||
|
}[self.filters.get('based_on')]
|
||||||
|
|
||||||
|
self.group_by_based_on = {
|
||||||
|
'Owner': '_assign',
|
||||||
|
'Sales Stage': 'sales_stage'
|
||||||
|
}[self.filters.get('pipeline_by')]
|
||||||
|
|
||||||
|
self.group_by_period = {
|
||||||
|
'Monthly': 'month(expected_closing)',
|
||||||
|
'Quarterly': 'QUARTER(expected_closing)'
|
||||||
|
}[self.filters.get('range')]
|
||||||
|
|
||||||
|
self.pipeline_by = {
|
||||||
|
'Owner': 'opportunity_owner',
|
||||||
|
'Sales Stage': 'sales_stage'
|
||||||
|
}[self.filters.get('pipeline_by')]
|
||||||
|
|
||||||
|
self.duration = {
|
||||||
|
'Monthly': 'monthname(expected_closing) as month',
|
||||||
|
'Quarterly': 'QUARTER(expected_closing) as quarter'
|
||||||
|
}[self.filters.get('range')]
|
||||||
|
|
||||||
|
self.period_by = {
|
||||||
|
'Monthly': 'month',
|
||||||
|
'Quarterly': '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=[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=[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([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({
|
||||||
|
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(self.pipeline_by, self.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'):
|
||||||
|
conditions.append({'opportunity_type': self.filters.get('opportunity_type')})
|
||||||
|
|
||||||
|
if self.filters.get('status'):
|
||||||
|
conditions.append({'status': self.filters.get('status')})
|
||||||
|
|
||||||
|
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')]])
|
||||||
|
|
||||||
|
return conditions
|
||||||
|
|
||||||
|
def get_chart_data(self):
|
||||||
|
labels = []
|
||||||
|
datasets = []
|
||||||
|
|
||||||
|
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':{
|
||||||
|
'labels': labels,
|
||||||
|
'datasets': datasets
|
||||||
|
},
|
||||||
|
'type':'line'
|
||||||
|
}
|
||||||
|
|
||||||
|
return self.chart
|
||||||
|
|
||||||
|
def get_periodic_data(self):
|
||||||
|
self.periodic_data = frappe._dict()
|
||||||
|
|
||||||
|
based_on = {
|
||||||
|
'Number': 'count',
|
||||||
|
'Amount': 'amount'
|
||||||
|
}[self.filters.get('based_on')]
|
||||||
|
|
||||||
|
pipeline_by = {
|
||||||
|
'Owner': 'opportunity_owner',
|
||||||
|
'Sales Stage': 'sales_stage'
|
||||||
|
}[self.filters.get('pipeline_by')]
|
||||||
|
|
||||||
|
frequency = {
|
||||||
|
'Monthly': 'month',
|
||||||
|
'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') == 'Quarterly':
|
||||||
|
period = f'Q{cint(info.get("quarter"))}'
|
||||||
|
|
||||||
|
value = info.get(pipeline_by)
|
||||||
|
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_or_amount, assigned_to, info)
|
||||||
|
|
||||||
|
else:
|
||||||
|
self.set_formatted_data(period, value, count_or_amount, None)
|
||||||
|
|
||||||
|
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] += 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] += count_or_amount
|
||||||
|
else:
|
||||||
|
value = assigned_to[0]
|
||||||
|
self.periodic_data.setdefault(value, frappe._dict()).setdefault(period, 0)
|
||||||
|
self.periodic_data[value][period] += count_or_amount
|
||||||
|
|
||||||
|
else:
|
||||||
|
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_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_or_amount, assigned_to)
|
||||||
|
else:
|
||||||
|
self.set_formatted_data(period, value, count_or_amount, assigned_to)
|
||||||
|
|
||||||
|
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, datasets):
|
||||||
|
range_by = {
|
||||||
|
'Monthly': 'month',
|
||||||
|
'Quarterly': 'quarter'
|
||||||
|
}[self.filters.get('range')]
|
||||||
|
|
||||||
|
based_on = {
|
||||||
|
'Amount': 'amount',
|
||||||
|
'Number': 'count'
|
||||||
|
}[self.filters.get('based_on')]
|
||||||
|
|
||||||
|
if self.filters.get('range') == 'Quarterly':
|
||||||
|
frequency_list = [1,2,3,4]
|
||||||
|
count = [0] * 4
|
||||||
|
|
||||||
|
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(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):
|
||||||
|
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') == 'Quarterly':
|
||||||
|
period = f'Q{cint(info.get(period_by))}'
|
||||||
|
|
||||||
|
count = period_data.get(period,0.0)
|
||||||
|
row[period] = count
|
||||||
|
|
||||||
|
self.data.append(row)
|
||||||
|
|
||||||
|
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
|
@ -0,0 +1,238 @@
|
|||||||
|
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",
|
||||||
|
'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':"Quarterly",
|
||||||
|
'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':"Quarterly",
|
||||||
|
'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':"Quarterly",
|
||||||
|
'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':"Quarterly",
|
||||||
|
'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()
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
||||||
|
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()
|
@ -147,6 +147,24 @@
|
|||||||
"onboard": 1,
|
"onboard": 1,
|
||||||
"type": "Link"
|
"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": "",
|
"dependencies": "",
|
||||||
"hidden": 0,
|
"hidden": 0,
|
||||||
@ -403,7 +421,7 @@
|
|||||||
"type": "Link"
|
"type": "Link"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"modified": "2021-08-05 12:15:56.913091",
|
"modified": "2021-08-19 19:08:08.728876",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "CRM",
|
"module": "CRM",
|
||||||
"name": "CRM",
|
"name": "CRM",
|
||||||
|
Loading…
Reference in New Issue
Block a user