From 2e0abf4bb2c6bdcfd260239382efdd83a3512bef Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Tue, 7 Apr 2020 14:54:53 +0530 Subject: [PATCH 01/76] fix: setup invalid for grid forms Co-authored-by: Prssanna Desai --- frappe/public/js/frappe/form/controls/base_input.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/frappe/public/js/frappe/form/controls/base_input.js b/frappe/public/js/frappe/form/controls/base_input.js index 0dbaaeb63c..685aac90a3 100644 --- a/frappe/public/js/frappe/form/controls/base_input.js +++ b/frappe/public/js/frappe/form/controls/base_input.js @@ -180,7 +180,12 @@ frappe.ui.form.ControlInput = frappe.ui.form.Control.extend({ this.$wrapper.toggleClass("has-error", (this.df.reqd && is_null(value)) ? true : false); }, set_invalid: function () { - this.$wrapper.toggleClass("has-error", (this.df.invalid ? true : false)); + if(this.$input) { + this.$wrapper.toggleClass("has-error", (this.df.invalid ? true : false)); + } + if(this.disp_area) { + $(this.disp_area).toggleClass("has-error", (this.df.invalid ? true : false)); + } }, set_bold: function() { if(this.$input) { From a23ba1fc56d4ee98bb0aa3a4025926a9eaaed299 Mon Sep 17 00:00:00 2001 From: prssanna Date: Tue, 7 Apr 2020 17:52:10 +0530 Subject: [PATCH 02/76] feat: data validation for child table --- .../public/js/frappe/form/controls/base_control.js | 6 ++++-- frappe/public/js/frappe/form/controls/base_input.js | 12 +++++++----- frappe/public/js/frappe/form/grid_row.js | 5 ++++- frappe/public/less/form_grid.less | 8 ++++++++ 4 files changed, 23 insertions(+), 8 deletions(-) diff --git a/frappe/public/js/frappe/form/controls/base_control.js b/frappe/public/js/frappe/form/controls/base_control.js index c1ba41ab16..41e06537e1 100644 --- a/frappe/public/js/frappe/form/controls/base_control.js +++ b/frappe/public/js/frappe/form/controls/base_control.js @@ -152,12 +152,14 @@ frappe.ui.form.Control = Class.extend({ () => me.set_model_value(value), () => { me.set_mandatory && me.set_mandatory(value); - me.set_invalid && me.set_invalid(); if(me.df.change || me.df.onchange) { // onchange event specified in df - return (me.df.change || me.df.onchange).apply(me, [e]); + let set = (me.df.change || me.df.onchange).apply(me, [e]); + me.set_invalid && me.set_invalid(); + return set; } + me.set_invalid && me.set_invalid(); } ]); }; diff --git a/frappe/public/js/frappe/form/controls/base_input.js b/frappe/public/js/frappe/form/controls/base_input.js index 685aac90a3..f3f04ec4d8 100644 --- a/frappe/public/js/frappe/form/controls/base_input.js +++ b/frappe/public/js/frappe/form/controls/base_input.js @@ -180,11 +180,13 @@ frappe.ui.form.ControlInput = frappe.ui.form.Control.extend({ this.$wrapper.toggleClass("has-error", (this.df.reqd && is_null(value)) ? true : false); }, set_invalid: function () { - if(this.$input) { - this.$wrapper.toggleClass("has-error", (this.df.invalid ? true : false)); - } - if(this.disp_area) { - $(this.disp_area).toggleClass("has-error", (this.df.invalid ? true : false)); + let invalid = !!this.df.invalid; + if (this.grid) { + this.$wrapper.parents('.grid-static-col').toggleClass('invalid', invalid); + this.$input.toggleClass('invalid', invalid); + this.grid_row.columns[this.df.fieldname].is_invalid = invalid; + } else { + this.$wrapper.toggleClass('has-error', invalid); } }, set_bold: function() { diff --git a/frappe/public/js/frappe/form/grid_row.js b/frappe/public/js/frappe/form/grid_row.js index 0e36e671cc..1fd0fcce24 100644 --- a/frappe/public/js/frappe/form/grid_row.js +++ b/frappe/public/js/frappe/form/grid_row.js @@ -268,7 +268,10 @@ export default class GridRow { if(df.reqd && !txt) { column.addClass('error'); } - if (df.reqd || df.bold) { + if (column.is_invalid) { + column.addClass('invalid'); + } + else if ((df.reqd || df.bold)) { column.addClass('bold'); } } diff --git a/frappe/public/less/form_grid.less b/frappe/public/less/form_grid.less index 28f08635ba..102719308c 100644 --- a/frappe/public/less/form_grid.less +++ b/frappe/public/less/form_grid.less @@ -80,6 +80,10 @@ background-color: @extra-light-yellow; } +.editable-form .grid-static-col.invalid { + background-color: @label-danger-bg; +} + .validated-form .grid-static-col.error { background-color: @label-danger-bg; } @@ -150,6 +154,10 @@ } } + input.form-control.invalid { + background-color: @label-danger-bg; + } + input[data-fieldtype="Int"], input[data-fieldtype="Float"], input[data-fieldtype="Currency"] { text-align: right; } From 23b3f65b3bae0c04177c9a670e8f0af5d06be13a Mon Sep 17 00:00:00 2001 From: "Chinmay D. Pai" Date: Thu, 16 Apr 2020 14:09:59 +0530 Subject: [PATCH 03/76] fix: limit file upload mimetype if user has no desk access limits file upload mimetype to jpg, png, and pdf in case the user does not have desk access, to prevent abuse of the servers as a file storage system Signed-off-by: Chinmay D. Pai --- frappe/handler.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/frappe/handler.py b/frappe/handler.py index 6e0bf7a6be..d3fec3cf8f 100755 --- a/frappe/handler.py +++ b/frappe/handler.py @@ -148,12 +148,14 @@ def uploadfile(): @frappe.whitelist(allow_guest=True) def upload_file(): + user = None if frappe.session.user == 'Guest': if frappe.get_system_settings('allow_guests_to_upload_files'): ignore_permissions = True else: return else: + user = frappe.get_doc("User", frappe.session.user) ignore_permissions = False files = frappe.request.files @@ -175,7 +177,7 @@ def upload_file(): frappe.local.uploaded_file = content frappe.local.uploaded_filename = filename - if frappe.session.user == 'Guest': + if frappe.session.user == 'Guest' or (user and not user.has_desk_access()): import mimetypes filetype = mimetypes.guess_type(filename)[0] if filetype not in ['image/png', 'image/jpeg', 'application/pdf']: From d7e9ef60b87ba8c947d2ce89f3c645691dd95bc4 Mon Sep 17 00:00:00 2001 From: "Chinmay D. Pai" Date: Thu, 16 Apr 2020 14:31:25 +0530 Subject: [PATCH 04/76] fix: add support for more document mimetypes Signed-off-by: Chinmay D. Pai --- frappe/handler.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/frappe/handler.py b/frappe/handler.py index d3fec3cf8f..9699ff6d44 100755 --- a/frappe/handler.py +++ b/frappe/handler.py @@ -14,6 +14,12 @@ from frappe.core.doctype.server_script.server_script_utils import run_server_scr from werkzeug.wrappers import Response from six import string_types +ALLOWED_MIMETYPES = ('image/png', 'image/jpeg', 'application/pdf', 'application/msword', + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + 'application/vnd.ms-excel', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + 'application/vnd.oasis.opendocument.text', 'application/vnd.oasis.opendocument.spreadsheet') + + def handle(): """handle request""" validate_auth() @@ -180,8 +186,8 @@ def upload_file(): if frappe.session.user == 'Guest' or (user and not user.has_desk_access()): import mimetypes filetype = mimetypes.guess_type(filename)[0] - if filetype not in ['image/png', 'image/jpeg', 'application/pdf']: - frappe.throw("You can only upload JPG, PNG or PDF files.") + if filetype not in ALLOWED_MIMETYPES: + frappe.throw("You can only upload JPG, PNG, PDF, or Microsoft documents.") if method: method = frappe.get_attr(method) From a877fba5d938876e1484c9a63ab53f003adc50e5 Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Fri, 17 Apr 2020 17:44:41 +0530 Subject: [PATCH 05/76] test: set_invalid controller for child tables --- cypress/integration/form.js | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/cypress/integration/form.js b/cypress/integration/form.js index 9d1210ca2b..a622a66e13 100644 --- a/cypress/integration/form.js +++ b/cypress/integration/form.js @@ -41,4 +41,21 @@ context('Form', () => { list_view.filter_area.filter_list.clear_filters(); }); }); + it('validates behaviour of Data options validations in child table', () => { + // test email validations for set_invalid controller + let website_input = 'website.in'; + let expectBackgroundColor = 'rgb(255, 220, 220)'; + + cy.visit('/desk#Form/Contact/New Contact 1'); + cy.get('.frappe-control[data-fieldname="email_ids"]').as('table'); + cy.get('@table').find('button.grid-add-row').click(); + cy.get('.grid-body .rows [data-fieldname="email_id"]').click(); + cy.get('@table').find('input.input-with-feedback.form-control').as('email_input'); + cy.get('@email_input').type(website_input, { waitForAnimations: false }); + cy.fill_field('company_name', 'Test Company'); + cy.get('@email_input').should($div => { + const style = window.getComputedStyle($div[0]); + expect(style.backgroundColor).to.equal(expectBackgroundColor); + }); + }); }); From 06ccba048fa25b22f4e89325852dd04f4945310c Mon Sep 17 00:00:00 2001 From: gavin Date: Fri, 17 Apr 2020 17:53:59 +0530 Subject: [PATCH 06/76] style: block style fixes (via slider) --- frappe/public/js/frappe/form/grid_row.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/frappe/public/js/frappe/form/grid_row.js b/frappe/public/js/frappe/form/grid_row.js index 65c11b43bf..0b8e7e8d23 100644 --- a/frappe/public/js/frappe/form/grid_row.js +++ b/frappe/public/js/frappe/form/grid_row.js @@ -270,8 +270,7 @@ export default class GridRow { } if (column.is_invalid) { column.addClass('invalid'); - } - else if ((df.reqd || df.bold)) { + } else if (df.reqd || df.bold) { column.addClass('bold'); } } From b66864b9b13631245c16cf3c803cc4cf57e2dff5 Mon Sep 17 00:00:00 2001 From: Saurabh Date: Thu, 23 Apr 2020 18:31:53 +0530 Subject: [PATCH 07/76] fix: check message id communication while linking communications --- .../doctype/email_account/email_account.py | 47 ++++++++++--------- 1 file changed, 26 insertions(+), 21 deletions(-) diff --git a/frappe/email/doctype/email_account/email_account.py b/frappe/email/doctype/email_account/email_account.py index c0a198f5e5..88a0980bee 100755 --- a/frappe/email/doctype/email_account/email_account.py +++ b/frappe/email/doctype/email_account/email_account.py @@ -533,28 +533,33 @@ class EmailAccount(Document): parent = None in_reply_to = (email.mail.get("In-Reply-To") or "").strip(" <>") - if in_reply_to and "@{0}".format(frappe.local.site) in in_reply_to: - # reply to a communication sent from the system - email_queue = frappe.db.get_value('Email Queue', dict(message_id=in_reply_to), ['communication','reference_doctype', 'reference_name']) - if email_queue: - parent_communication, parent_doctype, parent_name = email_queue - if parent_communication: - communication.in_reply_to = parent_communication + if in_reply_to: + if "@{0}".format(frappe.local.site) in in_reply_to: + # reply to a communication sent from the system + email_queue = frappe.db.get_value('Email Queue', dict(message_id=in_reply_to), ['communication','reference_doctype', 'reference_name']) + if email_queue: + parent_communication, parent_doctype, parent_name = email_queue + if parent_communication: + communication.in_reply_to = parent_communication + else: + reference, domain = in_reply_to.split("@", 1) + parent_doctype, parent_name = 'Communication', reference + + if frappe.db.exists(parent_doctype, parent_name): + parent = frappe._dict(doctype=parent_doctype, name=parent_name) + + # set in_reply_to of current communication + if parent_doctype=='Communication': + # communication.in_reply_to = email_queue.communication + + if parent.reference_name: + # the true parent is the communication parent + parent = frappe.get_doc(parent.reference_doctype, + parent.reference_name) else: - reference, domain = in_reply_to.split("@", 1) - parent_doctype, parent_name = 'Communication', reference - - if frappe.db.exists(parent_doctype, parent_name): - parent = frappe._dict(doctype=parent_doctype, name=parent_name) - - # set in_reply_to of current communication - if parent_doctype=='Communication': - # communication.in_reply_to = email_queue.communication - - if parent.reference_name: - # the true parent is the communication parent - parent = frappe.get_doc(parent.reference_doctype, - parent.reference_name) + comm = frappe.db.get_value('Communication', dict(message_id=in_reply_to), ['reference_doctype', 'reference_name'], as_dict=1) + if comm: + parent = frappe._dict(doctype=comm.reference_doctype, name=comm.reference_name) return parent From a9be0758d4517d2b1b042ddfeefdc71ff75b979c Mon Sep 17 00:00:00 2001 From: Saurabh Date: Thu, 23 Apr 2020 18:46:15 +0530 Subject: [PATCH 08/76] feat: compare meesage-id only with the communications created between last 30 days --- frappe/email/doctype/email_account/email_account.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/frappe/email/doctype/email_account/email_account.py b/frappe/email/doctype/email_account/email_account.py index 88a0980bee..082b16c17a 100755 --- a/frappe/email/doctype/email_account/email_account.py +++ b/frappe/email/doctype/email_account/email_account.py @@ -10,7 +10,7 @@ import socket import time from frappe import _ from frappe.model.document import Document -from frappe.utils import validate_email_address, cint, get_datetime, DATE_FORMAT, strip, comma_or, sanitize_html +from frappe.utils import validate_email_address, cint, get_datetime, DATE_FORMAT, strip, comma_or, sanitize_html, add_days from frappe.utils.user import is_system_user from frappe.utils.jinja import render_template from frappe.email.smtp import SMTPServer @@ -557,7 +557,11 @@ class EmailAccount(Document): parent = frappe.get_doc(parent.reference_doctype, parent.reference_name) else: - comm = frappe.db.get_value('Communication', dict(message_id=in_reply_to), ['reference_doctype', 'reference_name'], as_dict=1) + comm = frappe.db.get_value('Communication', + dict( + message_id=in_reply_to, + creation=['>=', add_days(get_datetime(), -30)]), + ['reference_doctype', 'reference_name'], as_dict=1) if comm: parent = frappe._dict(doctype=comm.reference_doctype, name=comm.reference_name) From e7a163168b3f463d3ff69f9a906fc78f59b7b8c4 Mon Sep 17 00:00:00 2001 From: prssanna Date: Sat, 18 Apr 2020 15:59:02 +0530 Subject: [PATCH 09/76] feat: Dashboard view for doctypes --- frappe/desk/doctype/dashboard/dashboard.js | 8 + .../dashboard_chart/dashboard_chart.js | 7 + .../dashboard_chart/dashboard_chart.json | 13 +- .../dashboard_chart/dashboard_chart.py | 22 +- frappe/desk/doctype/number_card/__init__.py | 0 .../desk/doctype/number_card/number_card.js | 120 +++++ .../desk/doctype/number_card/number_card.json | 124 +++++ .../desk/doctype/number_card/number_card.py | 48 ++ .../doctype/number_card/test_number_card.py | 10 + frappe/public/build.json | 2 + frappe/public/js/frappe/form/layout.js | 11 +- frappe/public/js/frappe/list/base_list.js | 2 +- .../public/js/frappe/list/list_sidebar.html | 2 + .../frappe/views/dashboard/dashboard_view.js | 453 ++++++++++++++++++ .../public/js/frappe/widgets/base_widget.js | 28 +- .../public/js/frappe/widgets/chart_widget.js | 78 +-- frappe/public/js/frappe/widgets/new_widget.js | 14 +- .../js/frappe/widgets/number_card_widget.js | 143 ++++++ frappe/public/js/frappe/widgets/utils.js | 12 +- .../public/js/frappe/widgets/widget_dialog.js | 187 +++++++- .../public/js/frappe/widgets/widget_group.js | 13 + frappe/public/less/dashboard_view.less | 40 ++ frappe/public/less/desktop.less | 50 ++ 23 files changed, 1301 insertions(+), 86 deletions(-) create mode 100644 frappe/desk/doctype/number_card/__init__.py create mode 100644 frappe/desk/doctype/number_card/number_card.js create mode 100644 frappe/desk/doctype/number_card/number_card.json create mode 100644 frappe/desk/doctype/number_card/number_card.py create mode 100644 frappe/desk/doctype/number_card/test_number_card.py create mode 100644 frappe/public/js/frappe/views/dashboard/dashboard_view.js create mode 100644 frappe/public/js/frappe/widgets/number_card_widget.js create mode 100644 frappe/public/less/dashboard_view.less diff --git a/frappe/desk/doctype/dashboard/dashboard.js b/frappe/desk/doctype/dashboard/dashboard.js index 19ce8eb1fd..dc2991a346 100644 --- a/frappe/desk/doctype/dashboard/dashboard.js +++ b/frappe/desk/doctype/dashboard/dashboard.js @@ -4,5 +4,13 @@ frappe.ui.form.on('Dashboard', { refresh: function(frm) { frm.add_custom_button(__("Show Dashboard"), () => frappe.set_route('dashboard', frm.doc.name)); + + frm.set_query("chart", "charts", function(doc, cdt, cdn) { + return { + filters: { + is_standard: 1 + } + }; + }); } }); diff --git a/frappe/desk/doctype/dashboard_chart/dashboard_chart.js b/frappe/desk/doctype/dashboard_chart/dashboard_chart.js index 275028fc15..b869f60ce5 100644 --- a/frappe/desk/doctype/dashboard_chart/dashboard_chart.js +++ b/frappe/desk/doctype/dashboard_chart/dashboard_chart.js @@ -9,6 +9,12 @@ frappe.ui.form.on('Dashboard Chart', { frm.add_fetch('source', 'timeseries', 'timeseries'); }, + before_save: function(frm) { + if (frm.is_new() && (frappe.user.has_role('System Manager') || frappe.user.has_role('Dashboard Manager'))) { + frm.doc.is_standard = 1; + } + }, + refresh: function(frm) { frm.chart_filters = null; frm.add_custom_button('Add Chart to Dashboard', () => { @@ -348,6 +354,7 @@ frappe.ui.form.on('Dashboard Chart', { on_change: () => {}, }); + console.log('filters', filters); frm.filter_group.add_filters_to_filter_group(filters); } diff --git a/frappe/desk/doctype/dashboard_chart/dashboard_chart.json b/frappe/desk/doctype/dashboard_chart/dashboard_chart.json index cd32292783..7530d70fdc 100644 --- a/frappe/desk/doctype/dashboard_chart/dashboard_chart.json +++ b/frappe/desk/doctype/dashboard_chart/dashboard_chart.json @@ -35,7 +35,8 @@ "color", "custom_options", "section_break_10", - "last_synced_on" + "last_synced_on", + "is_standard" ], "fields": [ { @@ -220,10 +221,18 @@ "fieldname": "custom_options", "fieldtype": "Code", "label": "Custom Options" + }, + { + "default": "0", + "fieldname": "is_standard", + "fieldtype": "Check", + "hidden": 1, + "label": "Is Standard", + "permlevel": 1 } ], "links": [], - "modified": "2020-04-20 23:49:11.389909", + "modified": "2020-04-17 18:11:33.486037", "modified_by": "Administrator", "module": "Desk", "name": "Dashboard Chart", diff --git a/frappe/desk/doctype/dashboard_chart/dashboard_chart.py b/frappe/desk/doctype/dashboard_chart/dashboard_chart.py index 36a75bd9d5..8242e3752d 100644 --- a/frappe/desk/doctype/dashboard_chart/dashboard_chart.py +++ b/frappe/desk/doctype/dashboard_chart/dashboard_chart.py @@ -92,20 +92,28 @@ def get(chart_name = None, chart = None, no_cache = None, filters = None, from_d return chart_config @frappe.whitelist() -def create_report_chart(args): +def create_dashboard_chart(args): args = frappe.parse_json(args) - _doc = frappe.new_doc('Dashboard Chart') + doc = frappe.new_doc('Dashboard Chart') + roles = frappe.get_roles(frappe.session.user) + if 'Sytem Manager' in roles or 'Dashboard Manager' in roles: + doc.is_standard = 1 - _doc.update(args) + doc.update(args) - if (args.get("custom_options")): - _doc.custom_options = json.dumps(args.get("custom_options")) + if args.get('custom_options'): + doc.custom_options = json.dumps(args.get('custom_options')) if frappe.db.exists('Dashboard Chart', args.chart_name): args.chart_name = append_number_if_name_exists('Dashboard Chart', args.chart_name) - _doc.chart_name = args.chart_name - _doc.insert(ignore_permissions=True) + doc.chart_name = args.chart_name + doc.insert(ignore_permissions=True) + return doc + +@frappe.whitelist() +def create_report_chart(args): + create_dashboard_chart() if args.dashboard: add_chart_to_dashboard(json.dumps(args)) diff --git a/frappe/desk/doctype/number_card/__init__.py b/frappe/desk/doctype/number_card/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/desk/doctype/number_card/number_card.js b/frappe/desk/doctype/number_card/number_card.js new file mode 100644 index 0000000000..2bf99b9107 --- /dev/null +++ b/frappe/desk/doctype/number_card/number_card.js @@ -0,0 +1,120 @@ +// Copyright (c) 2020, Frappe Technologies and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Number Card', { + refresh: function(frm) { + frm.set_df_property("filters_section", "hidden", 1); + frm.trigger('set_options'); + frm.trigger('render_filters_table'); + }, + + before_save: function(frm) { + if (frm.is_new() && (frappe.user.has_role('System Manager') || frappe.user.has_role('Dashboard Manager'))) { + frm.doc.is_standard = 1; + } + }, + + document_type: function(frm) { + frm.set_query('document_type', function() { + return { + filters: { + 'issingle': false + } + } + }); + frm.set_value('filters_json', '[]'); + frm.set_value('aggregate_function_based_on', ''); + if (frm.doc.document_type) { + frm.trigger('set_options'); + } + }, + + set_options: function(frm) { + let aggregate_based_on_fields = []; + const doctype = frm.doc.document_type; + + frappe.model.with_doctype(doctype, () => { + frappe.get_meta(doctype).fields.map(df => { + if (frappe.model.numeric_fieldtypes.includes(df.fieldtype)) { + aggregate_based_on_fields.push({label: df.label, value: df.fieldname}); + } + }); + + frm.set_df_property('aggregate_function_based_on', 'options', aggregate_based_on_fields); + }); + }, + + render_filters_table: function(frm) { + frm.set_df_property("filters_section", "hidden", 0); + + let wrapper = $(frm.get_field('filters_json').wrapper).empty(); + frm.filter_table = $(` + + + + + + + + +
${__('Filter')}${__('Condition')}${__('Value')}
`).appendTo(wrapper); + + frm.filters = JSON.parse(frm.doc.filters_json || '[]'); + + frm.trigger('set_filters_in_table'); + + frm.filter_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)); + frm.trigger('set_filters_in_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); + }); + + }, + + set_filters_in_table: function(frm) { + if (!frm.filters.length) { + const filter_row = $(` + ${__("Click to Set Filters")}`); + frm.filter_table.find('tbody').html(filter_row); + } else { + let filter_rows = ''; + frm.filters.forEach(filter => { + filter_rows += + ` + ${filter[1]} + ${filter[2] || ""} + ${filter[3]} + `; + + }); + frm.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 new file mode 100644 index 0000000000..222e4ed942 --- /dev/null +++ b/frappe/desk/doctype/number_card/number_card.json @@ -0,0 +1,124 @@ +{ + "actions": [], + "autoname": "CARD.#####", + "creation": "2020-04-15 18:06:39.444683", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "label", + "function", + "column_break_2", + "document_type", + "aggregate_function_based_on", + "filters_section", + "filters_json", + "color", + "is_standard" + ], + "fields": [ + { + "fieldname": "document_type", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Document Type", + "options": "DocType", + "reqd": 1 + }, + { + "depends_on": "eval: doc.document_type", + "fieldname": "function", + "fieldtype": "Select", + "label": "Function", + "options": "Count\nSum\nAverage\nMinimum\nMaximum", + "reqd": 1 + }, + { + "depends_on": "eval: doc.function !== 'Count'", + "fieldname": "aggregate_function_based_on", + "fieldtype": "Select", + "label": "Aggregate Function Based On", + "mandatory_depends_on": "eval: doc.function !== 'Count'" + }, + { + "fieldname": "filters_json", + "fieldtype": "Code", + "label": "Filters JSON", + "options": "JSON" + }, + { + "fieldname": "label", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Label", + "reqd": 1 + }, + { + "fieldname": "color", + "fieldtype": "Color", + "label": "Color" + }, + { + "default": "0", + "fieldname": "is_standard", + "fieldtype": "Check", + "hidden": 1, + "label": "Is Standard", + "permlevel": 1 + }, + { + "fieldname": "column_break_2", + "fieldtype": "Column Break" + }, + { + "fieldname": "filters_section", + "fieldtype": "Section Break", + "label": "Filters Section" + } + ], + "links": [], + "modified": "2020-04-18 14:12:06.949445", + "modified_by": "Administrator", + "module": "Desk", + "name": "Number Card", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Dashboard Manager", + "share": 1, + "write": 1 + }, + { + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "All", + "share": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "title_field": "label", + "track_changes": 1 +} \ No newline at end of file diff --git a/frappe/desk/doctype/number_card/number_card.py b/frappe/desk/doctype/number_card/number_card.py new file mode 100644 index 0000000000..509995392c --- /dev/null +++ b/frappe/desk/doctype/number_card/number_card.py @@ -0,0 +1,48 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +from frappe.model.document import Document +from frappe.model.naming import append_number_if_name_exists + +class NumberCard(Document): + pass + + +@frappe.whitelist() +def get_result(doc): + doc = frappe.parse_json(doc) + fields = [] + sql_function_map = { + 'Count': 'count', + 'Sum': 'sum', + 'Average': 'avg', + 'Minimum': 'min', + 'Maximum': 'max' + } + + function = sql_function_map[doc.function] + + if function == 'count': + fields = ['{function}(*) as result'.format(function=function)] + else: + fields = ['{function}({based_on}) as result'.format(function=function, based_on=doc.aggregate_function_based_on)] + + filters = frappe.parse_json(doc.filters_json) + number = frappe.db.get_all(doc.document_type, fields = fields, filters = filters)[0]['result'] + number = round(number, 2) if isinstance(number, float) else number + + return number + +@frappe.whitelist() +def create_number_card(args): + args = frappe.parse_json(args) + doc = frappe.new_doc('Number Card') + roles = frappe.get_roles(frappe.session.user) + if 'Sytem Manager' in roles or 'Dashboard Manager' in roles: + doc.is_standard = 1 + doc.update(args) + doc.insert(ignore_permissions=True) + return doc \ No newline at end of file diff --git a/frappe/desk/doctype/number_card/test_number_card.py b/frappe/desk/doctype/number_card/test_number_card.py new file mode 100644 index 0000000000..4aa1ecf282 --- /dev/null +++ b/frappe/desk/doctype/number_card/test_number_card.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies and Contributors +# See license.txt +from __future__ import unicode_literals + +# import frappe +import unittest + +class TestNumberCard(unittest.TestCase): + pass diff --git a/frappe/public/build.json b/frappe/public/build.json index 7f55924a6b..df3d71f537 100755 --- a/frappe/public/build.json +++ b/frappe/public/build.json @@ -104,6 +104,7 @@ "public/less/form.less", "public/less/mobile.less", "public/less/kanban.less", + "public/less/dashboard_view.less", "public/less/controls.less", "public/less/chat.less", "public/less/filters.less", @@ -296,6 +297,7 @@ "public/js/frappe/views/gantt/gantt_view.js", "public/js/frappe/views/calendar/calendar.js", + "public/js/frappe/views/dashboard/dashboard_view.js", "public/js/frappe/views/image/image_view.js", "public/js/frappe/views/kanban/kanban_view.js", "public/js/frappe/views/inbox/inbox_view.js", diff --git a/frappe/public/js/frappe/form/layout.js b/frappe/public/js/frappe/form/layout.js index 8a88ee0c0d..5aeb29b1ed 100644 --- a/frappe/public/js/frappe/form/layout.js +++ b/frappe/public/js/frappe/form/layout.js @@ -498,11 +498,18 @@ frappe.ui.form.Layout = Class.extend({ }, set_dependant_property: function(condition, fieldname, property) { let set_property = this.evaluate_depends_on_value(condition); + let form_obj; + if (this.frm) { + form_obj = this.frm; + } else if (this.is_dialog) { + form_obj = this; + } + if (form_obj) { if (set_property) { - this.frm.set_df_property(fieldname, property, 1); + form_obj.set_df_property(fieldname, property, 1); } else { - this.frm.set_df_property(fieldname, property, 0); + form_obj.set_df_property(fieldname, property, 0); } } }, diff --git a/frappe/public/js/frappe/list/base_list.js b/frappe/public/js/frappe/list/base_list.js index 0aae8b361f..76bda11f0a 100644 --- a/frappe/public/js/frappe/list/base_list.js +++ b/frappe/public/js/frappe/list/base_list.js @@ -686,5 +686,5 @@ class FilterArea { } // utility function to validate view modes -frappe.views.view_modes = ['List', 'Gantt', 'Kanban', 'Calendar', 'Image', 'Inbox', 'Report']; +frappe.views.view_modes = ['List', 'Gantt', 'Kanban', 'Calendar', 'Image', 'Inbox', 'Report', 'Dashboard']; frappe.views.is_valid = view_mode => frappe.views.view_modes.includes(view_mode); diff --git a/frappe/public/js/frappe/list/list_sidebar.html b/frappe/public/js/frappe/list/list_sidebar.html index 880a91cf81..37f0dafb96 100644 --- a/frappe/public/js/frappe/list/list_sidebar.html +++ b/frappe/public/js/frappe/list/list_sidebar.html @@ -26,6 +26,8 @@
  • +