From d097eaef6304a247f7f7681e79acd38bc5ce3349 Mon Sep 17 00:00:00 2001 From: Anupam K Date: Tue, 5 May 2020 15:57:49 +0530 Subject: [PATCH 01/21] Appending Email and Phone in Child Table --- erpnext/selling/doctype/customer/customer.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/erpnext/selling/doctype/customer/customer.py b/erpnext/selling/doctype/customer/customer.py index 50e719f02e..d0db6d62a0 100644 --- a/erpnext/selling/doctype/customer/customer.py +++ b/erpnext/selling/doctype/customer/customer.py @@ -164,6 +164,8 @@ class Customer(TransactionBase): contact.phone = lead.phone contact.mobile_no = lead.mobile_no contact.is_primary_contact = 1 + contact.append('email_ids', dict(email_id=lead.email_id, is_primary=1)) + contact.append('phone_nos', dict(phone=lead.mobile_no, is_primary_mobile_no=1)) contact.append('links', dict(link_doctype='Customer', link_name=self.name)) contact.flags.ignore_permissions = self.flags.ignore_permissions contact.autoname() From be6eb201b71150202f3c63fa2c632a5a9594cd95 Mon Sep 17 00:00:00 2001 From: Anupam K Date: Fri, 8 May 2020 17:33:21 +0530 Subject: [PATCH 02/21] Appending Email and Phone in Child Table --- erpnext/selling/doctype/customer/customer.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/erpnext/selling/doctype/customer/customer.py b/erpnext/selling/doctype/customer/customer.py index d0db6d62a0..3d172ac7a2 100644 --- a/erpnext/selling/doctype/customer/customer.py +++ b/erpnext/selling/doctype/customer/customer.py @@ -164,9 +164,11 @@ class Customer(TransactionBase): contact.phone = lead.phone contact.mobile_no = lead.mobile_no contact.is_primary_contact = 1 - contact.append('email_ids', dict(email_id=lead.email_id, is_primary=1)) - contact.append('phone_nos', dict(phone=lead.mobile_no, is_primary_mobile_no=1)) contact.append('links', dict(link_doctype='Customer', link_name=self.name)) + if lead.email_id: + contact.append('email_ids', dict(email_id=lead.email_id, is_primary=1)) + if lead.mobile_no: + contact.append('phone_nos', dict(phone=lead.mobile_no, is_primary_mobile_no=1)) contact.flags.ignore_permissions = self.flags.ignore_permissions contact.autoname() if not frappe.db.exists("Contact", contact.name): From d543830a59d1a87bf6de83244303347ac168e769 Mon Sep 17 00:00:00 2001 From: Abhishek Balam Date: Sat, 9 May 2020 21:56:53 +0530 Subject: [PATCH 03/21] feat(Selling): Added Territory wise treeview to 'Customer Acquistion and Loyalty' report --- .../customer_acquisition_and_loyalty.js | 20 +- .../customer_acquisition_and_loyalty.py | 239 ++++++++++++++---- erpnext/setup/doctype/territory/territory.py | 5 +- 3 files changed, 212 insertions(+), 52 deletions(-) diff --git a/erpnext/selling/report/customer_acquisition_and_loyalty/customer_acquisition_and_loyalty.js b/erpnext/selling/report/customer_acquisition_and_loyalty/customer_acquisition_and_loyalty.js index a854fa9969..654614b4bb 100644 --- a/erpnext/selling/report/customer_acquisition_and_loyalty/customer_acquisition_and_loyalty.js +++ b/erpnext/selling/report/customer_acquisition_and_loyalty/customer_acquisition_and_loyalty.js @@ -3,6 +3,13 @@ frappe.query_reports["Customer Acquisition and Loyalty"] = { "filters": [ + { + "fieldname":"view_type", + "label": __("View Type"), + "fieldtype": "Select", + "default": "Time Series", + "options": ["Time Series", "Territory Tree"] + }, { "fieldname":"company", "label": __("Company"), @@ -24,6 +31,13 @@ frappe.query_reports["Customer Acquisition and Loyalty"] = { "fieldtype": "Date", "default": frappe.defaults.get_user_default("year_end_date"), "reqd": 1 - }, - ] -} + } + ], + 'formatter': function(value, row, column, data, default_formatter) { + value = default_formatter(value, row, column, data); + if (data && data.bold) { + value = value.bold(); + } + return value + } +} \ No newline at end of file diff --git a/erpnext/selling/report/customer_acquisition_and_loyalty/customer_acquisition_and_loyalty.py b/erpnext/selling/report/customer_acquisition_and_loyalty/customer_acquisition_and_loyalty.py index aa57665a81..f2033f1fff 100644 --- a/erpnext/selling/report/customer_acquisition_and_loyalty/customer_acquisition_and_loyalty.py +++ b/erpnext/selling/report/customer_acquisition_and_loyalty/customer_acquisition_and_loyalty.py @@ -2,65 +2,210 @@ # License: GNU General Public License v3. See license.txt from __future__ import unicode_literals +import calendar import frappe from frappe import _ -from frappe.utils import getdate, cint, cstr -import calendar +from frappe.utils import cint, cstr def execute(filters=None): - # key yyyy-mm - new_customers_in = {} - repeat_customers_in = {} - customers = [] - company_condition = "" + common_columns = [ + { + 'label': _('New Customers'), + 'fieldname': 'new_customers', + 'fieldtype': 'Int', + 'default': 0, + 'width': 100 + }, + { + 'label': _('Repeat Customers'), + 'fieldname': 'repeat_customers', + 'fieldtype': 'Int', + 'default': 0, + 'width': 100 + }, + { + 'label': _('Total'), + 'fieldname': 'total', + 'fieldtype': 'Int', + 'default': 0, + 'width': 100 + }, + { + 'label': _('New Customer Revenue'), + 'fieldname': 'new_customer_revenue', + 'fieldtype': 'Currency', + 'default': 0.0, + 'width': 150 + }, + { + 'label': _('Repeat Customer Revenue'), + 'fieldname': 'repeat_customer_revenue', + 'fieldtype': 'Currency', + 'default': 0.0, + 'width': 150 + }, + { + 'label': _('Total Revenue'), + 'fieldname': 'total_revenue', + 'fieldtype': 'Currency', + 'default': 0.0, + 'width': 150 + } + ] + if filters.get('view_type') == 'Territory Tree': + return get_data_by_territory(filters, common_columns) + else: + return get_data_by_time(filters, common_columns) - if filters.get("company"): - company_condition = ' and company=%(company)s' +def get_data_by_time(filters, common_columns): + # key yyyy-mm + columns = [ + { + 'label': 'Year', + 'fieldname': 'year', + 'fieldtype': 'Data', + 'width': 100 + }, + { + 'label': 'Month', + 'fieldname': 'month', + 'fieldtype': 'Data', + 'width': 100 + }, + ] + columns += common_columns - for si in frappe.db.sql("""select posting_date, customer, base_grand_total from `tabSales Invoice` - where docstatus=1 and posting_date <= %(to_date)s - {company_condition} order by posting_date""".format(company_condition=company_condition), - filters, as_dict=1): + customers_in = get_customer_stats(filters) - key = si.posting_date.strftime("%Y-%m") - if not si.customer in customers: - new_customers_in.setdefault(key, [0, 0.0]) - new_customers_in[key][0] += 1 - new_customers_in[key][1] += si.base_grand_total - customers.append(si.customer) - else: - repeat_customers_in.setdefault(key, [0, 0.0]) - repeat_customers_in[key][0] += 1 - repeat_customers_in[key][1] += si.base_grand_total + # time series + from_year, from_month, temp = filters.get('from_date').split('-') + to_year, to_month, temp = filters.get('to_date').split('-') - # time series - from_year, from_month, temp = filters.get("from_date").split("-") - to_year, to_month, temp = filters.get("to_date").split("-") + from_year, from_month, to_year, to_month = \ + cint(from_year), cint(from_month), cint(to_year), cint(to_month) - from_year, from_month, to_year, to_month = \ - cint(from_year), cint(from_month), cint(to_year), cint(to_month) + out = [] + for year in range(from_year, to_year+1): + for month in range(from_month if year==from_year else 1, (to_month+1) if year==to_year else 13): + key = '{year}-{month:02d}'.format(year=year, month=month) + data = customers_in.get(key) + new = data['new'] if data else [0, 0.0] + repeat = data['repeat'] if data else [0, 0.0] + out.append({ + 'year': cstr(year), + 'month': calendar.month_name[month], + 'new_customers': new[0], + 'repeat_customers': repeat[0], + 'total': new[0] + repeat[0], + 'new_customer_revenue': new[1], + 'repeat_customer_revenue': repeat[1], + 'total_revenue': new[1] + repeat[1] + }) + return columns, out - out = [] - for year in range(from_year, to_year+1): - for month in range(from_month if year==from_year else 1, (to_month+1) if year==to_year else 13): - key = "{year}-{month:02d}".format(year=year, month=month) +def get_data_by_territory(filters, common_columns): + columns = [{ + 'label': 'Territory', + 'fieldname': 'territory', + 'fieldtype': 'Link', + 'options': 'Territory', + 'width': 150 + }] + columns += common_columns - new = new_customers_in.get(key, [0,0.0]) - repeat = repeat_customers_in.get(key, [0,0.0]) + customers_in = get_customer_stats(filters, tree_view=True) - out.append([cstr(year), calendar.month_name[month], - new[0], repeat[0], new[0] + repeat[0], - new[1], repeat[1], new[1] + repeat[1]]) + territory_dict = {} + for t in frappe.db.sql('''SELECT name, lft, parent_territory, is_group FROM `tabTerritory` ORDER BY lft''', as_dict=1): + territory_dict.update({ + t.name: { + 'parent': t.parent_territory, + 'is_group': t.is_group + } + }) - return [ - _("Year") + "::100", - _("Month") + "::100", - _("New Customers") + ":Int:100", - _("Repeat Customers") + ":Int:100", - _("Total") + ":Int:100", - _("New Customer Revenue") + ":Currency:150", - _("Repeat Customer Revenue") + ":Currency:150", - _("Total Revenue") + ":Currency:150" - ], out + depth_map = frappe._dict() + for name, info in territory_dict.items(): + default = depth_map.get(info['parent']) + 1 if info['parent'] else 0 + depth_map.setdefault(name, default) + data = [] + for name, indent in depth_map.items(): + condition = customers_in.get(name) + new = customers_in[name]['new'] if condition else [0, 0.0] + repeat = customers_in[name]['repeat'] if condition else [0, 0.0] + temp = { + 'territory': name, + 'indent': indent, + 'new_customers': new[0], + 'repeat_customers': repeat[0], + 'total': new[0] + repeat[0], + 'new_customer_revenue': new[1], + 'repeat_customer_revenue': repeat[1], + 'total_revenue': new[1] + repeat[1], + 'bold': 0 if condition else 1 + } + data.append(temp) + node_list = [x for x in territory_dict.keys() if territory_dict[x]['is_group'] == 0] + root_node = [x for x in territory_dict.keys() if territory_dict[x]['parent'] is None][0] + for node in node_list: + data = update_groups(node, data, root_node, territory_dict) + + for group in [x for x in territory_dict.keys() if territory_dict[x]['parent'] == root_node]: + group_data = [x for x in data if x['territory'] == group][0] + root_data = [x for x in data if x['territory'] == root_node][0] + for key in group_data.keys(): + if key not in ['indent', 'territory', 'bold']: + root_data[key] += group_data[key] + + return columns, data, None, None, None, 1 + +def update_groups(node, data, root_node, territory_dict): + ''' Adds values of child territories to parent node except root ''' + parent_node = territory_dict[node]['parent'] + if parent_node != root_node and parent_node: + node_data = [x for x in data if x['territory'] == node][0] + parent_data = [x for x in data if x['territory'] == parent_node][0] + for key in parent_data.keys(): + if key not in ['indent', 'territory', 'bold']: + parent_data[key] += node_data[key] + return update_groups(parent_node, data, root_node, territory_dict) + else: + return data + +def get_customer_stats(filters, tree_view=False): + ''' Calculates number of new and repeated customers ''' + company_condition = '' + if filters.get('company'): + company_condition = ' and company=%(company)s' + + customers = [] + customers_in = {} + new_customers_in = {} + repeat_customers_in = {} + + for si in frappe.db.sql('''select territory, posting_date, customer, base_grand_total from `tabSales Invoice` + where docstatus=1 and posting_date <= %(to_date)s and posting_date >= %(from_date)s + {company_condition} order by posting_date'''.format(company_condition=company_condition), + filters, as_dict=1): + if tree_view: + key = si.territory + else: + key = si.posting_date.strftime('%Y-%m') + if not si.customer in customers: + new_customers_in.setdefault(key, [0, 0.0]) + new_customers_in[key][0] += 1 + new_customers_in[key][1] += si.base_grand_total + customers.append(si.customer) + else: + repeat_customers_in.setdefault(key, [0, 0.0]) + repeat_customers_in[key][0] += 1 + repeat_customers_in[key][1] += si.base_grand_total + customers_in.update({ + key: { + 'new': new_customers_in[key] if new_customers_in.get(key) else [0, 0.0], + 'repeat': repeat_customers_in[key] if repeat_customers_in.get(key) else [0, 0.0], + } + }) + return customers_in diff --git a/erpnext/setup/doctype/territory/territory.py b/erpnext/setup/doctype/territory/territory.py index 095bd1c179..4f2ab70b2c 100644 --- a/erpnext/setup/doctype/territory/territory.py +++ b/erpnext/setup/doctype/territory/territory.py @@ -3,8 +3,6 @@ from __future__ import unicode_literals import frappe - - from frappe.utils import flt from frappe import _ @@ -14,6 +12,9 @@ class Territory(NestedSet): nsm_parent_field = 'parent_territory' def validate(self): + if frappe.db.sql("SELECT COUNT(name) FROM `tabTerritory` WHERE parent IS NULL")[0][0] > 1: + frappe.throw('Only one Root Territory is allowed, please select a Parent Territory!') + for d in self.get('targets') or []: if not flt(d.target_qty) and not flt(d.target_amount): frappe.throw(_("Either target qty or target amount is mandatory")) From b59f2780ebf974779b4b0236e67e3f1bbee7781a Mon Sep 17 00:00:00 2001 From: Abhishek Balam Date: Sat, 9 May 2020 22:09:02 +0530 Subject: [PATCH 04/21] fix: renamed view types, added default --- .../customer_acquisition_and_loyalty.js | 7 ++++--- .../customer_acquisition_and_loyalty.py | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/erpnext/selling/report/customer_acquisition_and_loyalty/customer_acquisition_and_loyalty.js b/erpnext/selling/report/customer_acquisition_and_loyalty/customer_acquisition_and_loyalty.js index 654614b4bb..c24d2e2bdd 100644 --- a/erpnext/selling/report/customer_acquisition_and_loyalty/customer_acquisition_and_loyalty.js +++ b/erpnext/selling/report/customer_acquisition_and_loyalty/customer_acquisition_and_loyalty.js @@ -4,11 +4,12 @@ frappe.query_reports["Customer Acquisition and Loyalty"] = { "filters": [ { - "fieldname":"view_type", + "fieldname": "view_type", "label": __("View Type"), "fieldtype": "Select", - "default": "Time Series", - "options": ["Time Series", "Territory Tree"] + "options": ["Monthly", "Territory Wise"], + "default": "Monthly", + "reqd": 1 }, { "fieldname":"company", diff --git a/erpnext/selling/report/customer_acquisition_and_loyalty/customer_acquisition_and_loyalty.py b/erpnext/selling/report/customer_acquisition_and_loyalty/customer_acquisition_and_loyalty.py index f2033f1fff..d8cc763ed9 100644 --- a/erpnext/selling/report/customer_acquisition_and_loyalty/customer_acquisition_and_loyalty.py +++ b/erpnext/selling/report/customer_acquisition_and_loyalty/customer_acquisition_and_loyalty.py @@ -52,7 +52,7 @@ def execute(filters=None): 'width': 150 } ] - if filters.get('view_type') == 'Territory Tree': + if filters.get('view_type') == 'Territory Wise': return get_data_by_territory(filters, common_columns) else: return get_data_by_time(filters, common_columns) From 7cbc902d90a3f43b985c9c12abbf0ab8caf82dc2 Mon Sep 17 00:00:00 2001 From: Abhishek Balam Date: Sat, 9 May 2020 22:35:28 +0530 Subject: [PATCH 05/21] removed validation for root node in territory, codacy recommended changed --- .../customer_acquisition_and_loyalty.js | 2 +- .../customer_acquisition_and_loyalty.py | 4 ++-- erpnext/setup/doctype/territory/territory.py | 2 -- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/erpnext/selling/report/customer_acquisition_and_loyalty/customer_acquisition_and_loyalty.js b/erpnext/selling/report/customer_acquisition_and_loyalty/customer_acquisition_and_loyalty.js index c24d2e2bdd..d93ffb7266 100644 --- a/erpnext/selling/report/customer_acquisition_and_loyalty/customer_acquisition_and_loyalty.js +++ b/erpnext/selling/report/customer_acquisition_and_loyalty/customer_acquisition_and_loyalty.js @@ -39,6 +39,6 @@ frappe.query_reports["Customer Acquisition and Loyalty"] = { if (data && data.bold) { value = value.bold(); } - return value + return value; } } \ No newline at end of file diff --git a/erpnext/selling/report/customer_acquisition_and_loyalty/customer_acquisition_and_loyalty.py b/erpnext/selling/report/customer_acquisition_and_loyalty/customer_acquisition_and_loyalty.py index d8cc763ed9..0121a8267f 100644 --- a/erpnext/selling/report/customer_acquisition_and_loyalty/customer_acquisition_and_loyalty.py +++ b/erpnext/selling/report/customer_acquisition_and_loyalty/customer_acquisition_and_loyalty.py @@ -162,7 +162,7 @@ def get_data_by_territory(filters, common_columns): return columns, data, None, None, None, 1 def update_groups(node, data, root_node, territory_dict): - ''' Adds values of child territories to parent node except root ''' + ''' Adds values of child territories to parent node except root. ''' parent_node = territory_dict[node]['parent'] if parent_node != root_node and parent_node: node_data = [x for x in data if x['territory'] == node][0] @@ -175,7 +175,7 @@ def update_groups(node, data, root_node, territory_dict): return data def get_customer_stats(filters, tree_view=False): - ''' Calculates number of new and repeated customers ''' + ''' Calculates number of new and repeated customers. ''' company_condition = '' if filters.get('company'): company_condition = ' and company=%(company)s' diff --git a/erpnext/setup/doctype/territory/territory.py b/erpnext/setup/doctype/territory/territory.py index 4f2ab70b2c..808b5386ab 100644 --- a/erpnext/setup/doctype/territory/territory.py +++ b/erpnext/setup/doctype/territory/territory.py @@ -12,8 +12,6 @@ class Territory(NestedSet): nsm_parent_field = 'parent_territory' def validate(self): - if frappe.db.sql("SELECT COUNT(name) FROM `tabTerritory` WHERE parent IS NULL")[0][0] > 1: - frappe.throw('Only one Root Territory is allowed, please select a Parent Territory!') for d in self.get('targets') or []: if not flt(d.target_qty) and not flt(d.target_amount): From 5ec5584319e18341e839d354bd251b9b7a382ca7 Mon Sep 17 00:00:00 2001 From: Abhishek Balam Date: Sun, 10 May 2020 00:24:43 +0530 Subject: [PATCH 06/21] In treeview, bold only for root territory, looks cleaner --- .../customer_acquisition_and_loyalty.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/selling/report/customer_acquisition_and_loyalty/customer_acquisition_and_loyalty.py b/erpnext/selling/report/customer_acquisition_and_loyalty/customer_acquisition_and_loyalty.py index 0121a8267f..b7bb021056 100644 --- a/erpnext/selling/report/customer_acquisition_and_loyalty/customer_acquisition_and_loyalty.py +++ b/erpnext/selling/report/customer_acquisition_and_loyalty/customer_acquisition_and_loyalty.py @@ -143,7 +143,7 @@ def get_data_by_territory(filters, common_columns): 'new_customer_revenue': new[1], 'repeat_customer_revenue': repeat[1], 'total_revenue': new[1] + repeat[1], - 'bold': 0 if condition else 1 + 'bold': 0 if indent else 1 } data.append(temp) node_list = [x for x in territory_dict.keys() if territory_dict[x]['is_group'] == 0] From 500dff63e764d7a679747546f15f1ee671a2fdc8 Mon Sep 17 00:00:00 2001 From: Abhishek Balam Date: Sun, 10 May 2020 14:53:59 +0530 Subject: [PATCH 07/21] fix: adjusted width of colums to see full column names, also fixes #21556 --- .../customer_acquisition_and_loyalty.py | 12 ++++---- .../territory_wise_sales.py | 30 +++++++++++-------- 2 files changed, 23 insertions(+), 19 deletions(-) diff --git a/erpnext/selling/report/customer_acquisition_and_loyalty/customer_acquisition_and_loyalty.py b/erpnext/selling/report/customer_acquisition_and_loyalty/customer_acquisition_and_loyalty.py index b7bb021056..6e3f397fd6 100644 --- a/erpnext/selling/report/customer_acquisition_and_loyalty/customer_acquisition_and_loyalty.py +++ b/erpnext/selling/report/customer_acquisition_and_loyalty/customer_acquisition_and_loyalty.py @@ -14,21 +14,21 @@ def execute(filters=None): 'fieldname': 'new_customers', 'fieldtype': 'Int', 'default': 0, - 'width': 100 + 'width': 150 }, { 'label': _('Repeat Customers'), 'fieldname': 'repeat_customers', 'fieldtype': 'Int', 'default': 0, - 'width': 100 + 'width': 150 }, { 'label': _('Total'), 'fieldname': 'total', 'fieldtype': 'Int', 'default': 0, - 'width': 100 + 'width': 150 }, { 'label': _('New Customer Revenue'), @@ -52,10 +52,10 @@ def execute(filters=None): 'width': 150 } ] - if filters.get('view_type') == 'Territory Wise': - return get_data_by_territory(filters, common_columns) - else: + if filters.get('view_type') == 'Monthly': return get_data_by_time(filters, common_columns) + else: + return get_data_by_territory(filters, common_columns) def get_data_by_time(filters, common_columns): # key yyyy-mm diff --git a/erpnext/selling/report/territory_wise_sales/territory_wise_sales.py b/erpnext/selling/report/territory_wise_sales/territory_wise_sales.py index f2db478686..e883500170 100644 --- a/erpnext/selling/report/territory_wise_sales/territory_wise_sales.py +++ b/erpnext/selling/report/territory_wise_sales/territory_wise_sales.py @@ -20,31 +20,36 @@ def get_columns(): "label": _("Territory"), "fieldname": "territory", "fieldtype": "Link", - "options": "Territory" + "options": "Territory", + "width": 150 }, { "label": _("Opportunity Amount"), "fieldname": "opportunity_amount", "fieldtype": "Currency", - "options": currency + "options": currency, + "width": 150 }, { "label": _("Quotation Amount"), "fieldname": "quotation_amount", "fieldtype": "Currency", - "options": currency + "options": currency, + "width": 150 }, { "label": _("Order Amount"), "fieldname": "order_amount", "fieldtype": "Currency", - "options": currency + "options": currency, + "width": 150 }, { "label": _("Billing Amount"), "fieldname": "billing_amount", "fieldtype": "Currency", - "options": currency + "options": currency, + "width": 150 } ] @@ -62,8 +67,7 @@ def get_data(filters=None): territory_opportunities = list(filter(lambda x: x.territory == territory.name, opportunities)) t_opportunity_names = [] if territory_opportunities: - t_opportunity_names = [t.name for t in territory_opportunities] - + t_opportunity_names = [t.name for t in territory_opportunities] territory_quotations = [] if t_opportunity_names and quotations: territory_quotations = list(filter(lambda x: x.opportunity in t_opportunity_names, quotations)) @@ -76,7 +80,7 @@ def get_data(filters=None): list(filter(lambda x: x.quotation in t_quotation_names, sales_orders)) t_order_names = [] if territory_orders: - t_order_names = [t.name for t in territory_orders] + t_order_names = [t.name for t in territory_orders] territory_invoices = list(filter(lambda x: x.sales_order in t_order_names, sales_invoices)) if t_order_names and sales_invoices else [] @@ -96,12 +100,12 @@ def get_opportunities(filters): if filters.get('transaction_date'): conditions = " WHERE transaction_date between {0} and {1}".format( - frappe.db.escape(filters['transaction_date'][0]), + frappe.db.escape(filters['transaction_date'][0]), frappe.db.escape(filters['transaction_date'][1])) - + if filters.company: if conditions: - conditions += " AND" + conditions += " AND" else: conditions += " WHERE" conditions += " company = %(company)s" @@ -115,7 +119,7 @@ def get_opportunities(filters): def get_quotations(opportunities): if not opportunities: return [] - + opportunity_names = [o.name for o in opportunities] return frappe.db.sql(""" @@ -155,5 +159,5 @@ def _get_total(doclist, amount_field="base_grand_total"): total = 0 for doc in doclist: total += doc.get(amount_field, 0) - + return total From 87776c335beb693c471608fa318a4997171f2dbd Mon Sep 17 00:00:00 2001 From: Abhishek Balam Date: Mon, 11 May 2020 15:18:40 +0530 Subject: [PATCH 08/21] code improvements --- .../customer_acquisition_and_loyalty.py | 45 ++++++++----------- 1 file changed, 18 insertions(+), 27 deletions(-) diff --git a/erpnext/selling/report/customer_acquisition_and_loyalty/customer_acquisition_and_loyalty.py b/erpnext/selling/report/customer_acquisition_and_loyalty/customer_acquisition_and_loyalty.py index 6e3f397fd6..38fbd60008 100644 --- a/erpnext/selling/report/customer_acquisition_and_loyalty/customer_acquisition_and_loyalty.py +++ b/erpnext/selling/report/customer_acquisition_and_loyalty/customer_acquisition_and_loyalty.py @@ -14,42 +14,42 @@ def execute(filters=None): 'fieldname': 'new_customers', 'fieldtype': 'Int', 'default': 0, - 'width': 150 + 'width': 125 }, { 'label': _('Repeat Customers'), 'fieldname': 'repeat_customers', 'fieldtype': 'Int', 'default': 0, - 'width': 150 + 'width': 125 }, { 'label': _('Total'), 'fieldname': 'total', 'fieldtype': 'Int', 'default': 0, - 'width': 150 + 'width': 100 }, { 'label': _('New Customer Revenue'), 'fieldname': 'new_customer_revenue', 'fieldtype': 'Currency', 'default': 0.0, - 'width': 150 + 'width': 175 }, { 'label': _('Repeat Customer Revenue'), 'fieldname': 'repeat_customer_revenue', 'fieldtype': 'Currency', 'default': 0.0, - 'width': 150 + 'width': 175 }, { 'label': _('Total Revenue'), 'fieldname': 'total_revenue', 'fieldtype': 'Currency', 'default': 0.0, - 'width': 150 + 'width': 175 } ] if filters.get('view_type') == 'Monthly': @@ -148,13 +148,13 @@ def get_data_by_territory(filters, common_columns): data.append(temp) node_list = [x for x in territory_dict.keys() if territory_dict[x]['is_group'] == 0] root_node = [x for x in territory_dict.keys() if territory_dict[x]['parent'] is None][0] + root_data = [x for x in data if x['territory'] == root_node][0] for node in node_list: data = update_groups(node, data, root_node, territory_dict) for group in [x for x in territory_dict.keys() if territory_dict[x]['parent'] == root_node]: group_data = [x for x in data if x['territory'] == group][0] - root_data = [x for x in data if x['territory'] == root_node][0] for key in group_data.keys(): if key not in ['indent', 'territory', 'bold']: root_data[key] += group_data[key] @@ -162,7 +162,7 @@ def get_data_by_territory(filters, common_columns): return columns, data, None, None, None, 1 def update_groups(node, data, root_node, territory_dict): - ''' Adds values of child territories to parent node except root. ''' + """ Adds values of child territories to parent node except root. """ parent_node = territory_dict[node]['parent'] if parent_node != root_node and parent_node: node_data = [x for x in data if x['territory'] == node][0] @@ -175,37 +175,28 @@ def update_groups(node, data, root_node, territory_dict): return data def get_customer_stats(filters, tree_view=False): - ''' Calculates number of new and repeated customers. ''' + """ Calculates number of new and repeated customers. """ company_condition = '' if filters.get('company'): company_condition = ' and company=%(company)s' customers = [] customers_in = {} - new_customers_in = {} - repeat_customers_in = {} for si in frappe.db.sql('''select territory, posting_date, customer, base_grand_total from `tabSales Invoice` where docstatus=1 and posting_date <= %(to_date)s and posting_date >= %(from_date)s {company_condition} order by posting_date'''.format(company_condition=company_condition), filters, as_dict=1): - if tree_view: - key = si.territory - else: - key = si.posting_date.strftime('%Y-%m') + + key = si.territory if tree_view else si.posting_date.strftime('%Y-%m') + customers_in.setdefault(key, {'new': [0, 0.0], 'repeat': [0, 0.0]}) + if not si.customer in customers: - new_customers_in.setdefault(key, [0, 0.0]) - new_customers_in[key][0] += 1 - new_customers_in[key][1] += si.base_grand_total + customers_in[key]['new'][0] += 1 + customers_in[key]['new'][1] += si.base_grand_total customers.append(si.customer) else: - repeat_customers_in.setdefault(key, [0, 0.0]) - repeat_customers_in[key][0] += 1 - repeat_customers_in[key][1] += si.base_grand_total - customers_in.update({ - key: { - 'new': new_customers_in[key] if new_customers_in.get(key) else [0, 0.0], - 'repeat': repeat_customers_in[key] if repeat_customers_in.get(key) else [0, 0.0], - } - }) + customers_in[key]['repeat'][0] += 1 + customers_in[key]['repeat'][1] += si.base_grand_total + return customers_in From 85a89812a42a8d8e5f80dbec89c77e5fe5e82988 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Mon, 11 May 2020 19:23:18 +0530 Subject: [PATCH 09/21] fix: Run income-tax-slab patch only if slab already exists in payroll period (#21684) --- ...ve_tax_slabs_from_payroll_period_to_income_tax_slab.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/erpnext/patches/v13_0/move_tax_slabs_from_payroll_period_to_income_tax_slab.py b/erpnext/patches/v13_0/move_tax_slabs_from_payroll_period_to_income_tax_slab.py index 179be2cfde..ec94cd01d1 100644 --- a/erpnext/patches/v13_0/move_tax_slabs_from_payroll_period_to_income_tax_slab.py +++ b/erpnext/patches/v13_0/move_tax_slabs_from_payroll_period_to_income_tax_slab.py @@ -7,7 +7,7 @@ import frappe from frappe.model.utils.rename_field import rename_field def execute(): - if not frappe.db.table_exists("Payroll Period"): + if not (frappe.db.table_exists("Payroll Period") and frappe.db.table_exists("Taxable Salary Slab")): return for doctype in ("income_tax_slab", "salary_structure_assignment", "employee_other_income", "income_tax_slab_other_charges"): @@ -60,6 +60,9 @@ def execute(): """, (income_tax_slab.name, company.name, period.start_date)) # move other incomes to separate document + if not frappe.db.table_exists("Employee Tax Exemption Proof Submission"): + return + migrated = [] proofs = frappe.get_all("Employee Tax Exemption Proof Submission", filters = {'docstatus': 1}, @@ -79,6 +82,9 @@ def execute(): except: pass + if not frappe.db.table_exists("Employee Tax Exemption Declaration"): + return + declerations = frappe.get_all("Employee Tax Exemption Declaration", filters = {'docstatus': 1}, fields =['payroll_period', 'employee', 'company', 'income_from_other_sources'] From 28c9468dad126985ee062d9d155449124196e1e7 Mon Sep 17 00:00:00 2001 From: sahil28297 <37302950+sahil28297@users.noreply.github.com> Date: Mon, 11 May 2020 19:28:10 +0530 Subject: [PATCH 10/21] fix(patch): use translated string while setting notification template (#21679) --- erpnext/patches/v11_0/set_default_email_template_in_hr.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/erpnext/patches/v11_0/set_default_email_template_in_hr.py b/erpnext/patches/v11_0/set_default_email_template_in_hr.py index 14954fbeb3..4622376109 100644 --- a/erpnext/patches/v11_0/set_default_email_template_in_hr.py +++ b/erpnext/patches/v11_0/set_default_email_template_in_hr.py @@ -1,8 +1,9 @@ from __future__ import unicode_literals +from frappe import _ import frappe def execute(): hr_settings = frappe.get_single("HR Settings") - hr_settings.leave_approval_notification_template = "Leave Approval Notification" - hr_settings.leave_status_notification_template = "Leave Status Notification" - hr_settings.save() \ No newline at end of file + hr_settings.leave_approval_notification_template = _("Leave Approval Notification") + hr_settings.leave_status_notification_template = _("Leave Status Notification") + hr_settings.save() From 30f26b4457e0633266c33ab19d0ab0096db89490 Mon Sep 17 00:00:00 2001 From: Chinmay Pai Date: Mon, 11 May 2020 19:54:46 +0530 Subject: [PATCH 11/21] fix: do not setup charts if not setup_complete or no company found (#21670) * fix: do not setup charts if not setup_complete or no company found Signed-off-by: Chinmay D. Pai * chore: remove check for setup_complete moved the check for setup_complete to frappe Signed-off-by: Chinmay D. Pai * chore: return an empty dict from get_data if company not set Signed-off-by: Chinmay D. Pai --- erpnext/accounts/dashboard_fixtures.py | 176 +++++++++++++------------ 1 file changed, 90 insertions(+), 86 deletions(-) diff --git a/erpnext/accounts/dashboard_fixtures.py b/erpnext/accounts/dashboard_fixtures.py index a106f70dd0..cdd166134f 100644 --- a/erpnext/accounts/dashboard_fixtures.py +++ b/erpnext/accounts/dashboard_fixtures.py @@ -1,15 +1,22 @@ # Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors # License: GNU General Public License v3. See license.txt +from erpnext import get_default_company import frappe import json def get_data(): - return frappe._dict({ - "dashboards": get_dashboards(), - "charts": get_charts(), + data = frappe._dict({ + "dashboards": [], + "charts": [] }) + company = get_company_for_dashboards() + if company: + company_doc = frappe.get_doc("Company", company) + data.dashboards = get_dashboards() + data.charts = get_charts(company_doc) + return data def get_dashboards(): return [{ @@ -24,88 +31,87 @@ def get_dashboards(): ] }] -def get_charts(): - company = frappe.get_doc("Company", get_company_for_dashboards()) +def get_charts(company): income_account = company.default_income_account or get_account("Income Account", company.name) expense_account = company.default_expense_account or get_account("Expense Account", company.name) bank_account = company.default_bank_account or get_account("Bank", company.name) return [ - { - "doctype": "Dashboard Chart", - "time_interval": "Quarterly", - "name": "Income", - "chart_name": "Income", - "timespan": "Last Year", - "color": None, - "filters_json": json.dumps({"company": company.name, "account": income_account}), - "source": "Account Balance Timeline", - "chart_type": "Custom", - "timeseries": 1, - "owner": "Administrator", - "type": "Line" - }, - { - "doctype": "Dashboard Chart", - "time_interval": "Quarterly", - "name": "Expenses", - "chart_name": "Expenses", - "timespan": "Last Year", - "color": None, - "filters_json": json.dumps({"company": company.name, "account": expense_account}), - "source": "Account Balance Timeline", - "chart_type": "Custom", - "timeseries": 1, - "owner": "Administrator", - "type": "Line" - }, - { - "doctype": "Dashboard Chart", - "time_interval": "Quarterly", - "name": "Bank Balance", - "chart_name": "Bank Balance", - "timespan": "Last Year", - "color": "#ffb868", - "filters_json": json.dumps({"company": company.name, "account": bank_account}), - "source": "Account Balance Timeline", - "chart_type": "Custom", - "timeseries": 1, - "owner": "Administrator", - "type": "Line" - }, - { - "doctype": "Dashboard Chart", - "time_interval": "Monthly", - "name": "Incoming Bills (Purchase Invoice)", - "chart_name": "Incoming Bills (Purchase Invoice)", - "timespan": "Last Year", - "color": "#a83333", - "value_based_on": "base_grand_total", - "filters_json": json.dumps({}), - "chart_type": "Sum", - "timeseries": 1, - "based_on": "posting_date", - "owner": "Administrator", - "document_type": "Purchase Invoice", - "type": "Bar" - }, - { - "doctype": "Dashboard Chart", - "time_interval": "Monthly", - "name": "Outgoing Bills (Sales Invoice)", - "chart_name": "Outgoing Bills (Sales Invoice)", - "timespan": "Last Year", - "color": "#7b933d", - "value_based_on": "base_grand_total", - "filters_json": json.dumps({}), - "chart_type": "Sum", - "timeseries": 1, - "based_on": "posting_date", - "owner": "Administrator", - "document_type": "Sales Invoice", - "type": "Bar" - } - ] + { + "doctype": "Dashboard Chart", + "time_interval": "Quarterly", + "name": "Income", + "chart_name": "Income", + "timespan": "Last Year", + "color": None, + "filters_json": json.dumps({"company": company.name, "account": income_account}), + "source": "Account Balance Timeline", + "chart_type": "Custom", + "timeseries": 1, + "owner": "Administrator", + "type": "Line" + }, + { + "doctype": "Dashboard Chart", + "time_interval": "Quarterly", + "name": "Expenses", + "chart_name": "Expenses", + "timespan": "Last Year", + "color": None, + "filters_json": json.dumps({"company": company.name, "account": expense_account}), + "source": "Account Balance Timeline", + "chart_type": "Custom", + "timeseries": 1, + "owner": "Administrator", + "type": "Line" + }, + { + "doctype": "Dashboard Chart", + "time_interval": "Quarterly", + "name": "Bank Balance", + "chart_name": "Bank Balance", + "timespan": "Last Year", + "color": "#ffb868", + "filters_json": json.dumps({"company": company.name, "account": bank_account}), + "source": "Account Balance Timeline", + "chart_type": "Custom", + "timeseries": 1, + "owner": "Administrator", + "type": "Line" + }, + { + "doctype": "Dashboard Chart", + "time_interval": "Monthly", + "name": "Incoming Bills (Purchase Invoice)", + "chart_name": "Incoming Bills (Purchase Invoice)", + "timespan": "Last Year", + "color": "#a83333", + "value_based_on": "base_grand_total", + "filters_json": json.dumps({}), + "chart_type": "Sum", + "timeseries": 1, + "based_on": "posting_date", + "owner": "Administrator", + "document_type": "Purchase Invoice", + "type": "Bar" + }, + { + "doctype": "Dashboard Chart", + "time_interval": "Monthly", + "name": "Outgoing Bills (Sales Invoice)", + "chart_name": "Outgoing Bills (Sales Invoice)", + "timespan": "Last Year", + "color": "#7b933d", + "value_based_on": "base_grand_total", + "filters_json": json.dumps({}), + "chart_type": "Sum", + "timeseries": 1, + "based_on": "posting_date", + "owner": "Administrator", + "document_type": "Sales Invoice", + "type": "Bar" + } + ] def get_account(account_type, company): accounts = frappe.get_list("Account", filters={"account_type": account_type, "company": company}) @@ -113,11 +119,9 @@ def get_account(account_type, company): return accounts[0].name def get_company_for_dashboards(): - company = frappe.defaults.get_defaults().company - if company: - return company - else: + company = get_default_company() + if not company: company_list = frappe.get_list("Company") if company_list: - return company_list[0].name - return None \ No newline at end of file + company = company_list[0].name + return company From b52e40037abe4453934cc8673b3aa36bb35aaf59 Mon Sep 17 00:00:00 2001 From: Deepesh Garg <42651287+deepeshgarg007@users.noreply.github.com> Date: Mon, 11 May 2020 20:01:57 +0530 Subject: [PATCH 12/21] fix: Remove domain restriction from Location doctype (#21659) --- erpnext/assets/doctype/location/location.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/erpnext/assets/doctype/location/location.json b/erpnext/assets/doctype/location/location.json index 6a35130f30..f56fd05d98 100644 --- a/erpnext/assets/doctype/location/location.json +++ b/erpnext/assets/doctype/location/location.json @@ -141,7 +141,7 @@ ], "is_tree": 1, "links": [], - "modified": "2020-03-18 18:00:08.885805", + "modified": "2020-05-08 16:11:11.375701", "modified_by": "Administrator", "module": "Assets", "name": "Location", @@ -221,7 +221,6 @@ } ], "quick_entry": 1, - "restrict_to_domain": "Agriculture", "show_name_in_global_search": 1, "sort_field": "modified", "sort_order": "DESC", From 97715f2877320486353a17662ea5f57f8eaf4f8c Mon Sep 17 00:00:00 2001 From: Marica Date: Mon, 11 May 2020 20:45:37 +0530 Subject: [PATCH 13/21] fix: Message for missing valuation rate (#21686) --- erpnext/stock/doctype/stock_entry/stock_entry.py | 2 +- erpnext/stock/stock_ledger.py | 13 +++++++++++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 10f2555475..62c9eb1eb2 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -470,7 +470,7 @@ class StockEntry(StockController): "qty": item.s_warehouse and -1*flt(item.transfer_qty) or flt(item.transfer_qty), "serial_no": item.serial_no, "voucher_type": self.doctype, - "voucher_no": item.name, + "voucher_no": self.name, "company": self.company, "allow_zero_valuation": item.allow_zero_valuation_rate, }) diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index b4cb8cadb4..e1b3730f2f 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -548,7 +548,16 @@ def get_valuation_rate(item_code, warehouse, voucher_type, voucher_no, if not allow_zero_rate and not valuation_rate and raise_error_if_no_rate \ and cint(erpnext.is_perpetual_inventory_enabled(company)): frappe.local.message_log = [] - frappe.throw(_("Valuation rate not found for the Item {0}, which is required to do accounting entries for {1} {2}. If the item is transacting as a zero valuation rate item in the {1}, please mention that in the {1} Item table. Otherwise, please create an incoming stock transaction for the item or mention valuation rate in the Item record, and then try submiting / cancelling this entry.") - .format(item_code, voucher_type, voucher_no)) + form_link = frappe.utils.get_link_to_form("Item", item_code) + + message = _("Valuation Rate for the Item {0}, is required to do accounting entries for {1} {2}.").format(form_link, voucher_type, voucher_no) + message += "

" + _(" Here are the options to proceed:") + solutions = "
  • " + _("If the item is transacting as a Zero Valuation Rate item in this entry, please enable 'Allow Zero Valuation Rate' in the {0} Item table.").format(voucher_type) + "
  • " + solutions += "
  • " + _("If not, you can Cancel / Submit this entry ") + _("{0}").format(frappe.bold("after")) + _(" performing either one below:") + "
  • " + sub_solutions = "
    • " + _("Create an incoming stock transaction for the Item.") + "
    • " + sub_solutions += "
    • " + _("Mention Valuation Rate in the Item master.") + "
    " + msg = message + solutions + sub_solutions + "" + + frappe.throw(msg=msg, title=_("Valuation Rate Missing")) return valuation_rate From 498f6583210e730207e922823c5e6d87579c55b8 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Mon, 11 May 2020 20:47:08 +0530 Subject: [PATCH 14/21] refactor: rename getting started to home (#21674) * feat: rename getting started to home * feat: added patch for renaming --- erpnext/patches.txt | 1 + .../getting_started.json => home/home.json} | 14 ++++---------- 2 files changed, 5 insertions(+), 10 deletions(-) rename erpnext/setup/desk_page/{getting_started/getting_started.json => home/home.json} (97%) diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 3f90d36916..f72172474c 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -680,3 +680,4 @@ erpnext.patches.v12_0.update_appointment_reminder_scheduler_entry erpnext.patches.v12_0.retain_permission_rules_for_video_doctype erpnext.patches.v13_0.patch_to_fix_reverse_linking_in_additional_salary_encashment_and_incentive execute:frappe.delete_doc_if_exists("Page", "appointment-analytic") +execute:frappe.rename_doc("Desk Page", "Getting Started", "Home", force=True) diff --git a/erpnext/setup/desk_page/getting_started/getting_started.json b/erpnext/setup/desk_page/home/home.json similarity index 97% rename from erpnext/setup/desk_page/getting_started/getting_started.json rename to erpnext/setup/desk_page/home/home.json index 63d8984c40..63cd5c5cec 100644 --- a/erpnext/setup/desk_page/getting_started/getting_started.json +++ b/erpnext/setup/desk_page/home/home.json @@ -47,26 +47,20 @@ } ], "category": "Modules", - "charts": [ - { - "chart_name": "Bank Balance", - "label": "Bank Balance" - } - ], + "charts": [], "creation": "2020-01-23 13:46:38.833076", "developer_mode_only": 0, "disable_user_customization": 0, "docstatus": 0, "doctype": "Desk Page", "extends_another_page": 0, - "icon": "", "idx": 0, "is_standard": 1, - "label": "Getting Started", - "modified": "2020-04-01 11:30:19.763099", + "label": "Home", + "modified": "2020-05-11 10:20:37.358701", "modified_by": "Administrator", "module": "Setup", - "name": "Getting Started", + "name": "Home", "owner": "Administrator", "pin_to_bottom": 0, "pin_to_top": 1, From 411b12590648e602565829d6e5f26b57ce56b62c Mon Sep 17 00:00:00 2001 From: Abhishek Balam Date: Tue, 12 May 2020 15:25:35 +0530 Subject: [PATCH 15/21] new parent updating logic, made requested changes --- .../customer_acquisition_and_loyalty.py | 35 ++++++------------- 1 file changed, 10 insertions(+), 25 deletions(-) diff --git a/erpnext/selling/report/customer_acquisition_and_loyalty/customer_acquisition_and_loyalty.py b/erpnext/selling/report/customer_acquisition_and_loyalty/customer_acquisition_and_loyalty.py index 38fbd60008..88bd9c135d 100644 --- a/erpnext/selling/report/customer_acquisition_and_loyalty/customer_acquisition_and_loyalty.py +++ b/erpnext/selling/report/customer_acquisition_and_loyalty/customer_acquisition_and_loyalty.py @@ -61,13 +61,13 @@ def get_data_by_time(filters, common_columns): # key yyyy-mm columns = [ { - 'label': 'Year', + 'label': _('Year'), 'fieldname': 'year', 'fieldtype': 'Data', 'width': 100 }, { - 'label': 'Month', + 'label': _('Month'), 'fieldname': 'month', 'fieldtype': 'Data', 'width': 100 @@ -136,6 +136,7 @@ def get_data_by_territory(filters, common_columns): repeat = customers_in[name]['repeat'] if condition else [0, 0.0] temp = { 'territory': name, + 'parent_territory': territory_dict[name]['parent'], 'indent': indent, 'new_customers': new[0], 'repeat_customers': repeat[0], @@ -146,34 +147,18 @@ def get_data_by_territory(filters, common_columns): 'bold': 0 if indent else 1 } data.append(temp) - node_list = [x for x in territory_dict.keys() if territory_dict[x]['is_group'] == 0] - root_node = [x for x in territory_dict.keys() if territory_dict[x]['parent'] is None][0] - root_data = [x for x in data if x['territory'] == root_node][0] - for node in node_list: - data = update_groups(node, data, root_node, territory_dict) + loop_data = sorted(data, key=lambda k: k['indent'], reverse=True) - for group in [x for x in territory_dict.keys() if territory_dict[x]['parent'] == root_node]: - group_data = [x for x in data if x['territory'] == group][0] - for key in group_data.keys(): - if key not in ['indent', 'territory', 'bold']: - root_data[key] += group_data[key] + for ld in loop_data: + if ld['parent_territory']: + parent_data = [x for x in data if x['territory'] == ld['parent_territory']][0] + for key in parent_data.keys(): + if key not in ['indent', 'territory', 'parent_territory', 'bold']: + parent_data[key] += ld[key] return columns, data, None, None, None, 1 -def update_groups(node, data, root_node, territory_dict): - """ Adds values of child territories to parent node except root. """ - parent_node = territory_dict[node]['parent'] - if parent_node != root_node and parent_node: - node_data = [x for x in data if x['territory'] == node][0] - parent_data = [x for x in data if x['territory'] == parent_node][0] - for key in parent_data.keys(): - if key not in ['indent', 'territory', 'bold']: - parent_data[key] += node_data[key] - return update_groups(parent_node, data, root_node, territory_dict) - else: - return data - def get_customer_stats(filters, tree_view=False): """ Calculates number of new and repeated customers. """ company_condition = '' From 673e704bb5daefbc3c128bafee2f7051eb60d7cb Mon Sep 17 00:00:00 2001 From: Abhishek Balam Date: Tue, 12 May 2020 19:09:27 +0530 Subject: [PATCH 16/21] typo in error message in loan_security_pledge.py --- .../doctype/loan_security_pledge/loan_security_pledge.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/loan_management/doctype/loan_security_pledge/loan_security_pledge.py b/erpnext/loan_management/doctype/loan_security_pledge/loan_security_pledge.py index f97e5965a5..961c05c9c1 100644 --- a/erpnext/loan_management/doctype/loan_security_pledge/loan_security_pledge.py +++ b/erpnext/loan_management/doctype/loan_security_pledge/loan_security_pledge.py @@ -38,7 +38,7 @@ class LoanSecurityPledge(Document): for pledge in self.securities: if not pledge.qty and not pledge.amount: - frappe.throw(_("Qty or Amount is mandatroy for loan security")) + frappe.throw(_("Qty or Amount is mandatory for loan security!")) if not (self.loan_application and pledge.loan_security_price): pledge.loan_security_price = get_loan_security_price(pledge.loan_security) From 1aaedd68b9d7213804a6e244d4783674cc8865d2 Mon Sep 17 00:00:00 2001 From: Anupam K Date: Wed, 13 May 2020 17:48:11 +0530 Subject: [PATCH 17/21] Twitter and LinkedIn Auth fix --- .../linkedin_settings/linkedin_settings.py | 4 +-- .../twitter_settings/twitter_settings.json | 34 +++++++++---------- .../twitter_settings/twitter_settings.py | 14 ++++---- 3 files changed, 26 insertions(+), 26 deletions(-) diff --git a/erpnext/crm/doctype/linkedin_settings/linkedin_settings.py b/erpnext/crm/doctype/linkedin_settings/linkedin_settings.py index bdde9eed37..377e061fdf 100644 --- a/erpnext/crm/doctype/linkedin_settings/linkedin_settings.py +++ b/erpnext/crm/doctype/linkedin_settings/linkedin_settings.py @@ -15,7 +15,7 @@ class LinkedInSettings(Document): params = urlencode({ "response_type":"code", "client_id": self.consumer_key, - "redirect_uri": get_site_url(frappe.local.site) + "/api/method/erpnext.crm.doctype.linkedin_settings.linkedin_settings.callback?", + "redirect_uri": "{0}/api/method/erpnext.crm.doctype.linkedin_settings.linkedin_settings.callback?".format(frappe.utils.get_url()), "scope": "r_emailaddress w_organization_social r_basicprofile r_liteprofile r_organization_social rw_organization_admin w_member_social" }) @@ -30,7 +30,7 @@ class LinkedInSettings(Document): "code": code, "client_id": self.consumer_key, "client_secret": self.get_password(fieldname="consumer_secret"), - "redirect_uri": get_site_url(frappe.local.site) + "/api/method/erpnext.crm.doctype.linkedin_settings.linkedin_settings.callback?", + "redirect_uri": "{0}/api/method/erpnext.crm.doctype.linkedin_settings.linkedin_settings.callback?".format(frappe.utils.get_url()), } headers = { "Content-Type": "application/x-www-form-urlencoded" diff --git a/erpnext/crm/doctype/twitter_settings/twitter_settings.json b/erpnext/crm/doctype/twitter_settings/twitter_settings.json index f92e7f0495..36776e5c20 100644 --- a/erpnext/crm/doctype/twitter_settings/twitter_settings.json +++ b/erpnext/crm/doctype/twitter_settings/twitter_settings.json @@ -11,8 +11,8 @@ "consumer_key", "column_break_5", "consumer_secret", - "oauth_token", - "oauth_secret", + "access_token", + "access_token_secret", "session_status" ], "fields": [ @@ -41,20 +41,6 @@ "label": "API Secret Key", "reqd": 1 }, - { - "fieldname": "oauth_token", - "fieldtype": "Data", - "hidden": 1, - "label": "OAuth Token", - "read_only": 1 - }, - { - "fieldname": "oauth_secret", - "fieldtype": "Password", - "hidden": 1, - "label": "OAuth Token Secret", - "read_only": 1 - }, { "fieldname": "column_break_5", "fieldtype": "Column Break" @@ -72,12 +58,26 @@ "label": "Session Status", "options": "Expired\nActive", "read_only": 1 + }, + { + "fieldname": "access_token", + "fieldtype": "Data", + "hidden": 1, + "label": "Access Token", + "read_only": 1 + }, + { + "fieldname": "access_token_secret", + "fieldtype": "Data", + "hidden": 1, + "label": "Access Token Secret", + "read_only": 1 } ], "image_field": "profile_pic", "issingle": 1, "links": [], - "modified": "2020-04-21 22:06:43.726798", + "modified": "2020-05-13 17:50:47.934776", "modified_by": "Administrator", "module": "CRM", "name": "Twitter Settings", diff --git a/erpnext/crm/doctype/twitter_settings/twitter_settings.py b/erpnext/crm/doctype/twitter_settings/twitter_settings.py index 7616b4c027..976a23dfc7 100644 --- a/erpnext/crm/doctype/twitter_settings/twitter_settings.py +++ b/erpnext/crm/doctype/twitter_settings/twitter_settings.py @@ -31,13 +31,13 @@ class TwitterSettings(Document): try: auth.get_access_token(oauth_verifier) - api = self.get_api() + api = self.get_api(auth.access_token, auth.access_token_secret) user = api.me() profile_pic = (user._json["profile_image_url"]).replace("_normal","") frappe.db.set_value(self.doctype, self.name, { - "oauth_token" : auth.access_token, - "oauth_secret" : auth.access_token_secret, + "access_token" : auth.access_token, + "access_token_secret" : auth.access_token_secret, "account_name" : user._json["screen_name"], "profile_pic" : profile_pic, "session_status" : "Active" @@ -49,11 +49,11 @@ class TwitterSettings(Document): frappe.msgprint(_("Error! Failed to get access token.")) frappe.throw(_('Invalid Consumer Key or Consumer Secret Key')) - def get_api(self): + def get_api(self, access_token, access_token_secret): # authentication of consumer key and secret auth = tweepy.OAuthHandler(self.consumer_key, self.get_password(fieldname="consumer_secret")) # authentication of access token and secret - auth.set_access_token(self.oauth_token, self.get_password(fieldname="oauth_secret")) + auth.set_access_token(access_token, access_token_secret) return tweepy.API(auth) @@ -67,13 +67,13 @@ class TwitterSettings(Document): def upload_image(self, media): media = get_file_path(media) - api = self.get_api() + api = self.get_api(self.access_token, self.access_token_secret) media = api.media_upload(media) return media.media_id def send_tweet(self, text, media_id=None): - api = self.get_api() + api = self.get_api(self.access_token, self.access_token_secret) try: if media_id: response = api.update_status(status = text, media_ids = [media_id]) From bd7e5358857b66f5025e38ab3cf82d589dec6506 Mon Sep 17 00:00:00 2001 From: Afshan <33727827+AfshanKhan@users.noreply.github.com> Date: Wed, 13 May 2020 19:48:42 +0530 Subject: [PATCH 18/21] Fix: Set General Ledger 'Group By' filter as 'Group by Voucher(Consolidated)' when opened from Invoice (#21673) * fix for issue #21419 * changing group by filter to default Group by Voucher (Consolidated) --- erpnext/accounts/report/general_ledger/general_ledger.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/report/general_ledger/general_ledger.js b/erpnext/accounts/report/general_ledger/general_ledger.js index 1188beaa0f..2aecd6b717 100644 --- a/erpnext/accounts/report/general_ledger/general_ledger.js +++ b/erpnext/accounts/report/general_ledger/general_ledger.js @@ -53,7 +53,7 @@ frappe.query_reports["General Ledger"] = { "label": __("Voucher No"), "fieldtype": "Data", on_change: function() { - frappe.query_report.set_filter_value('group_by', ""); + frappe.query_report.set_filter_value('group_by', "Group by Voucher (Consolidated)"); } }, { From dde39c3d1aa323072de5703b8c30eabe07b3960c Mon Sep 17 00:00:00 2001 From: Suraj Shetty <13928957+surajshetty3416@users.noreply.github.com> Date: Thu, 14 May 2020 12:09:36 +0530 Subject: [PATCH 19/21] chore: Drop Python2 support (#21704) * chore: Drop Python2 support * test: Fix test redundancy by removing countries 3 countries seems ennough to test coa template feature --- .travis.yml | 17 ++--------------- erpnext/setup/doctype/company/test_company.py | 4 +--- 2 files changed, 3 insertions(+), 18 deletions(-) diff --git a/.travis.yml b/.travis.yml index 213445b806..77d427e5a5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,5 @@ -dist: trusty - language: python +dist: trusty git: depth: 1 @@ -14,21 +13,10 @@ addons: jobs: include: - - name: "Python 2.7 Server Side Test" - python: 2.7 - script: bench --site test_site run-tests --app erpnext --coverage - - name: "Python 3.6 Server Side Test" python: 3.6 script: bench --site test_site run-tests --app erpnext --coverage - - name: "Python 2.7 Patch Test" - python: 2.7 - before_script: - - wget http://build.erpnext.com/20171108_190013_955977f8_database.sql.gz - - bench --site test_site --force restore ~/frappe-bench/20171108_190013_955977f8_database.sql.gz - script: bench --site test_site migrate - - name: "Python 3.6 Patch Test" python: 3.6 before_script: @@ -40,8 +28,7 @@ install: - cd ~ - nvm install 10 - - git clone https://github.com/frappe/bench --depth 1 - - pip install -e ./bench + - pip install frappe-bench - git clone https://github.com/frappe/frappe --branch $TRAVIS_BRANCH --depth 1 - bench init --skip-assets --frappe-path ~/frappe --python $(which python) frappe-bench diff --git a/erpnext/setup/doctype/company/test_company.py b/erpnext/setup/doctype/company/test_company.py index b37cc17ba9..29f6c3731d 100644 --- a/erpnext/setup/doctype/company/test_company.py +++ b/erpnext/setup/doctype/company/test_company.py @@ -47,9 +47,7 @@ class TestCompany(unittest.TestCase): frappe.delete_doc("Company", "COA from Existing Company") def test_coa_based_on_country_template(self): - countries = ["India", "Brazil", "United Arab Emirates", "Canada", "Germany", "France", - "Guatemala", "Indonesia", "Italy", "Mexico", "Nicaragua", "Netherlands", "Singapore", - "Brazil", "Argentina", "Hungary", "Taiwan"] + countries = ["Canada", "Germany", "France"] for country in countries: templates = get_charts_for_country(country) From 39cb749f955f0fe40dc2289cdbc32bcb7f9ba939 Mon Sep 17 00:00:00 2001 From: prssanna Date: Thu, 14 May 2020 18:25:58 +0530 Subject: [PATCH 20/21] fix: add heatmap_year parameter to get --- .../account_balance_timeline/account_balance_timeline.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/dashboard_chart_source/account_balance_timeline/account_balance_timeline.py b/erpnext/accounts/dashboard_chart_source/account_balance_timeline/account_balance_timeline.py index 5decccb486..39bf4b053a 100644 --- a/erpnext/accounts/dashboard_chart_source/account_balance_timeline/account_balance_timeline.py +++ b/erpnext/accounts/dashboard_chart_source/account_balance_timeline/account_balance_timeline.py @@ -14,7 +14,7 @@ from frappe.utils.nestedset import get_descendants_of @frappe.whitelist() @cache_source def get(chart_name = None, chart = None, no_cache = None, filters = None, from_date = None, - to_date = None, timespan = None, time_interval = None): + to_date = None, timespan = None, time_interval = None, heatmap_year = None): if chart_name: chart = frappe.get_doc('Dashboard Chart', chart_name) else: From 41b47a68b36a377d040784b3125e6a2b62d931a9 Mon Sep 17 00:00:00 2001 From: Saqib Date: Fri, 15 May 2020 04:24:36 +0530 Subject: [PATCH 21/21] fix: item price not fetching when customer is unset in item price (#21488) * fix: item price not fetching when customer is unset in item price * fix: item price of selling type has hidden supplier value * fix: remove test variable * fix: test * patch: invalid customer/supplier based on item price type * fix: invalid query * fix: patch Co-authored-by: Marica --- erpnext/patches.txt | 1 + ...stomer_supplier_based_on_type_of_item_price.py | 15 +++++++++++++++ erpnext/stock/doctype/item_price/item_price.py | 7 +++++++ erpnext/stock/get_item_details.py | 2 +- 4 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 erpnext/patches/v12_0/unset_customer_supplier_based_on_type_of_item_price.py diff --git a/erpnext/patches.txt b/erpnext/patches.txt index f72172474c..0edadcc66d 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -681,3 +681,4 @@ erpnext.patches.v12_0.retain_permission_rules_for_video_doctype erpnext.patches.v13_0.patch_to_fix_reverse_linking_in_additional_salary_encashment_and_incentive execute:frappe.delete_doc_if_exists("Page", "appointment-analytic") execute:frappe.rename_doc("Desk Page", "Getting Started", "Home", force=True) +erpnext.patches.v12_0.unset_customer_supplier_based_on_type_of_item_price diff --git a/erpnext/patches/v12_0/unset_customer_supplier_based_on_type_of_item_price.py b/erpnext/patches/v12_0/unset_customer_supplier_based_on_type_of_item_price.py new file mode 100644 index 0000000000..60aec05466 --- /dev/null +++ b/erpnext/patches/v12_0/unset_customer_supplier_based_on_type_of_item_price.py @@ -0,0 +1,15 @@ +from __future__ import unicode_literals +import frappe + +def execute(): + invalid_selling_item_price = frappe.db.sql( + """SELECT name FROM `tabItem Price` WHERE selling = 1 and buying = 0 and (supplier IS NOT NULL or supplier = '')""" + ) + invalid_buying_item_price = frappe.db.sql( + """SELECT name FROM `tabItem Price` WHERE selling = 0 and buying = 1 and (customer IS NOT NULL or customer = '')""" + ) + docs_to_modify = invalid_buying_item_price + invalid_selling_item_price + for d in docs_to_modify: + # saving the doc will auto reset invalid customer/supplier field + doc = frappe.get_doc("Item Price", d[0]) + doc.save() \ No newline at end of file diff --git a/erpnext/stock/doctype/item_price/item_price.py b/erpnext/stock/doctype/item_price/item_price.py index 957c41546b..8e39eb5037 100644 --- a/erpnext/stock/doctype/item_price/item_price.py +++ b/erpnext/stock/doctype/item_price/item_price.py @@ -69,3 +69,10 @@ class ItemPrice(Document): self.reference = self.customer if self.buying: self.reference = self.supplier + + if self.selling and not self.buying: + # if only selling then remove supplier + self.supplier = None + if self.buying and not self.selling: + # if only buying then remove customer + self.customer = None diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index d50712aee7..11b6403419 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -630,7 +630,7 @@ def get_item_price(args, item_code, ignore_party=False): elif args.get("supplier"): conditions += " and supplier=%(supplier)s" else: - conditions += " and (customer is null or customer = '') and (supplier is null or supplier = '')" + conditions += "and (customer is null or customer = '') and (supplier is null or supplier = '')" if args.get('transaction_date'): conditions += """ and %(transaction_date)s between