From 7e1150d51017c31429056f7ac967a02696255f6a Mon Sep 17 00:00:00 2001 From: hrwx Date: Sat, 7 Nov 2020 19:06:28 +0530 Subject: [PATCH 1/2] chore: port dashboards to v12 --- frappe/core/page/dashboard/dashboard.css | 62 ++- frappe/core/page/dashboard/dashboard.py | 35 +- frappe/desk/doctype/dashboard/dashboard.json | 125 +---- .../dashboard_chart/dashboard_chart.js | 286 ++++++++--- .../dashboard_chart/dashboard_chart.json | 50 +- .../dashboard_chart/dashboard_chart.py | 266 +++++++--- .../dashboard_chart/test_dashboard_chart.py | 71 +-- .../doctype/dashboard_chart_field/__init__.py | 0 .../dashboard_chart_field.json | 37 ++ .../dashboard_chart_field.py | 10 + .../dashboard_chart_link.json | 60 +-- .../dashboard_chart_source.json | 124 +---- frappe/desk/page/user_profile/user_profile.js | 58 +-- frappe/desk/page/user_profile/user_profile.py | 90 ++-- frappe/model/db_query.py | 4 +- frappe/patches.txt | 1 + ...change_existing_dashboard_chart_filters.py | 26 + frappe/public/build.json | 4 +- .../public/js/frappe/form/controls/color.js | 1 - frappe/public/js/frappe/form/grid.js | 2 +- frappe/public/js/frappe/ui/dashboard_chart.js | 412 +++++++++++++++ .../js/frappe/ui/filters/filter_list.js | 16 + .../public/js/frappe/utils/dashboard_utils.js | 58 +++ .../js/frappe/views/reports/query_report.js | 477 +++++++++++------- .../js/frappe/views/reports/report_utils.js | 140 +++++ 25 files changed, 1629 insertions(+), 786 deletions(-) create mode 100644 frappe/desk/doctype/dashboard_chart_field/__init__.py create mode 100644 frappe/desk/doctype/dashboard_chart_field/dashboard_chart_field.json create mode 100644 frappe/desk/doctype/dashboard_chart_field/dashboard_chart_field.py create mode 100644 frappe/patches/v12_0/change_existing_dashboard_chart_filters.py create mode 100644 frappe/public/js/frappe/ui/dashboard_chart.js create mode 100644 frappe/public/js/frappe/utils/dashboard_utils.js create mode 100644 frappe/public/js/frappe/views/reports/report_utils.js diff --git a/frappe/core/page/dashboard/dashboard.css b/frappe/core/page/dashboard/dashboard.css index 51db4bc78ad6..cf1a581c6dba 100644 --- a/frappe/core/page/dashboard/dashboard.css +++ b/frappe/core/page/dashboard/dashboard.css @@ -1,46 +1,62 @@ .chart-wrapper { - border: 1px solid #d1d8dd; - border-radius: 4px; - height: 320px; - margin: 15px 0; - padding-left: 15px; - padding-right: 15px; + border: 1px solid #d1d8dd; + border-radius: 4px; + margin: 15px 0; + padding-left: 15px; + padding-right: 15px; + height: 320px; } .chart-container { - margin-top: 30px; + top: 50%; + transform: translateY(-50%); } .frappe-chart > text.title { - margin: 0px; - font-size: 14px !important; - font-weight: bold; + margin: 0px; + font-size: 14px !important; + font-weight: bold; } .chart-loading-state, .chart-empty-state { - height: 100%; - margin-top: 160px; - text-align: center; + height: 100%; + margin-top: 160px; + text-align: center; } .chart-actions { - position: absolute; - right: 30px; - top: 26px; + position: relative; + right: 0px; + top: 20px; + margin-right: 5px; +} + +.filter-chart { + position: relative; + right: 5px; + top: 20px; +} + +.dashboard-date-field { + width: 14%; + height: 0; + margin-right: 10px; + position: relative; + top: 0px; } .chart-column-container { - position: relative; + position: relative; } .last-synced-text { - position: absolute; - top: 28px; - right: 60px; - font-size: 12px; + position: absolute; + top: 28px; + left: 50px; + font-size: 12px; } .dashboard-graph { - padding-top: 15px; - overflow: hidden; + padding-top: 15px; + overflow: hidden; } \ No newline at end of file diff --git a/frappe/core/page/dashboard/dashboard.py b/frappe/core/page/dashboard/dashboard.py index cc19977080ef..2589071cf1fc 100644 --- a/frappe/core/page/dashboard/dashboard.py +++ b/frappe/core/page/dashboard/dashboard.py @@ -4,10 +4,12 @@ import json import frappe from frappe import _ +from functools import wraps from frappe.utils import add_to_date, get_link_to_form def cache_source(function): + @wraps(function) def wrapper(*args, **kwargs): if kwargs.get("chart_name"): chart = frappe.get_doc('Dashboard Chart', kwargs.get("chart_name")) @@ -19,21 +21,29 @@ def wrapper(*args, **kwargs): chart_name = frappe.parse_json(chart).name cache_key = "chart-data:{}".format(chart_name) if int(kwargs.get("refresh") or 0): - results = generate_and_cache_results(chart, chart_name, function, cache_key) + results = generate_and_cache_results(kwargs, function, cache_key, chart) else: cached_results = frappe.cache().get_value(cache_key) if cached_results: results = frappe.parse_json(frappe.safe_decode(cached_results)) else: - results = generate_and_cache_results(chart, chart_name, function, cache_key) + results = generate_and_cache_results(kwargs, function, cache_key, chart) return results return wrapper -def generate_and_cache_results(chart, chart_name, function, cache_key): +def generate_and_cache_results(args, function, cache_key, chart): try: - results = function(chart_name = chart_name) + args = frappe._dict(args) + results = function( + chart_name = args.chart_name, + filters = args.filters or None, + from_date = args.from_date or None, + to_date = args.to_date or None, + time_interval = args.time_interval or None, + timespan = args.timespan or None, + ) except TypeError as e: - if e.message == "'NoneType' object is not iterable": + if str(e) == "'NoneType' object is not iterable": # Probably because of invalid link filter # # Note: Do not try to find the right way of doing this because @@ -45,19 +55,20 @@ def generate_and_cache_results(chart, chart_name, function, cache_key): else: raise - frappe.cache().set_value(cache_key, json.dumps(results, default=str)) - frappe.db.set_value("Dashboard Chart", chart_name, "last_synced_on", frappe.utils.now(), update_modified = False) + frappe.db.set_value("Dashboard Chart", args.chart_name, "last_synced_on", frappe.utils.now(), update_modified = False) return results def get_from_date_from_timespan(to_date, timespan): days = months = years = 0 - if "Last Week" == timespan: + if timespan == "Last Week": days = -7 - if "Last Month" == timespan: + if timespan == "Last Month": months = -1 - elif "Last Quarter" == timespan: + elif timespan == "Last Quarter": months = -3 - elif "Last Year" == timespan: + elif timespan == "Last Year": years = -1 + elif timespan == "All Time": + years = -50 return add_to_date(to_date, years=years, months=months, days=days, - as_datetime=True) + as_datetime=True) \ No newline at end of file diff --git a/frappe/desk/doctype/dashboard/dashboard.json b/frappe/desk/doctype/dashboard/dashboard.json index 2e80ef9e3347..239f35bea8a6 100644 --- a/frappe/desk/doctype/dashboard/dashboard.json +++ b/frappe/desk/doctype/dashboard/dashboard.json @@ -1,162 +1,61 @@ { - "allow_copy": 0, - "allow_events_in_timeline": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, + "actions": [], "autoname": "field:dashboard_name", - "beta": 0, "creation": "2019-01-10 12:54:40.938705", - "custom": 0, - "docstatus": 0, "doctype": "DocType", - "document_type": "", "editable_grid": 1, "engine": "InnoDB", + "field_order": [ + "dashboard_name", + "is_default", + "charts" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, "fieldname": "dashboard_name", "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, "in_list_view": 1, - "in_standard_filter": 0, "label": "Dashboard Name", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, "unique": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, + "default": "0", "fieldname": "is_default", "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Is Default", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "Is Default" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, "fieldname": "charts", "fieldtype": "Table", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Charts", - "length": 0, - "no_copy": 0, "options": "Dashboard Chart Link", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "reqd": 1 } ], - "has_web_view": 0, - "hide_toolbar": 0, - "idx": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2019-04-12 13:07:08.950346", + "links": [], + "modified": "2020-01-26 20:00:10.069817", "modified_by": "Administrator", "module": "Desk", "name": "Dashboard", - "name_case": "", "owner": "Administrator", "permissions": [ { - "amend": 0, - "cancel": 0, "create": 1, "delete": 1, "email": 1, "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, "print": 1, "read": 1, "report": 1, "role": "System Manager", - "set_user_permissions": 0, "share": 1, - "submit": 0, "write": 1 } ], "quick_entry": 1, - "read_only": 0, - "show_name_in_global_search": 0, "sort_field": "modified", "sort_order": "DESC", "title_field": "dashboard_name", - "track_changes": 1, - "track_seen": 0, - "track_views": 0 + "track_changes": 1 } \ No newline at end of file diff --git a/frappe/desk/doctype/dashboard_chart/dashboard_chart.js b/frappe/desk/doctype/dashboard_chart/dashboard_chart.js index f48f2e7c1bc3..cfa6bfc4ba53 100644 --- a/frappe/desk/doctype/dashboard_chart/dashboard_chart.js +++ b/frappe/desk/doctype/dashboard_chart/dashboard_chart.js @@ -11,8 +11,54 @@ frappe.ui.form.on('Dashboard Chart', { refresh: function(frm) { frm.chart_filters = null; + frm.add_custom_button('Add Chart to Dashboard', () => { + const d = new frappe.ui.Dialog({ + title: __('Add to Dashboard'), + fields: [ + { + label: __('Select Dashboard'), + fieldtype: 'Link', + fieldname: 'dashboard', + options: 'Dashboard', + } + ], + primary_action: (values) => { + values.chart_name = frm.doc.chart_name; + frappe.xcall( + 'frappe.desk.doctype.dashboard_chart.dashboard_chart.add_chart_to_dashboard', + {args: values} + ).then(()=> { + let dashboard_route_html = + `${values.dashboard}`; + let message = + __(`Dashboard Chart ${values.chart_name} add to Dashboard ` + dashboard_route_html); + + frappe.msgprint(message); + }); + + d.hide(); + } + }); + + if (!frm.doc.chart_name) { + frappe.msgprint(__('Please create chart first')); + } else { + d.show(); + } + }); + frm.set_df_property("filters_section", "hidden", 1); + frm.set_query('document_type', function() { + return { + filters: { + 'issingle': false + } + } + }); frm.trigger('update_options'); + if (frm.doc.report_name) { + frm.trigger('set_chart_report_filters'); + } }, source: function(frm) { @@ -20,13 +66,30 @@ frappe.ui.form.on('Dashboard Chart', { }, chart_type: function(frm) { - // set timeseries based on chart type - if (['Count', 'Average', 'Sum'].includes(frm.doc.chart_type)) { - frm.set_value('timeseries', 1); + if (frm.doc.chart_type == 'Report') { + frm.set_query('report_name', () => { + return { + filters: { + 'report_type': ['!=', 'Report Builder'] + } + } + }); } else { - frm.set_value('timeseries', 0); + // set timeseries based on chart type + if (['Count', 'Average', 'Sum'].includes(frm.doc.chart_type)) { + frm.set_value('timeseries', 1); + } else { + frm.set_value('timeseries', 0); + } + + if (frm.doc.chart_type == 'Group By') { + frm.set_df_property('type', 'options', ['Line', 'Bar', 'Percentage', 'Pie']); + } else { + frm.set_df_property('type', 'options', ['Line', 'Bar']); + } + + frm.set_value('document_type', ''); } - frm.set_value('document_type', ''); }, document_type: function(frm) { @@ -34,13 +97,81 @@ frappe.ui.form.on('Dashboard Chart', { frm.set_value('source', ''); frm.set_value('based_on', ''); frm.set_value('value_based_on', ''); - frm.set_value('filters_json', '{}'); + frm.set_value('filters_json', '[]'); frm.trigger('update_options'); }, + report_name: function(frm) { + frm.set_value('x_field', ''); + frm.set_value('y_axis', []); + frm.set_df_property('x_field', 'options', []); + frm.set_value('filters_json', '{}'); + frm.trigger('set_chart_report_filters'); + }, + + + set_chart_report_filters: function(frm) { + let report_name = frm.doc.report_name; + + if (report_name) { + if (frm.doc.filters_json.length > 2) { + frm.trigger('show_filters'); + frm.trigger('set_chart_field_options'); + } else { + frappe.report_utils.get_report_filters(report_name).then(filters => { + frappe.after_ajax(()=> { + if (filters) { + frm.chart_filters = filters; + let filter_values = frappe.report_utils.get_filter_values(filters); + frm.set_value('filters_json', JSON.stringify(filter_values)); + } + frm.trigger('show_filters'); + frm.trigger('set_chart_field_options'); + }); + }); + } + + } + }, + + set_chart_field_options: function(frm) { + let filters = frm.doc.filters_json.length > 2? JSON.parse(frm.doc.filters_json): null; + frappe.xcall( + 'frappe.desk.query_report.run', + { + report_name: frm.doc.report_name, + filters: filters + } + ).then(data => { + frm.report_data = data; + if (!data.chart) { + frm.set_value('is_custom', 0); + frm.set_df_property('is_custom', 'hidden', 1); + } else { + frm.set_df_property('is_custom', 'hidden', 0); + } + + if (!frm.doc.is_custom) { + if (data.result.length) { + frm.field_options = frappe.report_utils.get_possible_chart_options(data.columns, data); + frm.set_df_property('x_field', 'options', frm.field_options.non_numeric_fields); + if (!frm.field_options.numeric_fields.length) { + frappe.msgprint(__(`Report has no numeric fields, please change the Report Name`)); + } else { + let y_field_df = frappe.meta.get_docfield('Dashboard Chart Field', 'y_field', frm.doc.name); + y_field_df.options = frm.field_options.numeric_fields; + } + } else { + frappe.msgprint(__('Report has no data, please modify the filters or change the Report Name')); + } + } + }); + }, + timespan: function(frm) { const time_interval_options = { "Select Date Range": ["Quarterly", "Monthly", "Weekly", "Daily"], + "All Time": ["Yearly", "Monthly"], "Last Year": ["Quarterly", "Monthly", "Weekly", "Daily"], "Last Quarter": ["Monthly", "Weekly", "Daily"], "Last Month": ["Weekly", "Daily"], @@ -95,58 +226,28 @@ frappe.ui.form.on('Dashboard Chart', { }, show_filters: function(frm) { - if (frm.chart_filters && frm.chart_filters.length) { - frm.trigger('render_filters_table'); - } else { - if (frm.doc.chart_type==='Custom') { - if (frm.doc.source) { - frappe.xcall('frappe.desk.doctype.dashboard_chart_source.dashboard_chart_source.get_config', {name: frm.doc.source}) - .then(config => { - frappe.dom.eval(config); - frm.chart_filters = frappe.dashboards.chart_sources[frm.doc.source].filters; - frm.trigger('render_filters_table'); - }); - } else { - frm.chart_filters = []; - frm.trigger('render_filters_table'); - } - } else { - // standard filters - if (frm.doc.document_type) { - frappe.model.with_doctype(frm.doc.document_type, () => { - frm.chart_filters = []; - frappe.get_meta(frm.doc.document_type).fields.map(df => { - if (['Link', 'Select'].includes(df.fieldtype)) { - let _df = copy_dict(df); - - // nothing is mandatory - _df.reqd = 0; - _df.default = null; - _df.depends_on = null; - _df.read_only = 0; - _df.permlevel = 1; - _df.hidden = 0; - - frm.chart_filters.push(_df); - } - }); - frm.trigger('render_filters_table'); - }); + frm.chart_filters = []; + frappe.dashboard_utils.get_filters_for_chart_type(frm.doc).then(filters => { + frappe.after_ajax(() => { + if (filters) { + frm.chart_filters = filters; } - } - } + frm.trigger('render_filters_table'); + }); + }); }, render_filters_table: function(frm) { frm.set_df_property("filters_section", "hidden", 0); - let fields = frm.chart_filters; + let is_document_type = frm.doc.chart_type!== 'Report' && frm.doc.chart_type!=='Custom'; let wrapper = $(frm.get_field('filters_json').wrapper).empty(); let table = $(` - + + @@ -154,41 +255,104 @@ frappe.ui.form.on('Dashboard Chart', {
${__('Filter')}${__('Filter')}${__('Condition')} ${__('Value')}
`).appendTo(wrapper); $(`

${__("Click table to edit")}

`).appendTo(wrapper); - let filters = JSON.parse(frm.doc.filters_json || '{}'); + let filters = JSON.parse(frm.doc.filters_json || '[]'); var filters_set = false; - fields.map(f => { - if (filters[f.fieldname]) { - const filter_row = $(`${f.label}${filters[f.fieldname] || ""}`); - table.find('tbody').append(filter_row); - filters_set = true; + + let fields; + if (is_document_type) { + fields = [ + { + fieldtype: 'HTML', + fieldname: 'filter_area', + } + ]; + + if (filters.length > 0) { + filters.forEach( filter => { + const filter_row = + $(` + ${filter[1]} + ${filter[2] || ""} + ${filter[3]} + `); + + table.find('tbody').append(filter_row); + filters_set = true; + }); } - }); + } else if (frm.chart_filters.length) { + fields = frm.chart_filters.filter(f => { + if (f.on_change && !f.reqd) { + return false; + } + if (f.get_query || f.get_data) { + f.read_only = 1; + } + + return f.fieldname; + }); + + fields.map( f => { + if (filters[f.fieldname]) { + let condition = '='; + const filter_row = + $(` + ${f.label} + ${condition} + ${filters[f.fieldname] || ""} + `); + + table.find('tbody').append(filter_row); + filters_set = true; + } + }); + } if (!filters_set) { - const filter_row = $(` + const filter_row = $(` ${__("Click to Set Filters")}`); table.find('tbody').append(filter_row); } table.on('click', () => { + let dialog = new frappe.ui.Dialog({ title: __('Set Filters'), fields: fields, primary_action: function() { let values = this.get_values(); - if(values) { + if (values) { this.hide(); - frm.set_value('filters_json', JSON.stringify(values)); + if (is_document_type) { + let filters = frm.filter_group.get_filters(); + frm.set_value('filters_json', JSON.stringify(filters)); + } else { + frm.set_value('filters_json', JSON.stringify(values)); + } + frm.trigger('show_filters'); + if (frm.doc.chart_type == 'Report') { + frm.trigger('set_chart_report_filters'); + } } }, primary_action_label: "Set" }); + frappe.dashboards.filters_dialog = dialog; + + if (is_document_type) { + frm.filter_group = new frappe.ui.FilterGroup({ + parent: dialog.get_field('filter_area').$wrapper, + doctype: frm.doc.document_type, + on_change: () => {}, + }); + + frm.filter_group.add_filters_to_filter_group(filters); + } + dialog.show(); dialog.set_values(filters); - frappe.dashboards.filters_dialog = dialog; }); - } -}); - + }, +}); diff --git a/frappe/desk/doctype/dashboard_chart/dashboard_chart.json b/frappe/desk/doctype/dashboard_chart/dashboard_chart.json index 4187e54fd0a8..f181f6d7e461 100644 --- a/frappe/desk/doctype/dashboard_chart/dashboard_chart.json +++ b/frappe/desk/doctype/dashboard_chart/dashboard_chart.json @@ -1,4 +1,5 @@ { + "actions": [], "allow_rename": 1, "autoname": "field:chart_name", "creation": "2019-01-10 12:28:06.282875", @@ -8,6 +9,10 @@ "field_order": [ "chart_name", "chart_type", + "report_name", + "is_custom", + "x_field", + "y_axis", "source", "document_type", "based_on", @@ -45,7 +50,7 @@ "fieldname": "chart_type", "fieldtype": "Select", "label": "Chart Type", - "options": "Count\nSum\nAverage\nCustom\nGroup By" + "options": "Count\nSum\nAverage\nGroup By\nCustom\nReport" }, { "depends_on": "eval:doc.chart_type === 'Custom'", @@ -55,14 +60,14 @@ "options": "Dashboard Chart Source" }, { - "depends_on": "eval: doc.chart_type !== 'Custom'", + "depends_on": "eval: doc.chart_type !== 'Custom' && doc.chart_type !== 'Report'", "fieldname": "document_type", "fieldtype": "Link", "label": "Document Type", "options": "DocType" }, { - "depends_on": "eval: ['Count', 'Sum', 'Average'].includes(doc.chart_type)", + "depends_on": "eval: doc.timeseries && ['Count', 'Sum', 'Average'].includes(doc.chart_type)", "fieldname": "based_on", "fieldtype": "Select", "label": "Time Series Based On" @@ -89,10 +94,10 @@ "fieldname": "time_interval", "fieldtype": "Select", "label": "Time Interval", - "options": "Quarterly\nMonthly\nWeekly\nDaily" + "options": "Yearly\nQuarterly\nMonthly\nWeekly\nDaily" }, { - "default": "1", + "default": "0", "depends_on": "eval:doc.chart_type !== 'Group By'", "fieldname": "timeseries", "fieldtype": "Check", @@ -119,7 +124,7 @@ "fieldname": "type", "fieldtype": "Select", "label": "Type", - "options": "Line\nBar", + "options": "Line\nBar\nPercentage\nPie", "reqd": 1 }, { @@ -134,6 +139,7 @@ "fieldtype": "Column Break" }, { + "depends_on": "eval:doc.chart_type !== 'Report'", "fieldname": "color", "fieldtype": "Color", "label": "Color" @@ -185,9 +191,39 @@ "fieldname": "to_date", "fieldtype": "Date", "label": "To Date" + }, + { + "depends_on": "eval:doc.chart_type == 'Report' && doc.report_name && !doc.is_custom", + "fieldname": "x_field", + "fieldtype": "Select", + "label": "X Field", + "mandatory_depends_on": "eval: doc.report_name && !doc.is_custom" + }, + { + "depends_on": "eval:doc.chart_type === 'Report'", + "fieldname": "report_name", + "fieldtype": "Link", + "label": "Report Name", + "options": "Report" + }, + { + "default": "0", + "depends_on": "eval: doc.report_name", + "fieldname": "is_custom", + "fieldtype": "Check", + "label": "Is Custom" + }, + { + "depends_on": "eval:doc.chart_type == 'Report' && doc.report_name && !doc.is_custom", + "fieldname": "y_axis", + "fieldtype": "Table", + "label": "Y Axis", + "mandatory_depends_on": "eval:doc.report_name && !doc.is_custom", + "options": "Dashboard Chart Field" } ], - "modified": "2020-03-17 17:08:12.758690", + "links": [], + "modified": "2020-03-01 22:08:47.135523", "modified_by": "Administrator", "module": "Desk", "name": "Dashboard Chart", diff --git a/frappe/desk/doctype/dashboard_chart/dashboard_chart.py b/frappe/desk/doctype/dashboard_chart/dashboard_chart.py index b48687b959c2..5ed81ccaeed6 100644 --- a/frappe/desk/doctype/dashboard_chart/dashboard_chart.py +++ b/frappe/desk/doctype/dashboard_chart/dashboard_chart.py @@ -6,29 +6,40 @@ import frappe from frappe import _ import datetime +import json from frappe.core.page.dashboard.dashboard import cache_source, get_from_date_from_timespan -from frappe.utils import nowdate, add_to_date, getdate, get_last_day, formatdate +from frappe.utils import nowdate, add_to_date, getdate, get_last_day, formatdate, get_datetime +from frappe.model.naming import append_number_if_name_exists from frappe.model.document import Document @frappe.whitelist() @cache_source -def get(chart_name = None, chart = None, no_cache = None, from_date = None, to_date = None, refresh = None): +def get(chart_name = None, chart = None, no_cache = None, filters = None, from_date = None, + to_date = None, timespan = None, time_interval = None, refresh = None): if chart_name: chart = frappe.get_doc('Dashboard Chart', chart_name) else: chart = frappe._dict(frappe.parse_json(chart)) - timespan = chart.timespan - if chart.timespan == 'Select Date Range': - from_date = chart.from_date - to_date = chart.to_date + timespan = timespan or chart.timespan - timegrain = chart.time_interval - filters = frappe.parse_json(chart.filters_json) + if timespan == 'Select Date Range': + if from_date and len(from_date): + from_date = get_datetime(from_date) + else: + from_date = chart.from_date + + if to_date and len(to_date): + to_date = get_datetime(to_date) + else: + to_date = chart.to_date + + timegrain = time_interval or chart.time_interval + filters = frappe.parse_json(filters) or frappe.parse_json(chart.filters_json) # don't include cancelled documents - filters['docstatus'] = ('<', 2) + filters.append([chart.document_type, 'docstatus', '<', 2, False]) if chart.chart_type == 'Group By': chart_config = get_group_by_chart_config(chart, filters) @@ -37,6 +48,30 @@ def get(chart_name = None, chart = None, no_cache = None, from_date = None, to_d return chart_config +@frappe.whitelist() +def create_report_chart(args): + args = frappe.parse_json(args) + _doc = frappe.new_doc('Dashboard Chart') + + _doc.update(args) + if frappe.db.exists('Dashboard Chart', args.chart_name): + args.chart_name = append_number_if_name_exists('Dashboard Chart', args.chart_name) + _doc.chart_name = args.chart_name + _doc.insert(ignore_permissions=True) + + if args.dashboard: + add_chart_to_dashboard(json.dumps(args)) + +@frappe.whitelist() +def add_chart_to_dashboard(args): + args = frappe.parse_json(args) + dashboard = frappe.get_doc('Dashboard', args.dashboard) + dashboard_link = frappe.new_doc('Dashboard Chart Link') + dashboard_link.chart = args.chart_name + + dashboard.append('charts', dashboard_link) + dashboard.save() + frappe.db.commit() def get_chart_config(chart, filters, timespan, timegrain, from_date, to_date): if not from_date: @@ -44,33 +79,37 @@ def get_chart_config(chart, filters, timespan, timegrain, from_date, to_date): if not to_date: to_date = datetime.datetime.now() - # get conditions from filters - conditions, values = frappe.db.build_conditions(filters) - # query will return year, unit and aggregate value - data = frappe.db.sql(''' - select - {unit} as _unit, - {aggregate_function}({value_field}) - from `tab{doctype}` - where - {conditions} - and {datefield} BETWEEN '{from_date}' and '{to_date}' - group by _unit - order by _unit asc - '''.format( - unit = chart.based_on, - datefield = chart.based_on, - aggregate_function = get_aggregate_function(chart.chart_type), - value_field = chart.value_based_on or '1', - doctype = chart.document_type, - conditions = conditions, - from_date = from_date.strftime('%Y-%m-%d'), - to_date = to_date - ), values) + doctype = chart.document_type + unit_function = get_unit_function(doctype, chart.based_on, timegrain) + datefield = chart.based_on + aggregate_function = get_aggregate_function(chart.chart_type) + value_field = chart.value_based_on or '1' + from_date = from_date.strftime('%Y-%m-%d') + to_date = to_date + + filters.append([doctype, datefield, '>=', from_date, False]) + filters.append([doctype, datefield, '<=', to_date, False]) + + data = frappe.db.get_all( + doctype, + fields = [ + 'extract(year from `tab{doctype}`.{datefield}) as _year'.format(doctype=doctype, datefield=datefield), + '{} as _unit'.format(unit_function), + '{aggregate_function}({value_field})'.format(aggregate_function=aggregate_function, value_field=value_field), + ], + filters = filters, + group_by = '_year, _unit', + order_by = '_year asc, _unit asc', + as_list = True, + ignore_ifnull = True + ) + + + # result given as year, unit -> convert it to end of period of that unit + result = convert_to_dates(data, timegrain) # add missing data points for periods where there was no result - result = get_result(data, timegrain, from_date, to_date) - + result = add_missing_values(result, timegrain, timespan, from_date, to_date) chart_config = { "labels": [formatdate(r[0].strftime('%Y-%m-%d')) for r in result], "datasets": [{ @@ -83,23 +122,23 @@ def get_chart_config(chart, filters, timespan, timegrain, from_date, to_date): def get_group_by_chart_config(chart, filters): - conditions, values = frappe.db.build_conditions(filters) - data = frappe.db.sql(''' - select - {aggregate_function}({value_field}) as count, - {group_by_field} as name - from `tab{doctype}` - where {conditions} - group by {group_by_field} - order by count desc - '''.format( - aggregate_function = get_aggregate_function(chart.group_by_type), - value_field = chart.aggregate_function_based_on or '1', - field = chart.aggregate_function_based_on or chart.group_by_based_on, - group_by_field = chart.group_by_based_on, - doctype = chart.document_type, - conditions = conditions, - ), values, as_dict = True) + + aggregate_function = get_aggregate_function(chart.group_by_type) + value_field = chart.aggregate_function_based_on or '1' + group_by_field = chart.group_by_based_on + doctype = chart.document_type + + data = frappe.db.get_all( + doctype, + fields = [ + '{} as name'.format(group_by_field), + '{aggregate_function}({value_field}) as count'.format(aggregate_function=aggregate_function, value_field=value_field), + ], + filters = filters, + group_by = group_by_field, + order_by = 'count desc', + ignore_ifnull = True + ) if data: if chart.number_of_groups and chart.number_of_groups < len(data): @@ -116,6 +155,7 @@ def get_group_by_chart_config(chart, filters): "values": [item['count'] for item in data] }] } + return chart_config else: return None @@ -128,25 +168,78 @@ def get_aggregate_function(chart_type): "Average": "AVG", }[chart_type] -def get_result(data, timegrain, from_date, to_date): - start_date = getdate(from_date) - end_date = getdate(to_date) + +def convert_to_dates(data, timegrain): + """ Converts individual dates within data to the end of period """ result = [] + for d in data: + if d[2] != 0: + if timegrain == 'Daily': + result.append([add_to_date('{:d}-01-01'.format(int(d[0])), days = d[1] - 1), d[2]]) + elif timegrain == 'Weekly': + result.append([add_to_date(add_to_date('{:d}-01-01'.format(int(d[0])), weeks = d[1] + 1), days = -1), d[2]]) + elif timegrain == 'Monthly': + result.append([add_to_date(add_to_date('{:d}-01-01'.format(int(d[0])), months=d[1]), days = -1), d[2]]) + elif timegrain == 'Quarterly': + result.append([add_to_date(add_to_date('{:d}-01-01'.format(int(d[0])), months=d[1] * 3), days = -1), d[2]]) + elif timegrain == 'Yearly': + result.append([add_to_date(add_to_date('{:d}-01-01'.format(int(d[0])), months=12), days = -1), d[2]]) + result[-1][0] = getdate(result[-1][0]) - while start_date <= end_date: - next_date = get_next_expected_date(start_date, timegrain) - result.append([next_date, 0.0]) - start_date = next_date + return result - data_index = 0 - if data: - for i, d in enumerate(result): - while data_index < len(data) and getdate(data[data_index][0]) <= d[0]: - d[1] += data[data_index][1] - data_index += 1 +def get_unit_function(doctype, datefield, timegrain): + unit_function = '' + if timegrain=='Daily': + if frappe.db.db_type == 'mariadb': + unit_function = 'dayofyear(`tab{doctype}`.{datefield})'.format( + doctype=doctype, datefield=datefield) + else: + unit_function = 'extract(doy from `tab{doctype}`.{datefield})'.format( + doctype=doctype, datefield=datefield) - return result + else: + unit_function = 'extract({unit} from `tab{doctype}`.{datefield})'.format( + unit = timegrain[:-2].lower(), doctype=doctype, datefield=datefield) + + return unit_function + +def add_missing_values(data, timegrain, timespan, from_date, to_date): + # add missing intervals + result = [] + + if timespan != 'All Time': + first_expected_date = get_period_ending(from_date, timegrain) + # fill out data before the first data point + first_data_point_date = data[0][0] if data else getdate(add_to_date(to_date, days=1)) + while first_data_point_date > first_expected_date: + result.append([first_expected_date, 0.0]) + first_expected_date = get_next_expected_date(first_expected_date, timegrain) + + # fill data points and missing points + for i, d in enumerate(data): + result.append(d) + + next_expected_date = get_next_expected_date(d[0], timegrain) + + if i < len(data)-1: + next_date = data[i+1][0] + else: + # already reached at end of data, see if we need any more dates + next_date = getdate(nowdate()) + + # if next data point is earler than the expected date + # need to fill out missing data points + while next_date > next_expected_date: + # fill missing value + result.append([next_expected_date, 0.0]) + next_expected_date = get_next_expected_date(next_expected_date, timegrain) + # add date for the last period (if missing) + if result and get_period_ending(to_date, timegrain) > result[-1][0]: + result.append([get_period_ending(to_date, timegrain), 0.0]) + + return result def get_next_expected_date(date, timegrain): next_date = None @@ -159,24 +252,31 @@ def get_next_expected_date(date, timegrain): def get_period_ending(date, timegrain): date = getdate(date) - if timegrain=='Daily': + if timegrain == 'Daily': pass - elif timegrain=='Weekly': + elif timegrain == 'Weekly': date = get_week_ending(date) - elif timegrain=='Monthly': + elif timegrain == 'Monthly': date = get_month_ending(date) - elif timegrain=='Quarterly': + elif timegrain == 'Quarterly': date = get_quarter_ending(date) + elif timegrain == 'Yearly': + date = get_year_ending(date) return getdate(date) def get_week_ending(date): - # week starts on monday - from datetime import timedelta - start = date - timedelta(days = date.weekday()) - end = start + timedelta(days=6) + # fun fact: week ends on the day before 1st Jan of the year. + # for 2019 it is Monday - return end + week_of_the_year = int(date.strftime('%U')) + + if week_of_the_year == 52: + date = add_to_date(date, years=1) + # first day of next week + date = add_to_date('{}-01-01'.format(date.year), weeks = (week_of_the_year + 1)%52) + # last day of this week + return add_to_date(date, days=-1) def get_month_ending(date): month_of_the_year = int(date.strftime('%m')) @@ -184,7 +284,7 @@ def get_month_ending(date): date = add_to_date('{}-01-01'.format(date.year), months = month_of_the_year) # last day of this month - return add_to_date(date, days = -1) + return add_to_date(date, days=-1) def get_quarter_ending(date): date = getdate(date) @@ -200,14 +300,24 @@ def get_quarter_ending(date): return date +def get_year_ending(date): + ''' returns year ending of the given date ''' + + # first day of next year (note year starts from 1) + date = add_to_date('{}-01-01'.format(date.year), months = 12) + # last day of this month + return add_to_date(date, days=-1) + class DashboardChart(Document): + def on_update(self): frappe.cache().delete_key('chart-data:{}'.format(self.name)) def validate(self): - if self.chart_type != 'Custom': + if self.chart_type != 'Custom' and self.chart_type != 'Report': self.check_required_field() + self.check_document_type() def check_required_field(self): if not self.document_type: @@ -221,3 +331,7 @@ def check_required_field(self): else: if not self.based_on: frappe.throw(_("Time series based on is required to create a dashboard chart")) + + def check_document_type(self): + if frappe.get_meta(self.document_type).issingle: + frappe.throw("You cannot create a dashboard chart from single DocTypes") \ No newline at end of file diff --git a/frappe/desk/doctype/dashboard_chart/test_dashboard_chart.py b/frappe/desk/doctype/dashboard_chart/test_dashboard_chart.py index a550746f766d..660acef09c5a 100644 --- a/frappe/desk/doctype/dashboard_chart/test_dashboard_chart.py +++ b/frappe/desk/doctype/dashboard_chart/test_dashboard_chart.py @@ -17,9 +17,10 @@ def test_period_ending(self): self.assertEqual(get_period_ending('2019-04-10', 'Daily'), getdate('2019-04-10')) - # week starts on monday + # fun fact: week ends on the day before 1st Jan of the year. + # for 2019 it is Monday self.assertEqual(get_period_ending('2019-04-10', 'Weekly'), - getdate('2019-04-14')) + getdate('2019-04-15')) self.assertEqual(get_period_ending('2019-04-10', 'Monthly'), getdate('2019-04-30')) @@ -35,6 +36,9 @@ def test_period_ending(self): self.assertEqual(get_period_ending('2019-10-01', 'Quarterly'), getdate('2019-12-31')) + self.assertEqual(get_period_ending('2019-10-01', 'Yearly'), + getdate('2019-12-31')) + def test_dashboard_chart(self): if frappe.db.exists('Dashboard Chart', 'Test Dashboard Chart'): frappe.delete_doc('Dashboard Chart', 'Test Dashboard Chart') @@ -47,7 +51,7 @@ def test_dashboard_chart(self): based_on = 'creation', timespan = 'Last Year', time_interval = 'Monthly', - filters_json = '{}', + filters_json = '[]', timeseries = 1 )).insert() @@ -79,7 +83,7 @@ def test_empty_dashboard_chart(self): based_on = 'creation', timespan = 'Last Year', time_interval = 'Monthly', - filters_json = '{}', + filters_json = '[]', timeseries = 1 )).insert() @@ -111,7 +115,7 @@ def test_chart_wih_one_value(self): based_on = 'creation', timespan = 'Last Year', time_interval = 'Monthly', - filters_json = '{}', + filters_json = '[]', timeseries = 1 )).insert() @@ -129,34 +133,6 @@ def test_chart_wih_one_value(self): frappe.db.rollback() - def test_weekly_dashboard_chart(self): - insert_test_records() - - if frappe.db.exists('Dashboard Chart', 'Test Weekly Dashboard Chart'): - frappe.delete_doc('Dashboard Chart', 'Test Weekly Dashboard Chart') - - frappe.get_doc(dict( - doctype = 'Dashboard Chart', - chart_name = 'Test Weekly Dashboard Chart', - chart_type = 'Sum', - document_type = 'Communication', - based_on = 'communication_date', - value_based_on = 'rating', - timespan = 'Select Date Range', - time_interval = 'Weekly', - from_date = datetime(2018, 12, 30), - to_date = datetime(2019, 1, 15), - filters_json = '{}', - timeseries = 1 - )).insert() - - result = get(chart_name ='Test Weekly Dashboard Chart', refresh = 1) - - self.assertEqual(result.get('datasets')[0].get('values'), [200.0, 400.0, 0.0]) - self.assertEqual(result.get('labels'), [formatdate('2019-01-06'), formatdate('2019-01-13'), formatdate('2019-01-20')]) - - frappe.db.rollback() - def test_group_by_chart_type(self): if frappe.db.exists('Dashboard Chart', 'Test Group By Dashboard Chart'): frappe.delete_doc('Dashboard Chart', 'Test Group By Dashboard Chart') @@ -169,7 +145,7 @@ def test_group_by_chart_type(self): chart_type = 'Group By', document_type = 'ToDo', group_by_based_on = 'status', - filters_json = '{}', + filters_json = '[]', )).insert() result = get(chart_name ='Test Group By Dashboard Chart', refresh = 1) @@ -179,16 +155,17 @@ def test_group_by_chart_type(self): frappe.db.rollback() -def insert_test_records(): - create_new_communication(datetime(2019, 1, 10), 100) - create_new_communication(datetime(2019, 1, 6), 200) - create_new_communication(datetime(2019, 1, 8), 300) - -def create_new_communication(date, rating): - communication = { - 'doctype': 'Communication', - 'subject': 'Test Communication', - 'rating': rating, - 'communication_date': date - } - frappe.get_doc(communication).insert() + def test_dashboard_with_single_doctype(self): + if frappe.db.exists('Dashboard Chart', 'Test Single DocType In Dashboard Chart'): + frappe.delete_doc('Dashboard Chart', 'Test Single DocType In Dashboard Chart') + + chart_doc = frappe.get_doc(dict( + doctype = 'Dashboard Chart', + chart_name = 'Test Single DocType In Dashboard Chart', + chart_type = 'Count', + document_type = 'System Settings', + group_by_based_on = 'Created On', + filters_json = '{}', + )) + + self.assertRaises(frappe.ValidationError, chart_doc.insert) \ No newline at end of file diff --git a/frappe/desk/doctype/dashboard_chart_field/__init__.py b/frappe/desk/doctype/dashboard_chart_field/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/frappe/desk/doctype/dashboard_chart_field/dashboard_chart_field.json b/frappe/desk/doctype/dashboard_chart_field/dashboard_chart_field.json new file mode 100644 index 000000000000..6347be40ff00 --- /dev/null +++ b/frappe/desk/doctype/dashboard_chart_field/dashboard_chart_field.json @@ -0,0 +1,37 @@ +{ + "actions": [], + "creation": "2020-02-28 11:40:27.017380", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "y_field", + "color" + ], + "fields": [ + { + "fieldname": "y_field", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Y Field" + }, + { + "fieldname": "color", + "fieldtype": "Color", + "in_list_view": 1, + "label": "Color" + } + ], + "istable": 1, + "links": [], + "modified": "2020-02-28 11:48:24.731946", + "modified_by": "Administrator", + "module": "Desk", + "name": "Dashboard Chart Field", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/frappe/desk/doctype/dashboard_chart_field/dashboard_chart_field.py b/frappe/desk/doctype/dashboard_chart_field/dashboard_chart_field.py new file mode 100644 index 000000000000..734f27cc280b --- /dev/null +++ b/frappe/desk/doctype/dashboard_chart_field/dashboard_chart_field.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +# import frappe +from frappe.model.document import Document + +class DashboardChartField(Document): + pass diff --git a/frappe/desk/doctype/dashboard_chart_link/dashboard_chart_link.json b/frappe/desk/doctype/dashboard_chart_link/dashboard_chart_link.json index df278fb4c12d..361361bc97c8 100644 --- a/frappe/desk/doctype/dashboard_chart_link/dashboard_chart_link.json +++ b/frappe/desk/doctype/dashboard_chart_link/dashboard_chart_link.json @@ -1,77 +1,29 @@ { - "allow_copy": 0, - "allow_events_in_timeline": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "beta": 0, "creation": "2019-03-12 15:00:57.052684", - "custom": 0, - "docstatus": 0, "doctype": "DocType", - "document_type": "", "editable_grid": 1, "engine": "InnoDB", + "field_order": [ + "chart" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, "fieldname": "chart", "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, "in_list_view": 1, - "in_standard_filter": 0, "label": "Chart", - "length": 0, - "no_copy": 0, - "options": "Dashboard Chart", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "options": "Dashboard Chart" } ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, "istable": 1, - "max_attachments": 0, - "modified": "2019-03-12 15:01:31.639414", + "modified": "2020-11-07 04:47:04.548265", "modified_by": "Administrator", "module": "Desk", "name": "Dashboard Chart Link", - "name_case": "", "owner": "Administrator", "permissions": [], "quick_entry": 1, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, "sort_field": "modified", "sort_order": "ASC", - "track_changes": 1, - "track_seen": 0, - "track_views": 0 + "track_changes": 1 } \ No newline at end of file diff --git a/frappe/desk/doctype/dashboard_chart_source/dashboard_chart_source.json b/frappe/desk/doctype/dashboard_chart_source/dashboard_chart_source.json index 7f6532ce1fbb..f2c0e13f874a 100644 --- a/frappe/desk/doctype/dashboard_chart_source/dashboard_chart_source.json +++ b/frappe/desk/doctype/dashboard_chart_source/dashboard_chart_source.json @@ -1,162 +1,58 @@ { - "allow_copy": 0, - "allow_events_in_timeline": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, "autoname": "field:source_name", - "beta": 0, "creation": "2019-02-06 07:55:29.579840", - "custom": 0, - "docstatus": 0, "doctype": "DocType", - "document_type": "", "editable_grid": 1, "engine": "InnoDB", + "field_order": [ + "source_name", + "module", + "timeseries" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, "fieldname": "source_name", "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, "in_list_view": 1, - "in_standard_filter": 0, "label": "Source Name", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, "unique": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, "fieldname": "module", "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, "in_list_view": 1, - "in_standard_filter": 0, "label": "Module", - "length": 0, - "no_copy": 0, "options": "Module Def", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "reqd": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, + "default": "0", "fieldname": "timeseries", "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Timeseries", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "Timeseries" } ], - "has_web_view": 0, - "hide_toolbar": 0, - "idx": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2019-04-09 14:20:51.548207", + "modified": "2020-11-07 04:47:08.787802", "modified_by": "Administrator", "module": "Desk", "name": "Dashboard Chart Source", - "name_case": "", "owner": "Administrator", "permissions": [ { - "amend": 0, - "cancel": 0, "create": 1, "delete": 1, "email": 1, "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, "print": 1, "read": 1, "report": 1, "role": "System Manager", - "set_user_permissions": 0, "share": 1, - "submit": 0, "write": 1 } ], - "quick_entry": 0, - "read_only": 0, - "show_name_in_global_search": 0, "sort_field": "modified", "sort_order": "DESC", - "title_field": "", - "track_changes": 1, - "track_seen": 0, - "track_views": 0 + "track_changes": 1 } \ No newline at end of file diff --git a/frappe/desk/page/user_profile/user_profile.js b/frappe/desk/page/user_profile/user_profile.js index 80b5ecc7971e..975937728ab7 100644 --- a/frappe/desk/page/user_profile/user_profile.js +++ b/frappe/desk/page/user_profile/user_profile.js @@ -125,7 +125,7 @@ class UserProfile { } render_line_chart() { - this.line_chart_filters = {'user': this.user_id}; + this.line_chart_filters = [['Energy Point Log', 'user', '=', this.user_id, false]]; this.line_chart_config = { timespan: 'Last Month', time_interval: 'Daily', @@ -200,14 +200,17 @@ class UserProfile { label: 'All', options: ['All', 'Auto', 'Criticism', 'Appreciation', 'Revert'], action: (selected_item) => { - if (selected_item === 'All') delete this.line_chart_filters.type; - else this.line_chart_filters.type = selected_item; + if (selected_item === 'All') { + if (this.line_chart_filters.length > 1) this.line_chart_filters.pop(); + } else { + this.line_chart_filters[1] = ['Energy Point Log', 'type', '=', selected_item, false]; + } this.update_line_chart_data(); } }, { label: 'Last Month', - options: ['Last Week', 'Last Month', 'Last Quarter'], + options: ['Last Week', 'Last Month', 'Last Quarter', 'Last Year'], action: (selected_item) => { this.line_chart_config.timespan = selected_item; this.update_line_chart_data(); @@ -222,7 +225,7 @@ class UserProfile { } }, ]; - this.render_chart_filters(filters, '.line-chart-container', 1); + frappe.dashboard_utils.render_chart_filters(filters, 'chart-filter', '.line-chart-container', 1); } create_percentage_chart_filters() { @@ -237,7 +240,7 @@ class UserProfile { } }, ]; - this.render_chart_filters(filters, '.percentage-chart-container'); + frappe.dashboard_utils.render_chart_filters(filters, 'chart-filter', '.percentage-chart-container'); } create_heatmap_chart_filters() { @@ -250,47 +253,9 @@ class UserProfile { } }, ]; - this.render_chart_filters(filters, '.heatmap-container'); + frappe.dashboard_utils.render_chart_filters(filters, 'chart-filter', '.heatmap-container'); } - render_chart_filters(filters, container, append) { - filters.forEach(filter => { - let chart_filter_html = `
- `; - let options_html; - - if (filter.fieldnames) { - options_html = filter.options.map((option, i) => - `
  • ${option}
  • `).join(''); - } else { - options_html = filter.options.map( option => `
  • ${option}
  • `).join(''); - } - - let dropdown_html = chart_filter_html + `
    `; - let $chart_filter = $(dropdown_html); - - if (append) { - $chart_filter.prependTo(this.wrapper.find(container)); - } else $chart_filter.appendTo(this.wrapper.find(container)); - - $chart_filter.find('.dropdown-menu').on('click', 'li a', (e) => { - let $el = $(e.currentTarget); - let fieldname; - if ($el.attr('data-fieldname')) { - fieldname = $el.attr('data-fieldname'); - } - let selected_item = $el.text(); - $el.parents('.chart-filter').find('.filter-label').text(selected_item); - filter.action(selected_item, fieldname); - }); - }); - - } edit_profile() { let edit_profile_dialog = new frappe.ui.Dialog({ @@ -423,7 +388,8 @@ class UserProfile { let get_recent_energy_points_html = (field) => { let message_html = frappe.energy_points.format_history_log(field); - return `

    ${message_html}

    `; + return `

    +${message_html}

    `; }; frappe.xcall('frappe.desk.page.user_profile.user_profile.get_energy_points_list', { diff --git a/frappe/desk/page/user_profile/user_profile.py b/frappe/desk/page/user_profile/user_profile.py index 009ebf782f7a..c3d711c99520 100644 --- a/frappe/desk/page/user_profile/user_profile.py +++ b/frappe/desk/page/user_profile/user_profile.py @@ -16,63 +16,63 @@ def get_energy_points_heatmap_data(user, date): @frappe.whitelist() def get_energy_points_percentage_chart_data(user, field): - result = frappe.db.get_all('Energy Point Log', - filters = {'user': user, 'type': ['!=', 'Review']}, - group_by = field, - order_by = field, - fields = [field, 'ABS(sum(points)) as points'], - as_list = True) + result = frappe.db.get_all('Energy Point Log', + filters = {'user': user, 'type': ['!=', 'Review']}, + group_by = field, + order_by = field, + fields = [field, 'ABS(sum(points)) as points'], + as_list = True) - return { - "labels": [r[0] for r in result if r[0] != None], - "datasets": [{ - "values": [r[1] for r in result] - }] - } + return { + "labels": [r[0] for r in result if r[0] != None], + "datasets": [{ + "values": [r[1] for r in result] + }] + } @frappe.whitelist() def get_user_rank(user): - month_start = datetime.today().replace(day=1) - monthly_rank = frappe.db.get_all('Energy Point Log', - group_by = 'user', - filters = {'creation': ['>', month_start], 'type' : ['!=', 'Review']}, - fields = ['user', 'sum(points)'], - order_by = 'sum(points) desc', - as_list = True) + month_start = datetime.today().replace(day=1) + monthly_rank = frappe.db.get_all('Energy Point Log', + group_by = 'user', + filters = {'creation': ['>', month_start], 'type' : ['!=', 'Review']}, + fields = ['user', 'sum(points)'], + order_by = 'sum(points) desc', + as_list = True) - all_time_rank = frappe.db.get_all('Energy Point Log', - group_by = 'user', - filters = {'type' : ['!=', 'Review']}, - fields = ['user', 'sum(points)'], - order_by = 'sum(points) desc', - as_list = True) + all_time_rank = frappe.db.get_all('Energy Point Log', + group_by = 'user', + filters = {'type' : ['!=', 'Review']}, + fields = ['user', 'sum(points)'], + order_by = 'sum(points) desc', + as_list = True) - return { - 'monthly_rank': [i+1 for i, r in enumerate(monthly_rank) if r[0] == user], - 'all_time_rank': [i+1 for i, r in enumerate(all_time_rank) if r[0] == user] - } + return { + 'monthly_rank': [i+1 for i, r in enumerate(monthly_rank) if r[0] == user], + 'all_time_rank': [i+1 for i, r in enumerate(all_time_rank) if r[0] == user] + } @frappe.whitelist() def update_profile_info(profile_info): - profile_info = frappe.parse_json(profile_info) - keys = ['location', 'interest', 'user_image', 'bio'] + profile_info = frappe.parse_json(profile_info) + keys = ['location', 'interest', 'user_image', 'bio'] - for key in keys: - if key not in profile_info: - profile_info[key] = None + for key in keys: + if key not in profile_info: + profile_info[key] = None - user = frappe.get_doc('User', frappe.session.user) - user.update(profile_info) - user.save() - return user + user = frappe.get_doc('User', frappe.session.user) + user.update(profile_info) + user.save() + return user @frappe.whitelist() def get_energy_points_list(start, limit, user): - return frappe.db.get_list('Energy Point Log', - filters = {'user': user, 'type': ['!=', 'Review']}, - fields = ['name','user', 'points', 'reference_doctype', 'reference_name', 'reason', - 'type', 'seen', 'rule', 'owner', 'creation', 'revert_of'], - start = start, - limit = limit, - order_by = 'creation desc') + return frappe.db.get_list('Energy Point Log', + filters = {'user': user, 'type': ['!=', 'Review']}, + fields = ['name','user', 'points', 'reference_doctype', 'reference_name', 'reason', + 'type', 'seen', 'rule', 'owner', 'creation', 'revert_of'], + start = start, + limit = limit, + order_by = 'creation desc') \ No newline at end of file diff --git a/frappe/model/db_query.py b/frappe/model/db_query.py index f9016d7fcf10..596aa18b096c 100644 --- a/frappe/model/db_query.py +++ b/frappe/model/db_query.py @@ -261,7 +261,7 @@ def extract_tables(self): if self.fields: for f in self.fields: if ( not ("tab" in f and "." in f) ) or ("locate(" in f) or ("strpos(" in f) or \ - ("count(" in f) or ("avg(" in f) or ("sum(" in f): + ("count(" in f) or ("avg(" in f) or ("sum(" in f) or ("extract(" in f) or ("dayofyear(" in f): continue table_name = f.split('.')[0] @@ -285,7 +285,7 @@ def set_field_tables(self): '''If there are more than one table, the fieldname must not be ambiguous. If the fieldname is not explicitly mentioned, set the default table''' def _in_standard_sql_methods(field): - methods = ('count(', 'avg(', 'sum(') + methods = ('count(', 'avg(', 'sum(', 'extract(', 'dayofyear(') return field.lower().startswith(methods) if len(self.tables) > 1: diff --git a/frappe/patches.txt b/frappe/patches.txt index f0a9c384750f..21852d77477d 100644 --- a/frappe/patches.txt +++ b/frappe/patches.txt @@ -269,3 +269,4 @@ frappe.patches.v12_0.remove_parent_and_parenttype_from_print_formats execute:from frappe.desk.page.setup_wizard.install_fixtures import update_genders;update_genders() frappe.patches.v12_0.email_unsubscribe frappe.patches.v12_0.replace_old_data_import +frappe.patches.v12_0.change_existing_dashboard_chart_filters \ No newline at end of file diff --git a/frappe/patches/v12_0/change_existing_dashboard_chart_filters.py b/frappe/patches/v12_0/change_existing_dashboard_chart_filters.py new file mode 100644 index 000000000000..f61c2f0f9517 --- /dev/null +++ b/frappe/patches/v12_0/change_existing_dashboard_chart_filters.py @@ -0,0 +1,26 @@ +import frappe +import json + +def execute(): + + charts_to_modify = frappe.db.get_all('Dashboard Chart', + fields = ['name', 'filters_json', 'document_type'], + filters = {'chart_type': ['not in', ['Report', 'Custom']]} + ) + + for chart in charts_to_modify: + old_filters = frappe.parse_json(chart.filters_json) + + if chart.filters_json and isinstance(old_filters, dict): + new_filters = [] + doctype = chart.document_type + + for key in old_filters.keys(): + filter_value = old_filters[key] + if isinstance(filter_value, list): + new_filters.append([doctype, key, filter_value[0], filter_value[1], 0]) + else: + new_filters.append([doctype, key, '=', filter_value, 0]) + + new_filters_json = json.dumps(new_filters) + frappe.db.set_value('Dashboard Chart', chart.name, 'filters_json', new_filters_json) \ No newline at end of file diff --git a/frappe/public/build.json b/frappe/public/build.json index a5e3108dee54..b718f1820f65 100755 --- a/frappe/public/build.json +++ b/frappe/public/build.json @@ -221,6 +221,7 @@ "public/js/frappe/chat.js", "public/js/frappe/social/social_factory.js", "public/js/frappe/utils/energy_point_utils.js", + "public/js/frappe/utils/dashboard_utils.js", "public/js/frappe/ui/chart.js", "public/js/frappe/barcode_scanner/index.js" ], @@ -310,7 +311,8 @@ "public/js/frappe/views/reports/print_grid.html", "public/js/frappe/views/reports/print_tree.html", "public/js/frappe/ui/group_by/group_by.html", - "public/js/frappe/ui/group_by/group_by.js" + "public/js/frappe/ui/group_by/group_by.js", + "public/js/frappe/views/reports/report_utils.js" ], "js/web_form.min.js": [ "public/js/frappe/utils/datetime.js", diff --git a/frappe/public/js/frappe/form/controls/color.js b/frappe/public/js/frappe/form/controls/color.js index 331180708c27..f954002b33c0 100644 --- a/frappe/public/js/frappe/form/controls/color.js +++ b/frappe/public/js/frappe/form/controls/color.js @@ -19,7 +19,6 @@ frappe.ui.form.ControlColor = frappe.ui.form.ControlData.extend({ }, make_color_input: function () { this.$wrapper - .find('.control-input-wrapper') .append(`
    `); diff --git a/frappe/public/js/frappe/form/grid.js b/frappe/public/js/frappe/form/grid.js index 34a4269a0e2d..da099a02535c 100644 --- a/frappe/public/js/frappe/form/grid.js +++ b/frappe/public/js/frappe/form/grid.js @@ -372,7 +372,7 @@ export default class Grid { return data; } get_modal_data() { - return this.df.get_data() ? this.df.get_data().filter(data => { + return this.df.get_data ? this.df.get_data().filter(data => { if (!this.deleted_docs || !in_list(this.deleted_docs, data.name)) { return data; } diff --git a/frappe/public/js/frappe/ui/dashboard_chart.js b/frappe/public/js/frappe/ui/dashboard_chart.js new file mode 100644 index 000000000000..cc19c707f376 --- /dev/null +++ b/frappe/public/js/frappe/ui/dashboard_chart.js @@ -0,0 +1,412 @@ +frappe.provide('ui') +frappe.provide('frappe.dashboards'); +frappe.provide('frappe.dashboards.chart_sources'); + +frappe.ui.DashboardChart = class DashboardChart { + constructor(chart_doc, chart_container, options) { + this.chart_doc = chart_doc; + this.container = chart_container; + this.options = options || {}; + this.chart_args = {}; + } + + show() { + this.get_settings().then(() => { + this.prepare_chart_object(); + this.prepare_container(); + this.setup_filter_button(); + + if (!this.options.hide_actions || this.options.hide_actions == undefined) { + if (this.chart_doc.timeseries && this.chart_doc.chart_type !== 'Custom') { + this.render_time_series_filters(); + } + + this.prepare_chart_actions(); + } + + this.fetch(this.filters).then( data => { + if (this.chart_doc.chart_type == 'Report') { + data = this.get_report_chart_data(data); + } + if (!this.options.hide_last_sync || this.options.hide_last_sync == undefined) { + this.update_last_synced(); + } + this.data = data; + this.render(); + }); + }); + } + + prepare_container() { + const column_width_map = { + "Half": "6", + "Full": "12", + }; + let columns = column_width_map[this.chart_doc.width]; + this.chart_container = $(`
    +
    +
    ${__("Loading...")}
    +
    ${__("No Data")}
    +
    +
    `); + this.chart_container.appendTo(this.container); + + if (!this.options.hide_last_sync || this.options.hide_last_sync == undefined) { + let last_synced_text = $(``); + last_synced_text.prependTo(this.chart_container); + } + } + + render_time_series_filters() { + let filters = [ + { + label: this.chart_doc.timespan, + options: ['Select Date Range', 'Last Year', 'Last Quarter', 'Last Month', 'Last Week'], + action: (selected_item) => { + this.selected_timespan = selected_item; + + if (this.selected_timespan === 'Select Date Range') { + this.render_date_range_fields(); + } else { + this.selected_from_date = null; + this.selected_to_date = null; + if (this.date_field_wrapper) this.date_field_wrapper.hide(); + this.fetch_and_update_chart(); + } + } + }, + { + label: this.chart_doc.time_interval, + options: ['Yearly', 'Quarterly', 'Monthly', 'Weekly', 'Daily'], + action: (selected_item) => { + this.selected_time_interval = selected_item; + this.fetch_and_update_chart(); + } + }, + ]; + + frappe.dashboard_utils.render_chart_filters(filters, 'chart-actions', this.chart_container, 1); + } + + fetch_and_update_chart() { + this.args = { + timespan: this.selected_timespan, + time_interval: this.selected_time_interval, + from_date: this.selected_from_date, + to_date: this.selected_to_date + }; + + this.fetch(this.filters, true, this.args).then(data => { + if (this.chart_doc.chart_type == 'Report') { + data = this.get_report_chart_data(data); + } + + this.update_chart_object(); + this.data = data; + this.render(); + }); + } + + render_date_range_fields() { + if (!this.date_field_wrapper || !this.date_field_wrapper.is(':visible')) { + this.date_field_wrapper = + $(`
    `) + .insertBefore(this.chart_container.find('.chart-wrapper')); + + this.date_range_field = frappe.ui.form.make_control({ + df: { + fieldtype: 'DateRange', + fieldname: 'from_date', + placeholder: 'Date Range', + input_class: 'input-xs', + reqd: 1, + change: () => { + let selected_date_range = this.date_range_field.get_value(); + this.selected_from_date = selected_date_range[0]; + this.selected_to_date = selected_date_range[1]; + + if (selected_date_range && selected_date_range.length == 2) { + this.fetch_and_update_chart(); + } + } + }, + parent: this.date_field_wrapper, + render_input: 1 + }); + } + } + + get_report_chart_data(result) { + if (result.chart && this.chart_doc.is_custom) { + return result.chart.data; + } else { + let y_fields = []; + this.chart_doc.y_axis.map( field => { + y_fields.push(field.y_field); + }); + + let chart_fields = { + y_fields: y_fields, + x_field: this.chart_doc.x_field, + chart_type: this.chart_doc.type, + color: this.chart_doc.color + }; + let columns = result.columns.map((col)=> { + return frappe.report_utils.prepare_field_from_column(col); + }); + + let data = frappe.report_utils.make_chart_options(columns, result, chart_fields).data; + return data; + } + } + + prepare_chart_actions() { + let actions = [ + { + label: __("Refresh"), + action: 'action-refresh', + handler: () => { + this.fetch_and_update_chart(); + } + }, + { + label: __("Edit..."), + action: 'action-edit', + handler: () => { + frappe.set_route('Form', 'Dashboard Chart', this.chart_doc.name); + } + } + ]; + if (this.chart_doc.document_type) { + actions.push({ + label: __("{0} List", [this.chart_doc.document_type]), + action: 'action-list', + handler: () => { + frappe.set_route('List', this.chart_doc.document_type); + } + }); + } else if (this.chart_doc.chart_type === 'Report') { + actions.push({ + label: __("{0} Report", [this.chart_doc.report_name]), + action: 'action-list', + handler: () => { + frappe.set_route('query-report', this.chart_doc.report_name); + } + }) + } + this.set_chart_actions(actions); + } + + setup_filter_button() { + + this.is_document_type = this.chart_doc.chart_type!== 'Report' && this.chart_doc.chart_type!=='Custom'; + this.filter_button = + $(`
    ${__("Filter")}
    `); + this.filter_button.prependTo(this.chart_container); + + this.filter_button.on('click', () => { + let fields; + + frappe.dashboard_utils.get_filters_for_chart_type(this.chart_doc) + .then(filters => { + if (!this.is_document_type) { + if (!filters) { + fields = [{ + fieldtype: "HTML", + options: __("No Filters Set") + }]; + } else { + fields = filters.filter(f => { + if (f.on_change && !f.reqd) { + return false; + } + if (f.get_query || f.get_data) { + f.read_only = 1; + } + return f.fieldname; + }); + } + } else { + fields = [{ + fieldtype: 'HTML', + fieldname: 'filter_area', + }]; + } + + this.setup_filter_dialog(fields); + }); + }); + } + + setup_filter_dialog(fields) { + + let me = this; + let dialog = new frappe.ui.Dialog({ + title: __(`Set Filters for ${this.chart_doc.chart_name}`), + fields: fields, + primary_action: function() { + let values = this.get_values(); + if (values) { + this.hide(); + if (me.is_document_type) { + me.filters = me.filter_group.get_filters(); + } else { + me.filters = values; + } + me.fetch_and_update_chart(); + } + }, + primary_action_label: "Set" + }); + + if (this.is_document_type) { + this.create_filter_group_and_add_filters(dialog.get_field('filter_area').$wrapper); + } + + dialog.show(); + dialog.set_values(this.filters); + + } + + create_filter_group_and_add_filters(parent) { + this.filter_group = new frappe.ui.FilterGroup({ + parent: parent, + doctype: this.chart_doc.document_type, + on_change: () => {}, + }); + + frappe.model.with_doctype(this.chart_doc.document_type, () => { + this.filter_group.add_filters_to_filter_group(this.filters); + }); + } + + set_chart_actions(actions) { + this.chart_actions = $(` + `); + + this.chart_actions.find("a[data-action]").each((i, o) => { + const action = o.dataset.action; + $(o).click(actions.find(a => a.action === action)); + }); + this.chart_actions.prependTo(this.chart_container); + } + + fetch(filters, refresh=false, args) { + this.chart_container.find('.chart-loading-state').removeClass('hide'); + let method = this.settings ? this.settings.method + : 'frappe.desk.doctype.dashboard_chart.dashboard_chart.get'; + + if (this.chart_doc.chart_type == 'Report') { + args = { + report_name: this.chart_doc.report_name, + filters: filters, + }; + } else { + args = { + chart_name: this.chart_doc.name, + filters: filters, + refresh: refresh ? 1 : 0, + time_interval: args && args.time_interval? args.time_interval: null, + timespan: args && args.timespan? args.timespan: null, + from_date: args && args.from_date? args.from_date: null, + to_date: args && args.to_date? args.to_date: null, + }; + } + return frappe.xcall( + method, + args + ); + } + + render() { + const chart_type_map = { + 'Line': 'line', + 'Bar': 'bar', + 'Percentage': 'percentage', + 'Pie': 'pie' + }; + + let colors = []; + + if (this.chart_doc.y_axis.length) { + this.chart_doc.y_axis.map( field => { + colors.push(field.color); + }); + } else if (['Line', 'Bar'].includes(this.chart_doc.type)) { + colors = [this.chart_doc.color || "light-blue"]; + } + + this.chart_container.find('.chart-loading-state').addClass('hide'); + if (!this.data) { + this.chart_container.find('.chart-empty-state').removeClass('hide'); + } else { + let title = null; + if (!this.options.hide_title || this.options.hide_title == undefined) { + title = this.chart_doc.chart_name; + } + + let chart_args = { + title: title, + data: this.data, + type: chart_type_map[this.chart_doc.type], + colors: colors, + axisOptions: { + xIsSeries: this.chart_doc.timeseries, + shortenYAxisNumbers: 1 + } + }; + if (!this.chart) { + this.chart = new frappe.Chart(this.chart_container.find(".chart-wrapper")[0], chart_args); + } else { + this.chart.update(this.data); + } + } + } + + update_last_synced() { + let last_synced_text = __("Last synced {0}", [comment_when(this.chart_doc.last_synced_on)]); + this.container.find(".last-synced-text").html(last_synced_text); + } + + update_chart_object() { + frappe.db.get_doc("Dashboard Chart", this.chart_doc.name).then(doc => { + this.chart_doc = doc; + this.prepare_chart_object(); + this.update_last_synced(); + }); + } + + prepare_chart_object() { + this.filters = this.filters || JSON.parse(this.chart_doc.filters_json || '[]'); + } + + get_settings() { + if (this.chart_doc.chart_type == 'Custom') { + // custom source + if (frappe.dashboards.chart_sources[this.chart_doc.source]) { + this.settings = frappe.dashboards.chart_sources[this.chart_doc.source]; + return Promise.resolve(); + } else { + const method = 'frappe.desk.doctype.dashboard_chart_source.dashboard_chart_source.get_config'; + return frappe.xcall(method, {name: this.chart_doc.source}).then(config => { + frappe.dom.eval(config); + this.settings = frappe.dashboards.chart_sources[this.chart_doc.source]; + }); + } + } else if (this.chart_doc.chart_type == 'Report') { + this.settings = { + 'method': 'frappe.desk.query_report.run' + }; + return Promise.resolve(); + } else { + return Promise.resolve(); + } + } +} \ No newline at end of file diff --git a/frappe/public/js/frappe/ui/filters/filter_list.js b/frappe/public/js/frappe/ui/filters/filter_list.js index 77d3cbfcd34d..db6398ca7887 100644 --- a/frappe/public/js/frappe/ui/filters/filter_list.js +++ b/frappe/public/js/frappe/ui/filters/filter_list.js @@ -167,4 +167,20 @@ frappe.ui.FilterGroup = class {
    `); } + + get_filters_as_object() { + let filters = this.get_filters().reduce((acc, filter) => { + return Object.assign(acc, { + [filter[1]]: [filter[2], filter[3]] + }); + }, {}); + return filters; + } + + add_filters_to_filter_group(filters) { + + filters.forEach(filter => { + this.add_filter(filter[0], filter[1], filter[2], filter[3]); + }); + } }; diff --git a/frappe/public/js/frappe/utils/dashboard_utils.js b/frappe/public/js/frappe/utils/dashboard_utils.js new file mode 100644 index 000000000000..7e64f5c1437c --- /dev/null +++ b/frappe/public/js/frappe/utils/dashboard_utils.js @@ -0,0 +1,58 @@ +frappe.dashboard_utils = { + render_chart_filters: function(filters, button_class, container, append) { + filters.forEach(filter => { + let chart_filter_html = + ``; + let $chart_filter = $(dropdown_html); + + if (append) { + $chart_filter.prependTo(container); + } else $chart_filter.appendTo(container); + + $chart_filter.find('.dropdown-menu').on('click', 'li a', (e) => { + let $el = $(e.currentTarget); + let fieldname; + if ($el.attr('data-fieldname')) { + fieldname = $el.attr('data-fieldname'); + } + + let selected_item = $el.text(); + $el.parents(`.${button_class}`).find('.filter-label').text(selected_item); + filter.action(selected_item, fieldname); + }); + }); + + }, + + get_filters_for_chart_type: function(chart) { + if (chart.chart_type === 'Custom' && chart.source) { + const method = 'frappe.desk.doctype.dashboard_chart_source.dashboard_chart_source.get_config'; + return frappe.xcall(method, {name: chart.source}).then(config => { + frappe.dom.eval(config); + return frappe.dashboards.chart_sources[chart.source].filters; + }); + } else if (chart.chart_type === 'Report') { + return frappe.report_utils.get_report_filters(chart.report_name).then(filters => { + return filters; + }); + } else { + return Promise.resolve(); + } + } +}; \ No newline at end of file diff --git a/frappe/public/js/frappe/views/reports/query_report.js b/frappe/public/js/frappe/views/reports/query_report.js index 1f1a8ea459fd..b833516b1186 100644 --- a/frappe/public/js/frappe/views/reports/query_report.js +++ b/frappe/public/js/frappe/views/reports/query_report.js @@ -121,25 +121,121 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { this.datatable = null; this.prepared_report_action = "New"; - frappe.run_serially([ () => this.get_report_doc(), () => this.get_report_settings(), () => this.setup_progress_bar(), () => this.setup_page_head(), () => this.refresh_report(), - () => this.add_make_chart_button() + () => this.add_chart_buttons_to_toolbar(true) ]); } - add_make_chart_button(){ - this.page.add_inner_button(__("Set Chart"), () => { - this.get_possible_chart_options(); + add_chart_buttons_to_toolbar(show) { + if (show) { + this.page.add_inner_button(__("Set Chart"), () => { + this.open_create_chart_dialog(); + }); + + if (this.chart_fields || this.chart_options) { + this.page.add_inner_button(__("Add Chart to Dashboard"), () => { + this.add_chart_to_dashboard(); + }); + } + } else { + this.page.clear_inner_toolbar(); + } + } + + add_chart_to_dashboard() { + if (this.chart_fields || this.chart_options) { + const dialog = new frappe.ui.Dialog({ + title: __('Create Chart'), + fields: [ + { + fieldname: 'dashboard', + label: 'Choose Dashboard', + fieldtype: 'Link', + options: 'Dashboard', + }, + { + fieldname: 'dashboard_chart_name', + label: 'Chart Name', + fieldtype: 'Data', + } + ], + primary_action_label: __('Add'), + primary_action: (values) => { + this.create_dashboard_chart( + this.chart_fields || this.chart_options, + values.dashboard, + values.dashboard_chart_name + ); + dialog.hide(); + } + }); + + dialog.show(); + } else { + frappe.msgprint(__('Please Set Chart')); + } + } + + create_dashboard_chart(chart_args, dashboard_name, chart_name) { + + let args = { + 'dashboard': dashboard_name || null, + 'chart_type': 'Report', + 'report_name': this.report_name, + 'type': chart_args.chart_type || frappe.model.unscrub(chart_args.type), + 'color': chart_args.color, + 'filters_json': JSON.stringify(this.get_filter_values()), + }; + + if (this.chart_fields) { + let x_field_title = toTitle(chart_args.x_field); + let y_field_title = toTitle(chart_args.y_fields[0]); + chart_name = chart_name || (`${this.report_name}: ${x_field_title} vs ${y_field_title}`); + + Object.assign(args, + { + 'chart_name': chart_name, + 'x_field': chart_args.x_field, + 'y_axis': chart_args.y_axis_fields.map(f => { + return {'y_field': f.y_field, 'color': f.color}; + }), + 'is_custom': 0 + } + ); + } else { + chart_name = chart_name || this.report_name; + Object.assign(args, + { + 'chart_name': chart_name, + 'is_custom': 1 + } + ); + } + + frappe.xcall( + 'frappe.desk.doctype.dashboard_chart.dashboard_chart.create_report_chart', + {args: args} + ).then( () => { + let message; + if (dashboard_name) { + let dashboard_route_html = `${dashboard_name}`; + message = __(`New Dashboard Chart ${chart_name} added to Dashboard ` + dashboard_route_html); + } else { + message = __(`New chart ${chart_name} created`); + } + + frappe.msgprint(message, __('New Chart Created')); }); } refresh_report() { this.toggle_message(true); + this.toggle_report(false); return frappe.run_serially([ () => this.setup_filters(), @@ -159,25 +255,27 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { } get_report_settings() { - return new Promise((resolve, reject) => { - if (frappe.query_reports[this.report_name]) { - this.report_settings = frappe.query_reports[this.report_name]; - resolve(); - } else { - frappe.xcall('frappe.desk.query_report.get_script', { - report_name: this.report_name - }).then(settings => { - frappe.dom.eval(settings.script || ''); - frappe.after_ajax(() => { - this.report_settings = this.get_local_report_settings(); - this.report_settings.html_format = settings.html_format; - this.report_settings.execution_time = settings.execution_time || 0; - frappe.query_reports[this.report_name] = this.report_settings; - resolve(); - }); - }).catch(reject); - } + if (frappe.query_reports[this.report_name]) { + this.report_settings = this.get_local_report_settings(); + return this._load_script; + } + + this._load_script = (new Promise(resolve => frappe.call({ + method: 'frappe.desk.query_report.get_script', + args: { report_name: this.report_name }, + callback: resolve + }))).then(r => { + frappe.dom.eval(r.message.script || ''); + return r; + }).then(r => { + return frappe.after_ajax(() => { + this.report_settings = this.get_local_report_settings(); + this.report_settings.html_format = r.message.html_format; + this.report_settings.execution_time = r.message.execution_time || 0; + }); }); + + return this._load_script; } get_local_report_settings() { @@ -285,6 +383,7 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { refresh() { this.toggle_message(true); + this.toggle_report(false); let filters = this.get_filter_values(true); // only one refresh at a time @@ -334,27 +433,39 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { } this.add_prepared_report_buttons(data.doc); } + + if (data.report_summary) { + this.$summary.empty(); + this.render_summary(data.report_summary); + } + this.toggle_message(false); if (data.result && data.result.length) { this.prepare_report_data(data); + this.chart_options = this.get_chart_options(data); - const chart_options = this.get_chart_options(data); this.$chart.empty(); - if(chart_options) { - this.render_chart(chart_options); + if (this.chart_options) { + this.render_chart(this.chart_options); } else { this.$chart.empty(); if (this.chart_fields) { - const chart_options = this.make_chart_options(this.chart_fields); - chart_options && this.render_chart(chart_options); + this.chart_options = + frappe.report_utils.make_chart_options( + this.columns, + this.raw_data, + this.chart_fields + ); + this.chart_options && this.render_chart(this.chart_options); } } - this.render_datatable(); + this.add_chart_buttons_to_toolbar(true); } else { this.data = []; this.toggle_nothing_to_show(true); + this.add_chart_buttons_to_toolbar(false); } this.show_footer_message(); @@ -362,6 +473,32 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { }); } + render_summary(data) { + let build_summary_item = (summary) => { + let df = {fieldtype: summary.datatype}; + let doc = null; + + if (summary.datatype == "Currency") { + df.options = "currency"; + doc = {currency: summary.currency}; + } + + let value = frappe.format(summary.value, df, null, doc); + let indicator = summary.indicator ? `indicator ${ summary.indicator.toLowerCase() }` : ''; + + return $(`
    + ${summary.label} +

    ${ value }

    +
    `); + }; + + data.forEach((summary) => { + build_summary_item(summary).appendTo(this.$summary); + }) + + this.$summary.show(); + } + get_query_params() { const query_string = frappe.utils.get_query_string(frappe.get_route_str()); const query_params = frappe.utils.get_query_params(query_string); @@ -444,7 +581,6 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { prepare_report_data(data) { this.raw_data = data; this.columns = this.prepare_columns(data.columns); - this.custom_columns = []; this.data = this.prepare_data(data.result); this.linked_doctypes = this.get_linked_doctypes(); this.tree_report = this.data.some(d => 'indent' in d); @@ -490,6 +626,7 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { if (this.report_settings.after_datatable_render) { this.report_settings.after_datatable_render(this.datatable); } + this.$report.show(); } get_chart_options(data) { @@ -518,106 +655,65 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { render_chart(options) { this.$chart.empty(); - this.chart = new frappe.Chart(this.$chart[0], options); this.$chart.show(); + this.chart = new frappe.Chart(this.$chart[0], options); } - make_chart_options({ y_field, x_field, chart_type, color }) { - const type = chart_type.toLowerCase(); - const colors = color ? [color] : undefined; - - let columns = this.columns; - let rows = this.raw_data.result.filter(value => Object.keys(value).length); - - let labels = get_column_values(x_field); - - let dataset_values = get_column_values(y_field).map(d => Number(d)); - - if(this.raw_data.add_total_row) { - labels = labels.slice(0, -1); - dataset_values = dataset_values.slice(0, -1); - } - - return { - data: { - labels: labels, - datasets: [ - { values: dataset_values } - ] - }, - truncateLegends: 1, - type: type, - colors: colors, - axisOptions: { - shortenYAxisNumbers: 1 - } - }; - - function get_column_values(column_name) { - if (Array.isArray(rows[0])) { - let column_index = columns.findIndex(column => column.fieldname == column_name); - return rows.map(row => row[column_index]); - } else { - return rows.map(row => row[column_name]); + open_create_chart_dialog() { + const me = this; + let field_options = frappe.report_utils.get_possible_chart_options(this.columns, this.raw_data); + + function set_chart_values(values) { + values.y_fields = []; + values.colors = []; + if (values.y_axis_fields) { + values.y_axis_fields.map(f => { + values.y_fields.push(f.y_field); + values.colors.push(f.color); + }); } - } - } - get_possible_chart_options() { - const columns = this.columns; - const rows = this.raw_data.result.filter(value => Object.keys(value).length); - const first_row = Array.isArray(rows[0]) ? rows[0] : columns.map(col => rows[0][col.fieldname]); - const me = this + values.y_fields = + values.y_fields + .map(d => d.trim()) + .filter(Boolean); - const indices = first_row.reduce((accumulator, current_value, current_index) => { - if (Number.isFinite(current_value)) { - accumulator.push(current_index); - } - return accumulator; - }, []); + return values; + } function preview_chart() { const wrapper = $(dialog.fields_dict["chart_preview"].wrapper); - const values = dialog.get_values(true); - if (values.x_field && values.y_field) { - let options = me.make_chart_options(values); + let values = dialog.get_values(true); + values = set_chart_values(values); + + if (values.x_field && values.y_fields.length) { + let options = frappe.report_utils.make_chart_options(me.columns, me.raw_data, values); + me.chart_fields = values; wrapper.empty(); new frappe.Chart(wrapper[0], options); wrapper.find('.chart-container .title, .chart-container .sub-title').hide(); wrapper.show(); + + dialog.fields_dict['create_dashoard_chart'].df.hidden = 0; + dialog.refresh(); } else { - wrapper[0].innerHTML = `
    + wrapper[0].innerHTML = + `
    Please select X and Y fields
    `; } } - function get_options(fields) { - return fields.map((field) => { - return {label: field.label, value: field.fieldname}; - }); - } - - const numeric_fields = columns.filter((col, i) => indices.includes(i)); - const non_numeric_fields = columns.filter((col, i) => !indices.includes(i)) - const dialog = new frappe.ui.Dialog({ title: __('Create Chart'), fields: [ - { - fieldname: 'y_field', - label: 'Y Field', - fieldtype: 'Select', - options: get_options(numeric_fields), - onchange: preview_chart - }, { fieldname: 'x_field', label: 'X Field', fieldtype: 'Select', - options: get_options(non_numeric_fields), - onchange: preview_chart + default: me.chart_fields? me.chart_fields.x_field: null, + options: field_options.non_numeric_fields, }, { fieldname: 'cb_1', @@ -628,18 +724,41 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { label: 'Type of Chart', fieldtype: 'Select', options: ['Bar', 'Line', 'Percentage', 'Pie', 'Donut'], - default: 'Bar', - onchange: preview_chart + default: me.chart_fields? me.chart_fields.chart_type: 'Bar', }, { - fieldname: 'color', - label: 'Color', - fieldtype: 'Color', - depends_on: doc => ['Bar', 'Line'].includes(doc.chart_type), - onchange: preview_chart, + fieldname: 'sb_1', + fieldtype: 'Section Break', + label: 'Y axis' }, { - fieldname: 'sb_1', + fieldname: 'y_axis_fields', fieldtype: 'Table', + fields: [ + { + fieldtype: 'Select', + fieldname: 'y_field', + name: 'y_field', + label: __('Y Field'), + options: field_options.numeric_fields, + in_list_view: 1, + }, + { + fieldtype: 'Color', + fieldname: 'color', + name: 'color', + label: __('Color'), + in_list_view: 1, + }, + ], + }, + { + fieldname: 'preview_chart_button', + fieldtype: 'Button', + label: 'Preview Chart', + click: preview_chart + }, + { + fieldname: 'sb_2', fieldtype: 'Section Break', label: 'Chart Preview' }, @@ -647,19 +766,43 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { fieldname: 'chart_preview', label: 'Chart Preview', fieldtype: 'HTML', + }, + { + fieldname: 'create_dashoard_chart', + label: 'Add Chart to Dashboard', + fieldtype: 'Button', + hidden: 1, + click: () => { + dialog.hide(); + this.add_chart_to_dashboard(); + } } ], primary_action_label: __('Create'), primary_action: (values) => { - let options = me.make_chart_options(values); + values = set_chart_values(values); + + let options = + frappe.report_utils.make_chart_options( + this.columns, + this.raw_data, + values + ); me.chart_fields = values - let x_field_label = numeric_fields.filter((field) => field.fieldname == values.y_field)[0].label; - let y_field_label = non_numeric_fields.filter((field) => field.fieldname == values.x_field)[0].label; + let x_field_label = + field_options.numeric_fields.filter(field => + field.value == values.y_fields[0] + )[0].label; + let y_field_label = + field_options.non_numeric_fields.filter(field => + field.value == values.x_field + )[0].label; options.title = __(`${this.report_name}: ${x_field_label} vs ${y_field_label}`); this.render_chart(options); + this.add_chart_buttons_to_toolbar(true); dialog.hide(); } @@ -680,47 +823,14 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { prepare_columns(columns) { return columns.map(column => { - if (typeof column === 'string') { - if (column.includes(':')) { - let [label, fieldtype, width] = column.split(':'); - let options; - - if (fieldtype.includes('/')) { - [fieldtype, options] = fieldtype.split('/'); - } - - column = { - label, - fieldname: label, - fieldtype, - width, - options - }; - } else { - column = { - label: column, - fieldname: column, - fieldtype: 'Data' - }; - } - } + column = frappe.report_utils.prepare_field_from_column(column); const format_cell = (value, row, column, data) => { if (column.isHeader && !data && this.data) { // totalRow doesn't have a data object // proxy it using the first data object - // applied to Float, Currency fields, needed only for currency formatting. - // make first data column have value 'Total' - let index = 1; - if (this.datatable && this.datatable.options.checkboxColumn) index = 2; - - if (column.colIndex === index && !value) { - value = "Total"; - column.fieldtype = "Data"; // avoid type issues for value if Date column - } else if (in_list(["Currency", "Float"], column.fieldtype)) { - // proxy for currency and float - data = this.data[0]; - } + // this is needed only for currency formatting + data = this.data[0]; } return frappe.format(value, column, {for_print: false, always_show_decimals: true}, data); @@ -874,7 +984,7 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { }); } - pdf_report(print_settings, get_html=false) { + pdf_report(print_settings) { const base_url = frappe.urllib.get_base_url(); const print_css = frappe.boot.print_css; const landscape = print_settings.orientation == 'Landscape'; @@ -907,9 +1017,7 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { landscape: landscape, columns: columns }); - if (get_html) { - return html; - } + frappe.render_pdf(html, print_settings); } @@ -948,12 +1056,7 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { ], ({ file_format, include_indentation }) => { this.make_access_log('Export', file_format); if (file_format === 'CSV') { - const column_row = this.columns.reduce((acc, col) => { - if (!col.hidden) { - acc.push(col.label); - } - return acc; - }, []); + const column_row = this.columns.map(col => col.label); const data = this.get_data_for_csv(include_indentation); const out = [column_row].concat(data); @@ -972,7 +1075,6 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { const args = { cmd: 'frappe.desk.query_report.export_query', report_name: this.report_name, - custom_columns: this.custom_columns.length? this.custom_columns: [], file_format_type: file_format, filters: filters, visible_idx, @@ -1016,7 +1118,6 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { if (this.raw_data.add_total_row) { let totalRow = this.datatable.bodyRenderer.getTotalRow().reduce((row, cell) => { row[cell.column.id] = cell.content; - row.is_total_row = true; return row; }, {}); @@ -1040,7 +1141,7 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { } get_menu_items() { - return [ + let items = [ { label: __('Refresh'), action: () => this.refresh(), @@ -1139,20 +1240,16 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { primary_action: (values) => { const custom_columns = []; let df = frappe.meta.get_docfield(values.doctype, values.field); - const insert_after_index = this.columns - .findIndex(column => column.label === values.insert_after); custom_columns.push({ fieldname: df.fieldname, fieldtype: df.fieldtype, label: df.label, - insert_after_index: insert_after_index, link_field: this.doctype_field_map[values.doctype], doctype: values.doctype, options: df.fieldtype === "Link" ? df.options : undefined, width: 100 }); - this.custom_columns = this.custom_columns.concat(custom_columns); frappe.call({ method: 'frappe.desk.query_report.get_data_for_custom_field', args: { @@ -1162,8 +1259,7 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { callback: (r) => { const custom_data = r.message; const link_field = this.doctype_field_map[values.doctype]; - - this.add_custom_column(custom_columns, custom_data, link_field, values.field, insert_after_index); + this.add_custom_column(custom_columns, custom_data, link_field, values.field, values.insert_after); d.hide(); } }); @@ -1176,6 +1272,18 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { standard: true }, { + label: __('User Permissions'), + action: () => frappe.set_route('List', 'User Permission', { + doctype: 'Report', + name: this.report_name + }), + condition: () => frappe.model.can_set_user_permissions('Report'), + standard: true + } + ]; + + if (frappe.user.is_report_manager()) { + items.push({ label: __('Save'), action: () => { let d = new frappe.ui.Dialog({ @@ -1186,6 +1294,7 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { fieldname: 'report_name', label: __("Report Name"), default: this.report_doc.is_standard == 'No' ? this.report_name : "", + reqd: true } ], primary_action: (values) => { @@ -1207,17 +1316,10 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { d.show(); }, standard: true - }, - { - label: __('User Permissions'), - action: () => frappe.set_route('List', 'User Permission', { - doctype: 'Report', - name: this.report_name - }), - condition: () => frappe.model.can_set_user_permissions('Report'), - standard: true - } - ]; + }) + } + + return items; } add_portrait_warning(dialog) { @@ -1232,9 +1334,11 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { } } - add_custom_column(custom_column, custom_data, link_field, column_field, insert_after_index) { + add_custom_column(custom_column, custom_data, link_field, column_field, insert_after) { const column = this.prepare_columns(custom_column); + const insert_after_index = this.columns + .findIndex(column => column.label === insert_after); this.columns.splice(insert_after_index + 1, 0, column[0]); this.data.forEach(row => { @@ -1295,6 +1399,9 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { this.$status = $(`
    `) .hide().insertAfter(page_form); + this.$summary = $(`
    `) + .hide().appendTo(this.page.main); + this.$chart = $('
    ').hide().appendTo(this.page.main); this.$report = $('
    ').appendTo(this.page.main); this.$message = $(this.message_div('')).hide().appendTo(this.page.main); @@ -1380,8 +1487,12 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { } else { this.$message.hide(); } - this.$report.toggle(!flag); - this.$chart.toggle(!flag); + } + + toggle_report(flag) { + this.$report.toggle(flag); + this.$chart.toggle(flag); + this.$summary.toggle(flag); } // backward compatibility get get_values() { diff --git a/frappe/public/js/frappe/views/reports/report_utils.js b/frappe/public/js/frappe/views/reports/report_utils.js new file mode 100644 index 000000000000..0ecfbdac4a2a --- /dev/null +++ b/frappe/public/js/frappe/views/reports/report_utils.js @@ -0,0 +1,140 @@ +frappe.provide('frappe.report_utils'); + +frappe.report_utils = { + + make_chart_options: function(columns, raw_data, { y_fields, x_field, chart_type, colors }) { + const type = chart_type.toLowerCase(); + + let rows = raw_data.result.filter(value => Object.keys(value).length); + + let labels = get_column_values(x_field); + let datasets = y_fields.map(y_field => ({ + name: frappe.model.unscrub(y_field), + values: get_column_values(y_field).map(d => Number(d)) + })); + + if (raw_data.add_total_row) { + labels = labels.slice(0, -1); + datasets[0].values = datasets[0].values.slice(0, -1); + } + + return { + data: { + labels: labels, + datasets: datasets + }, + truncateLegends: 1, + type: type, + colors: colors, + axisOptions: { + shortenYAxisNumbers: 1 + } + }; + + function get_column_values(column_name) { + if (Array.isArray(rows[0])) { + let column_index = columns.findIndex(column => column.fieldname == column_name); + return rows.map(row => row[column_index]); + } else { + return rows.map(row => row[column_name]); + } + } + }, + + get_possible_chart_options: function(columns, data) { + const rows = data.result.filter(value => Object.keys(value).length); + const first_row = Array.isArray(rows[0]) ? rows[0] : columns.map(col => rows[0][col.fieldname]); + + const indices = first_row.reduce((accumulator, current_value, current_index) => { + if (Number.isFinite(current_value)) { + accumulator.push(current_index); + } + return accumulator; + }, []); + + function get_options(fields) { + return fields.map((field) => { + if (field.fieldname) { + return {label: field.label, value: field.fieldname}; + } else { + field = frappe.report_utils.prepare_field_from_column(field); + return {label: field.label, value: field.fieldname}; + } + }); + } + + const numeric_fields = columns.filter((col, i) => indices.includes(i)); + const non_numeric_fields = columns.filter((col, i) => !indices.includes(i)); + + let numeric_field_options = get_options(numeric_fields); + let non_numeric_field_options = get_options(non_numeric_fields); + + return { + 'numeric_fields': numeric_field_options, + 'non_numeric_fields': non_numeric_field_options + }; + }, + + prepare_field_from_column: function(column) { + if (typeof column === 'string') { + if (column.includes(':')) { + let [label, fieldtype, width] = column.split(':'); + let options; + + if (fieldtype.includes('/')) { + [fieldtype, options] = fieldtype.split('/'); + } + + column = { + label, + fieldname: label, + fieldtype, + width, + options + }; + } else { + column = { + label: column, + fieldname: column, + fieldtype: 'Data' + }; + } + } + return column; + }, + + get_report_filters: function(report_name) { + + if (frappe.query_reports[report_name]) { + let filters = frappe.query_reports[report_name].filters; + return Promise.resolve(filters); + } + + return frappe.xcall( + 'frappe.desk.query_report.get_script', + { + report_name: report_name + } + ).then(r => { + frappe.dom.eval(r.script || ''); + let filters = frappe.query_reports[report_name].filters; + return Promise.resolve(filters); + }); + }, + + get_filter_values(filters) { + let filter_values = filters + .map(f => { + var v = f.default; + return { + [f.fieldname]: v + }; + }) + .reduce((acc, f) => { + Object.assign(acc, f); + return acc; + }, {}); + return filter_values; + }, + +}; \ No newline at end of file From 921cea84ccb38fe5f846223a4e9f94c976b3b757 Mon Sep 17 00:00:00 2001 From: Himanshu Date: Fri, 11 Dec 2020 12:26:10 +0530 Subject: [PATCH 2/2] chore: docstring formatting as per PEP 257 --- frappe/utils/data.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/frappe/utils/data.py b/frappe/utils/data.py index 578ce4e30176..8ad4476f0dbd 100644 --- a/frappe/utils/data.py +++ b/frappe/utils/data.py @@ -227,11 +227,9 @@ def get_quarter_ending(date): return date def get_year_ending(date): - ''' returns year ending of the given date ''' - # first day of next year (note year starts from 1) - date = add_to_date('{}-01-01'.format(date.year), months = 12) - # last day of this month - return add_to_date(date, days=-1) + '''Returns year ending of the given date.''' + date = add_to_date('{}-01-01'.format(date.year), months = 12) # first day of next year (note year starts from 1) + return add_to_date(date, days=-1) # last day of this month def get_time(time_str): if isinstance(time_str, datetime.datetime): @@ -1152,4 +1150,4 @@ def _get_time_format(time_str): def get_date_str(date_obj): if isinstance(date_obj, string_types): date_obj = get_datetime(date_obj) - return date_obj.strftime(DATE_FORMAT) \ No newline at end of file + return date_obj.strftime(DATE_FORMAT)