Sales Goal by Company (#9723)

* [sales goal] in company; dashboard, graph, notifs, wiz

* [test] target notifications

* cache past year monthly sales of every company daily, patch

* [minor] query fixes

* update sales goal docs
This commit is contained in:
Prateeksha Singh 2017-07-18 10:35:12 +05:30 committed by Makarand Bauskar
parent e2d0d0a0c1
commit e012e24423
18 changed files with 2199 additions and 1861 deletions

View File

@ -98,6 +98,26 @@ erpnext.accounts.SalesInvoiceController = erpnext.selling.SellingController.exte
this.set_default_print_format(); this.set_default_print_format();
}, },
on_submit: function(doc, dt, dn) {
var me = this;
$.each(doc["items"], function(i, row) {
if(row.delivery_note) frappe.model.clear_doc("Delivery Note", row.delivery_note)
})
if(this.frm.doc.is_pos) {
this.frm.msgbox = frappe.msgprint(
`<a class="btn btn-primary" onclick="cur_frm.print_preview.printit(true)" style="margin-right: 5px;">
${__('Print')}</a>
<a class="btn btn-default" href="javascript:frappe.new_doc(cur_frm.doctype);">
${__('New')}</a>`
);
} else if(cint(frappe.boot.notification_settings.sales_invoice)) {
this.frm.email_doc(frappe.boot.notification_settings.sales_invoice_message);
}
},
set_default_print_format: function() { set_default_print_format: function() {
// set default print format to POS type // set default print format to POS type
if(cur_frm.doc.is_pos) { if(cur_frm.doc.is_pos) {
@ -306,7 +326,7 @@ erpnext.accounts.SalesInvoiceController = erpnext.selling.SellingController.exte
this.frm.refresh_fields(); this.frm.refresh_fields();
}, },
company_address: function() { company_address: function() {
var me = this; var me = this;
if(this.frm.doc.company_address) { if(this.frm.doc.company_address) {
@ -445,24 +465,6 @@ cur_frm.cscript.cost_center = function(doc, cdt, cdn) {
erpnext.utils.copy_value_in_all_row(doc, cdt, cdn, "items", "cost_center"); erpnext.utils.copy_value_in_all_row(doc, cdt, cdn, "items", "cost_center");
} }
cur_frm.cscript.on_submit = function(doc, cdt, cdn) {
$.each(doc["items"], function(i, row) {
if(row.delivery_note) frappe.model.clear_doc("Delivery Note", row.delivery_note)
})
if(cur_frm.doc.is_pos) {
cur_frm.msgbox = frappe.msgprint(
`<a class="btn btn-primary" onclick="cur_frm.print_preview.printit(true)" style="margin-right: 5px;">
${__('Print')}</a>
<a class="btn btn-default" href="javascript:frappe.new_doc(cur_frm.doctype);">
${__('New')}</a>`
);
} else if(cint(frappe.boot.notification_settings.sales_invoice)) {
cur_frm.email_doc(frappe.boot.notification_settings.sales_invoice_message);
}
}
cur_frm.set_query("debit_to", function(doc) { cur_frm.set_query("debit_to", function(doc) {
// filter on Account // filter on Account
if (doc.customer) { if (doc.customer) {

View File

@ -83,10 +83,10 @@ class SalesInvoice(SellingController):
if not self.is_opening: if not self.is_opening:
self.is_opening = 'No' self.is_opening = 'No'
if self._action != 'submit' and self.update_stock and not self.is_return: if self._action != 'submit' and self.update_stock and not self.is_return:
set_batch_nos(self, 'warehouse', True) set_batch_nos(self, 'warehouse', True)
self.set_against_income_account() self.set_against_income_account()
self.validate_c_form() self.validate_c_form()
@ -98,7 +98,7 @@ class SalesInvoice(SellingController):
self.set_billing_hours_and_amount() self.set_billing_hours_and_amount()
self.update_timesheet_billing_for_project() self.update_timesheet_billing_for_project()
self.set_status() self.set_status()
def before_save(self): def before_save(self):
set_account_for_mode_of_payment(self) set_account_for_mode_of_payment(self)
@ -139,6 +139,8 @@ class SalesInvoice(SellingController):
self.update_time_sheet(self.name) self.update_time_sheet(self.name)
frappe.enqueue('erpnext.setup.doctype.company.company.update_company_current_month_sales', company=self.company)
def validate_pos_paid_amount(self): def validate_pos_paid_amount(self):
if len(self.payments) == 0 and self.is_pos: if len(self.payments) == 0 and self.is_pos:
frappe.throw(_("At least one mode of payment is required for POS invoice.")) frappe.throw(_("At least one mode of payment is required for POS invoice."))
@ -816,7 +818,7 @@ class SalesInvoice(SellingController):
self.validate_serial_against_sales_invoice() self.validate_serial_against_sales_invoice()
def validate_serial_against_delivery_note(self): def validate_serial_against_delivery_note(self):
""" """
validate if the serial numbers in Sales Invoice Items are same as in validate if the serial numbers in Sales Invoice Items are same as in
Delivery Note Item Delivery Note Item
""" """
@ -918,7 +920,6 @@ def make_delivery_note(source_name, target_doc=None):
return doclist return doclist
@frappe.whitelist() @frappe.whitelist()
def make_sales_return(source_name, target_doc=None): def make_sales_return(source_name, target_doc=None):
from erpnext.controllers.sales_and_purchase_return import make_return_doc from erpnext.controllers.sales_and_purchase_return import make_return_doc

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 MiB

View File

@ -15,5 +15,6 @@ third-party-backups
workflows workflows
bar-code bar-code
company-setup company-setup
setting-company-sales-goal
calculate-incentive-for-sales-team calculate-incentive-for-sales-team
articles articles

View File

@ -0,0 +1,15 @@
# Setting Company Sales Goal
Monthly sales targets can be set for a company via the Company master. By default, the Company master dashboard features past sales stats.
<img class="screenshot" alt="Sales Graph" src="{{docs_base_url}}/assets/img/sales_goal/sales_history_graph.png">
You can set the **Sales Target** field to track progress to track progress with respect to it.
<img class="screenshot" alt="Setting Sales Goal" src="{{docs_base_url}}/assets/img/sales_goal/setting_sales_goal.gif">
The target progress is also shown in notifications:
<img class="screenshot" alt="Sales Notification" src="{{docs_base_url}}/assets/img/sales_goal/sales_goal_notification.png">
{next}

View File

@ -183,7 +183,8 @@ scheduler_events = {
"erpnext.projects.doctype.task.task.set_tasks_as_overdue", "erpnext.projects.doctype.task.task.set_tasks_as_overdue",
"erpnext.accounts.doctype.asset.depreciation.post_depreciation_entries", "erpnext.accounts.doctype.asset.depreciation.post_depreciation_entries",
"erpnext.hr.doctype.daily_work_summary_settings.daily_work_summary_settings.send_summary", "erpnext.hr.doctype.daily_work_summary_settings.daily_work_summary_settings.send_summary",
"erpnext.stock.doctype.serial_no.serial_no.update_maintenance_status" "erpnext.stock.doctype.serial_no.serial_no.update_maintenance_status",
"erpnext.setup.doctype.company.company.cache_companies_monthly_sales_history"
] ]
} }

View File

@ -418,4 +418,5 @@ erpnext.patches.v8_1.allow_invoice_copy_to_edit_after_submit
erpnext.patches.v8_1.add_hsn_sac_codes erpnext.patches.v8_1.add_hsn_sac_codes
erpnext.patches.v8_1.update_gst_state #17-07-2017 erpnext.patches.v8_1.update_gst_state #17-07-2017
erpnext.patches.v8_1.removed_report_support_hours erpnext.patches.v8_1.removed_report_support_hours
erpnext.patches.v8_1.add_indexes_in_transaction_doctypes erpnext.patches.v8_1.add_indexes_in_transaction_doctypes
erpnext.patches.v8_3.update_company_total_sales

View File

View File

@ -0,0 +1,15 @@
# Copyright (c) 2017, Frappe and Contributors
# License: GNU General Public License v3. See license.txt
from __future__ import unicode_literals
import frappe
from erpnext.setup.doctype.company.company import update_company_current_month_sales, update_company_monthly_sales
def execute():
'''Update company monthly sales history based on sales invoices'''
frappe.reload_doctype("Company")
companies = [d['name'] for d in frappe.get_list("Company")]
for company in companies:
update_company_current_month_sales(company)
update_company_monthly_sales(company)

View File

@ -216,6 +216,17 @@ var erpnext_slides = [
] ]
}, },
{
// Sales Target
name: 'Goals',
domains: ['manufacturing', 'services', 'retail', 'distribution'],
title: __("Set your Target"),
help: __("Set a sales target you'd like to achieve."),
fields: [
{fieldtype:"Currency", fieldname:"sales_target", label:__("Monthly Sales Target")},
]
},
{ {
// Taxes // Taxes
name: 'taxes', name: 'taxes',

File diff suppressed because it is too large Load Diff

View File

@ -147,7 +147,7 @@ class Company(Document):
def validate_perpetual_inventory(self): def validate_perpetual_inventory(self):
if not self.get("__islocal"): if not self.get("__islocal"):
if cint(self.enable_perpetual_inventory) == 1 and not self.default_inventory_account: if cint(self.enable_perpetual_inventory) == 1 and not self.default_inventory_account:
frappe.msgprint(_("Set default inventory account for perpetual inventory"), frappe.msgprint(_("Set default inventory account for perpetual inventory"),
alert=True, indicator='orange') alert=True, indicator='orange')
def set_default_accounts(self): def set_default_accounts(self):
@ -310,3 +310,45 @@ def get_name_with_abbr(name, company):
parts.append(company_abbr) parts.append(company_abbr)
return " - ".join(parts) return " - ".join(parts)
def update_company_current_month_sales(company):
from frappe.utils import today, formatdate
current_month_year = formatdate(today(), "MM-yyyy")
results = frappe.db.sql(('''
select
sum(grand_total) as total, date_format(posting_date, '%m-%Y') as month_year
from
`tabSales Invoice`
where
date_format(posting_date, '%m-%Y')="{0}" and
company = "{1}"
group by
month_year;
''').format(current_month_year, frappe.db.escape(company)), as_dict = True)
monthly_total = results[0]['total'] if len(results) > 0 else 0
frappe.db.sql(('''
update tabCompany set total_monthly_sales = %s where name=%s
'''), (monthly_total, frappe.db.escape(company)))
frappe.db.commit()
def update_company_monthly_sales(company):
'''Cache past year monthly sales of every company based on sales invoices'''
from frappe.utils.goal import get_monthly_results
import json
filter_str = 'company = "'+ company +'" and status != "Draft"'
month_to_value_dict = get_monthly_results("Sales Invoice", "grand_total", "posting_date", filter_str, "sum")
frappe.db.sql(('''
update tabCompany set sales_monthly_history = %s where name=%s
'''), (json.dumps(month_to_value_dict), frappe.db.escape(company)))
frappe.db.commit()
def cache_companies_monthly_sales_history():
companies = [d['name'] for d in frappe.get_list("Company")]
for company in companies:
update_company_monthly_sales(company)
frappe.db.commit()

View File

@ -0,0 +1,42 @@
from frappe import _
def get_data():
return {
'heatmap': True,
'heatmap_message': _('This is based on transactions against this Company. See timeline below for details'),
'graph': True,
'graph_method': "frappe.utils.goal.get_monthly_goal_graph_data",
'graph_method_args': {
'title': 'Sales',
'goal_value_field': 'sales_target',
'goal_total_field': 'total_monthly_sales',
'goal_history_field': 'sales_monthly_history',
'goal_doctype': 'Sales Invoice',
'goal_doctype_link': 'company',
'goal_field': 'grand_total',
'date_field': 'posting_date',
'filter_str': 'status != "Draft"',
'aggregation': 'sum'
},
'fieldname': 'company',
'transactions': [
{
'label': _('Pre Sales'),
'items': ['Quotation']
},
{
'label': _('Orders'),
'items': ['Sales Order', 'Delivery Note', 'Sales Invoice']
},
{
'label': _('Support'),
'items': ['Issue']
},
{
'label': _('Projects'),
'items': ['Project']
}
]
}

View File

@ -91,7 +91,8 @@ def create_fiscal_year_and_company(args):
'country': args.get('country'), 'country': args.get('country'),
'create_chart_of_accounts_based_on': 'Standard Template', 'create_chart_of_accounts_based_on': 'Standard Template',
'chart_of_accounts': args.get('chart_of_accounts'), 'chart_of_accounts': args.get('chart_of_accounts'),
'domain': args.get('domain') 'domain': args.get('domain'),
'sales_target': args.get('sales_target')
}).insert() }).insert()
#Enable shopping cart #Enable shopping cart

View File

@ -5,7 +5,7 @@ from __future__ import unicode_literals
import frappe import frappe
def get_notification_config(): def get_notification_config():
notification_for_doctype = { "for_doctype": notifications = { "for_doctype":
{ {
"Issue": {"status": "Open"}, "Issue": {"status": "Open"},
"Warranty Claim": {"status": "Open"}, "Warranty Claim": {"status": "Open"},
@ -56,12 +56,20 @@ def get_notification_config():
"Production Order": { "status": ("in", ("Draft", "Not Started", "In Process")) }, "Production Order": { "status": ("in", ("Draft", "Not Started", "In Process")) },
"BOM": {"docstatus": 0}, "BOM": {"docstatus": 0},
"Timesheet": {"status": "Draft"} "Timesheet": {"status": "Draft"}
},
"targets": {
"Company": {
"filters" : { "sales_target": ( ">", 0 ) },
"target_field" : "sales_target",
"value_field" : "total_monthly_sales"
}
} }
} }
doctype = [d for d in notification_for_doctype.get('for_doctype')] doctype = [d for d in notifications.get('for_doctype')]
for doc in frappe.get_all('DocType', for doc in frappe.get_all('DocType',
fields= ["name"], filters = {"name": ("not in", doctype), 'is_submittable': 1}): fields= ["name"], filters = {"name": ("not in", doctype), 'is_submittable': 1}):
notification_for_doctype["for_doctype"][doc.name] = {"docstatus": 0} notifications["for_doctype"][doc.name] = {"docstatus": 0}
return notification_for_doctype return notifications

View File

@ -0,0 +1,46 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
from __future__ import unicode_literals
import unittest
from frappe.desk import notifications
from frappe.test_runner import make_test_objects
class TestNotifications(unittest.TestCase):
def setUp(self):
test_records = [
{
"abbr": "_TC6",
"company_name": "_Test Company 6",
"country": "India",
"default_currency": "INR",
"doctype": "Company",
"domain": "Manufacturing",
"sales_target": 2000,
"chart_of_accounts": "Standard"
},
{
"abbr": "_TC7",
"company_name": "_Test Company 7",
"country": "United States",
"default_currency": "USD",
"doctype": "Company",
"domain": "Retail",
"sales_target": 10000,
"total_monthly_sales": 1000,
"chart_of_accounts": "Standard"
},
]
make_test_objects('Company', test_records=test_records, reset=True)
def test_get_notifications_for_targets(self):
'''
Test notification config entries for targets as percentages
'''
config = notifications.get_notification_config()
doc_target_percents = notifications.get_notifications_for_targets(config, {})
self.assertEquals(doc_target_percents['Company']['_Test Company 7'], 10)
self.assertEquals(doc_target_percents['Company']['_Test Company 6'], 0)