From a111f78566e5bef817439795f2e4e9d1e771bf8b Mon Sep 17 00:00:00 2001 From: Ayush Shukla Date: Tue, 20 Jun 2017 13:04:45 +0530 Subject: [PATCH] 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 --- erpnext/utilities/page/__init__.py | 0 .../utilities/page/leaderboard/__init__.py | 0 .../page/leaderboard/leaderboard.css | 54 ++++ .../page/leaderboard/leaderboard.html | 23 ++ .../utilities/page/leaderboard/leaderboard.js | 248 ++++++++++++++++++ .../page/leaderboard/leaderboard.json | 19 ++ .../utilities/page/leaderboard/leaderboard.py | 182 +++++++++++++ .../leaderboard/leaderboard_main_head.html | 8 + .../leaderboard/leaderboard_row_head.html | 3 + 9 files changed, 537 insertions(+) create mode 100644 erpnext/utilities/page/__init__.py create mode 100644 erpnext/utilities/page/leaderboard/__init__.py create mode 100644 erpnext/utilities/page/leaderboard/leaderboard.css create mode 100644 erpnext/utilities/page/leaderboard/leaderboard.html create mode 100644 erpnext/utilities/page/leaderboard/leaderboard.js create mode 100644 erpnext/utilities/page/leaderboard/leaderboard.json create mode 100644 erpnext/utilities/page/leaderboard/leaderboard.py create mode 100644 erpnext/utilities/page/leaderboard/leaderboard_main_head.html create mode 100644 erpnext/utilities/page/leaderboard/leaderboard_row_head.html diff --git a/erpnext/utilities/page/__init__.py b/erpnext/utilities/page/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/utilities/page/leaderboard/__init__.py b/erpnext/utilities/page/leaderboard/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/utilities/page/leaderboard/leaderboard.css b/erpnext/utilities/page/leaderboard/leaderboard.css new file mode 100644 index 0000000000..1f4fc5159a --- /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 0000000000..8df224750c --- /dev/null +++ b/erpnext/utilities/page/leaderboard/leaderboard.html @@ -0,0 +1,23 @@ +
+
+
+
+ +
+ +
+ +
+
+
+
+
+
\ 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 0000000000..eed9bd1865 --- /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()} +
+ ${me.render_result(items)} +
`; + + 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 = + `
+
+ ${ + me.options.selected_filter + .map(filter => { + const col = me.map_field(filter.field); + return ( + `
+ + ${col} + + +
`); + }).join("") + } +
+
`; + 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 = $(`
`).append($value); + return $item_container[0].outerHTML; + }).join(""); + + let html = + `
+
+ ${_html} +
+
`; + + return html; + }, + + render_message: function () { + var me = this; + + let html = + `
+
+

No Item found

+
+
`; + + 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 = + `
+ ${ + 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 ( + `
+ ${ + col === "Title" + ? ` ${val} ` + : ` ${val}` + } +
`); + }).join("") + } +
`; + + 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 0000000000..8cba76587a --- /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 0000000000..0a754100fe --- /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 0000000000..257d4ed3ae --- /dev/null +++ b/erpnext/utilities/page/leaderboard/leaderboard_main_head.html @@ -0,0 +1,8 @@ +
+ + {{col}} +
\ 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 0000000000..5a4e1dd22e --- /dev/null +++ b/erpnext/utilities/page/leaderboard/leaderboard_row_head.html @@ -0,0 +1,3 @@ +
+ {{ main }} +
\ No newline at end of file