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)"); } }, { 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..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 @@ -3,6 +3,14 @@ frappe.query_reports["Customer Acquisition and Loyalty"] = { "filters": [ + { + "fieldname": "view_type", + "label": __("View Type"), + "fieldtype": "Select", + "options": ["Monthly", "Territory Wise"], + "default": "Monthly", + "reqd": 1 + }, { "fieldname":"company", "label": __("Company"), @@ -24,6 +32,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..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 @@ -2,65 +2,186 @@ # 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': 125 + }, + { + 'label': _('Repeat Customers'), + 'fieldname': 'repeat_customers', + 'fieldtype': 'Int', + 'default': 0, + 'width': 125 + }, + { + 'label': _('Total'), + 'fieldname': 'total', + 'fieldtype': 'Int', + 'default': 0, + 'width': 100 + }, + { + 'label': _('New Customer Revenue'), + 'fieldname': 'new_customer_revenue', + 'fieldtype': 'Currency', + 'default': 0.0, + 'width': 175 + }, + { + 'label': _('Repeat Customer Revenue'), + 'fieldname': 'repeat_customer_revenue', + 'fieldtype': 'Currency', + 'default': 0.0, + 'width': 175 + }, + { + 'label': _('Total Revenue'), + 'fieldname': 'total_revenue', + 'fieldtype': 'Currency', + 'default': 0.0, + 'width': 175 + } + ] + if filters.get('view_type') == 'Monthly': + return get_data_by_time(filters, common_columns) + else: + return get_data_by_territory(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, + 'parent_territory': territory_dict[name]['parent'], + '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 indent else 1 + } + data.append(temp) + loop_data = sorted(data, key=lambda k: k['indent'], reverse=True) + + 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 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 = {} + + 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): + + 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: + customers_in[key]['new'][0] += 1 + customers_in[key]['new'][1] += si.base_grand_total + customers.append(si.customer) + else: + customers_in[key]['repeat'][0] += 1 + customers_in[key]['repeat'][1] += si.base_grand_total + + return customers_in 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 diff --git a/erpnext/setup/doctype/territory/territory.py b/erpnext/setup/doctype/territory/territory.py index 095bd1c179..808b5386ab 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,7 @@ class Territory(NestedSet): nsm_parent_field = 'parent_territory' def validate(self): + 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"))