From e952a3f72b93657793ba8ab92b75486520ff21eb Mon Sep 17 00:00:00 2001 From: "Chinmay D. Pai" Date: Fri, 3 Jul 2020 10:52:10 +0530 Subject: [PATCH 01/29] fix: escape unwanted tags before displaying printview prevents xss on the printview page Signed-off-by: Chinmay D. Pai --- frappe/www/printview.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frappe/www/printview.py b/frappe/www/printview.py index 4853bf1cb9..545e5d581d 100644 --- a/frappe/www/printview.py +++ b/frappe/www/printview.py @@ -8,7 +8,7 @@ from frappe import _ from frappe.modules import get_doc_path from frappe.core.doctype.access_log.access_log import make_access_log -from frappe.utils import cint, strip_html +from frappe.utils import cint, sanitize_html, strip_html from six import string_types no_cache = 1 @@ -20,9 +20,9 @@ def get_context(context): """Build context for print""" if not ((frappe.form_dict.doctype and frappe.form_dict.name) or frappe.form_dict.doc): return { - "body": """

Error

+ "body": sanitize_html("""

Error

Parameters doctype and name required

-
%s
""" % repr(frappe.form_dict) +
%s
""" % repr(frappe.form_dict)) } if frappe.form_dict.doc: From 3b09087c5bd2b891042e8af17d255232714afc08 Mon Sep 17 00:00:00 2001 From: Faris Ansari Date: Wed, 15 Jul 2020 14:46:39 +0530 Subject: [PATCH 02/29] fix: Markdown content in Blog Post `markdown` checks if the content is html, if not, converts to markdown. This breaks when there are html tags in markdown. Instead we can use `md_to_html` directly. --- frappe/website/utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frappe/website/utils.py b/frappe/website/utils.py index f5e35976eb..dc981fe30a 100644 --- a/frappe/website/utils.py +++ b/frappe/website/utils.py @@ -9,7 +9,7 @@ import frappe from six import iteritems from past.builtins import cmp -from frappe.utils import markdown +from frappe.utils import md_to_html def delete_page_cache(path): @@ -357,7 +357,7 @@ def get_html_content_based_on_type(doc, fieldname, content_type): content = doc.get(fieldname) if content_type == 'Markdown': - content = markdown(doc.get(fieldname + '_md')) + content = md_to_html(doc.get(fieldname + '_md')) elif content_type == 'HTML': content = doc.get(fieldname + '_html') From fdf501c2fa755069c17556edb125e8204d3baee3 Mon Sep 17 00:00:00 2001 From: prssanna Date: Thu, 16 Jul 2020 00:59:34 +0530 Subject: [PATCH 03/29] feat: report type number cards --- .../desk/doctype/number_card/number_card.json | 55 +++++++++++++------ 1 file changed, 37 insertions(+), 18 deletions(-) diff --git a/frappe/desk/doctype/number_card/number_card.json b/frappe/desk/doctype/number_card/number_card.json index 41362a8982..2a7b5ef478 100644 --- a/frappe/desk/doctype/number_card/number_card.json +++ b/frappe/desk/doctype/number_card/number_card.json @@ -1,6 +1,7 @@ { "actions": [], "allow_rename": 1, + "allow_workflow": 1, "creation": "2020-04-15 18:06:39.444683", "doctype": "DocType", "editable_grid": 1, @@ -9,10 +10,13 @@ "is_standard", "module", "label", + "type", + "report_name", "function", "aggregate_function_based_on", "column_break_2", "document_type", + "report_field", "is_public", "stats_section", "show_percentage_stats", @@ -26,20 +30,21 @@ ], "fields": [ { + "depends_on": "eval: doc.type == 'Document Type'", "fieldname": "document_type", "fieldtype": "Link", "in_list_view": 1, "label": "Document Type", - "options": "DocType", - "reqd": 1 + "mandatory_depends_on": "eval: doc.type == 'Document Type'", + "options": "DocType" }, { - "depends_on": "eval: doc.document_type", + "depends_on": "eval: doc.type == 'Document Type'", "fieldname": "function", "fieldtype": "Select", "label": "Function", - "options": "Count\nSum\nAverage\nMinimum\nMaximum", - "reqd": 1 + "mandatory_depends_on": "eval: doc.type == 'Document Type'", + "options": "Count\nSum\nAverage\nMinimum\nMaximum" }, { "depends_on": "eval: doc.function !== 'Count'", @@ -98,6 +103,7 @@ "options": "Daily\nWeekly\nMonthly\nYearly" }, { + "depends_on": "eval: doc.type == 'Document Type'", "fieldname": "stats_section", "fieldtype": "Section Break", "label": "Stats" @@ -114,34 +120,47 @@ "fieldtype": "Link", "label": "Module", "mandatory_depends_on": "eval: doc.is_standard", - "options": "Module Def", - "show_days": 1, - "show_seconds": 1 + "options": "Module Def" }, { "fieldname": "dynamic_filters_json", "fieldtype": "Code", "label": "Dynamic Filters JSON", - "options": "JSON", - "show_days": 1, - "show_seconds": 1 + "options": "JSON" }, { "fieldname": "section_break_16", - "fieldtype": "Section Break", - "show_days": 1, - "show_seconds": 1 + "fieldtype": "Section Break" }, { "fieldname": "dynamic_filters_section", "fieldtype": "Section Break", - "label": "Dynamic Filters Section", - "show_days": 1, - "show_seconds": 1 + "label": "Dynamic Filters Section" + }, + { + "fieldname": "type", + "fieldtype": "Select", + "label": "Type", + "options": "Document Type\nReport" + }, + { + "depends_on": "eval: doc.type == 'Report'", + "fieldname": "report_name", + "fieldtype": "Link", + "label": "Report Name", + "mandatory_depends_on": "eval: doc.type == 'Report'", + "options": "Report" + }, + { + "depends_on": "eval: doc.type == 'Report'", + "fieldname": "report_field", + "fieldtype": "Select", + "label": "Field", + "mandatory_depends_on": "eval: doc.type == 'Report'" } ], "links": [], - "modified": "2020-07-10 17:55:35.873222", + "modified": "2020-07-16 00:58:30.426929", "modified_by": "Administrator", "module": "Desk", "name": "Number Card", From 17f58beeac0e2b487a050c45207afea526971956 Mon Sep 17 00:00:00 2001 From: prssanna Date: Thu, 16 Jul 2020 01:59:17 +0530 Subject: [PATCH 04/29] fix: report filters in number card --- .../dashboard_chart/dashboard_chart.js | 47 +-- .../desk/doctype/number_card/number_card.js | 378 ++++++++++++------ .../public/js/frappe/utils/dashboard_utils.js | 48 +++ .../js/frappe/views/reports/report_utils.js | 2 +- 4 files changed, 311 insertions(+), 164 deletions(-) diff --git a/frappe/desk/doctype/dashboard_chart/dashboard_chart.js b/frappe/desk/doctype/dashboard_chart/dashboard_chart.js index 6f071a6e2b..c8783f0e64 100644 --- a/frappe/desk/doctype/dashboard_chart/dashboard_chart.js +++ b/frappe/desk/doctype/dashboard_chart/dashboard_chart.js @@ -193,7 +193,7 @@ frappe.ui.form.on('Dashboard Chart', { if (!frm.doc.is_custom) { if (data.result.length) { - frm.field_options = frappe.report_utils.get_possible_chart_options(data.columns, data); + frm.field_options = frappe.report_utils.get_field_options_from_report(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`)); @@ -435,49 +435,10 @@ frappe.ui.form.on('Dashboard Chart', { frm.trigger('set_dynamic_filters_in_table'); let filters = JSON.parse(frm.doc.filters_json || '[]'); - let fields = [ - { - fieldtype: 'HTML', - fieldname: 'description', - options: - `
-

Set dynamic filter values in JavaScript for the required fields here. -

-

Ex: - frappe.defaults.get_user_default("Company") -

-
` - } - ]; - if (is_document_type) { - if (frm.dynamic_filters) { - filters = [...filters, ...frm.dynamic_filters]; - } - filters.forEach(f => { - for (let field of fields) { - if (field.fieldname == f[0] + ':' + f[1]) { - return; - } - } - if (f[2] == '=') { - fields.push({ - label: `${f[1]} (${f[0]})`, - fieldname: f[0] + ':' + f[1], - fieldtype: 'Data', - }); - } - }); - } else { - filters = {...frm.dynamic_filters, ...filters}; - for (let key of Object.keys(filters)) { - fields.push({ - label: key, - fieldname: key, - fieldtype: 'Data', - }); - } - } + let fields = frappe.dashboard_utils.get_fields_for_dynamic_filter_dialog( + is_document_type, filters, frm.dynamic_filters + ); frm.dynamic_filter_table.on('click', () => { let dialog = new frappe.ui.Dialog({ diff --git a/frappe/desk/doctype/number_card/number_card.js b/frappe/desk/doctype/number_card/number_card.js index d7abe57e2a..b2c7e544e2 100644 --- a/frappe/desk/doctype/number_card/number_card.js +++ b/frappe/desk/doctype/number_card/number_card.js @@ -9,8 +9,19 @@ frappe.ui.form.on('Number Card', { frm.set_df_property("filters_section", "hidden", 1); frm.set_df_property("dynamic_filters_section", "hidden", 1); frm.trigger('set_options'); + + if (!frm.doc.type) { + frm.set_value('type', 'Document Type'); + } + + if (frm.doc.type == 'Report') { + frm.trigger('set_report_filters'); + } + frm.trigger('render_filters_table'); - frm.trigger('render_dynamic_filters_table'); + if (frappe.boot.developer_mode && frm.doc.is_standard) { + frm.trigger('render_dynamic_filters_table'); + } }, before_save: function(frm) { @@ -31,6 +42,29 @@ frappe.ui.form.on('Number Card', { } }, + type: function(frm) { + if (frm.doc.type == 'Report') { + frm.set_query('report_name', () => { + return { + filters: { + 'report_type': ['!=', 'Report Builder'] + } + }; + }); + frm.trigger('set_report_filters'); + } + frm.set_value('filters_json', 'null'); + frm.filters = null; + frm.trigger('render_filters_table'); + }, + + report_name: function(frm) { + frm.trigger('set_report_filters'); + frm.set_value('filters_json', '{}'); + frm.set_value('dynamic_filters_json', '{}'); + frm.filters = null; + }, + document_type: function(frm) { frm.set_query('document_type', function() { return { @@ -46,6 +80,10 @@ frappe.ui.form.on('Number Card', { }, set_options: function(frm) { + if (!frm.doc.type == 'Document Type') { + return; + } + let aggregate_based_on_fields = []; const doctype = frm.doc.document_type; @@ -64,72 +102,56 @@ frappe.ui.form.on('Number Card', { frm.set_df_property('aggregate_function_based_on', 'options', aggregate_based_on_fields); }); + frm.trigger('render_filters_table'); } }, + set_report_filters: function(frm) { + const report_name = frm.doc.report_name; + if (report_name) { + frappe.report_utils.get_report_filters(report_name).then(filters => { + if (filters) { + frm.filters = filters; + const filter_values = frappe.report_utils.get_filter_values(filters); + if (!JSON.parse(frm.doc.filters_json)) { + frm.set_value('filters_json', JSON.stringify(filter_values)); + } + } + frm.trigger('render_filters_table'); + frm.trigger('set_report_field_options'); + }); + } + }, + + set_report_field_options: function(frm) { + let filters = JSON.parse(frm.doc.filters_json); + frappe.xcall( + 'frappe.desk.query_report.run', + { + report_name: frm.doc.report_name, + filters: filters, + ignore_prepared_report: 1 + } + ).then(data => { + frm.report_data = data; + if (data.result.length) { + frm.field_options = frappe.report_utils.get_field_options_from_report(data.columns, data); + frm.set_df_property('report_field', 'options', frm.field_options.numeric_fields); + if (!frm.field_options.numeric_fields.length) { + frappe.msgprint(__(`Report has no numeric fields, please change the Report Name`)); + } + } else { + frappe.msgprint(__('Report has no data, please modify the filters or change the Report Name')); + } + }); + }, + render_filters_table: function(frm) { frm.set_df_property("filters_section", "hidden", 0); + let is_document_type = frm.doc.type == 'Document Type'; + let is_dynamic_filter = f => ['Date', 'DateRange'].includes(f.fieldtype) && f.default; let wrapper = $(frm.get_field('filters_json').wrapper).empty(); - let table = $(` - - - - - - - - -
${__('Filter')}${__('Condition')}${__('Value')}
`).appendTo(wrapper); - - frm.filters = JSON.parse(frm.doc.filters_json || '[]'); - - set_filters_in_table(frm.filters, table); - - table.on('click', () => { - let dialog = new frappe.ui.Dialog({ - title: __('Set Filters'), - fields: [{ - fieldtype: 'HTML', - fieldname: 'filter_area', - }], - primary_action: function() { - let values = this.get_values(); - if (values) { - this.hide(); - frm.filters = frm.filter_group.get_filters(); - frm.set_value('filters_json', JSON.stringify(frm.filters)); - set_filters_in_table(frm.filters, table); - frm.trigger('render_dynamic_filters_table'); - } - }, - primary_action_label: "Set" - }); - - frappe.dashboards.filters_dialog = dialog; - - 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(frm.filters); - - dialog.show(); - dialog.set_values(frm.filters); - }); - - }, - - render_dynamic_filters_table: function(frm) { - if (!frappe.boot.developer_mode || !frm.doc.is_standard) { - return; - } - let wrapper = $(frm.get_field('dynamic_filters_json').wrapper).empty(); - - frm.set_df_property("dynamic_filters_section", "hidden", 0); - let table = $(` @@ -140,92 +162,208 @@ frappe.ui.form.on('Number Card', {
`).appendTo(wrapper); - - frm.dynamic_filters = JSON.parse(frm.doc.dynamic_filters_json || '[]'); - - set_filters_in_table(frm.dynamic_filters, table); + $(`

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

`).appendTo(wrapper); let filters = JSON.parse(frm.doc.filters_json || '[]'); - let fields = [ - { - fieldtype: 'HTML', - fieldname: 'description', - options: - `
-

Set dynamic filter values in JavaScript for the required fields here. -

-

Ex: - frappe.defaults.get_user_default("Company") -

-
` - } - ]; + let filters_set = false; - if (frm.dynamic_filters.length) { - filters = [...filters, ...frm.dynamic_filters]; + // Set dynamic filters for reports + if (frm.doc.type == 'Report') { + let set_filters = false; + frm.filters.forEach(f => { + if (is_dynamic_filter(f)) { + filters[f.fieldname] = f.default; + set_filters = true; + } + }); + set_filters && frm.set_value('filters_json', JSON.stringify(filters)); } - filters.forEach(f => { - for (let field of fields) { - if (field.fieldname == f[0] + ':' + f[1]) { - return; + let fields; + if (is_document_type) { + fields = [ + { + fieldtype: 'HTML', + fieldname: 'filter_area', } - } - if (f[2] == '=') { - fields.push({ - label: `${f[1]} (${f[0]})`, - fieldname: f[0] + ':' + f[1], - fieldtype: 'Data', + ]; + + if (filters.length) { + filters.forEach(filter => { + const filter_row = + $(` + ${filter[1]} + ${filter[2] || ""} + ${filter[3]} + `); + + table.find('tbody').append(filter_row); }); + filters_set = true; } - }); + } else if (frm.filters.length) { + filters_set = true; + fields = frm.filters.filter(f => 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); + } + }); + } + + if (!filters_set) { + 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: () => { - let values = dialog.get_values(); + fields: fields.filter(f => !is_dynamic_filter(f)), + primary_action: function() { + let values = this.get_values(); if (values) { - dialog.hide(); - let dynamic_filters = []; - for (let key of Object.keys(values)) { - let [doctype, fieldname] = key.split(':'); - dynamic_filters.push([doctype, fieldname, '=', values[key]]); + this.hide(); + 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.set_value('dynamic_filters_json', JSON.stringify(dynamic_filters)); - frm.dynamic_filters = dynamic_filters; - set_filters_in_table(frm.dynamic_filters, table); + frm.trigger('render_filters_table'); } }, primary_action_label: "Set" }); + 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: () => {}, + }); + filters && frm.filter_group.add_filters_to_filter_group(filters); + } + dialog.show(); + + if (frm.doc.type == 'Report') { + //Set query report object so that it can be used while fetching filter values in the report + frappe.query_report = new frappe.views.QueryReport({'filters': dialog.fields_list}); + frappe.query_reports[frm.doc.report_name] + && frappe.query_reports[frm.doc.report_name].onload + && frappe.query_reports[frm.doc.report_name].onload(frappe.query_report); + } + dialog.set_values(filters); }); }, -}); + render_dynamic_filters_table(frm) { + frm.set_df_property("dynamic_filters_section", "hidden", 0); -function set_filters_in_table(filters, table) { - if (!filters.length) { - const filter_row = $(` - ${__("Click to Set Filters")}`); - table.find('tbody').html(filter_row); - } else { - let filter_rows = ''; - filters.forEach(filter => { - filter_rows += - ` - ${filter[1]} - ${filter[2] || ""} - ${filter[3]} - `; + let is_document_type = frm.doc.type == 'Document Type'; + let wrapper = $(frm.get_field('dynamic_filters_json').wrapper).empty(); + + frm.dynamic_filter_table = $(` + + + + + + + + +
${__('Filter')}${__('Condition')}${__('Value')}
`).appendTo(wrapper); + + frm.dynamic_filters = frm.doc.dynamic_filters_json && frm.doc.dynamic_filters_json.length > 2 + ? JSON.parse(frm.doc.dynamic_filters_json) + : null; + + frm.trigger('set_dynamic_filters_in_table'); + + let filters = JSON.parse(frm.doc.filters_json || '[]'); + + let fields = frappe.dashboard_utils.get_fields_for_dynamic_filter_dialog( + is_document_type, filters, frm.dynamic_filters + ); + + frm.dynamic_filter_table.on('click', () => { + let dialog = new frappe.ui.Dialog({ + title: __('Set Dynamic Filters'), + fields: fields, + primary_action: () => { + let values = dialog.get_values(); + dialog.hide(); + let dynamic_filters = []; + for (let key of Object.keys(values)) { + if (is_document_type) { + let [doctype, fieldname] = key.split(':'); + dynamic_filters.push([doctype, fieldname, '=', values[key]]); + } + } + + if (is_document_type) { + frm.set_value('dynamic_filters_json', JSON.stringify(dynamic_filters)); + } else { + frm.set_value('dynamic_filters_json', JSON.stringify(values)); + } + frm.trigger('set_dynamic_filters_in_table'); + }, + primary_action_label: "Set" + }); + + dialog.show(); + dialog.set_values(frm.dynamic_filters); }); - table.find('tbody').html(filter_rows); + }, + + set_dynamic_filters_in_table: function(frm) { + frm.dynamic_filters = frm.doc.dynamic_filters_json && frm.doc.dynamic_filters_json.length > 2 + ? JSON.parse(frm.doc.dynamic_filters_json) + : null; + + if (!frm.dynamic_filters) { + const filter_row = $(` + ${__("Click to Set Dynamic Filters")}`); + frm.dynamic_filter_table.find('tbody').html(filter_row); + } else { + let filter_rows = ''; + if ($.isArray(frm.dynamic_filters)) { + frm.dynamic_filters.forEach(filter => { + filter_rows += + ` + ${filter[1]} + ${filter[2] || ""} + ${filter[3]} + `; + }); + } else { + let condition = '='; + for (let [key, val] of Object.entries(frm.dynamic_filters)) { + filter_rows += + ` + ${key} + ${condition} + ${val || ""} + ` + ; + } + } + + frm.dynamic_filter_table.find('tbody').html(filter_rows); + } } -} + +}); diff --git a/frappe/public/js/frappe/utils/dashboard_utils.js b/frappe/public/js/frappe/utils/dashboard_utils.js index f737c6ad12..c98de6b4ac 100644 --- a/frappe/public/js/frappe/utils/dashboard_utils.js +++ b/frappe/public/js/frappe/utils/dashboard_utils.js @@ -119,6 +119,54 @@ frappe.dashboard_utils = { } return static_filters; + }, + + get_fields_for_dynamic_filter_dialog(is_document_type, filters, dynamic_filters) { + let fields = [ + { + fieldtype: 'HTML', + fieldname: 'description', + options: + `
+

Set dynamic filter values in JavaScript for the required fields here. +

+

Ex: + frappe.defaults.get_user_default("Company") +

+
` + } + ]; + + if (is_document_type) { + if (dynamic_filters) { + filters = [...filters, ...dynamic_filters]; + } + filters.forEach(f => { + for (let field of fields) { + if (field.fieldname == f[0] + ':' + f[1]) { + return; + } + } + if (f[2] == '=') { + fields.push({ + label: `${f[1]} (${f[0]})`, + fieldname: f[0] + ':' + f[1], + fieldtype: 'Data', + }); + } + }); + } else { + filters = {...dynamic_filters, ...filters}; + for (let key of Object.keys(filters)) { + fields.push({ + label: key, + fieldname: key, + fieldtype: 'Data', + }); + } + } + + return fields; } }; \ No newline at end of file diff --git a/frappe/public/js/frappe/views/reports/report_utils.js b/frappe/public/js/frappe/views/reports/report_utils.js index 7b1205482f..856262e468 100644 --- a/frappe/public/js/frappe/views/reports/report_utils.js +++ b/frappe/public/js/frappe/views/reports/report_utils.js @@ -41,7 +41,7 @@ frappe.report_utils = { } }, - get_possible_chart_options: function(columns, data) { + get_field_options_from_report: 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]); From 1d31df649f4da2164fb2e6e383784124072744b7 Mon Sep 17 00:00:00 2001 From: prssanna Date: Thu, 16 Jul 2020 02:09:16 +0530 Subject: [PATCH 05/29] feat: custom type number card --- .../desk/doctype/number_card/number_card.js | 41 +++++++++++++++++++ .../desk/doctype/number_card/number_card.json | 27 +++++++++++- 2 files changed, 66 insertions(+), 2 deletions(-) diff --git a/frappe/desk/doctype/number_card/number_card.js b/frappe/desk/doctype/number_card/number_card.js index b2c7e544e2..20da748516 100644 --- a/frappe/desk/doctype/number_card/number_card.js +++ b/frappe/desk/doctype/number_card/number_card.js @@ -18,6 +18,14 @@ frappe.ui.form.on('Number Card', { frm.trigger('set_report_filters'); } + if (frm.doc.type == 'Custom') { + if (!frappe.boot.developer_mode) { + frm.disable_form(); + } + frm.filters = eval(frm.doc.filters_config); + frm.trigger('set_filters_description'); + } + frm.trigger('render_filters_table'); if (frappe.boot.developer_mode && frm.doc.is_standard) { frm.trigger('render_dynamic_filters_table'); @@ -42,7 +50,33 @@ frappe.ui.form.on('Number Card', { } }, + set_filters_description: function(frm) { + if (frm.doc.type == 'Custom') { + frm.fields_dict.filters_config.set_description(` + Set the filters here. For example: +
+
+[{
+	fieldname: "company",
+	label: __("Company"),
+	fieldtype: "Link",
+	options: "Company",
+	default: frappe.defaults.get_user_default("Company"),
+	reqd: 1
+},
+{
+	fieldname: "account",
+	label: __("Account"),
+	fieldtype: "Link",
+	options: "Account",
+	reqd: 1
+}]
+
`); + } + }, + type: function(frm) { + frm.trigger('set_filters_description'); if (frm.doc.type == 'Report') { frm.set_query('report_name', () => { return { @@ -65,6 +99,13 @@ frappe.ui.form.on('Number Card', { frm.filters = null; }, + filters_config: function(frm) { + frm.filters = eval(frm.doc.filters_config); + const filter_values = frappe.report_utils.get_filter_values(frm.filters); + frm.set_value('filters_json', JSON.stringify(filter_values)); + frm.trigger('render_filters_table'); + }, + document_type: function(frm) { frm.set_query('document_type', function() { return { diff --git a/frappe/desk/doctype/number_card/number_card.json b/frappe/desk/doctype/number_card/number_card.json index 2a7b5ef478..83e00e48cb 100644 --- a/frappe/desk/doctype/number_card/number_card.json +++ b/frappe/desk/doctype/number_card/number_card.json @@ -12,12 +12,15 @@ "label", "type", "report_name", + "method", "function", "aggregate_function_based_on", "column_break_2", "document_type", "report_field", "is_public", + "custom_configuration_section", + "filters_config", "stats_section", "show_percentage_stats", "stats_time_interval", @@ -141,7 +144,7 @@ "fieldname": "type", "fieldtype": "Select", "label": "Type", - "options": "Document Type\nReport" + "options": "Document Type\nReport\nCustom" }, { "depends_on": "eval: doc.type == 'Report'", @@ -157,10 +160,30 @@ "fieldtype": "Select", "label": "Field", "mandatory_depends_on": "eval: doc.type == 'Report'" + }, + { + "depends_on": "eval: doc.type == 'Custom'", + "description": "This should contain the path to a whitelisted function that will return the number on the card", + "fieldname": "method", + "fieldtype": "Data", + "label": "Method", + "mandatory_depends_on": "eval: doc.type == 'Custom'" + }, + { + "depends_on": "eval: doc.type == 'Custom'", + "fieldname": "custom_configuration_section", + "fieldtype": "Section Break", + "label": "Custom Configuration" + }, + { + "fieldname": "filters_config", + "fieldtype": "Code", + "label": "Filters Configuration", + "options": "JSON" } ], "links": [], - "modified": "2020-07-16 00:58:30.426929", + "modified": "2020-07-16 02:03:09.175701", "modified_by": "Administrator", "module": "Desk", "name": "Number Card", From 44968de176c9f6d450648be1b2d702d04c79e3d6 Mon Sep 17 00:00:00 2001 From: prssanna Date: Thu, 16 Jul 2020 12:06:04 +0530 Subject: [PATCH 06/29] feat: number card widgets with type --- .../desk/doctype/number_card/number_card.js | 35 ++++----- .../js/frappe/views/reports/report_utils.js | 10 +++ .../js/frappe/widgets/number_card_widget.js | 78 +++++++++++++++---- 3 files changed, 87 insertions(+), 36 deletions(-) diff --git a/frappe/desk/doctype/number_card/number_card.js b/frappe/desk/doctype/number_card/number_card.js index 20da748516..db181c91e8 100644 --- a/frappe/desk/doctype/number_card/number_card.js +++ b/frappe/desk/doctype/number_card/number_card.js @@ -14,7 +14,7 @@ frappe.ui.form.on('Number Card', { frm.set_value('type', 'Document Type'); } - if (frm.doc.type == 'Report') { + if (frm.doc.type == 'Report' && frm.doc.report_name) { frm.trigger('set_report_filters'); } @@ -25,11 +25,6 @@ frappe.ui.form.on('Number Card', { frm.filters = eval(frm.doc.filters_config); frm.trigger('set_filters_description'); } - - frm.trigger('render_filters_table'); - if (frappe.boot.developer_mode && frm.doc.is_standard) { - frm.trigger('render_dynamic_filters_table'); - } }, before_save: function(frm) { @@ -43,11 +38,8 @@ frappe.ui.form.on('Number Card', { }, is_standard: function(frm) { - if (frappe.boot.developer_mode && frm.doc.is_standard) { - frm.trigger('render_dynamic_filters_table'); - } else { - frm.set_df_property("dynamic_filters_section", "hidden", 1); - } + frm.trigger('render_dynamic_filters_table'); + frm.set_df_property("dynamic_filters_section", "hidden", 1); }, set_filters_description: function(frm) { @@ -85,18 +77,15 @@ frappe.ui.form.on('Number Card', { } }; }); - frm.trigger('set_report_filters'); } - frm.set_value('filters_json', 'null'); - frm.filters = null; - frm.trigger('render_filters_table'); + }, report_name: function(frm) { - frm.trigger('set_report_filters'); frm.set_value('filters_json', '{}'); frm.set_value('dynamic_filters_json', '{}'); - frm.filters = null; + frm.set_df_property('report_field', 'options', []); + frm.trigger('set_report_filters'); }, filters_config: function(frm) { @@ -121,7 +110,7 @@ frappe.ui.form.on('Number Card', { }, set_options: function(frm) { - if (!frm.doc.type == 'Document Type') { + if (frm.doc.type !== 'Document Type') { return; } @@ -154,18 +143,19 @@ frappe.ui.form.on('Number Card', { if (filters) { frm.filters = filters; const filter_values = frappe.report_utils.get_filter_values(filters); - if (!JSON.parse(frm.doc.filters_json)) { + if (frm.doc.filters_json.length <= 2) { frm.set_value('filters_json', JSON.stringify(filter_values)); } } frm.trigger('render_filters_table'); frm.trigger('set_report_field_options'); + frm.trigger('render_dynamic_filters_table'); }); } }, set_report_field_options: function(frm) { - let filters = JSON.parse(frm.doc.filters_json); + let filters = frm.doc.filters_json.length > 2 ? JSON.parse(frm.doc.filters_json) : null; frappe.xcall( 'frappe.desk.query_report.run', { @@ -174,7 +164,6 @@ frappe.ui.form.on('Number Card', { ignore_prepared_report: 1 } ).then(data => { - frm.report_data = data; if (data.result.length) { frm.field_options = frappe.report_utils.get_field_options_from_report(data.columns, data); frm.set_df_property('report_field', 'options', frm.field_options.numeric_fields); @@ -311,6 +300,10 @@ frappe.ui.form.on('Number Card', { }, render_dynamic_filters_table(frm) { + if (!frappe.boot.developer_mode || !frm.doc.is_standard) { + return + } + frm.set_df_property("dynamic_filters_section", "hidden", 0); let is_document_type = frm.doc.type == 'Document Type'; diff --git a/frappe/public/js/frappe/views/reports/report_utils.js b/frappe/public/js/frappe/views/reports/report_utils.js index 856262e468..158dbd653b 100644 --- a/frappe/public/js/frappe/views/reports/report_utils.js +++ b/frappe/public/js/frappe/views/reports/report_utils.js @@ -138,4 +138,14 @@ frappe.report_utils = { return filter_values; }, + get_result_of_fn(fn, values) { + const get_result = { + 'Minimum': values => values.reduce((min, val) => Math.min(min, val), values[0]), + 'Maximum': values => values.reduce((min, val) => Math.max(min, val), values[0]), + 'Average': values => values.reduce((a, b) => a + b, 0) / values.length, + 'Sum': values => values.reduce((a, b) => a + b, 0) + }; + return get_result[fn](values); + }, + }; diff --git a/frappe/public/js/frappe/widgets/number_card_widget.js b/frappe/public/js/frappe/widgets/number_card_widget.js index 77cb8a59c2..8a8b3309dc 100644 --- a/frappe/public/js/frappe/widgets/number_card_widget.js +++ b/frappe/public/js/frappe/widgets/number_card_widget.js @@ -59,8 +59,7 @@ export default class NumberCardWidget extends Widget { } ).then(doc => { this.name = doc.name; - this.card_doc.stats_time_interval = doc.stats_time_interval; - this.card_doc.name = this.name; + this.card_doc = doc; this.widget.attr('data-widget-name', this.name); }); } @@ -84,11 +83,43 @@ export default class NumberCardWidget extends Widget { }); } + get_settings(type) { + const settings_map = { + 'Custom': { + method: this.card_doc.method, + args: { + filters: JSON.parse(this.card_doc.filters_json || '{}') + } + }, + 'Report': { + method: 'frappe.desk.query_report.run', + args: { + report_name: this.card_doc.report_name, + filters: JSON.parse(this.card_doc.filters_json || '{}'), + ignore_prepared_report: 1 + } + }, + 'Document Type': { + method: 'frappe.desk.doctype.number_card.number_card.get_result', + args: { + doc: this.card_doc + } + } + }; + return settings_map[type]; + } + render_card() { this.prepare_actions(); this.set_title(); this.set_loading_state(); + if (!this.card_doc.type) { + this.card_doc.type = 'Document Type'; + } + + this.settings = this.get_settings(this.card_doc.type); + frappe.run_serially([ () => this.render_number(), () => this.render_stats(), @@ -102,29 +133,42 @@ export default class NumberCardWidget extends Widget { } get_number() { - return frappe.xcall('frappe.desk.doctype.number_card.number_card.get_result', { - doc: this.card_doc - }).then(res => { - this.number = res; - if (this.card_doc.function !== 'Count') { - return frappe.model.with_doctype(this.card_doc.document_type, () => { - this.get_formatted_number(); - }); + return frappe.xcall(this.settings.method, this.settings.args).then(res => { + if (this.card_doc.type == 'Report') { + this.get_number_for_report(res); } else { - this.number_html = res; + this.number = res; + if (this.card_doc.function !== 'Count') { + return frappe.model.with_doctype(this.card_doc.document_type, () => { + const based_on_df = + frappe.meta.get_docfield(this.card_doc.document_type, this.card_doc.aggregate_function_based_on); + this.get_formatted_number(based_on_df); + }); + } else { + this.number_html = res; + } } }); } - get_formatted_number() { - const based_on_df = - frappe.meta.get_docfield(this.card_doc.document_type, this.card_doc.aggregate_function_based_on); + get_number_for_report(res) { + const field = this.card_doc.report_field; + const vals = res.result.reduce((acc, col) => { + col[field] && acc.push(col[field]); + return acc; + }, []); + const col = res.columns.find(col => col.fieldname == field); + this.number = frappe.report_utils.get_result_of_fn(this.card_doc.report_function, vals); + this.get_formatted_number(col); + } + + get_formatted_number(df) { const default_country = frappe.sys_defaults.country; const shortened_number = shorten_number(this.number, default_country); let number_parts = shortened_number.split(' '); const symbol = number_parts[1] || ''; - const formatted_number = $(frappe.format(number_parts[0], based_on_df)).text(); + const formatted_number = $(frappe.format(number_parts[0], df)).text(); this.number_html = formatted_number + ' ' + symbol; } @@ -138,6 +182,10 @@ export default class NumberCardWidget extends Widget { } render_stats() { + if (this.card_doc.type !== 'Document Type') { + return; + } + let caret_html =''; let color_class = ''; From 7836141d33b45d8c8da460b0d2bc5e43adabf9bf Mon Sep 17 00:00:00 2001 From: prssanna Date: Thu, 16 Jul 2020 12:06:26 +0530 Subject: [PATCH 07/29] fix: remove is_standard from dashboard query --- frappe/desk/doctype/dashboard/dashboard.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/frappe/desk/doctype/dashboard/dashboard.js b/frappe/desk/doctype/dashboard/dashboard.js index 9be6b55d53..237b549433 100644 --- a/frappe/desk/doctype/dashboard/dashboard.js +++ b/frappe/desk/doctype/dashboard/dashboard.js @@ -13,7 +13,6 @@ frappe.ui.form.on('Dashboard', { return { filters: { is_public: 1, - is_standard: 1, } }; }); @@ -22,7 +21,6 @@ frappe.ui.form.on('Dashboard', { return { filters: { is_public: 1, - is_standard: 1, } }; }); From f5b80f21d45cf9aae3306600b97998b8b3ca824d Mon Sep 17 00:00:00 2001 From: prssanna Date: Thu, 16 Jul 2020 13:23:26 +0530 Subject: [PATCH 08/29] fix: get all filters for charts and cards --- .../desk/doctype/number_card/number_card.json | 11 +++++- .../desk/doctype/number_card/number_card.py | 25 ++++++++++---- .../public/js/frappe/utils/dashboard_utils.js | 32 +++++++++++++++++ .../public/js/frappe/widgets/chart_widget.js | 34 +------------------ .../js/frappe/widgets/number_card_widget.js | 14 ++++++-- 5 files changed, 72 insertions(+), 44 deletions(-) diff --git a/frappe/desk/doctype/number_card/number_card.json b/frappe/desk/doctype/number_card/number_card.json index 83e00e48cb..1d0528b298 100644 --- a/frappe/desk/doctype/number_card/number_card.json +++ b/frappe/desk/doctype/number_card/number_card.json @@ -18,6 +18,7 @@ "column_break_2", "document_type", "report_field", + "report_function", "is_public", "custom_configuration_section", "filters_config", @@ -180,10 +181,18 @@ "fieldtype": "Code", "label": "Filters Configuration", "options": "JSON" + }, + { + "depends_on": "eval: doc.type == 'Report'", + "fieldname": "report_function", + "fieldtype": "Select", + "label": "Function", + "mandatory_depends_on": "eval: doc.type == 'Report'", + "options": "Sum\nAverage\nMinimum\nMaximum" } ], "links": [], - "modified": "2020-07-16 02:03:09.175701", + "modified": "2020-07-16 13:04:03.470001", "modified_by": "Administrator", "module": "Desk", "name": "Number Card", diff --git a/frappe/desk/doctype/number_card/number_card.py b/frappe/desk/doctype/number_card/number_card.py index 2f5cfb561c..1243b1850f 100644 --- a/frappe/desk/doctype/number_card/number_card.py +++ b/frappe/desk/doctype/number_card/number_card.py @@ -52,7 +52,7 @@ def has_permission(doc, ptype, user): return False @frappe.whitelist() -def get_result(doc, to_date=None): +def get_result(doc, filters, to_date=None): doc = frappe.parse_json(doc) fields = [] sql_function_map = { @@ -70,13 +70,13 @@ def get_result(doc, to_date=None): else: fields = ['{function}({based_on}) as result'.format(function=function, based_on=doc.aggregate_function_based_on)] - filters = frappe.parse_json(doc.filters_json) + filters = frappe.parse_json(filters) if not filters: filters = [] if to_date: - filters.append([doc.document_type, 'creation', '<', to_date, False]) + filters.append([doc.document_type, 'creation', '<', to_date]) res = frappe.db.get_list(doc.document_type, fields=fields, filters=filters) number = res[0]['result'] if res else 0 @@ -84,7 +84,7 @@ def get_result(doc, to_date=None): return cint(number) @frappe.whitelist() -def get_percentage_difference(doc, result): +def get_percentage_difference(doc, filters, result): doc = frappe.parse_json(doc) result = frappe.parse_json(result) @@ -93,13 +93,13 @@ def get_percentage_difference(doc, result): if not doc.get('show_percentage_stats'): return - previous_result = calculate_previous_result(doc) + previous_result = calculate_previous_result(doc, filters) difference = (result - previous_result)/100.0 return difference -def calculate_previous_result(doc): +def calculate_previous_result(doc, filters): from frappe.utils import add_to_date current_date = frappe.utils.now() @@ -112,7 +112,7 @@ def calculate_previous_result(doc): else: previous_date = add_to_date(current_date, years=-1) - number = get_result(doc, previous_date) + number = get_result(doc, filters, previous_date) return number @frappe.whitelist() @@ -155,3 +155,14 @@ def get_cards_for_user(doctype, txt, searchfield, start, page_len, filters): search_conditions=search_conditions, conditions=conditions ), values) + +@frappe.whitelist() +def add_card_to_dashboard(args): + args = frappe.parse_json(args) + + dashboard = frappe.get_doc('Dashboard', args.dashboard) + dashboard_link = frappe.new_doc('Number Card Link') + dashboard_link.card = args.name + + dashboard.append('cards', dashboard_link) + dashboard.save() \ No newline at end of file diff --git a/frappe/public/js/frappe/utils/dashboard_utils.js b/frappe/public/js/frappe/utils/dashboard_utils.js index c98de6b4ac..8618f6dd59 100644 --- a/frappe/public/js/frappe/utils/dashboard_utils.js +++ b/frappe/public/js/frappe/utils/dashboard_utils.js @@ -167,6 +167,38 @@ frappe.dashboard_utils = { } return fields; + }, + + get_all_filters(doc) { + let filters = JSON.parse(doc.filters_json || "null"); + let dynamic_filters = JSON.parse(doc.dynamic_filters_json || "null"); + + if (!dynamic_filters) { + return filters; + } + + if ($.isArray(dynamic_filters)) { + dynamic_filters.forEach(f => { + try { + f[3] = eval(f[3]); + } catch (e) { + frappe.throw(__(`Invalid expression set in filter ${f[1]} (${f[0]})`)); + } + }); + filters = [...filters, ...dynamic_filters]; + } else { + for (let key of Object.keys(dynamic_filters)) { + try { + const val = eval(dynamic_filters[key]); + dynamic_filters[key] = val; + } catch (e) { + frappe.throw(__(`Invalid expression set in filter ${key}`)); + } + } + Object.assign(filters, dynamic_filters); + } + + return filters; } }; \ No newline at end of file diff --git a/frappe/public/js/frappe/widgets/chart_widget.js b/frappe/public/js/frappe/widgets/chart_widget.js index e31df3825b..674f53670e 100644 --- a/frappe/public/js/frappe/widgets/chart_widget.js +++ b/frappe/public/js/frappe/widgets/chart_widget.js @@ -639,7 +639,7 @@ export default class ChartWidget extends Widget { set_chart_filters() { let user_saved_filters = this.chart_settings.filters || null; - let chart_saved_filters = this.get_all_chart_filters(); + let chart_saved_filters = frappe.dashboard_utils.get_all_filters(this.chart_doc); if (this.chart_doc.chart_type == 'Report') { return frappe.dashboard_utils @@ -655,38 +655,6 @@ export default class ChartWidget extends Widget { } } - get_all_chart_filters() { - let filters = JSON.parse(this.chart_doc.filters_json || "null"); - let dynamic_filters = JSON.parse(this.chart_doc.dynamic_filters_json || "null"); - - if (!dynamic_filters) { - return filters; - } - - if ($.isArray(dynamic_filters)) { - dynamic_filters.forEach(f => { - try { - f[3] = eval(f[3]); - } catch (e) { - frappe.throw(__(`Invalid expression set in filter ${f[1]} (${f[0]})`)); - } - }); - filters = [...filters, ...dynamic_filters]; - } else { - for (let key of Object.keys(dynamic_filters)) { - try { - const val = eval(dynamic_filters[key]); - dynamic_filters[key] = val; - } catch (e) { - frappe.throw(__(`Invalid expression set in filter ${key}`)); - } - } - Object.assign(filters, dynamic_filters); - } - - return filters; - } - update_default_date_filters(report_filters, chart_filters) { report_filters.map(f => { if (['Date', 'DateRange'].includes(f.fieldtype) && f.default) { diff --git a/frappe/public/js/frappe/widgets/number_card_widget.js b/frappe/public/js/frappe/widgets/number_card_widget.js index 8a8b3309dc..f384e84af6 100644 --- a/frappe/public/js/frappe/widgets/number_card_widget.js +++ b/frappe/public/js/frappe/widgets/number_card_widget.js @@ -84,31 +84,38 @@ export default class NumberCardWidget extends Widget { } get_settings(type) { + this.filters = this.get_filters(); const settings_map = { 'Custom': { method: this.card_doc.method, args: { - filters: JSON.parse(this.card_doc.filters_json || '{}') + filters: this.filters } }, 'Report': { method: 'frappe.desk.query_report.run', args: { report_name: this.card_doc.report_name, - filters: JSON.parse(this.card_doc.filters_json || '{}'), + filters: this.filters, ignore_prepared_report: 1 } }, 'Document Type': { method: 'frappe.desk.doctype.number_card.number_card.get_result', args: { - doc: this.card_doc + doc: this.card_doc, + filters: this.filters, } } }; return settings_map[type]; } + get_filters() { + const filters = frappe.dashboard_utils.get_all_filters(this.card_doc); + return filters; + } + render_card() { this.prepare_actions(); this.set_title(); @@ -225,6 +232,7 @@ export default class NumberCardWidget extends Widget { get_percentage_stats() { return frappe.xcall('frappe.desk.doctype.number_card.number_card.get_percentage_difference', { doc: this.card_doc, + filters: this.filters, result: this.number }).then(res => { if (res !== undefined) { From 8f59711e1e7841483921497ab15bc97bee410abb Mon Sep 17 00:00:00 2001 From: prssanna Date: Thu, 16 Jul 2020 13:23:47 +0530 Subject: [PATCH 09/29] feat: add to dashboard button in number card form --- .../desk/doctype/number_card/number_card.js | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/frappe/desk/doctype/number_card/number_card.js b/frappe/desk/doctype/number_card/number_card.js index db181c91e8..202c0ce075 100644 --- a/frappe/desk/doctype/number_card/number_card.js +++ b/frappe/desk/doctype/number_card/number_card.js @@ -25,6 +25,47 @@ frappe.ui.form.on('Number Card', { frm.filters = eval(frm.doc.filters_config); frm.trigger('set_filters_description'); } + frm.trigger('create_add_to_dashboard_button'); + }, + + create_add_to_dashboard_button: function(frm) { + frm.add_custom_button('Add Card 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.name = frm.doc.name; + frappe.xcall( + 'frappe.desk.doctype.number_card.number_card.add_card_to_dashboard', + { + args: values + } + ).then(()=> { + let dashboard_route_html = + `${values.dashboard}`; + let message = + __(`Number Card ${values.name} add to Dashboard ` + dashboard_route_html); + + frappe.msgprint(message); + }); + + d.hide(); + } + }); + + if (!frm.doc.name) { + frappe.msgprint(__('Please create Card first')); + } else { + d.show(); + } + }); }, before_save: function(frm) { @@ -35,6 +76,7 @@ frappe.ui.form.on('Number Card', { frm.set_value('filters_json', JSON.stringify(static_filters)); frm.trigger('render_filters_table'); + frm.trigger('render_dynamic_filters_table'); }, is_standard: function(frm) { @@ -133,6 +175,7 @@ frappe.ui.form.on('Number Card', { frm.set_df_property('aggregate_function_based_on', 'options', aggregate_based_on_fields); }); frm.trigger('render_filters_table'); + frm.trigger('render_dynamic_filters_table'); } }, @@ -156,6 +199,9 @@ frappe.ui.form.on('Number Card', { set_report_field_options: function(frm) { let filters = frm.doc.filters_json.length > 2 ? JSON.parse(frm.doc.filters_json) : null; + if (frm.doc.dynamic_filters_json.length > 2) { + filters = {...filters, ...JSON.parse(frm.doc.dynamic_filters_json)}; + } frappe.xcall( 'frappe.desk.query_report.run', { From ba89bd948aa5f11ae60c001374ec7eb9d01e7a35 Mon Sep 17 00:00:00 2001 From: prssanna Date: Thu, 16 Jul 2020 13:24:06 +0530 Subject: [PATCH 10/29] fix: add dynamic filters in report filters for charts --- frappe/desk/doctype/dashboard_chart/dashboard_chart.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/frappe/desk/doctype/dashboard_chart/dashboard_chart.js b/frappe/desk/doctype/dashboard_chart/dashboard_chart.js index c8783f0e64..738d77ae27 100644 --- a/frappe/desk/doctype/dashboard_chart/dashboard_chart.js +++ b/frappe/desk/doctype/dashboard_chart/dashboard_chart.js @@ -175,6 +175,9 @@ frappe.ui.form.on('Dashboard Chart', { set_chart_field_options: function(frm) { let filters = frm.doc.filters_json.length > 2 ? JSON.parse(frm.doc.filters_json) : null; + if (frm.doc.dynamic_filters_json.length > 2) { + filters = {...filters, ...JSON.parse(frm.doc.dynamic_filters_json)}; + } frappe.xcall( 'frappe.desk.query_report.run', { From d8192de74f2d03feeeccb9d329427e30fcb5011f Mon Sep 17 00:00:00 2001 From: prssanna Date: Thu, 16 Jul 2020 13:31:31 +0530 Subject: [PATCH 11/29] feat: add card from report --- .../desk/doctype/number_card/number_card.py | 8 ++ .../js/frappe/views/reports/query_report.js | 105 ++++++++++++++++-- 2 files changed, 106 insertions(+), 7 deletions(-) diff --git a/frappe/desk/doctype/number_card/number_card.py b/frappe/desk/doctype/number_card/number_card.py index 1243b1850f..5b52b60474 100644 --- a/frappe/desk/doctype/number_card/number_card.py +++ b/frappe/desk/doctype/number_card/number_card.py @@ -156,6 +156,14 @@ def get_cards_for_user(doctype, txt, searchfield, start, page_len, filters): conditions=conditions ), values) +@frappe.whitelist() +def create_report_number_card(args): + card = create_number_card(args) + args = frappe.parse_json(args) + args.name = card.name + if args.dashboard: + add_card_to_dashboard(frappe.as_json(args)) + @frappe.whitelist() def add_card_to_dashboard(args): args = frappe.parse_json(args) diff --git a/frappe/public/js/frappe/views/reports/query_report.js b/frappe/public/js/frappe/views/reports/query_report.js index f82956adac..2d49948dfc 100644 --- a/frappe/public/js/frappe/views/reports/query_report.js +++ b/frappe/public/js/frappe/views/reports/query_report.js @@ -128,10 +128,17 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { () => this.setup_progress_bar(), () => this.setup_page_head(), () => this.refresh_report(), - () => this.add_chart_buttons_to_toolbar(true) + () => this.add_chart_buttons_to_toolbar(true), + () => this.add_card_button_to_toolbar(), ]); } + add_card_button_to_toolbar() { + this.page.add_inner_button(__("Create Card"), () => { + this.add_card_to_dashboard(); + }); + } + add_chart_buttons_to_toolbar(show) { if (show) { this.page.add_inner_button(__("Set Chart"), () => { @@ -148,6 +155,62 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { } } + add_card_to_dashboard() { + let field_options = frappe.report_utils.get_field_options_from_report(this.columns, this.raw_data); + const dialog = new frappe.ui.Dialog({ + title: __('Create Card'), + fields: [ + { + fieldname: 'report_field', + label: __('Field'), + fieldtype: 'Select', + options: field_options.numeric_fields, + }, + { + fieldname: 'cb_1', + fieldtype: 'Column Break' + }, + { + fieldname: 'report_function', + label: __('Function'), + options: ['Sum', 'Average', 'Minimum', 'Maximum'], + fieldtype: 'Select' + }, + { + fieldname: 'sb_1', + label: __('Add to Dashboard'), + fieldtype: 'Section Break' + }, + { + fieldname: 'dashboard', + label: __('Choose Dashboard'), + fieldtype: 'Link', + options: 'Dashboard', + }, + { + fieldname: 'cb_2', + fieldtype: 'Column Break' + }, + { + fieldname: 'label', + label: __('Card Label'), + fieldtype: 'Data', + } + ], + primary_action_label: __('Add'), + primary_action: (values) => { + if (!values.label) { + values.label = `${values.report_function} of ${toTitle(values.report_field)}`; + } + this.create_number_card(values, values.dashboard, values.label); + dialog.hide(); + } + }); + + dialog.show(); + + } + add_chart_to_dashboard() { if (this.chart_fields || this.chart_options) { const dialog = new frappe.ui.Dialog({ @@ -182,6 +245,24 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { } } + create_number_card(values, dashboard_name, card_name) { + let args = { + 'dashboard': dashboard_name || null, + 'type': 'Report', + 'report_name': this.report_name, + 'filters_json': JSON.stringify(this.get_filter_values()), + }; + Object.assign(args, values); + + this.add_to_dashboard( + 'frappe.desk.doctype.number_card.number_card.create_report_number_card', + args, + dashboard_name, + card_name, + 'Number Card' + ); + } + create_dashboard_chart(chart_args, dashboard_name, chart_name) { let args = { 'dashboard': dashboard_name || null, @@ -224,19 +305,29 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { ); } - frappe.xcall( + this.add_to_dashboard( 'frappe.desk.doctype.dashboard_chart.dashboard_chart.create_report_chart', + args, + dashboard_name, + chart_name, + 'Dashboard Chart' + ); + } + + add_to_dashboard(method, args, dashboard_name, name, doctype) { + frappe.xcall( + method, {args: args} - ).then( () => { + ).then(() => { let message; if (dashboard_name) { let dashboard_route_html = `${dashboard_name}`; - message = __(`New Dashboard Chart ${chart_name} added to Dashboard ` + dashboard_route_html); + message = __(`New {0} {1} added to Dashboard ` + dashboard_route_html, [doctype, name]); } else { - message = __(`New chart ${chart_name} created`); + message = __(`New {0} {1} created`, [doctype, name]); } - frappe.msgprint(message, __('New Chart Created')); + frappe.msgprint(message, __(`New {0} Created`, [doctype])); }); } @@ -700,7 +791,7 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { open_create_chart_dialog() { const me = this; - let field_options = frappe.report_utils.get_possible_chart_options(this.columns, this.raw_data); + let field_options = frappe.report_utils.get_field_options_from_report(this.columns, this.raw_data); function set_chart_values(values) { values.y_fields = []; From c574e3c20d925410ca938bc0b7448c6d0af177cf Mon Sep 17 00:00:00 2001 From: prssanna Date: Wed, 15 Jul 2020 16:06:08 +0530 Subject: [PATCH 12/29] fix: handle set route for report type cards --- .../js/frappe/widgets/number_card_widget.js | 28 ++++++++++++++++--- frappe/public/js/frappe/widgets/utils.js | 10 ------- 2 files changed, 24 insertions(+), 14 deletions(-) diff --git a/frappe/public/js/frappe/widgets/number_card_widget.js b/frappe/public/js/frappe/widgets/number_card_widget.js index f384e84af6..657ffde8ad 100644 --- a/frappe/public/js/frappe/widgets/number_card_widget.js +++ b/frappe/public/js/frappe/widgets/number_card_widget.js @@ -1,5 +1,5 @@ import Widget from "./base_widget.js"; -import { go_to_list_with_filters, shorten_number } from "./utils"; +import { generate_route, shorten_number } from "./utils"; export default class NumberCardWidget extends Widget { constructor(opts) { @@ -66,12 +66,32 @@ export default class NumberCardWidget extends Widget { set_events() { $(this.body).click(() => { - if (this.in_customize_mode) return; - let filters = JSON.parse(this.card_doc.filters_json); - go_to_list_with_filters(this.card_doc.document_type, filters); + if (this.in_customize_mode || this.card_doc.type == 'Custom') return; + this.set_route(); }); } + set_route() { + const is_document_type = this.card_doc.type !== 'Report'; + const name = is_document_type ? this.card_doc.document_type : this.card_doc.report_name; + const route = generate_route({ + name: name, + type: is_document_type ? 'doctype' : 'report', + is_query_report: !is_document_type, + }); + + if (is_document_type) { + const filters = JSON.parse(this.card_doc.filters_json); + frappe.route_options = filters.reduce((acc, filter) => { + return Object.assign(acc, { + [`${filter[0]}.${filter[1]}`]: [filter[2], filter[3]] + }); + }, {}); + } + + frappe.set_route(route); + } + set_doc_args() { this.card_doc = Object.assign({}, { document_type: this.document_type, diff --git a/frappe/public/js/frappe/widgets/utils.js b/frappe/public/js/frappe/widgets/utils.js index dff4db807e..1f235ea7a7 100644 --- a/frappe/public/js/frappe/widgets/utils.js +++ b/frappe/public/js/frappe/widgets/utils.js @@ -117,16 +117,6 @@ const build_summary_item = (summary) => { `); }; -function go_to_list_with_filters(doctype, filters) { - const route = `List/${doctype}/List`; - frappe.set_route(route).then(()=> { - let list_view = frappe.views.list_view[route]; - let filter_area = list_view.filter_area; - filter_area.clear(); - filter_area.filter_list.add_filters_to_filter_group(filters); - }); -} - function shorten_number(number, country) { country = (country == 'India') ? country : ''; const number_system = get_number_system(country); From ab845e8d54a4dd6a94a8ccff04becda1227b66d2 Mon Sep 17 00:00:00 2001 From: prssanna Date: Thu, 16 Jul 2020 13:50:05 +0530 Subject: [PATCH 13/29] fix: don't render dynamic filters for custom cards --- frappe/desk/doctype/number_card/number_card.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frappe/desk/doctype/number_card/number_card.js b/frappe/desk/doctype/number_card/number_card.js index 202c0ce075..dbb7f8d623 100644 --- a/frappe/desk/doctype/number_card/number_card.js +++ b/frappe/desk/doctype/number_card/number_card.js @@ -24,6 +24,7 @@ frappe.ui.form.on('Number Card', { } frm.filters = eval(frm.doc.filters_config); frm.trigger('set_filters_description'); + frm.trigger('render_filters_table'); } frm.trigger('create_add_to_dashboard_button'); }, @@ -278,7 +279,6 @@ frappe.ui.form.on('Number Card', { filters_set = true; } } else if (frm.filters.length) { - filters_set = true; fields = frm.filters.filter(f => f.fieldname); fields.map(f => { if (filters[f.fieldname]) { @@ -289,8 +289,8 @@ frappe.ui.form.on('Number Card', { ${condition} ${filters[f.fieldname] || ""} `); - table.find('tbody').append(filter_row); + if (!filters_set) filters_set = true; } }); } @@ -346,7 +346,7 @@ frappe.ui.form.on('Number Card', { }, render_dynamic_filters_table(frm) { - if (!frappe.boot.developer_mode || !frm.doc.is_standard) { + if (!frappe.boot.developer_mode || !frm.doc.is_standard || frm.doc.type == 'Custom') { return } From 75e15cbbe9716a11e6a12b4bc917d79d0076c116 Mon Sep 17 00:00:00 2001 From: prssanna Date: Thu, 16 Jul 2020 13:50:26 +0530 Subject: [PATCH 14/29] fix: don't export go_to_list_with_filters --- frappe/public/js/frappe/widgets/utils.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/public/js/frappe/widgets/utils.js b/frappe/public/js/frappe/widgets/utils.js index 1f235ea7a7..22b3167977 100644 --- a/frappe/public/js/frappe/widgets/utils.js +++ b/frappe/public/js/frappe/widgets/utils.js @@ -157,4 +157,4 @@ function get_number_system(country) { return number_system_map[country]; } -export { generate_route, generate_grid, build_summary_item, go_to_list_with_filters, shorten_number }; \ No newline at end of file +export { generate_route, generate_grid, build_summary_item, shorten_number }; \ No newline at end of file From 8304f1fa83a17f02caf0a41401ac7bf10b25088d Mon Sep 17 00:00:00 2001 From: prssanna Date: Thu, 16 Jul 2020 14:00:02 +0530 Subject: [PATCH 15/29] style: formatting fixes --- frappe/desk/doctype/number_card/number_card.js | 2 +- frappe/public/js/frappe/widgets/number_card_widget.js | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/frappe/desk/doctype/number_card/number_card.js b/frappe/desk/doctype/number_card/number_card.js index dbb7f8d623..9bc071b723 100644 --- a/frappe/desk/doctype/number_card/number_card.js +++ b/frappe/desk/doctype/number_card/number_card.js @@ -347,7 +347,7 @@ frappe.ui.form.on('Number Card', { render_dynamic_filters_table(frm) { if (!frappe.boot.developer_mode || !frm.doc.is_standard || frm.doc.type == 'Custom') { - return + return; } frm.set_df_property("dynamic_filters_section", "hidden", 0); diff --git a/frappe/public/js/frappe/widgets/number_card_widget.js b/frappe/public/js/frappe/widgets/number_card_widget.js index 657ffde8ad..6c85c03b0a 100644 --- a/frappe/public/js/frappe/widgets/number_card_widget.js +++ b/frappe/public/js/frappe/widgets/number_card_widget.js @@ -83,9 +83,9 @@ export default class NumberCardWidget extends Widget { if (is_document_type) { const filters = JSON.parse(this.card_doc.filters_json); frappe.route_options = filters.reduce((acc, filter) => { - return Object.assign(acc, { - [`${filter[0]}.${filter[1]}`]: [filter[2], filter[3]] - }); + return Object.assign(acc, { + [`${filter[0]}.${filter[1]}`]: [filter[2], filter[3]] + }); }, {}); } From 92a778c44a3e1a158389fae02f0427c6efe05ccc Mon Sep 17 00:00:00 2001 From: marination Date: Thu, 16 Jul 2020 14:03:11 +0530 Subject: [PATCH 16/29] fix: Made Workflow State Field Mandatory --- .../workflow/doctype/workflow/workflow.json | 324 ++---------------- 1 file changed, 29 insertions(+), 295 deletions(-) diff --git a/frappe/workflow/doctype/workflow/workflow.json b/frappe/workflow/doctype/workflow/workflow.json index 28bc186bc8..e22d21b5d3 100644 --- a/frappe/workflow/doctype/workflow/workflow.json +++ b/frappe/workflow/doctype/workflow/workflow.json @@ -1,389 +1,123 @@ { - "allow_copy": 0, - "allow_guest_to_view": 0, - "allow_import": 0, + "actions": [], "allow_rename": 1, "autoname": "field:workflow_name", - "beta": 0, "creation": "2012-12-28 10:49:55", - "custom": 0, "description": "Defines workflow states and rules for a document.", - "docstatus": 0, "doctype": "DocType", "document_type": "Document", - "editable_grid": 0, "engine": "InnoDB", + "field_order": [ + "workflow_name", + "document_type", + "is_active", + "override_status", + "send_email_alert", + "states_head", + "states", + "transition_rules", + "transitions", + "workflow_state_field" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "workflow_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": "Workflow Name", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "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 + "unique": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "description": "DocType on which this Workflow is applicable.", "fieldname": "document_type", "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": 1, "label": "Document Type", - "length": 0, - "no_copy": 0, "options": "DocType", - "permlevel": 0, - "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, + "default": "0", "description": "If checked, all other workflows become inactive.", "fieldname": "is_active", "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 Active", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "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 Active" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, + "default": "0", "description": "If Checked workflow status will not override status in list view", "fieldname": "override_status", "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": "Don't Override Status", - "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": "Don't Override Status" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "default": "1", "description": "Emails will be sent with next possible workflow actions", "fieldname": "send_email_alert", "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": "Send Email Alert", - "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": "Send Email Alert" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "description": "Different \"States\" this document can exist in. Like \"Open\", \"Pending Approval\" etc.", "fieldname": "states_head", "fieldtype": "Section Break", - "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": "States", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "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": "States" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "description": "All possible Workflow States and roles of the workflow. Docstatus Options: 0 is\"Saved\", 1 is \"Submitted\" and 2 is \"Cancelled\"", "fieldname": "states", "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": "Document States", - "length": 0, - "no_copy": 0, "options": "Workflow Document State", - "permlevel": 0, - "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, "description": "Rules for how states are transitions, like next state and which role is allowed to change state etc.", "fieldname": "transition_rules", "fieldtype": "Section Break", - "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": "Transition Rules", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "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": "Transition Rules" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "description": "Rules defining transition of state in the workflow.", "fieldname": "transitions", "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": "Transitions", - "length": 0, - "no_copy": 0, "options": "Workflow Transition", - "permlevel": 0, - "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, "default": "workflow_state", "description": "Field that represents the Workflow State of the transaction (if field is not present, a new hidden Custom Field will be created)", "fieldname": "workflow_state_field", "fieldtype": "Data", - "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": "Workflow State Field", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "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 + "reqd": 1 } ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, "icon": "fa fa-random", "idx": 1, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "menu_index": 0, - "modified": "2018-06-11 10:45:46.418470", + "links": [], + "modified": "2020-07-16 04:29:20.898040", "modified_by": "Administrator", "module": "Workflow", "name": "Workflow", "owner": "Administrator", "permissions": [ { - "amend": 0, - "cancel": 0, "create": 1, "delete": 1, "email": 1, - "export": 0, - "if_owner": 0, - "import": 0, - "permlevel": 0, "print": 1, "read": 1, - "report": 0, "role": "System Manager", - "set_user_permissions": 0, "share": 1, - "submit": 0, "write": 1 } ], - "quick_entry": 0, - "read_only": 0, - "read_only_onload": 0, "show_name_in_global_search": 1, + "sort_field": "modified", "sort_order": "ASC", - "track_changes": 1, - "track_seen": 0 + "track_changes": 1 } \ No newline at end of file From 79e3f88a6566df3da19bf16caf37fab6a291f975 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Thu, 16 Jul 2020 13:26:45 +0530 Subject: [PATCH 17/29] fix: Edit in full page broken link --- frappe/model/document.py | 7 +++++++ frappe/public/js/frappe/form/quick_entry.js | 9 ++------- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/frappe/model/document.py b/frappe/model/document.py index ea693167f8..69a781d6d1 100644 --- a/frappe/model/document.py +++ b/frappe/model/document.py @@ -403,9 +403,16 @@ class Document(BaseDocument): def set_new_name(self, force=False, set_name=None, set_child_names=True): """Calls `frappe.naming.set_new_name` for parent and child docs.""" + if self.flags.name_set and not force: return + # If autoname has set as Prompt (name) + if self.get("__newname"): + self.name = self.get("__newname") + self.flags.name_set = True + return + if set_name: self.name = set_name else: diff --git a/frappe/public/js/frappe/form/quick_entry.js b/frappe/public/js/frappe/form/quick_entry.js index 68444c8a3b..5a46e8ae8b 100644 --- a/frappe/public/js/frappe/form/quick_entry.js +++ b/frappe/public/js/frappe/form/quick_entry.js @@ -240,13 +240,8 @@ frappe.ui.form.QuickEntryForm = Class.extend({ var me = this; var data = this.dialog.get_values(true); $.each(data, function(key, value) { - if(key==='__newname') { - me.dialog.doc.name = value; - } - else { - if(!is_null(value)) { - me.dialog.doc[key] = value; - } + if(!is_null(value)) { + me.dialog.doc[key] = value; } }); return this.dialog.doc; From 434c892ac004cffbf724214132540a282d02ccf2 Mon Sep 17 00:00:00 2001 From: Faris Ansari Date: Thu, 16 Jul 2020 17:05:08 +0530 Subject: [PATCH 18/29] feat: Bench command to open ngrok tunnel (#11024) Co-authored-by: Suraj Shetty <13928957+surajshetty3416@users.noreply.github.com> --- frappe/commands/site.py | 26 +++++++++++++++++++++++++- requirements.txt | 5 +++-- 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/frappe/commands/site.py b/frappe/commands/site.py index ec80ee019d..26eb455338 100755 --- a/frappe/commands/site.py +++ b/frappe/commands/site.py @@ -631,6 +631,29 @@ def stop_recording(context): if not context.sites: raise SiteNotSpecifiedError +@click.command('ngrok') +@pass_context +def start_ngrok(context): + from pyngrok import ngrok + + site = get_site(context) + frappe.init(site=site) + + port = frappe.conf.http_port or frappe.conf.webserver_port + public_url = ngrok.connect(port=port, options={ + 'host_header': site + }) + print(f'Public URL: {public_url}') + print('Inspect logs at http://localhost:4040') + + ngrok_process = ngrok.get_ngrok_process() + try: + # Block until CTRL-C or some other terminating event + ngrok_process.proc.wait() + except KeyboardInterrupt: + print("Shutting down server...") + frappe.destroy() + ngrok.kill() commands = [ add_system_manager, @@ -656,5 +679,6 @@ commands = [ browse, start_recording, stop_recording, - add_to_hosts + add_to_hosts, + start_ngrok ] diff --git a/requirements.txt b/requirements.txt index 0d01886f05..e0ca1a6fad 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,8 +13,8 @@ dropbox==9.1.0 email-reply-parser==0.5.9 Faker==2.0.4 future==0.18.2 -GitPython==2.1.15 gitdb2==2.0.6;python_version<'3.4' +GitPython==2.1.15 google-api-python-client==1.9.3 google-auth-httplib2==0.0.3 google-auth-oauthlib==0.4.1 @@ -40,6 +40,7 @@ psycopg2-binary==2.8.4 pyasn1==0.4.8 PyJWT==1.7.1 PyMySQL==0.9.3 +pyngrok==4.1.6 pyOpenSSL==19.1.0 pyotp==2.3.0 PyPDF2==1.26.0 @@ -64,6 +65,6 @@ unittest-xml-reporting==2.5.2 urllib3==1.25.8 watchdog==0.8.0 Werkzeug==0.16.1 +Whoosh==2.7.4 xlrd==1.2.0 zxcvbn-python==4.4.24 -Whoosh==2.7.4 From 4cdaedcbad229376be203533b0d34f9bcf3def64 Mon Sep 17 00:00:00 2001 From: Suraj Shetty <13928957+surajshetty3416@users.noreply.github.com> Date: Thu, 16 Jul 2020 17:38:34 +0530 Subject: [PATCH 19/29] style: Add space after if --- frappe/public/js/frappe/form/quick_entry.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/public/js/frappe/form/quick_entry.js b/frappe/public/js/frappe/form/quick_entry.js index 5a46e8ae8b..b1df0a1080 100644 --- a/frappe/public/js/frappe/form/quick_entry.js +++ b/frappe/public/js/frappe/form/quick_entry.js @@ -277,7 +277,7 @@ frappe.ui.form.QuickEntryForm = Class.extend({ field.doctype = me.doc.doctype; field.docname = me.doc.name; - if(!is_null(me.doc[fieldname])) { + if (!is_null(me.doc[fieldname])) { field.set_input(me.doc[fieldname]); } }); From 80fb256421989ace15cb5c8ea96e9825963d8b26 Mon Sep 17 00:00:00 2001 From: Suraj Shetty <13928957+surajshetty3416@users.noreply.github.com> Date: Thu, 16 Jul 2020 17:41:37 +0530 Subject: [PATCH 20/29] style: Add space after if --- frappe/public/js/frappe/form/quick_entry.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/public/js/frappe/form/quick_entry.js b/frappe/public/js/frappe/form/quick_entry.js index b1df0a1080..2da7b8f236 100644 --- a/frappe/public/js/frappe/form/quick_entry.js +++ b/frappe/public/js/frappe/form/quick_entry.js @@ -240,7 +240,7 @@ frappe.ui.form.QuickEntryForm = Class.extend({ var me = this; var data = this.dialog.get_values(true); $.each(data, function(key, value) { - if(!is_null(value)) { + if (!is_null(value)) { me.dialog.doc[key] = value; } }); From f12ed9791a2da4a9619c7564ba2bb6dcc64cda80 Mon Sep 17 00:00:00 2001 From: "Chinmay D. Pai" Date: Fri, 17 Jul 2020 11:32:02 +0530 Subject: [PATCH 21/29] fix: remove example users email ids from notifications currently the system tries sending emails to all users that have 'thread_notify' enabled, including the default example emails that the Administrator and Guest accounts have. this patch disables email notifications for these accounts, and additionally tries to disable it for any other user accounts with example emails Signed-off-by: Chinmay D. Pai --- frappe/patches.txt | 3 ++- .../patches/v12_0/remove_example_email_thread_notify.py | 8 ++++++++ frappe/utils/install.py | 6 ++++-- 3 files changed, 14 insertions(+), 3 deletions(-) create mode 100644 frappe/patches/v12_0/remove_example_email_thread_notify.py diff --git a/frappe/patches.txt b/frappe/patches.txt index 1108f1fb1b..f8c767f5a3 100644 --- a/frappe/patches.txt +++ b/frappe/patches.txt @@ -273,6 +273,7 @@ execute:frappe.delete_doc_if_exists('DocType', 'GSuite Templates') execute:frappe.delete_doc_if_exists('DocType', 'GCalendar Account') execute:frappe.delete_doc_if_exists('DocType', 'GCalendar Settings') frappe.patches.v12_0.remove_parent_and_parenttype_from_print_formats +frappe.patches.v12_0.remove_example_email_thread_notify execute:from frappe.desk.page.setup_wizard.install_fixtures import update_genders;update_genders() frappe.patches.v12_0.set_correct_url_in_files frappe.patches.v13_0.website_theme_custom_scss @@ -293,4 +294,4 @@ execute:frappe.delete_doc("DocType", "Onboarding Slide Help Link") frappe.patches.v13_0.update_date_filters_in_user_settings frappe.patches.v13_0.update_duration_options frappe.patches.v13_0.replace_old_data_import # 2020-06-24 -frappe.patches.v13_0.create_custom_dashboards_cards_and_charts \ No newline at end of file +frappe.patches.v13_0.create_custom_dashboards_cards_and_charts diff --git a/frappe/patches/v12_0/remove_example_email_thread_notify.py b/frappe/patches/v12_0/remove_example_email_thread_notify.py new file mode 100644 index 0000000000..94959b6077 --- /dev/null +++ b/frappe/patches/v12_0/remove_example_email_thread_notify.py @@ -0,0 +1,8 @@ +import frappe + + +def execute(): + # remove all example.com email user accounts from notifications + frappe.db.sql("""UPDATE `tabUser` + SET thread_notify=0, send_me_a_copy=0 + WHERE email like '%@example.com'""") diff --git a/frappe/utils/install.py b/frappe/utils/install.py index e5bf122d81..bb384fb300 100644 --- a/frappe/utils/install.py +++ b/frappe/utils/install.py @@ -50,11 +50,13 @@ def install_basic_docs(): install_docs = [ {'doctype':'User', 'name':'Administrator', 'first_name':'Administrator', 'email':'admin@example.com', 'enabled':1, "is_admin": 1, - 'roles': [{'role': 'Administrator'}] + 'roles': [{'role': 'Administrator'}], + 'thread_notify': 0, 'send_me_a_copy': 0 }, {'doctype':'User', 'name':'Guest', 'first_name':'Guest', 'email':'guest@example.com', 'enabled':1, "is_guest": 1, - 'roles': [{'role': 'Guest'}] + 'roles': [{'role': 'Guest'}], + 'thread_notify': 0, 'send_me_a_copy': 0 }, {'doctype': "Role", "role_name": "Report Manager"}, {'doctype': "Role", "role_name": "Translator"}, From 654950fef341fcc4473413188eea513548090d14 Mon Sep 17 00:00:00 2001 From: Aditya Hase Date: Fri, 17 Jul 2020 13:14:31 +0530 Subject: [PATCH 22/29] fix: Use SameSite=Lax instead of SameSite=Strict Reference: https://web.dev/samesite-cookies-explained/ Scenario: A user is logged in on a frappe instance. (with, say, sid=abcd) In an OAuth like flow, a third party site, say, GitHub redirects the user to the frappe instance. Now because SameSite=Strict is set, the browser can't send the cookies it has (sid=abcd) to the frappe server. Once the frappe server receives this request without cookies, It assumes that this is an unauthenticated user, and sets cookies in response as sid=Guest. Reference: https://github.com/frappe/frappe/blob/f3e14b4ac71135fae5d83bb371c665879be36600/frappe/sessions.py#L178 Once the browser receives these values in response (sid=Guest) it overwrites the existing cookie values(sid=abcd) and sets sid=Guest, This effectively causes the user session to be terminated. --- frappe/auth.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/auth.py b/frappe/auth.py index ab3624bee8..64fea36748 100644 --- a/frappe/auth.py +++ b/frappe/auth.py @@ -337,7 +337,7 @@ class CookieManager: if frappe.session.session_country: self.set_cookie("country", frappe.session.session_country) - def set_cookie(self, key, value, expires=None, secure=False, httponly=False, samesite="Strict"): + def set_cookie(self, key, value, expires=None, secure=False, httponly=False, samesite="Lax"): if not secure: secure = frappe.local.request.scheme == "https" self.cookies[key] = { From e52341f3d6dda5db0d8c8ac45b3076644377b790 Mon Sep 17 00:00:00 2001 From: prssanna Date: Fri, 17 Jul 2020 15:19:13 +0530 Subject: [PATCH 23/29] fix: toggle card button if report has data --- frappe/public/js/frappe/views/reports/query_report.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/frappe/public/js/frappe/views/reports/query_report.js b/frappe/public/js/frappe/views/reports/query_report.js index 2d49948dfc..e2020e8690 100644 --- a/frappe/public/js/frappe/views/reports/query_report.js +++ b/frappe/public/js/frappe/views/reports/query_report.js @@ -129,11 +129,11 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { () => this.setup_page_head(), () => this.refresh_report(), () => this.add_chart_buttons_to_toolbar(true), - () => this.add_card_button_to_toolbar(), + () => this.add_card_button_to_toolbar(true), ]); } - add_card_button_to_toolbar() { + add_card_button_to_toolbar(show) { this.page.add_inner_button(__("Create Card"), () => { this.add_card_to_dashboard(); }); @@ -609,10 +609,12 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { } this.render_datatable(); this.add_chart_buttons_to_toolbar(true); + this.add_card_button_to_toolbar(true); } else { this.data = []; this.toggle_nothing_to_show(true); this.add_chart_buttons_to_toolbar(false); + this.add_card_button_to_toolbar(false); } this.show_footer_message(); From 8c93d91c5ad7681e98d0fd6d1472152635447d99 Mon Sep 17 00:00:00 2001 From: prssanna Date: Fri, 17 Jul 2020 15:27:07 +0530 Subject: [PATCH 24/29] fix: better variable name --- frappe/public/js/frappe/widgets/number_card_widget.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frappe/public/js/frappe/widgets/number_card_widget.js b/frappe/public/js/frappe/widgets/number_card_widget.js index 6c85c03b0a..657275418f 100644 --- a/frappe/public/js/frappe/widgets/number_card_widget.js +++ b/frappe/public/js/frappe/widgets/number_card_widget.js @@ -172,7 +172,7 @@ export default class NumberCardWidget extends Widget { this.get_formatted_number(based_on_df); }); } else { - this.number_html = res; + this.formatted_number = res; } } }); @@ -197,13 +197,13 @@ export default class NumberCardWidget extends Widget { const symbol = number_parts[1] || ''; const formatted_number = $(frappe.format(number_parts[0], df)).text(); - this.number_html = formatted_number + ' ' + symbol; + this.formatted_number = formatted_number + ' ' + symbol; } render_number() { return this.get_number().then(() => { $(this.body).html(`
-
${this.number_html}
+
${this.formatted_number}
`); }); } From c4a8852a68c0c42409315cbb04760ad78f3379bb Mon Sep 17 00:00:00 2001 From: prssanna Date: Fri, 17 Jul 2020 18:12:44 +0530 Subject: [PATCH 25/29] fix: handle result of custom cards --- frappe/public/js/frappe/widgets/number_card_widget.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/frappe/public/js/frappe/widgets/number_card_widget.js b/frappe/public/js/frappe/widgets/number_card_widget.js index 657275418f..829c6d70dd 100644 --- a/frappe/public/js/frappe/widgets/number_card_widget.js +++ b/frappe/public/js/frappe/widgets/number_card_widget.js @@ -163,6 +163,9 @@ export default class NumberCardWidget extends Widget { return frappe.xcall(this.settings.method, this.settings.args).then(res => { if (this.card_doc.type == 'Report') { this.get_number_for_report(res); + } else if (this.card_doc.type == 'Custom') { + this.number = res.value; + this.get_formatted_number(res); } else { this.number = res; if (this.card_doc.function !== 'Count') { @@ -213,7 +216,7 @@ export default class NumberCardWidget extends Widget { return; } - let caret_html =''; + let caret_html = ''; let color_class = ''; return this.get_percentage_stats().then(() => { From 3551fecd74a91b62e58df94d8bd85f2f4879114d Mon Sep 17 00:00:00 2001 From: prssanna Date: Fri, 17 Jul 2020 18:13:18 +0530 Subject: [PATCH 26/29] fix: update description of method field --- frappe/desk/doctype/number_card/number_card.js | 16 ++++++++++++++++ frappe/desk/doctype/number_card/number_card.json | 3 +-- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/frappe/desk/doctype/number_card/number_card.js b/frappe/desk/doctype/number_card/number_card.js index 9bc071b723..5ad5d76ac8 100644 --- a/frappe/desk/doctype/number_card/number_card.js +++ b/frappe/desk/doctype/number_card/number_card.js @@ -24,6 +24,7 @@ frappe.ui.form.on('Number Card', { } frm.filters = eval(frm.doc.filters_config); frm.trigger('set_filters_description'); + frm.trigger('set_method_description'); frm.trigger('render_filters_table'); } frm.trigger('create_add_to_dashboard_button'); @@ -110,6 +111,21 @@ frappe.ui.form.on('Number Card', { } }, + set_method_description: function(frm) { + console.log('called'); + if (frm.doc.type == 'Custom') { + frm.fields_dict.method.set_description(` + Set the path to a whitelisted function that will return the number on the card in the format: +
+
+{
+	"value": value,
+	"fieldtype": "Currency"
+}
+
`) + } + }, + type: function(frm) { frm.trigger('set_filters_description'); if (frm.doc.type == 'Report') { diff --git a/frappe/desk/doctype/number_card/number_card.json b/frappe/desk/doctype/number_card/number_card.json index 1d0528b298..e94a06dab8 100644 --- a/frappe/desk/doctype/number_card/number_card.json +++ b/frappe/desk/doctype/number_card/number_card.json @@ -164,7 +164,6 @@ }, { "depends_on": "eval: doc.type == 'Custom'", - "description": "This should contain the path to a whitelisted function that will return the number on the card", "fieldname": "method", "fieldtype": "Data", "label": "Method", @@ -192,7 +191,7 @@ } ], "links": [], - "modified": "2020-07-16 13:04:03.470001", + "modified": "2020-07-17 18:04:00.814756", "modified_by": "Administrator", "module": "Desk", "name": "Number Card", From b32a47ec587fba733f9facdbb465f605b1e16516 Mon Sep 17 00:00:00 2001 From: prssanna Date: Fri, 17 Jul 2020 18:29:29 +0530 Subject: [PATCH 27/29] refactor: set get_number method in settings --- .../js/frappe/widgets/number_card_widget.js | 51 +++++++++++-------- 1 file changed, 30 insertions(+), 21 deletions(-) diff --git a/frappe/public/js/frappe/widgets/number_card_widget.js b/frappe/public/js/frappe/widgets/number_card_widget.js index 829c6d70dd..220394919c 100644 --- a/frappe/public/js/frappe/widgets/number_card_widget.js +++ b/frappe/public/js/frappe/widgets/number_card_widget.js @@ -110,7 +110,8 @@ export default class NumberCardWidget extends Widget { method: this.card_doc.method, args: { filters: this.filters - } + }, + get_number: res => this.get_number_for_custom_card(res), }, 'Report': { method: 'frappe.desk.query_report.run', @@ -118,14 +119,16 @@ export default class NumberCardWidget extends Widget { report_name: this.card_doc.report_name, filters: this.filters, ignore_prepared_report: 1 - } + }, + get_number: res => this.get_number_for_report_card(res), }, 'Document Type': { method: 'frappe.desk.doctype.number_card.number_card.get_result', args: { doc: this.card_doc, filters: this.filters, - } + }, + get_number: res => this.get_number_for_doctype_card(res), } }; return settings_map[type]; @@ -161,27 +164,33 @@ export default class NumberCardWidget extends Widget { get_number() { return frappe.xcall(this.settings.method, this.settings.args).then(res => { - if (this.card_doc.type == 'Report') { - this.get_number_for_report(res); - } else if (this.card_doc.type == 'Custom') { - this.number = res.value; - this.get_formatted_number(res); - } else { - this.number = res; - if (this.card_doc.function !== 'Count') { - return frappe.model.with_doctype(this.card_doc.document_type, () => { - const based_on_df = - frappe.meta.get_docfield(this.card_doc.document_type, this.card_doc.aggregate_function_based_on); - this.get_formatted_number(based_on_df); - }); - } else { - this.formatted_number = res; - } - } + this.settings.get_number(res); }); } - get_number_for_report(res) { + get_number_for_custom_card(res) { + if (typeof res === 'object') { + this.number = res.value; + this.get_formatted_number(res); + } else { + this.formatted_number = res; + } + } + + get_number_for_doctype_card(res) { + this.number = res; + if (this.card_doc.function !== 'Count') { + return frappe.model.with_doctype(this.card_doc.document_type, () => { + const based_on_df = + frappe.meta.get_docfield(this.card_doc.document_type, this.card_doc.aggregate_function_based_on); + this.get_formatted_number(based_on_df); + }); + } else { + this.formatted_number = res; + } + } + + get_number_for_report_card(res) { const field = this.card_doc.report_field; const vals = res.result.reduce((acc, col) => { col[field] && acc.push(col[field]); From 6bc31f1d6a521f9dabc3feaef0976102efeb2ed7 Mon Sep 17 00:00:00 2001 From: prssanna Date: Fri, 17 Jul 2020 18:37:30 +0530 Subject: [PATCH 28/29] style: formatting fixes --- frappe/desk/doctype/number_card/number_card.js | 3 +-- frappe/public/js/frappe/views/reports/query_report.js | 5 ++--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/frappe/desk/doctype/number_card/number_card.js b/frappe/desk/doctype/number_card/number_card.js index 5ad5d76ac8..d5a743818a 100644 --- a/frappe/desk/doctype/number_card/number_card.js +++ b/frappe/desk/doctype/number_card/number_card.js @@ -112,7 +112,6 @@ frappe.ui.form.on('Number Card', { }, set_method_description: function(frm) { - console.log('called'); if (frm.doc.type == 'Custom') { frm.fields_dict.method.set_description(` Set the path to a whitelisted function that will return the number on the card in the format: @@ -122,7 +121,7 @@ frappe.ui.form.on('Number Card', { "value": value, "fieldtype": "Currency" } -`) +`); } }, diff --git a/frappe/public/js/frappe/views/reports/query_report.js b/frappe/public/js/frappe/views/reports/query_report.js index e2020e8690..40ebd1c497 100644 --- a/frappe/public/js/frappe/views/reports/query_report.js +++ b/frappe/public/js/frappe/views/reports/query_report.js @@ -133,7 +133,7 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { ]); } - add_card_button_to_toolbar(show) { + add_card_button_to_toolbar() { this.page.add_inner_button(__("Create Card"), () => { this.add_card_to_dashboard(); }); @@ -609,12 +609,11 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { } this.render_datatable(); this.add_chart_buttons_to_toolbar(true); - this.add_card_button_to_toolbar(true); + this.add_card_button_to_toolbar(); } else { this.data = []; this.toggle_nothing_to_show(true); this.add_chart_buttons_to_toolbar(false); - this.add_card_button_to_toolbar(false); } this.show_footer_message(); From 6f83cece05fd98cf9ddf8de2a8c9f17b8cf91f31 Mon Sep 17 00:00:00 2001 From: prssanna Date: Sat, 18 Jul 2020 17:09:23 +0530 Subject: [PATCH 29/29] fix: remove allow_worflow from number_card.json --- frappe/desk/doctype/number_card/number_card.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/frappe/desk/doctype/number_card/number_card.json b/frappe/desk/doctype/number_card/number_card.json index e94a06dab8..333885a7f9 100644 --- a/frappe/desk/doctype/number_card/number_card.json +++ b/frappe/desk/doctype/number_card/number_card.json @@ -1,7 +1,6 @@ { "actions": [], "allow_rename": 1, - "allow_workflow": 1, "creation": "2020-04-15 18:06:39.444683", "doctype": "DocType", "editable_grid": 1, @@ -191,7 +190,7 @@ } ], "links": [], - "modified": "2020-07-17 18:04:00.814756", + "modified": "2020-07-18 17:08:22.882538", "modified_by": "Administrator", "module": "Desk", "name": "Number Card",