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] = {
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/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,
}
};
});
diff --git a/frappe/desk/doctype/dashboard_chart/dashboard_chart.js b/frappe/desk/doctype/dashboard_chart/dashboard_chart.js
index 6f071a6e2b..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',
{
@@ -193,7 +196,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 +438,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..d5a743818a 100644
--- a/frappe/desk/doctype/number_card/number_card.js
+++ b/frappe/desk/doctype/number_card/number_card.js
@@ -9,8 +9,65 @@ 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');
- frm.trigger('render_filters_table');
- frm.trigger('render_dynamic_filters_table');
+
+ if (!frm.doc.type) {
+ frm.set_value('type', 'Document Type');
+ }
+
+ if (frm.doc.type == 'Report' && frm.doc.report_name) {
+ 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('set_method_description');
+ frm.trigger('render_filters_table');
+ }
+ 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) {
@@ -21,16 +78,81 @@ 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) {
- 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) {
+ 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
+}]
+
`);
}
},
+ set_method_description: function(frm) {
+ 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') {
+ frm.set_query('report_name', () => {
+ return {
+ filters: {
+ 'report_type': ['!=', 'Report Builder']
+ }
+ };
+ });
+ }
+
+ },
+
+ report_name: function(frm) {
+ frm.set_value('filters_json', '{}');
+ frm.set_value('dynamic_filters_json', '{}');
+ frm.set_df_property('report_field', 'options', []);
+ frm.trigger('set_report_filters');
+ },
+
+ 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 {
@@ -46,6 +168,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 +190,60 @@ 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');
}
},
+ 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 (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 = 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',
+ {
+ report_name: frm.doc.report_name,
+ filters: filters,
+ ignore_prepared_report: 1
+ }
+ ).then(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 +254,211 @@ 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) {
+ 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) filters_set = true;
+ }
+ });
+ }
+
+ 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) {
+ if (!frappe.boot.developer_mode || !frm.doc.is_standard || frm.doc.type == 'Custom') {
+ return;
+ }
-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]} |
-
`;
+ frm.set_df_property("dynamic_filters_section", "hidden", 0);
+ 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/desk/doctype/number_card/number_card.json b/frappe/desk/doctype/number_card/number_card.json
index 41362a8982..333885a7f9 100644
--- a/frappe/desk/doctype/number_card/number_card.json
+++ b/frappe/desk/doctype/number_card/number_card.json
@@ -9,11 +9,18 @@
"is_standard",
"module",
"label",
+ "type",
+ "report_name",
+ "method",
"function",
"aggregate_function_based_on",
"column_break_2",
"document_type",
+ "report_field",
+ "report_function",
"is_public",
+ "custom_configuration_section",
+ "filters_config",
"stats_section",
"show_percentage_stats",
"stats_time_interval",
@@ -26,20 +33,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 +106,7 @@
"options": "Daily\nWeekly\nMonthly\nYearly"
},
{
+ "depends_on": "eval: doc.type == 'Document Type'",
"fieldname": "stats_section",
"fieldtype": "Section Break",
"label": "Stats"
@@ -114,34 +123,74 @@
"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\nCustom"
+ },
+ {
+ "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'"
+ },
+ {
+ "depends_on": "eval: doc.type == 'Custom'",
+ "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"
+ },
+ {
+ "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-10 17:55:35.873222",
+ "modified": "2020-07-18 17:08:22.882538",
"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..5b52b60474 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,22 @@ def get_cards_for_user(doctype, txt, searchfield, start, page_len, filters):
search_conditions=search_conditions,
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)
+
+ 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/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/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/public/js/frappe/form/quick_entry.js b/frappe/public/js/frappe/form/quick_entry.js
index 68444c8a3b..2da7b8f236 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;
@@ -282,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]);
}
});
diff --git a/frappe/public/js/frappe/utils/dashboard_utils.js b/frappe/public/js/frappe/utils/dashboard_utils.js
index f737c6ad12..8618f6dd59 100644
--- a/frappe/public/js/frappe/utils/dashboard_utils.js
+++ b/frappe/public/js/frappe/utils/dashboard_utils.js
@@ -119,6 +119,86 @@ 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;
+ },
+
+ 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/views/reports/query_report.js b/frappe/public/js/frappe/views/reports/query_report.js
index f82956adac..40ebd1c497 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(true),
]);
}
+ 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]));
});
}
@@ -518,6 +609,7 @@ 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();
} else {
this.data = [];
this.toggle_nothing_to_show(true);
@@ -700,7 +792,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 = [];
diff --git a/frappe/public/js/frappe/views/reports/report_utils.js b/frappe/public/js/frappe/views/reports/report_utils.js
index 7b1205482f..158dbd653b 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]);
@@ -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/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 77cb8a59c2..220394919c 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) {
@@ -59,20 +59,39 @@ 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);
});
}
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,
@@ -84,11 +103,53 @@ 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: this.filters
+ },
+ get_number: res => this.get_number_for_custom_card(res),
+ },
+ 'Report': {
+ method: 'frappe.desk.query_report.run',
+ args: {
+ 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];
+ }
+
+ get_filters() {
+ const filters = frappe.dashboard_utils.get_all_filters(this.card_doc);
+ return filters;
+ }
+
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,43 +163,69 @@ 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();
- });
- } else {
- this.number_html = res;
- }
+ return frappe.xcall(this.settings.method, this.settings.args).then(res => {
+ this.settings.get_number(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_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]);
+ 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;
+ this.formatted_number = formatted_number + ' ' + symbol;
}
render_number() {
return this.get_number().then(() => {
$(this.body).html(`
-
${this.number_html}
+
${this.formatted_number}
`);
});
}
render_stats() {
- let caret_html ='';
+ if (this.card_doc.type !== 'Document Type') {
+ return;
+ }
+
+ let caret_html = '';
let color_class = '';
return this.get_percentage_stats().then(() => {
@@ -177,6 +264,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) {
diff --git a/frappe/public/js/frappe/widgets/utils.js b/frappe/public/js/frappe/widgets/utils.js
index dff4db807e..22b3167977 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);
@@ -167,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
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"},
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')
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
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:
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