Leaderboard of customers, items, suppliers and sales partner (#9354)

* First commit leaderboard working

* Styling and added href

* Changed timeline string

* Changes in item

* Cleanup

* Fix

* made changes to currency column

* Code cleanup for codacy

* Sorting bug fixed and formatting done

* Changed type to isinstance
diff --git a/erpnext/utilities/page/__init__.py b/erpnext/utilities/page/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/utilities/page/__init__.py
diff --git a/erpnext/utilities/page/leaderboard/__init__.py b/erpnext/utilities/page/leaderboard/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/utilities/page/leaderboard/__init__.py
diff --git a/erpnext/utilities/page/leaderboard/leaderboard.css b/erpnext/utilities/page/leaderboard/leaderboard.css
new file mode 100644
index 0000000..1f4fc51
--- /dev/null
+++ b/erpnext/utilities/page/leaderboard/leaderboard.css
@@ -0,0 +1,54 @@
+.list-filters {
+	overflow-y: hidden;
+	padding: 5px
+}
+
+.list-filter-item {
+	min-width: 150px;
+	float: left;
+	margin:5px;
+}
+
+.list-item_content{
+	flex: 1;
+	padding-right: 15px;
+	align-items: center;
+}
+
+.select-time, .select-doctype, .select-filter, .select-sort {
+	background: #f0f4f7;
+}
+
+.select-time:focus, .select-doctype:focus, .select-filter:focus, .select-sort:focus {
+	background: #f0f4f7;
+}
+
+.header-btn-base{
+	border:none;
+	outline:0;
+	vertical-align:middle;
+	overflow:hidden;
+	text-decoration:none;
+	color:inherit;
+	background-color:inherit;
+	cursor:pointer;
+	white-space:nowrap;
+}
+
+.header-btn-grey,.header-btn-grey:hover{
+	color:#000!important;
+	background-color:#bbb!important
+}
+
+.header-btn-round{
+	border-radius:4px
+}
+
+.item-title-bold{
+	font-weight: bold;
+}
+
+/*
+.header-btn-base:hover {
+	box-shadow:0 8px 16px 0 rgba(0,0,0,0.2),0 6px 20px 0 rgba(0,0,0,0.19)
+}*/
diff --git a/erpnext/utilities/page/leaderboard/leaderboard.html b/erpnext/utilities/page/leaderboard/leaderboard.html
new file mode 100644
index 0000000..8df2247
--- /dev/null
+++ b/erpnext/utilities/page/leaderboard/leaderboard.html
@@ -0,0 +1,23 @@
+<div class="frappe-list-area">
+	<div class="frappe-list">
+		<div class="list-filters">
+			<div class="list-filter-item">
+				<select class="form-control select-doctype">
+					{% for (var i=0; i < doctypes.length; i++) { %}
+						<option value="{{doctypes[i]}}">{{ doctypes[i] }}</option>
+					{% } %}
+				</select>
+			</div>
+
+			<div class="list-filter-item">
+				<select class="form-control select-time">
+					{% for (var i=0; i < timelines.length; i++) { %}
+						<option value="{{timelines[i]}}">{{ timelines[i] }}</option>
+					{% } %}
+				</select>
+			</div>
+		</div>
+		<div class="leaderboard">
+		</div>
+	</div>
+</div>
\ No newline at end of file
diff --git a/erpnext/utilities/page/leaderboard/leaderboard.js b/erpnext/utilities/page/leaderboard/leaderboard.js
new file mode 100644
index 0000000..eed9bd1
--- /dev/null
+++ b/erpnext/utilities/page/leaderboard/leaderboard.js
@@ -0,0 +1,248 @@
+
+frappe.Leaderboard = Class.extend({
+
+	init: function (parent) {
+		this.page = frappe.ui.make_app_page({
+			parent: parent,
+			title: "Leaderboard",
+			single_column: true
+		});
+
+		// const list of doctypes
+		this.doctypes = ["Customer", "Item", "Supplier", "Sales Partner"];
+		this.timelines = ["Week", "Month", "Quarter", "Year"];
+		this.desc_fields = ["total_amount", "total_request", "annual_billing", "commission_rate"];
+		this.filters = {
+			"Customer": this.map_array(["title", "total_amount", "total_item_purchased", "modified"]),
+			"Item": this.map_array(["title", "total_request", "total_purchase", "avg_price", "modified"]),
+			"Supplier": this.map_array(["title", "annual_billing", "total_unpaid", "modified"]),
+			"Sales Partner": this.map_array(["title", "commission_rate", "target_qty", "target_amount", "modified"]),
+		};
+
+		// for saving current selected filters
+		const _selected_filter = this.filters[this.doctypes[0]];
+		this.options = {
+			selected_doctype: this.doctypes[0],
+			selected_filter: _selected_filter,
+			selected_filter_item: _selected_filter[1],
+			selected_timeline: this.timelines[0],
+		};
+
+		this.message = null;
+		this.make();
+	},
+
+
+
+	make: function () {
+		var me = this;
+
+		var $leaderboard = $(frappe.render_template("leaderboard", this)).appendTo(this.page.main);
+
+		// events
+		$leaderboard.find(".select-doctype")
+			.on("change", function () {
+				me.options.selected_doctype = this.value;
+				me.options.selected_filter = me.filters[this.value];
+				me.options.selected_filter_item = me.filters[this.value][1];
+				me.make_request($leaderboard);
+			});
+
+		$leaderboard.find(".select-time")
+			.on("change", function () {
+				me.options.selected_timeline = this.value;
+				me.make_request($leaderboard);
+			});
+
+		// now get leaderboard
+		me.make_request($leaderboard);
+	},
+
+	make_request: function ($leaderboard) {
+		var me = this;
+
+		frappe.model.with_doctype(me.options.selected_doctype, function () {
+			me.get_leaderboard(me.get_leaderboard_data, $leaderboard);
+		});
+	},
+
+	get_leaderboard: function (notify, $leaderboard) {
+		var me = this;
+
+		frappe.call({
+			method: "erpnext.utilities.page.leaderboard.leaderboard.get_leaderboard",
+			args: {
+				obj: JSON.stringify(me.options)
+			},
+			callback: function (res) {
+				console.log(res)
+				notify(me, res, $leaderboard);
+			}
+		});
+	},
+
+	get_leaderboard_data: function (me, res, $leaderboard) {
+		if (res && res.message) {
+			me.message = null;
+			$leaderboard.find(".leaderboard").html(me.render_list_view(res.message));
+
+			// event to change arrow
+			$leaderboard.find(".leaderboard-item")
+				.click(function () {
+					const field = this.innerText.trim().toLowerCase().replace(new RegExp(" ", "g"), "_");
+					if (field && field !== "title") {
+						const _selected_filter_item = me.options.selected_filter
+							.filter(i => i.field === field);
+						if (_selected_filter_item.length > 0) {
+							me.options.selected_filter_item = _selected_filter_item[0];
+							me.options.selected_filter_item.value = _selected_filter_item[0].value === "ASC" ? "DESC" : "ASC";
+
+							const new_class_name = `icon-${me.options.selected_filter_item.field} fa fa-chevron-${me.options.selected_filter_item.value === "ASC" ? "up" : "down"}`;
+							$leaderboard.find(`.icon-${me.options.selected_filter_item.field}`)
+								.attr("class", new_class_name);
+
+							// now make request to web
+							me.make_request($leaderboard);
+						}
+					}
+				});
+		} else {
+			me.message = "No items found.";
+			$leaderboard.find(".leaderboard").html(me.render_list_view());
+		}
+	},
+
+	render_list_view: function (items = []) {
+		var me = this;
+
+		var html =
+			`${me.render_message()}
+			 <div class="result" style="${me.message ? "display:none;" : ""}">
+			 	${me.render_result(items)}
+			 </div>`;
+
+		return $(html);
+	},
+
+	render_result: function (items) {
+		var me = this;
+
+		var html =
+			`${me.render_list_header()}
+			 ${me.render_list_result(items)}`;
+
+		return html;
+	},
+
+	render_list_header: function () {
+		var me = this;
+		const _selected_filter = me.options.selected_filter
+			.map(i => me.map_field(i.field)).slice(1);
+
+		const html =
+			`<div class="list-headers">
+				<div class="list-item list-item--head" data-list-renderer="${"List"}">
+					${
+					me.options.selected_filter
+						.map(filter => {
+							const col = me.map_field(filter.field);
+							return (
+								`<div class="leaderboard-item list-item_content ellipsis text-muted list-item__content--flex-2
+									header-btn-base ${(col !== "Title" && col !== "Modified") ? "hidden-xs" : ""}
+									${(col && _selected_filter.indexOf(col) !== -1) ? "text-right" : ""}">
+									<span class="list-col-title ellipsis">
+										${col}
+										<i class="${"icon-" + filter.field} fa ${filter.value === "ASC" ? "fa-chevron-up" : "fa-chevron-down"}"
+											style="${col === "Title" ? "display:none;" : ""}"></i>
+									</span>
+								</div>`);
+						}).join("")
+					}
+				</div>
+			</div>`;
+		return html;
+	},
+
+	render_list_result: function (items) {
+		var me = this;
+
+		let _html = items.map((item) => {
+			const $value = $(me.get_item_html(item));
+			const $item_container = $(`<div class="list-item-container">`).append($value);
+			return $item_container[0].outerHTML;
+		}).join("");
+
+		let html =
+			`<div class="result-list">
+				<div class="list-items">
+					${_html}
+				</div>
+			</div>`;
+
+		return html;
+	},
+
+	render_message: function () {
+		var me = this;
+
+		let html =
+			`<div class="no-result text-center" style="${me.message ? "" : "display:none;"}">   
+				<div class="msg-box no-border">
+					<p>No Item found</p>
+				</div>  
+			</div>`;
+
+		return html;
+	},
+
+	get_item_html: function (item) {
+		var me = this;
+		const _selected_filter = me.options.selected_filter
+			.map(i => me.map_field(i.field)).slice(1);
+
+		const html =
+			`<div class="list-item">
+				${
+			me.options.selected_filter
+				.map(filter => {
+					const col = me.map_field(filter.field);
+					let val = item[filter.field];
+					if (col === "Modified") {
+						val = comment_when(val);
+					}
+					return (
+						`<div class="list-item_content ellipsis list-item__content--flex-2
+							${(col !== "Title" && col !== "Modified") ? "hidden-xs" : ""}
+							${(col && _selected_filter.indexOf(col) !== -1) ? "text-right" : ""}">
+							${
+								col === "Title"	
+									? `<a class="grey list-id ellipsis" href="${item["href"]}"> ${val} </a>` 
+									: `<span class="text-muted ellipsis"> ${val}</span>`
+							}
+						</div>`);
+					}).join("")
+				}
+			</div>`;
+
+		return html;
+	},
+
+	map_field: function (field) {
+		return field.replace(new RegExp("_", "g"), " ").replace(/(^|\s)[a-z]/g, f => f.toUpperCase())
+	},
+
+	map_array: function (_array) {
+		var me = this;
+		return _array.map((str) => {
+			let value = me.desc_fields.indexOf(str) > -1 ? "DESC" : "ASC";
+			return {
+				field: str,
+				value: value
+			};
+		});
+	}
+});
+
+frappe.pages["leaderboard"].on_page_load = function (wrapper) {
+	frappe.leaderboard = new frappe.Leaderboard(wrapper);
+}
diff --git a/erpnext/utilities/page/leaderboard/leaderboard.json b/erpnext/utilities/page/leaderboard/leaderboard.json
new file mode 100644
index 0000000..8cba765
--- /dev/null
+++ b/erpnext/utilities/page/leaderboard/leaderboard.json
@@ -0,0 +1,19 @@
+{
+ "content": null, 
+ "creation": "2017-06-06 02:54:24.785360", 
+ "docstatus": 0, 
+ "doctype": "Page", 
+ "idx": 0, 
+ "modified": "2017-06-06 02:54:27.504048", 
+ "modified_by": "Administrator", 
+ "module": "Utilities", 
+ "name": "leaderboard", 
+ "owner": "Administrator", 
+ "page_name": "leaderboard", 
+ "roles": [], 
+ "script": null, 
+ "standard": "Yes", 
+ "style": null, 
+ "system_page": 0, 
+ "title": "LeaderBoard"
+}
\ No newline at end of file
diff --git a/erpnext/utilities/page/leaderboard/leaderboard.py b/erpnext/utilities/page/leaderboard/leaderboard.py
new file mode 100644
index 0000000..0a75410
--- /dev/null
+++ b/erpnext/utilities/page/leaderboard/leaderboard.py
@@ -0,0 +1,182 @@
+# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors
+# MIT License. See license.txt
+
+from __future__ import unicode_literals, print_function
+import frappe
+import json
+from operator import itemgetter
+from frappe.utils import add_to_date
+from erpnext.accounts.party import get_dashboard_info
+from erpnext.accounts.utils import get_currency_precision
+
+@frappe.whitelist()
+def get_leaderboard(obj):
+	"""return top 10 items for that doctype based on conditions"""
+	obj = frappe._dict(json.loads(obj))
+
+	doctype = obj.selected_doctype
+	timeline = obj.selected_timeline
+	filters = {"modified":(">=", get_date_from_string(timeline))}
+	items = []
+	if doctype == "Customer":
+		items = get_all_customers(doctype, filters, [])
+	elif  doctype == "Item":
+		items = get_all_items(doctype, filters, [])
+	elif  doctype == "Supplier":
+		items = get_all_suppliers(doctype, filters, [])
+	elif  doctype == "Sales Partner":
+		items = get_all_sales_partner(doctype, filters, [])
+	
+	if len(items) > 0:
+		return filter_leaderboard_items(obj, items)
+	return []
+
+
+# filters start
+def filter_leaderboard_items(obj, items):
+	"""return items based on seleted filters"""
+	
+	reverse = False if obj.selected_filter_item and obj.selected_filter_item["value"] == "ASC" else True
+	# key : (x[field1], x[field2]) while sorting on 2 values
+	filtered_list = []
+	selected_field = obj.selected_filter_item and obj.selected_filter_item["field"]
+	if selected_field:
+		filtered_list  = sorted(items, key=itemgetter(selected_field), reverse=reverse)
+		value = items[0].get(selected_field)
+
+		allowed = isinstance(value, unicode) or isinstance(value, str)
+		# now sort by length
+		if allowed and '$' in value:
+			filtered_list.sort(key= lambda x: len(x[selected_field]), reverse=reverse)
+	
+	# return only 10 items'
+	return filtered_list[:10]
+
+# filters end
+
+
+# utils start
+def destructure_tuple_of_tuples(tup_of_tup):
+	"""return tuple(tuples) as list"""
+	return [y for x in tup_of_tup for y in x]
+
+def get_date_from_string(seleted_timeline):
+	"""return string for ex:this week as date:string"""
+	days = months = years = 0
+	if "month" == seleted_timeline.lower():
+		months = -1
+	elif "quarter" == seleted_timeline.lower():
+		months = -3
+	elif "year" == seleted_timeline.lower():
+		years = -1
+	else:
+		days = -7
+
+	return add_to_date(None, years=years, months=months, days=days, as_string=True, as_datetime=True)
+
+def get_filter_list(selected_filter):
+	"""return list of keys"""
+	return map((lambda y : y["field"]), filter(lambda x : not (x["field"] == "title" or x["field"] == "modified"), selected_filter))
+
+def get_avg(items):
+	"""return avg of list items"""
+	length = len(items)
+	if length > 0:
+		return sum(items) / length
+	return 0
+
+def get_formatted_value(value, add_symbol=True):
+	"""return formatted value"""
+	currency_precision = get_currency_precision() or 2
+	if not add_symbol:
+		return '{:.{pre}f}'.format(value, pre=currency_precision)
+	
+	company = frappe.db.get_default("company") or frappe.get_all("Company")[0].name
+	currency = frappe.get_doc("Company", company).default_currency or frappe.boot.sysdefaults.currency;
+	currency_symbol = frappe.db.get_value("Currency", currency, "symbol")
+	return  currency_symbol + ' ' + '{:.{pre}f}'.format(value, pre=currency_precision)
+
+# utils end
+
+
+# get data
+def get_all_customers(doctype, filters, items, start=0, limit=100):
+	"""return all customers"""
+
+	x = frappe.get_list(doctype, fields=["name", "modified"], filters=filters, limit_start=start, limit_page_length=limit)
+	
+	for val in x:
+		y = dict(frappe.db.sql('''select name, grand_total from `tabSales Invoice` where customer = %s''', (val.name)))
+		invoice_list = y.keys()
+		if len(invoice_list) > 0:
+			item_count = frappe.db.sql('''select count(name) from `tabSales Invoice Item` where parent in (%s)''' % ", ".join(
+				['%s'] * len(invoice_list)), tuple(invoice_list))
+			items.append({"title": val.name,
+				"total_amount": get_formatted_value(sum(y.values())),
+				"href":"#Form/Customer/" + val.name,
+				"total_item_purchased": sum(destructure_tuple_of_tuples(item_count)),
+				"modified": str(val.modified)})
+	if len(x) > 99:
+		start = start + 1
+		return get_all_customers(doctype, filters, items, start=start)
+	else:
+		return items
+
+def get_all_items(doctype, filters, items, start=0, limit=100):
+	"""return all items"""
+
+	x = frappe.get_list(doctype, fields=["name", "modified"], filters=filters, limit_start=start, limit_page_length=limit)
+	for val in x:
+		data = frappe.db.sql('''select item_code from `tabMaterial Request Item` where item_code = %s''', (val.name), as_list=1)
+		requests = destructure_tuple_of_tuples(data)
+		data = frappe.db.sql('''select price_list_rate from `tabItem Price` where item_code = %s''', (val.name), as_list=1)
+		avg_price = get_avg(destructure_tuple_of_tuples(data))
+		data = frappe.db.sql('''select item_code from `tabPurchase Invoice Item` where item_code = %s''', (val.name), as_list=1)
+		purchases = destructure_tuple_of_tuples(data)
+		
+		items.append({"title": val.name,
+			"total_request":len(requests),
+			"total_purchase": len(purchases), "href":"#Form/Item/" + val.name,
+			"avg_price": get_formatted_value(avg_price),
+			"modified": val.modified})
+	if len(x) > 99:
+		return get_all_items(doctype, filters, items, start=start)
+	else:
+		return items
+
+def get_all_suppliers(doctype, filters, items, start=0, limit=100):
+	"""return all suppliers"""
+
+	x = frappe.get_list(doctype, fields=["name", "modified"], filters=filters, limit_start=start, limit_page_length=limit)
+	
+	for val in x:
+		info = get_dashboard_info(doctype, val.name)
+		items.append({"title": val.name,
+		"annual_billing":  get_formatted_value(info["billing_this_year"]),
+		"total_unpaid": get_formatted_value(abs(info["total_unpaid"])),
+		"href":"#Form/Supplier/" + val.name,
+		"modified": val.modified})
+
+	if len(x) > 99:
+		return get_all_suppliers(doctype, filters, items, start=start)
+	else:
+		return items
+
+def get_all_sales_partner(doctype, filters, items, start=0, limit=100):
+	"""return all sales partner"""
+	
+	x = frappe.get_list(doctype, fields=["name", "commission_rate", "modified"], filters=filters, limit_start=start, limit_page_length=limit)
+	for val in x:
+		y = frappe.db.sql('''select target_qty, target_amount from `tabTarget Detail` where parent = %s''', (val.name), as_dict=1)
+		target_qty = sum([f["target_qty"] for f in y])
+		target_amount = sum([f["target_amount"] for f in y])
+		items.append({"title": val.name,
+			"commission_rate": get_formatted_value(val.commission_rate, False),
+			"target_qty": target_qty,
+			"target_amount": get_formatted_value(target_amount),
+			"href":"#Form/Sales Partner/" + val.name,
+			"modified": val.modified})
+	if len(x) > 99:
+		return get_all_sales_partner(doctype, filters, items, start=start)
+	else:
+		return items
\ No newline at end of file
diff --git a/erpnext/utilities/page/leaderboard/leaderboard_main_head.html b/erpnext/utilities/page/leaderboard/leaderboard_main_head.html
new file mode 100644
index 0000000..257d4ed
--- /dev/null
+++ b/erpnext/utilities/page/leaderboard/leaderboard_main_head.html
@@ -0,0 +1,8 @@
+<div class="list-item__content ellipsis text-muted list-item__content--flex-2
+	{% if(col !== "Title" && col !== "Modified") { %}
+		hidden-xs
+	{% } %}
+	{% if(col && selected_filter.indexOf(col) !== -1) { %}text-right{% } %}">
+	
+	<span class="list-col-title ellipsis">{{col}}</span>
+</div>
\ No newline at end of file
diff --git a/erpnext/utilities/page/leaderboard/leaderboard_row_head.html b/erpnext/utilities/page/leaderboard/leaderboard_row_head.html
new file mode 100644
index 0000000..5a4e1dd
--- /dev/null
+++ b/erpnext/utilities/page/leaderboard/leaderboard_row_head.html
@@ -0,0 +1,3 @@
+<div class="list-item list-item--head" data-list-renderer="{{__(" List ")}}">
+	{{ main }}
+</div>
\ No newline at end of file