[Enhancement] Improvement to the sales pipeline (#15524)
* Additions to sales pipeline * Codacy corrections * Codacy corrections * Codacy corrections * Replace _ with dummy for unused variable * Performance + dates corrections * Itertuples modification * Removing trailing whitespaces * Sales stage doctype * Divide sales stages fixtures in separate functions * Remove duplicate fixtures * Add newline after method * Missing requirement
This commit is contained in:
parent
efd776da46
commit
75fa6b3ee8
@ -107,6 +107,8 @@ erpnext.crm.Opportunity = frappe.ui.form.Controller.extend({
|
|||||||
if(!this.frm.doc.company && frappe.defaults.get_user_default("Company"))
|
if(!this.frm.doc.company && frappe.defaults.get_user_default("Company"))
|
||||||
set_multiple(this.frm.doc.doctype, this.frm.doc.name,
|
set_multiple(this.frm.doc.doctype, this.frm.doc.name,
|
||||||
{ company:frappe.defaults.get_user_default("Company") });
|
{ company:frappe.defaults.get_user_default("Company") });
|
||||||
|
if(!this.frm.doc.currency)
|
||||||
|
set_multiple(this.frm.doc.doctype, this.frm.doc.name, { currency:frappe.defaults.get_user_default("Currency") });
|
||||||
|
|
||||||
this.setup_queries();
|
this.setup_queries();
|
||||||
},
|
},
|
||||||
|
File diff suppressed because it is too large
Load Diff
0
erpnext/crm/doctype/sales_stage/__init__.py
Normal file
0
erpnext/crm/doctype/sales_stage/__init__.py
Normal file
8
erpnext/crm/doctype/sales_stage/sales_stage.js
Normal file
8
erpnext/crm/doctype/sales_stage/sales_stage.js
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
// Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
|
// For license information, please see license.txt
|
||||||
|
|
||||||
|
frappe.ui.form.on('Sales Stage', {
|
||||||
|
refresh: function(frm) {
|
||||||
|
|
||||||
|
}
|
||||||
|
});
|
96
erpnext/crm/doctype/sales_stage/sales_stage.json
Normal file
96
erpnext/crm/doctype/sales_stage/sales_stage.json
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
{
|
||||||
|
"allow_copy": 0,
|
||||||
|
"allow_events_in_timeline": 0,
|
||||||
|
"allow_guest_to_view": 0,
|
||||||
|
"allow_import": 0,
|
||||||
|
"allow_rename": 0,
|
||||||
|
"autoname": "field:stage_name",
|
||||||
|
"beta": 0,
|
||||||
|
"creation": "2018-10-01 09:28:16.399518",
|
||||||
|
"custom": 0,
|
||||||
|
"docstatus": 0,
|
||||||
|
"doctype": "DocType",
|
||||||
|
"document_type": "",
|
||||||
|
"editable_grid": 1,
|
||||||
|
"engine": "InnoDB",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"allow_bulk_edit": 0,
|
||||||
|
"allow_in_quick_entry": 0,
|
||||||
|
"allow_on_submit": 0,
|
||||||
|
"bold": 0,
|
||||||
|
"collapsible": 0,
|
||||||
|
"columns": 0,
|
||||||
|
"fieldname": "stage_name",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"hidden": 0,
|
||||||
|
"ignore_user_permissions": 0,
|
||||||
|
"ignore_xss_filter": 0,
|
||||||
|
"in_filter": 0,
|
||||||
|
"in_global_search": 0,
|
||||||
|
"in_list_view": 0,
|
||||||
|
"in_standard_filter": 0,
|
||||||
|
"label": "Stage Name",
|
||||||
|
"length": 0,
|
||||||
|
"no_copy": 0,
|
||||||
|
"permlevel": 0,
|
||||||
|
"precision": "",
|
||||||
|
"print_hide": 0,
|
||||||
|
"print_hide_if_no_value": 0,
|
||||||
|
"read_only": 0,
|
||||||
|
"remember_last_selected_value": 0,
|
||||||
|
"report_hide": 0,
|
||||||
|
"reqd": 0,
|
||||||
|
"search_index": 0,
|
||||||
|
"set_only_once": 0,
|
||||||
|
"translatable": 0,
|
||||||
|
"unique": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"has_web_view": 0,
|
||||||
|
"hide_heading": 0,
|
||||||
|
"hide_toolbar": 0,
|
||||||
|
"idx": 0,
|
||||||
|
"image_view": 0,
|
||||||
|
"in_create": 0,
|
||||||
|
"is_submittable": 0,
|
||||||
|
"issingle": 0,
|
||||||
|
"istable": 0,
|
||||||
|
"max_attachments": 0,
|
||||||
|
"modified": "2018-10-01 09:29:43.230378",
|
||||||
|
"modified_by": "Administrator",
|
||||||
|
"module": "CRM",
|
||||||
|
"name": "Sales Stage",
|
||||||
|
"name_case": "",
|
||||||
|
"owner": "Administrator",
|
||||||
|
"permissions": [
|
||||||
|
{
|
||||||
|
"amend": 0,
|
||||||
|
"cancel": 0,
|
||||||
|
"create": 1,
|
||||||
|
"delete": 1,
|
||||||
|
"email": 1,
|
||||||
|
"export": 1,
|
||||||
|
"if_owner": 0,
|
||||||
|
"import": 0,
|
||||||
|
"permlevel": 0,
|
||||||
|
"print": 1,
|
||||||
|
"read": 1,
|
||||||
|
"report": 1,
|
||||||
|
"role": "Sales Manager",
|
||||||
|
"set_user_permissions": 0,
|
||||||
|
"share": 1,
|
||||||
|
"submit": 0,
|
||||||
|
"write": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"quick_entry": 1,
|
||||||
|
"read_only": 0,
|
||||||
|
"read_only_onload": 0,
|
||||||
|
"show_name_in_global_search": 0,
|
||||||
|
"sort_field": "modified",
|
||||||
|
"sort_order": "DESC",
|
||||||
|
"track_changes": 1,
|
||||||
|
"track_seen": 0,
|
||||||
|
"track_views": 0
|
||||||
|
}
|
10
erpnext/crm/doctype/sales_stage/sales_stage.py
Normal file
10
erpnext/crm/doctype/sales_stage/sales_stage.py
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
|
# For license information, please see license.txt
|
||||||
|
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
import frappe
|
||||||
|
from frappe.model.document import Document
|
||||||
|
|
||||||
|
class SalesStage(Document):
|
||||||
|
pass
|
23
erpnext/crm/doctype/sales_stage/test_sales_stage.js
Normal file
23
erpnext/crm/doctype/sales_stage/test_sales_stage.js
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
/* eslint-disable */
|
||||||
|
// rename this file from _test_[name] to test_[name] to activate
|
||||||
|
// and remove above this line
|
||||||
|
|
||||||
|
QUnit.test("test: Sales Stage", function (assert) {
|
||||||
|
let done = assert.async();
|
||||||
|
|
||||||
|
// number of asserts
|
||||||
|
assert.expect(1);
|
||||||
|
|
||||||
|
frappe.run_serially([
|
||||||
|
// insert a new Sales Stage
|
||||||
|
() => frappe.tests.make('Sales Stage', [
|
||||||
|
// values to be set
|
||||||
|
{key: 'value'}
|
||||||
|
]),
|
||||||
|
() => {
|
||||||
|
assert.equal(cur_frm.doc.key, 'value');
|
||||||
|
},
|
||||||
|
() => done()
|
||||||
|
]);
|
||||||
|
|
||||||
|
});
|
10
erpnext/crm/doctype/sales_stage/test_sales_stage.py
Normal file
10
erpnext/crm/doctype/sales_stage/test_sales_stage.py
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
|
||||||
|
# See license.txt
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import frappe
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
class TestSalesStage(unittest.TestCase):
|
||||||
|
pass
|
@ -567,4 +567,5 @@ erpnext.patches.v10_0.delete_hub_documents # 12-08-2018
|
|||||||
erpnext.patches.v11_0.rename_healthcare_fields
|
erpnext.patches.v11_0.rename_healthcare_fields
|
||||||
erpnext.patches.v11_0.remove_land_unit_icon
|
erpnext.patches.v11_0.remove_land_unit_icon
|
||||||
erpnext.patches.v11_0.add_default_dispatch_notification_template
|
erpnext.patches.v11_0.add_default_dispatch_notification_template
|
||||||
erpnext.patches.v11_0.add_market_segments
|
erpnext.patches.v11_0.add_market_segments
|
||||||
|
erpnext.patches.v11_0.add_sales_stages
|
||||||
|
10
erpnext/patches/v11_0/add_sales_stages.py
Normal file
10
erpnext/patches/v11_0/add_sales_stages.py
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import frappe
|
||||||
|
from frappe import _
|
||||||
|
from erpnext.setup.setup_wizard.operations.install_fixtures import add_sale_stages
|
||||||
|
|
||||||
|
def execute():
|
||||||
|
frappe.reload_doc('crm', 'doctype', 'sales_stage')
|
||||||
|
|
||||||
|
frappe.local.lang = frappe.db.get_default("lang") or 'en'
|
||||||
|
|
||||||
|
add_sale_stages()
|
@ -13,23 +13,34 @@ frappe.pages['sales-funnel'].on_page_load = function(wrapper) {
|
|||||||
frappe.breadcrumbs.add("Selling");
|
frappe.breadcrumbs.add("Selling");
|
||||||
}
|
}
|
||||||
|
|
||||||
erpnext.SalesFunnel = Class.extend({
|
erpnext.SalesFunnel = class SalesFunnel {
|
||||||
init: function(wrapper) {
|
constructor(wrapper) {
|
||||||
var me = this;
|
var me = this;
|
||||||
// 0 setTimeout hack - this gives time for canvas to get width and height
|
// 0 setTimeout hack - this gives time for canvas to get width and height
|
||||||
setTimeout(function() {
|
setTimeout(function() {
|
||||||
me.setup(wrapper);
|
me.setup(wrapper);
|
||||||
me.get_data();
|
me.get_data();
|
||||||
}, 0);
|
}, 0);
|
||||||
},
|
}
|
||||||
|
|
||||||
setup: function(wrapper) {
|
setup(wrapper) {
|
||||||
var me = this;
|
var me = this;
|
||||||
|
|
||||||
|
this.company_field = wrapper.page.add_field({"fieldtype": "Link", "fieldname": "company", "options": "Company",
|
||||||
|
"label": __("Company"), "reqd": 1, "default": frappe.defaults.get_user_default('company'),
|
||||||
|
change: function() {
|
||||||
|
me.company = this.value || frappe.defaults.get_user_default('company');
|
||||||
|
me.get_data();
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
this.elements = {
|
this.elements = {
|
||||||
layout: $(wrapper).find(".layout-main"),
|
layout: $(wrapper).find(".layout-main"),
|
||||||
from_date: wrapper.page.add_date(__("From Date")),
|
from_date: wrapper.page.add_date(__("From Date")),
|
||||||
to_date: wrapper.page.add_date(__("To Date")),
|
to_date: wrapper.page.add_date(__("To Date")),
|
||||||
|
chart: wrapper.page.add_select(__("Chart"), [{value: 'sales_funnel', label:__("Sales Funnel")},
|
||||||
|
{value: 'sales_pipeline', label:__("Sales Pipeline")},
|
||||||
|
{value: 'opp_by_lead_source', label:__("Opportunities by lead source")}]),
|
||||||
refresh_btn: wrapper.page.set_primary_action(__("Refresh"),
|
refresh_btn: wrapper.page.set_primary_action(__("Refresh"),
|
||||||
function() { me.get_data(); }, "fa fa-refresh"),
|
function() { me.get_data(); }, "fa fa-refresh"),
|
||||||
};
|
};
|
||||||
@ -41,16 +52,27 @@ erpnext.SalesFunnel = Class.extend({
|
|||||||
this.elements.funnel_wrapper = $('<div class="funnel-wrapper text-center"></div>')
|
this.elements.funnel_wrapper = $('<div class="funnel-wrapper text-center"></div>')
|
||||||
.appendTo(this.elements.layout);
|
.appendTo(this.elements.layout);
|
||||||
|
|
||||||
|
this.company = frappe.defaults.get_user_default('company');
|
||||||
this.options = {
|
this.options = {
|
||||||
from_date: frappe.datetime.add_months(frappe.datetime.get_today(), -1),
|
from_date: frappe.datetime.add_months(frappe.datetime.get_today(), -1),
|
||||||
to_date: frappe.datetime.get_today()
|
to_date: frappe.datetime.get_today(),
|
||||||
|
chart: 'sales_funnel'
|
||||||
};
|
};
|
||||||
|
|
||||||
// set defaults and bind on change
|
// set defaults and bind on change
|
||||||
$.each(this.options, function(k, v) {
|
$.each(this.options, function(k, v) {
|
||||||
me.elements[k].val(frappe.datetime.str_to_user(v));
|
if (['from_date', 'to_date'].includes(k)) {
|
||||||
|
me.elements[k].val(frappe.datetime.str_to_user(v));
|
||||||
|
} else {
|
||||||
|
me.elements[k].val(v);
|
||||||
|
}
|
||||||
|
|
||||||
me.elements[k].on("change", function() {
|
me.elements[k].on("change", function() {
|
||||||
me.options[k] = frappe.datetime.user_to_str($(this).val());
|
if (['from_date', 'to_date'].includes(k)) {
|
||||||
|
me.options[k] = frappe.datetime.user_to_str($(this).val()) != 'Invalid date' ? frappe.datetime.user_to_str($(this).val()) : frappe.datetime.get_today();
|
||||||
|
} else {
|
||||||
|
me.options.chart = $(this).val();
|
||||||
|
}
|
||||||
me.get_data();
|
me.get_data();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -64,29 +86,90 @@ erpnext.SalesFunnel = Class.extend({
|
|||||||
$(window).resize(function() {
|
$(window).resize(function() {
|
||||||
me.render();
|
me.render();
|
||||||
});
|
});
|
||||||
},
|
}
|
||||||
|
|
||||||
get_data: function(btn) {
|
get_data(btn) {
|
||||||
var me = this;
|
var me = this;
|
||||||
frappe.call({
|
if (me.options.chart == 'sales_funnel'){
|
||||||
method: "erpnext.selling.page.sales_funnel.sales_funnel.get_funnel_data",
|
frappe.call({
|
||||||
args: {
|
method: "erpnext.selling.page.sales_funnel.sales_funnel.get_funnel_data",
|
||||||
from_date: this.options.from_date,
|
args: {
|
||||||
to_date: this.options.to_date
|
from_date: this.options.from_date,
|
||||||
},
|
to_date: this.options.to_date,
|
||||||
btn: btn,
|
company: this.company
|
||||||
callback: function(r) {
|
},
|
||||||
if(!r.exc) {
|
btn: btn,
|
||||||
me.options.data = r.message;
|
callback: function(r) {
|
||||||
me.render();
|
if(!r.exc) {
|
||||||
|
me.options.data = r.message;
|
||||||
|
if (me.options.data=='empty') {
|
||||||
|
const $parent = me.elements.funnel_wrapper;
|
||||||
|
$parent.html(__('No data for this period'));
|
||||||
|
} else {
|
||||||
|
me.render_funnel();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
});
|
} else if (me.options.chart == 'opp_by_lead_source'){
|
||||||
},
|
frappe.call({
|
||||||
|
method: "erpnext.selling.page.sales_funnel.sales_funnel.get_opp_by_lead_source",
|
||||||
|
args: {
|
||||||
|
from_date: this.options.from_date,
|
||||||
|
to_date: this.options.to_date,
|
||||||
|
company: this.company
|
||||||
|
},
|
||||||
|
btn: btn,
|
||||||
|
callback: function(r) {
|
||||||
|
if(!r.exc) {
|
||||||
|
me.options.data = r.message;
|
||||||
|
if (me.options.data=='empty') {
|
||||||
|
const $parent = me.elements.funnel_wrapper;
|
||||||
|
$parent.html(__('No data for this period'));
|
||||||
|
} else {
|
||||||
|
me.render_opp_by_lead_source();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else if (me.options.chart == 'sales_pipeline'){
|
||||||
|
frappe.call({
|
||||||
|
method: "erpnext.selling.page.sales_funnel.sales_funnel.get_pipeline_data",
|
||||||
|
args: {
|
||||||
|
from_date: this.options.from_date,
|
||||||
|
to_date: this.options.to_date,
|
||||||
|
company: this.company
|
||||||
|
},
|
||||||
|
btn: btn,
|
||||||
|
callback: function(r) {
|
||||||
|
if(!r.exc) {
|
||||||
|
me.options.data = r.message;
|
||||||
|
if (me.options.data=='empty') {
|
||||||
|
const $parent = me.elements.funnel_wrapper;
|
||||||
|
$parent.html(__('No data for this period'));
|
||||||
|
} else {
|
||||||
|
me.render_pipeline();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
render: function() {
|
render() {
|
||||||
|
let me = this;
|
||||||
|
if (me.options.chart == 'sales_funnel'){
|
||||||
|
me.render_funnel();
|
||||||
|
} else if (me.options.chart == 'opp_by_lead_source'){
|
||||||
|
me.render_opp_by_lead_source();
|
||||||
|
} else if (me.options.chart == 'sales_pipeline'){
|
||||||
|
me.render_pipeline();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render_funnel() {
|
||||||
var me = this;
|
var me = this;
|
||||||
this.prepare();
|
this.prepare_funnel();
|
||||||
|
|
||||||
var context = this.elements.context,
|
var context = this.elements.context,
|
||||||
x_start = 0.0,
|
x_start = 0.0,
|
||||||
@ -119,9 +202,9 @@ erpnext.SalesFunnel = Class.extend({
|
|||||||
|
|
||||||
me.draw_legend(x_mid, y_mid, me.options.width, me.options.height, d.value + " - " + d.title);
|
me.draw_legend(x_mid, y_mid, me.options.width, me.options.height, d.value + " - " + d.title);
|
||||||
});
|
});
|
||||||
},
|
}
|
||||||
|
|
||||||
prepare: function() {
|
prepare_funnel() {
|
||||||
var me = this;
|
var me = this;
|
||||||
|
|
||||||
this.elements.no_data.toggle(false);
|
this.elements.no_data.toggle(false);
|
||||||
@ -147,9 +230,9 @@ erpnext.SalesFunnel = Class.extend({
|
|||||||
.attr("height", this.options.height);
|
.attr("height", this.options.height);
|
||||||
|
|
||||||
this.elements.context = this.elements.canvas.get(0).getContext("2d");
|
this.elements.context = this.elements.canvas.get(0).getContext("2d");
|
||||||
},
|
}
|
||||||
|
|
||||||
draw_triangle: function(x_start, x_mid, x_end, y, height) {
|
draw_triangle(x_start, x_mid, x_end, y, height) {
|
||||||
var context = this.elements.context;
|
var context = this.elements.context;
|
||||||
context.beginPath();
|
context.beginPath();
|
||||||
context.moveTo(x_start, y);
|
context.moveTo(x_start, y);
|
||||||
@ -158,9 +241,9 @@ erpnext.SalesFunnel = Class.extend({
|
|||||||
context.lineTo(x_start, y);
|
context.lineTo(x_start, y);
|
||||||
context.closePath();
|
context.closePath();
|
||||||
context.fill();
|
context.fill();
|
||||||
},
|
}
|
||||||
|
|
||||||
draw_legend: function(x_mid, y_mid, width, height, title) {
|
draw_legend(x_mid, y_mid, width, height, title) {
|
||||||
var context = this.elements.context;
|
var context = this.elements.context;
|
||||||
|
|
||||||
if(y_mid == 0) {
|
if(y_mid == 0) {
|
||||||
@ -186,4 +269,44 @@ erpnext.SalesFunnel = Class.extend({
|
|||||||
context.font = "1.1em sans-serif";
|
context.font = "1.1em sans-serif";
|
||||||
context.fillText(__(title), width + 20, y_mid);
|
context.fillText(__(title), width + 20, y_mid);
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
render_opp_by_lead_source() {
|
||||||
|
let me = this;
|
||||||
|
let currency = frappe.defaults.get_default("currency");
|
||||||
|
|
||||||
|
let chart_data = me.options.data ? me.options.data : null;
|
||||||
|
|
||||||
|
const parent = me.elements.funnel_wrapper[0];
|
||||||
|
this.chart = new Chart(parent, {
|
||||||
|
title: __("Sales Opportunities by Source"),
|
||||||
|
height: 400,
|
||||||
|
data: chart_data,
|
||||||
|
type: 'bar',
|
||||||
|
barOptions: {
|
||||||
|
stacked: 1
|
||||||
|
},
|
||||||
|
tooltipOptions: {
|
||||||
|
formatTooltipY: d => format_currency(d, currency),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
render_pipeline() {
|
||||||
|
let me = this;
|
||||||
|
let currency = frappe.defaults.get_default("currency");
|
||||||
|
|
||||||
|
let chart_data = me.options.data ? me.options.data : null;
|
||||||
|
|
||||||
|
const parent = me.elements.funnel_wrapper[0];
|
||||||
|
this.chart = new Chart(parent, {
|
||||||
|
title: __("Sales Pipeline by Stage"),
|
||||||
|
height: 400,
|
||||||
|
data: chart_data,
|
||||||
|
type: 'bar',
|
||||||
|
tooltipOptions: {
|
||||||
|
formatTooltipY: d => format_currency(d, currency),
|
||||||
|
},
|
||||||
|
colors: ['light-green', 'green']
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
@ -1,16 +1,18 @@
|
|||||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
|
||||||
# License: GNU General Public License v3. See license.txt
|
# License: GNU General Public License v3. See license.txt
|
||||||
|
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
import frappe
|
import frappe
|
||||||
|
|
||||||
from frappe import _
|
from frappe import _
|
||||||
|
from erpnext.accounts.report.utils import convert
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_funnel_data(from_date, to_date):
|
def get_funnel_data(from_date, to_date, company):
|
||||||
active_leads = frappe.db.sql("""select count(*) from `tabLead`
|
active_leads = frappe.db.sql("""select count(*) from `tabLead`
|
||||||
where (date(`modified`) between %s and %s)
|
where (date(`modified`) between %s and %s)
|
||||||
and status != "Do Not Contact" """, (from_date, to_date))[0][0]
|
and status != "Do Not Contact" and company=%s""", (from_date, to_date, company))[0][0]
|
||||||
|
|
||||||
active_leads += frappe.db.sql("""select count(distinct contact.name) from `tabContact` contact
|
active_leads += frappe.db.sql("""select count(distinct contact.name) from `tabContact` contact
|
||||||
left join `tabDynamic Link` dl on (dl.parent=contact.name) where dl.link_doctype='Customer'
|
left join `tabDynamic Link` dl on (dl.parent=contact.name) where dl.link_doctype='Customer'
|
||||||
@ -18,14 +20,14 @@ def get_funnel_data(from_date, to_date):
|
|||||||
|
|
||||||
opportunities = frappe.db.sql("""select count(*) from `tabOpportunity`
|
opportunities = frappe.db.sql("""select count(*) from `tabOpportunity`
|
||||||
where (date(`creation`) between %s and %s)
|
where (date(`creation`) between %s and %s)
|
||||||
and status != "Lost" """, (from_date, to_date))[0][0]
|
and status != "Lost" and company=%s""", (from_date, to_date, company))[0][0]
|
||||||
|
|
||||||
quotations = frappe.db.sql("""select count(*) from `tabQuotation`
|
quotations = frappe.db.sql("""select count(*) from `tabQuotation`
|
||||||
where docstatus = 1 and (date(`creation`) between %s and %s)
|
where docstatus = 1 and (date(`creation`) between %s and %s)
|
||||||
and status != "Lost" """, (from_date, to_date))[0][0]
|
and status != "Lost" and company=%s""", (from_date, to_date, company))[0][0]
|
||||||
|
|
||||||
sales_orders = frappe.db.sql("""select count(*) from `tabSales Order`
|
sales_orders = frappe.db.sql("""select count(*) from `tabSales Order`
|
||||||
where docstatus = 1 and (date(`creation`) between %s and %s)""", (from_date, to_date))[0][0]
|
where docstatus = 1 and (date(`creation`) between %s and %s) and company=%s""", (from_date, to_date, company))[0][0]
|
||||||
|
|
||||||
return [
|
return [
|
||||||
{ "title": _("Active Leads / Customers"), "value": active_leads, "color": "#B03B46" },
|
{ "title": _("Active Leads / Customers"), "value": active_leads, "color": "#B03B46" },
|
||||||
@ -33,3 +35,54 @@ def get_funnel_data(from_date, to_date):
|
|||||||
{ "title": _("Quotations"), "value": quotations, "color": "#006685" },
|
{ "title": _("Quotations"), "value": quotations, "color": "#006685" },
|
||||||
{ "title": _("Sales Orders"), "value": sales_orders, "color": "#00AD65" }
|
{ "title": _("Sales Orders"), "value": sales_orders, "color": "#00AD65" }
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def get_opp_by_lead_source(from_date, to_date, company):
|
||||||
|
opportunities = frappe.get_all("Opportunity", filters=[['status', 'in', ['Open', 'Quotation', 'Replied']], ['company', '=', company], ['transaction_date', 'Between', [from_date, to_date]]], fields=['currency', 'sales_stage', 'opportunity_amount', 'probability', 'source'])
|
||||||
|
|
||||||
|
if opportunities:
|
||||||
|
default_currency = frappe.get_cached_value('Global Defaults', 'None', 'default_currency')
|
||||||
|
|
||||||
|
cp_opportunities = [dict(x, **{'compound_amount': (convert(x['opportunity_amount'], x['currency'], default_currency, to_date) * x['probability']/100)}) for x in opportunities]
|
||||||
|
|
||||||
|
df = pd.DataFrame(cp_opportunities).groupby(['source', 'sales_stage'], as_index=False).agg({'compound_amount': 'sum'})
|
||||||
|
|
||||||
|
result = {}
|
||||||
|
result['labels'] = list(set(df.source.values))
|
||||||
|
result['datasets'] = []
|
||||||
|
|
||||||
|
for s in set(df.sales_stage.values):
|
||||||
|
result['datasets'].append({'name': s, 'values': [0]*len(result['labels']), 'chartType': 'bar'})
|
||||||
|
|
||||||
|
for row in df.itertuples():
|
||||||
|
source_index = result['labels'].index(row.source)
|
||||||
|
|
||||||
|
for dataset in result['datasets']:
|
||||||
|
if dataset['name'] == row.sales_stage:
|
||||||
|
dataset['values'][source_index] = row.compound_amount
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
else:
|
||||||
|
return 'empty'
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def get_pipeline_data(from_date, to_date, company):
|
||||||
|
opportunities = frappe.get_all("Opportunity", filters=[['status', 'in', ['Open', 'Quotation', 'Replied']], ['company', '=', company], ['transaction_date', 'Between', [from_date, to_date]]], fields=['currency', 'sales_stage', 'opportunity_amount', 'probability'])
|
||||||
|
|
||||||
|
if opportunities:
|
||||||
|
default_currency = frappe.get_cached_value('Global Defaults', 'None', 'default_currency')
|
||||||
|
|
||||||
|
cp_opportunities = [dict(x, **{'compound_amount': (convert(x['opportunity_amount'], x['currency'], default_currency, to_date) * x['probability']/100)}) for x in opportunities]
|
||||||
|
|
||||||
|
df = pd.DataFrame(cp_opportunities).groupby(['sales_stage'], as_index=True).agg({'compound_amount': 'sum'}).to_dict()
|
||||||
|
|
||||||
|
result = {}
|
||||||
|
result['labels'] = df['compound_amount'].keys()
|
||||||
|
result['datasets'] = []
|
||||||
|
result['datasets'].append({'name': _("Total Amount"), 'values': df['compound_amount'].values(), 'chartType': 'bar'})
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
else:
|
||||||
|
return 'empty'
|
@ -308,6 +308,20 @@ def add_market_segments():
|
|||||||
|
|
||||||
make_fixture_records(records)
|
make_fixture_records(records)
|
||||||
|
|
||||||
|
def add_sale_stages():
|
||||||
|
# Sale Stages
|
||||||
|
records = [
|
||||||
|
{"doctype": "Sales Stage", "stage_name": _("Prospecting")},
|
||||||
|
{"doctype": "Sales Stage", "stage_name": _("Qualification")},
|
||||||
|
{"doctype": "Sales Stage", "stage_name": _("Needs Analysis")},
|
||||||
|
{"doctype": "Sales Stage", "stage_name": _("Value Proposition")},
|
||||||
|
{"doctype": "Sales Stage", "stage_name": _("Identifying Decision Makers")},
|
||||||
|
{"doctype": "Sales Stage", "stage_name": _("Perception Analysis")},
|
||||||
|
{"doctype": "Sales Stage", "stage_name": _("Proposal/Price Quote")},
|
||||||
|
{"doctype": "Sales Stage", "stage_name": _("Negotiation/Review")}
|
||||||
|
]
|
||||||
|
make_fixture_records(records)
|
||||||
|
|
||||||
def make_fixture_records(records):
|
def make_fixture_records(records):
|
||||||
from frappe.modules import scrub
|
from frappe.modules import scrub
|
||||||
for r in records:
|
for r in records:
|
||||||
|
@ -105,6 +105,7 @@ def setup_complete(args=None):
|
|||||||
def stage_fixtures(args):
|
def stage_fixtures(args):
|
||||||
install_fixtures.install(args.get("country"))
|
install_fixtures.install(args.get("country"))
|
||||||
install_fixtures.add_market_segments()
|
install_fixtures.add_market_segments()
|
||||||
|
install_fixtures.add_sale_stages()
|
||||||
|
|
||||||
def setup_company(args):
|
def setup_company(args):
|
||||||
defaults_setup.create_price_lists(args)
|
defaults_setup.create_price_lists(args)
|
||||||
|
@ -6,3 +6,4 @@ python-stdnum
|
|||||||
braintree
|
braintree
|
||||||
gocardless_pro
|
gocardless_pro
|
||||||
woocommerce
|
woocommerce
|
||||||
|
pandas
|
Loading…
x
Reference in New Issue
Block a user