Merge branch 'develop' of https://github.com/frappe/erpnext into accounts_dashboard
diff --git a/.travis.yml b/.travis.yml
index 213445b..77d427e 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,6 +1,5 @@
-dist: trusty
-
 language: python
+dist: trusty
 
 git:
   depth: 1
@@ -14,21 +13,10 @@
 
 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 @@
   - 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/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 5decccb..39bf4b0 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 @@
 @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:
diff --git a/erpnext/accounts/report/general_ledger/general_ledger.js b/erpnext/accounts/report/general_ledger/general_ledger.js
index 1188bea..2aecd6b 100644
--- a/erpnext/accounts/report/general_ledger/general_ledger.js
+++ b/erpnext/accounts/report/general_ledger/general_ledger.js
@@ -53,7 +53,7 @@
 			"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/loan_management/doctype/loan_security_pledge/loan_security_pledge.py b/erpnext/loan_management/doctype/loan_security_pledge/loan_security_pledge.py
index f97e596..961c05c 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 @@
 		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)
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 a854fa9..d93ffb7 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,6 +4,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"),
 			"fieldtype": "Link",
@@ -24,6 +32,13 @@
 			"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 aa57665..88bd9c1 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 f2db478..e883500 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 @@
 			"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 @@
 			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 @@
 			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 @@
 
 	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_quotations(opportunities):
 	if not opportunities:
 		return []
-	
+
 	opportunity_names = [o.name for o in opportunities]
 
 	return frappe.db.sql("""
@@ -155,5 +159,5 @@
 	total = 0
 	for doc in doclist:
 		total += doc.get(amount_field, 0)
-	
+
 	return total
diff --git a/erpnext/setup/doctype/company/test_company.py b/erpnext/setup/doctype/company/test_company.py
index b37cc17..29f6c37 100644
--- a/erpnext/setup/doctype/company/test_company.py
+++ b/erpnext/setup/doctype/company/test_company.py
@@ -47,9 +47,7 @@
 		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)
diff --git a/erpnext/setup/doctype/territory/territory.py b/erpnext/setup/doctype/territory/territory.py
index 095bd1c..808b538 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 @@
 	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"))