From 10edae65249582a4f8cb0e1d85fa17035297b075 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Mon, 24 Jan 2022 17:21:15 +0530 Subject: [PATCH 01/15] feat: new autocomplete control --- frappe/core/doctype/docfield/docfield.json | 4 ++-- frappe/database/mariadb/database.py | 3 ++- frappe/model/__init__.py | 3 ++- frappe/public/js/frappe/form/controls/autocomplete.js | 11 +++++++++++ frappe/public/js/frappe/form/formatters.js | 3 +++ 5 files changed, 20 insertions(+), 4 deletions(-) diff --git a/frappe/core/doctype/docfield/docfield.json b/frappe/core/doctype/docfield/docfield.json index 26ddce7d35..69c8c3505f 100644 --- a/frappe/core/doctype/docfield/docfield.json +++ b/frappe/core/doctype/docfield/docfield.json @@ -98,7 +98,7 @@ "label": "Type", "oldfieldname": "fieldtype", "oldfieldtype": "Select", - "options": "Attach\nAttach Image\nBarcode\nButton\nCheck\nCode\nColor\nColumn Break\nCurrency\nData\nDate\nDatetime\nDuration\nDynamic Link\nFloat\nFold\nGeolocation\nHeading\nHTML\nHTML Editor\nIcon\nImage\nInt\nLink\nLong Text\nMarkdown Editor\nPassword\nPercent\nRead Only\nRating\nSection Break\nSelect\nSignature\nSmall Text\nTab Break\nTable\nTable MultiSelect\nText\nText Editor\nTime", + "options": "Autocomplete\nAttach\nAttach Image\nBarcode\nButton\nCheck\nCode\nColor\nColumn Break\nCurrency\nData\nDate\nDatetime\nDuration\nDynamic Link\nFloat\nFold\nGeolocation\nHeading\nHTML\nHTML Editor\nIcon\nImage\nInt\nLink\nLong Text\nMarkdown Editor\nPassword\nPercent\nRead Only\nRating\nSection Break\nSelect\nSignature\nSmall Text\nTab Break\nTable\nTable MultiSelect\nText\nText Editor\nTime", "reqd": 1, "search_index": 1 }, @@ -540,7 +540,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2022-01-03 11:56:19.812863", + "modified": "2022-01-04 11:56:19.812863", "modified_by": "Administrator", "module": "Core", "name": "DocField", diff --git a/frappe/database/mariadb/database.py b/frappe/database/mariadb/database.py index 6b827a4e89..325b97081b 100644 --- a/frappe/database/mariadb/database.py +++ b/frappe/database/mariadb/database.py @@ -52,7 +52,8 @@ class MariaDBDatabase(Database): 'Barcode': ('longtext', ''), 'Geolocation': ('longtext', ''), 'Duration': ('decimal', '21,9'), - 'Icon': ('varchar', self.VARCHAR_LEN) + 'Icon': ('varchar', self.VARCHAR_LEN), + 'Autocomplete': ('varchar', self.VARCHAR_LEN), } def get_connection(self): diff --git a/frappe/model/__init__.py b/frappe/model/__init__.py index b50a0304a5..232ffbe771 100644 --- a/frappe/model/__init__.py +++ b/frappe/model/__init__.py @@ -35,7 +35,8 @@ data_fieldtypes = ( 'Barcode', 'Geolocation', 'Duration', - 'Icon' + 'Icon', + 'Autocomplete' ) attachment_fieldtypes = ( diff --git a/frappe/public/js/frappe/form/controls/autocomplete.js b/frappe/public/js/frappe/form/controls/autocomplete.js index 1bc0ffeb8a..ce3f75178a 100644 --- a/frappe/public/js/frappe/form/controls/autocomplete.js +++ b/frappe/public/js/frappe/form/controls/autocomplete.js @@ -15,6 +15,17 @@ frappe.ui.form.ControlAutocomplete = class ControlAutoComplete extends frappe.ui } } + format_for_input(value) { + if (value == null) { + return ""; + } else if (this._data && this._data.length) { + const item = this._data.find(i => i.value == value); + return item ? item.label : value; + } else { + return value; + } + } + get_awesomplete_settings() { var me = this; return { diff --git a/frappe/public/js/frappe/form/formatters.js b/frappe/public/js/frappe/form/formatters.js index fd3fcb1bc7..291a467d97 100644 --- a/frappe/public/js/frappe/form/formatters.js +++ b/frappe/public/js/frappe/form/formatters.js @@ -21,6 +21,9 @@ frappe.form.formatters = { } return value==null ? "" : value; }, + Autocomplete: function(value) { + return __(frappe.form.formatters["Data"](value)); + }, Select: function(value) { return __(frappe.form.formatters["Data"](value)); }, From 26c88f6aad9240a0f4b558b1d219cd9a7641b5b7 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Mon, 24 Jan 2022 17:21:27 +0530 Subject: [PATCH 02/15] feat: set_query for autocomplete fields --- .../js/frappe/form/controls/autocomplete.js | 73 +++++++++++++++++-- 1 file changed, 67 insertions(+), 6 deletions(-) diff --git a/frappe/public/js/frappe/form/controls/autocomplete.js b/frappe/public/js/frappe/form/controls/autocomplete.js index ce3f75178a..37476efdcf 100644 --- a/frappe/public/js/frappe/form/controls/autocomplete.js +++ b/frappe/public/js/frappe/form/controls/autocomplete.js @@ -86,12 +86,10 @@ frappe.ui.form.ControlAutocomplete = class ControlAutoComplete extends frappe.ui .find('.awesomplete ul') .css('min-width', '100%'); - this.$input.on( - 'input', - frappe.utils.debounce(() => { - this.awesomplete.list = this.get_data(); - }, 500) - ); + this.$input.on('input', frappe.utils.debounce((e) => { + this.execute_query_if_exists(e.target.value); + this.awesomplete.list = this.get_data(); + }, 500)); this.$input.on('focus', () => { if (!this.$input.val()) { @@ -100,6 +98,17 @@ frappe.ui.form.ControlAutocomplete = class ControlAutoComplete extends frappe.ui } }); + this.$input.on("blur", () => { + if(this.selected) { + this.selected = false; + return; + } + var value = this.get_input_value(); + if(value!==this.last_value) { + this.parse_validate_and_set_in_model(value); + } + }); + this.$input.on("awesomplete-open", () => { this.autocomplete_open = true; }); @@ -138,6 +147,58 @@ frappe.ui.form.ControlAutocomplete = class ControlAutoComplete extends frappe.ui return options; } + execute_query_if_exists(term) { + const args = { + txt: term + }; + var set_nulls = function(obj) { + $.each(obj, function(key, value) { + if(value!==undefined) { + obj[key] = value; + } + }); + return obj; + }; + let get_query = this.get_query || this.df.get_query; + if(get_query) { + // get_query by function + var q = (get_query)(this.frm && this.frm.doc || this.doc, this.doctype, this.docname); + + if($.isPlainObject(q)) { + // returns a plain object with filters + if(q.filters) { + set_nulls(q.filters); + } + + // turn off value translation + if(q.translate_values !== undefined) { + this.translate_values = q.translate_values; + } + + // extend args for custom functions + $.extend(args, q); + + // add "filters" for standard query (search.py) + args.filters = q.filters; + } + } + + if (args.query) { + frappe.call({ + method: args.query, + args: args, + callback: ({ message }) => { + if(!this.$input.is(":focus")) { + return; + } + // this.$input.cache[this.df.fieldname][term] = r.results; + // this.awesomplete.list = this.$input.cache[this.df.fieldname][term]; + this.set_data(message); + } + }) + } + } + get_data() { return this._data || []; } From 512e3b3162635202d8670459961828df5d07307c Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Sat, 29 Jan 2022 19:08:42 +0530 Subject: [PATCH 03/15] feat: cache options & parsing set_query --- .../js/frappe/form/controls/autocomplete.js | 89 +++++++++++++------ 1 file changed, 63 insertions(+), 26 deletions(-) diff --git a/frappe/public/js/frappe/form/controls/autocomplete.js b/frappe/public/js/frappe/form/controls/autocomplete.js index 37476efdcf..3a2706b51d 100644 --- a/frappe/public/js/frappe/form/controls/autocomplete.js +++ b/frappe/public/js/frappe/form/controls/autocomplete.js @@ -76,6 +76,18 @@ frappe.ui.form.ControlAutocomplete = class ControlAutoComplete extends frappe.ui }; } + init_option_cache() { + if (!this.$input.cache) { + this.$input.cache = {}; + } + if (!this.$input.cache[this.doctype]) { + this.$input.cache[this.doctype] = {}; + } + if (!this.$input.cache[this.doctype][this.df.fieldname]) { + this.$input.cache[this.doctype][this.df.fieldname] = {}; + } + } + setup_awesomplete() { this.awesomplete = new Awesomplete( this.input, @@ -86,9 +98,17 @@ frappe.ui.form.ControlAutocomplete = class ControlAutoComplete extends frappe.ui .find('.awesomplete ul') .css('min-width', '100%'); + this.init_option_cache(); + this.$input.on('input', frappe.utils.debounce((e) => { - this.execute_query_if_exists(e.target.value); - this.awesomplete.list = this.get_data(); + const cached_options = this.$input.cache[this.doctype][this.df.fieldname][e.target.value]; + if (cached_options) { + this.set_data(cached_options); + } else if (this.get_query || this.df.get_query) { + this.execute_query_if_exists(e.target.value); + } else { + this.awesomplete.list = this.get_data(); + } }, 500)); this.$input.on('focus', () => { @@ -148,38 +168,56 @@ frappe.ui.form.ControlAutocomplete = class ControlAutoComplete extends frappe.ui } execute_query_if_exists(term) { - const args = { - txt: term - }; - var set_nulls = function(obj) { + const args = { txt: term }; + let get_query = this.get_query || this.df.get_query; + + if (!get_query) { + return; + } + + let set_nulls = function(obj) { $.each(obj, function(key, value) { - if(value!==undefined) { + if (value !== undefined) { obj[key] = value; } }); return obj; }; - let get_query = this.get_query || this.df.get_query; - if(get_query) { - // get_query by function - var q = (get_query)(this.frm && this.frm.doc || this.doc, this.doctype, this.docname); - if($.isPlainObject(q)) { - // returns a plain object with filters - if(q.filters) { - set_nulls(q.filters); - } + let process_query_object = function(obj) { + if (obj.query) { + args.query = obj.query; + } - // turn off value translation - if(q.translate_values !== undefined) { - this.translate_values = q.translate_values; - } + if (obj.params) { + set_nulls(obj.params); + Object.assign(args, obj.params); + } - // extend args for custom functions - $.extend(args, q); + // turn off value translation + if (obj.translate_values !== undefined) { + this.translate_values = obj.translate_values; + } + }; - // add "filters" for standard query (search.py) - args.filters = q.filters; + if ($.isPlainObject(get_query)) { + process_query_object(get_query); + } else if (typeof get_query === "string") { + args.query = get_query; + } else { + // get_query by function + var q = get_query( + (this.frm && this.frm.doc) || this.doc, + this.doctype, + this.docname + ); + + if (typeof q === "string") { + // returns a string + args.query = q; + } else if ($.isPlainObject(q)) { + // returns an object + process_query_object(q); } } @@ -191,8 +229,7 @@ frappe.ui.form.ControlAutocomplete = class ControlAutoComplete extends frappe.ui if(!this.$input.is(":focus")) { return; } - // this.$input.cache[this.df.fieldname][term] = r.results; - // this.awesomplete.list = this.$input.cache[this.df.fieldname][term]; + this.$input.cache[this.doctype][this.df.fieldname][term] = message; this.set_data(message); } }) From 51dcf8fd47bdc02b6e89814b7c77dd60db46bd9b Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Sat, 29 Jan 2022 19:16:03 +0530 Subject: [PATCH 04/15] feat: add autocomplete option in custom fields --- .../doctype/custom_field/custom_field.json | 915 +++++++++--------- .../customize_form_field.json | 6 +- frappe/database/postgres/database.py | 3 +- frappe/model/__init__.py | 2 +- 4 files changed, 465 insertions(+), 461 deletions(-) diff --git a/frappe/custom/doctype/custom_field/custom_field.json b/frappe/custom/doctype/custom_field/custom_field.json index 235f11aad8..d8b12d13b7 100644 --- a/frappe/custom/doctype/custom_field/custom_field.json +++ b/frappe/custom/doctype/custom_field/custom_field.json @@ -1,458 +1,461 @@ { - "actions": [], - "allow_import": 1, - "creation": "2013-01-10 16:34:01", - "description": "Adds a custom field to a DocType", - "doctype": "DocType", - "document_type": "Setup", - "engine": "InnoDB", - "field_order": [ - "dt", - "module", - "label", - "label_help", - "fieldname", - "insert_after", - "length", - "column_break_6", - "fieldtype", - "precision", - "hide_seconds", - "hide_days", - "options", - "fetch_from", - "fetch_if_empty", - "options_help", - "section_break_11", - "collapsible", - "collapsible_depends_on", - "default", - "depends_on", - "mandatory_depends_on", - "read_only_depends_on", - "properties", - "non_negative", - "reqd", - "unique", - "read_only", - "ignore_user_permissions", - "hidden", - "print_hide", - "print_hide_if_no_value", - "print_width", - "no_copy", - "allow_on_submit", - "in_list_view", - "in_standard_filter", - "in_global_search", - "in_preview", - "bold", - "report_hide", - "search_index", - "allow_in_quick_entry", - "ignore_xss_filter", - "translatable", - "hide_border", - "description", - "permlevel", - "width", - "columns" - ], - "fields": [{ - "bold": 1, - "fieldname": "dt", - "fieldtype": "Link", - "in_filter": 1, - "in_list_view": 1, - "label": "Document", - "oldfieldname": "dt", - "oldfieldtype": "Link", - "options": "DocType", - "reqd": 1, - "search_index": 1 - }, - { - "bold": 1, - "fieldname": "label", - "fieldtype": "Data", - "in_filter": 1, - "label": "Label", - "no_copy": 1, - "oldfieldname": "label", - "oldfieldtype": "Data" - }, - { - "fieldname": "label_help", - "fieldtype": "HTML", - "label": "Label Help", - "oldfieldtype": "HTML" - }, - { - "fieldname": "fieldname", - "fieldtype": "Data", - "in_list_view": 1, - "label": "Fieldname", - "no_copy": 1, - "oldfieldname": "fieldname", - "oldfieldtype": "Data", - "read_only": 1 - }, - { - "description": "Select the label after which you want to insert new field.", - "fieldname": "insert_after", - "fieldtype": "Select", - "label": "Insert After", - "no_copy": 1, - "oldfieldname": "insert_after", - "oldfieldtype": "Select" - }, - { - "fieldname": "column_break_6", - "fieldtype": "Column Break" - }, - { - "bold": 1, - "default": "Data", - "fieldname": "fieldtype", - "fieldtype": "Select", - "in_filter": 1, - "in_list_view": 1, - "label": "Field Type", - "oldfieldname": "fieldtype", - "oldfieldtype": "Select", - "options": "Attach\nAttach Image\nBarcode\nButton\nCheck\nCode\nColor\nColumn Break\nCurrency\nData\nDate\nDatetime\nDuration\nDynamic Link\nFloat\nFold\nGeolocation\nHeading\nHTML\nHTML Editor\nIcon\nImage\nInt\nLink\nLong Text\nMarkdown Editor\nPassword\nPercent\nRead Only\nRating\nSection Break\nSelect\nSmall Text\nTable\nTable MultiSelect\nText\nText Editor\nTime\nSignature\nTab Break", - "reqd": 1 - }, - { - "depends_on": "eval:in_list([\"Float\", \"Currency\", \"Percent\"], doc.fieldtype)", - "description": "Set non-standard precision for a Float or Currency field", - "fieldname": "precision", - "fieldtype": "Select", - "label": "Precision", - "options": "\n0\n1\n2\n3\n4\n5\n6\n7\n8\n9" - }, - { - "fieldname": "options", - "fieldtype": "Small Text", - "in_list_view": 1, - "label": "Options", - "oldfieldname": "options", - "oldfieldtype": "Text" - }, - { - "fieldname": "fetch_from", - "fieldtype": "Small Text", - "label": "Fetch From" - }, - { - "default": "0", - "description": "If checked, this field will be not overwritten based on Fetch From if a value already exists.", - "fieldname": "fetch_if_empty", - "fieldtype": "Check", - "label": "Fetch If Empty" - }, - { - "fieldname": "options_help", - "fieldtype": "HTML", - "label": "Options Help", - "oldfieldtype": "HTML" - }, - { - "fieldname": "section_break_11", - "fieldtype": "Section Break" - }, - { - "default": "0", - "depends_on": "eval:doc.fieldtype==\"Section Break\"", - "fieldname": "collapsible", - "fieldtype": "Check", - "label": "Collapsible" - }, - { - "depends_on": "eval:doc.fieldtype==\"Section Break\"", - "fieldname": "collapsible_depends_on", - "fieldtype": "Code", - "label": "Collapsible Depends On" - }, - { - "fieldname": "default", - "fieldtype": "Text", - "label": "Default Value", - "oldfieldname": "default", - "oldfieldtype": "Text" - }, - { - "fieldname": "depends_on", - "fieldtype": "Code", - "label": "Depends On", - "length": 255 - }, - { - "fieldname": "description", - "fieldtype": "Text", - "label": "Field Description", - "oldfieldname": "description", - "oldfieldtype": "Text", - "print_width": "300px", - "width": "300px" - }, - { - "default": "0", - "fieldname": "permlevel", - "fieldtype": "Int", - "label": "Permission Level", - "oldfieldname": "permlevel", - "oldfieldtype": "Int" - }, - { - "fieldname": "width", - "fieldtype": "Data", - "label": "Width", - "oldfieldname": "width", - "oldfieldtype": "Data" - }, - { - "description": "Number of columns for a field in a List View or a Grid (Total Columns should be less than 11)", - "fieldname": "columns", - "fieldtype": "Int", - "label": "Columns" - }, - { - "fieldname": "properties", - "fieldtype": "Column Break", - "oldfieldtype": "Column Break", - "print_width": "50%", - "width": "50%" - }, - { - "default": "0", - "fieldname": "reqd", - "fieldtype": "Check", - "in_list_view": 1, - "label": "Is Mandatory Field", - "oldfieldname": "reqd", - "oldfieldtype": "Check" - }, - { - "default": "0", - "fieldname": "unique", - "fieldtype": "Check", - "label": "Unique" - }, - { - "default": "0", - "fieldname": "read_only", - "fieldtype": "Check", - "label": "Read Only" - }, - { - "default": "0", - "depends_on": "eval:doc.fieldtype===\"Link\"", - "fieldname": "ignore_user_permissions", - "fieldtype": "Check", - "label": "Ignore User Permissions" - }, - { - "default": "0", - "fieldname": "hidden", - "fieldtype": "Check", - "label": "Hidden" - }, - { - "default": "0", - "fieldname": "print_hide", - "fieldtype": "Check", - "label": "Print Hide", - "oldfieldname": "print_hide", - "oldfieldtype": "Check" - }, - { - "default": "0", - "depends_on": "eval:[\"Int\", \"Float\", \"Currency\", \"Percent\"].indexOf(doc.fieldtype)!==-1", - "fieldname": "print_hide_if_no_value", - "fieldtype": "Check", - "label": "Print Hide If No Value" - }, - { - "fieldname": "print_width", - "fieldtype": "Data", - "hidden": 1, - "label": "Print Width", - "no_copy": 1, - "print_hide": 1 - }, - { - "default": "0", - "fieldname": "no_copy", - "fieldtype": "Check", - "label": "No Copy", - "oldfieldname": "no_copy", - "oldfieldtype": "Check" - }, - { - "default": "0", - "fieldname": "allow_on_submit", - "fieldtype": "Check", - "label": "Allow on Submit", - "oldfieldname": "allow_on_submit", - "oldfieldtype": "Check" - }, - { - "default": "0", - "fieldname": "in_list_view", - "fieldtype": "Check", - "label": "In List View" - }, - { - "default": "0", - "fieldname": "in_standard_filter", - "fieldtype": "Check", - "label": "In Standard Filter" - }, - { - "default": "0", - "depends_on": "eval:([\"Data\", \"Select\", \"Table\", \"Text\", \"Text Editor\", \"Link\", \"Small Text\", \"Long Text\", \"Read Only\", \"Heading\", \"Dynamic Link\"].indexOf(doc.fieldtype) !== -1)", - "fieldname": "in_global_search", - "fieldtype": "Check", - "label": "In Global Search" - }, - { - "default": "0", - "fieldname": "bold", - "fieldtype": "Check", - "label": "Bold" - }, - { - "default": "0", - "fieldname": "report_hide", - "fieldtype": "Check", - "label": "Report Hide", - "oldfieldname": "report_hide", - "oldfieldtype": "Check" - }, - { - "default": "0", - "fieldname": "search_index", - "fieldtype": "Check", - "hidden": 1, - "label": "Index", - "no_copy": 1, - "print_hide": 1 - }, - { - "default": "0", - "description": "Don't HTML Encode HTML tags like <script> or just characters like < or >, as they could be intentionally used in this field", - "fieldname": "ignore_xss_filter", - "fieldtype": "Check", - "label": "Ignore XSS Filter" - }, - { - "default": "1", - "depends_on": "eval:['Data', 'Select', 'Text', 'Small Text', 'Text Editor'].includes(doc.fieldtype)", - "fieldname": "translatable", - "fieldtype": "Check", - "label": "Translatable" - }, - { - "depends_on": "eval:in_list(['Data', 'Link', 'Dynamic Link', 'Password', 'Select', 'Read Only', 'Attach', 'Attach Image', 'Int'], doc.fieldtype)", - "fieldname": "length", - "fieldtype": "Int", - "label": "Length" - }, - { - "fieldname": "mandatory_depends_on", - "fieldtype": "Code", - "label": "Mandatory Depends On", - "length": 255 - }, - { - "fieldname": "read_only_depends_on", - "fieldtype": "Code", - "label": "Read Only Depends On", - "length": 255 - }, - { - "default": "0", - "fieldname": "allow_in_quick_entry", - "fieldtype": "Check", - "label": "Allow in Quick Entry" - }, - { - "default": "0", - "depends_on": "eval:!in_list(['Table', 'Table MultiSelect'], doc.fieldtype);", - "fieldname": "in_preview", - "fieldtype": "Check", - "label": "In Preview" - }, - { - "default": "0", - "depends_on": "eval:doc.fieldtype=='Duration'", - "fieldname": "hide_seconds", - "fieldtype": "Check", - "label": "Hide Seconds" - }, - { - "default": "0", - "depends_on": "eval:doc.fieldtype=='Duration'", - "fieldname": "hide_days", - "fieldtype": "Check", - "label": "Hide Days" - }, - { - "default": "0", - "depends_on": "eval:doc.fieldtype=='Section Break'", - "fieldname": "hide_border", - "fieldtype": "Check", - "label": "Hide Border" - }, - { - "default": "0", - "depends_on": "eval:in_list([\"Int\", \"Float\", \"Currency\"], doc.fieldtype)", - "fieldname": "non_negative", - "fieldtype": "Check", - "label": "Non Negative" - }, - { - "fieldname": "module", - "fieldtype": "Link", - "label": "Module (for export)", - "options": "Module Def" - } - ], - "icon": "fa fa-glass", - "idx": 1, - "index_web_pages_for_search": 1, - "links": [], - "modified": "2021-09-04 12:45:23.810120", - "modified_by": "Administrator", - "module": "Custom", - "name": "Custom Field", - "owner": "Administrator", - "permissions": [{ - "create": 1, - "delete": 1, - "email": 1, - "print": 1, - "read": 1, - "report": 1, - "role": "Administrator", - "share": 1, - "write": 1 - }, - { - "create": 1, - "delete": 1, - "email": 1, - "print": 1, - "read": 1, - "report": 1, - "role": "System Manager", - "share": 1, - "write": 1 - } - ], - "search_fields": "dt,label,fieldtype,options", - "sort_field": "modified", - "sort_order": "ASC", - "track_changes": 1 + "actions": [], + "allow_import": 1, + "creation": "2022-01-25 14:14:01.217507", + "description": "Adds a custom field to a DocType", + "doctype": "DocType", + "document_type": "Setup", + "engine": "InnoDB", + "field_order": [ + "dt", + "module", + "label", + "label_help", + "fieldname", + "insert_after", + "length", + "column_break_6", + "fieldtype", + "precision", + "hide_seconds", + "hide_days", + "options", + "fetch_from", + "fetch_if_empty", + "options_help", + "section_break_11", + "collapsible", + "collapsible_depends_on", + "default", + "depends_on", + "mandatory_depends_on", + "read_only_depends_on", + "properties", + "non_negative", + "reqd", + "unique", + "read_only", + "ignore_user_permissions", + "hidden", + "print_hide", + "print_hide_if_no_value", + "print_width", + "no_copy", + "allow_on_submit", + "in_list_view", + "in_standard_filter", + "in_global_search", + "in_preview", + "bold", + "report_hide", + "search_index", + "allow_in_quick_entry", + "ignore_xss_filter", + "translatable", + "hide_border", + "description", + "permlevel", + "width", + "columns" + ], + "fields": [ + { + "bold": 1, + "fieldname": "dt", + "fieldtype": "Link", + "in_filter": 1, + "in_list_view": 1, + "label": "Document", + "oldfieldname": "dt", + "oldfieldtype": "Link", + "options": "DocType", + "reqd": 1, + "search_index": 1 + }, + { + "bold": 1, + "fieldname": "label", + "fieldtype": "Data", + "in_filter": 1, + "label": "Label", + "no_copy": 1, + "oldfieldname": "label", + "oldfieldtype": "Data" + }, + { + "fieldname": "label_help", + "fieldtype": "HTML", + "label": "Label Help", + "oldfieldtype": "HTML" + }, + { + "fieldname": "fieldname", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Fieldname", + "no_copy": 1, + "oldfieldname": "fieldname", + "oldfieldtype": "Data", + "read_only": 1 + }, + { + "description": "Select the label after which you want to insert new field.", + "fieldname": "insert_after", + "fieldtype": "Select", + "label": "Insert After", + "no_copy": 1, + "oldfieldname": "insert_after", + "oldfieldtype": "Select" + }, + { + "fieldname": "column_break_6", + "fieldtype": "Column Break" + }, + { + "bold": 1, + "default": "Data", + "fieldname": "fieldtype", + "fieldtype": "Select", + "in_filter": 1, + "in_list_view": 1, + "label": "Field Type", + "oldfieldname": "fieldtype", + "oldfieldtype": "Select", + "options": "Autocomplete\nAttach\nAttach Image\nBarcode\nButton\nCheck\nCode\nColor\nColumn Break\nCurrency\nData\nDate\nDatetime\nDuration\nDynamic Link\nFloat\nFold\nGeolocation\nHeading\nHTML\nHTML Editor\nIcon\nImage\nInt\nLink\nLong Text\nMarkdown Editor\nPassword\nPercent\nRead Only\nRating\nSection Break\nSelect\nSmall Text\nTable\nTable MultiSelect\nText\nText Editor\nTime\nSignature\nTab Break", + "reqd": 1 + }, + { + "depends_on": "eval:in_list([\"Float\", \"Currency\", \"Percent\"], doc.fieldtype)", + "description": "Set non-standard precision for a Float or Currency field", + "fieldname": "precision", + "fieldtype": "Select", + "label": "Precision", + "options": "\n0\n1\n2\n3\n4\n5\n6\n7\n8\n9" + }, + { + "fieldname": "options", + "fieldtype": "Small Text", + "in_list_view": 1, + "label": "Options", + "oldfieldname": "options", + "oldfieldtype": "Text" + }, + { + "fieldname": "fetch_from", + "fieldtype": "Small Text", + "label": "Fetch From" + }, + { + "default": "0", + "description": "If checked, this field will be not overwritten based on Fetch From if a value already exists.", + "fieldname": "fetch_if_empty", + "fieldtype": "Check", + "label": "Fetch If Empty" + }, + { + "fieldname": "options_help", + "fieldtype": "HTML", + "label": "Options Help", + "oldfieldtype": "HTML" + }, + { + "fieldname": "section_break_11", + "fieldtype": "Section Break" + }, + { + "default": "0", + "depends_on": "eval:doc.fieldtype==\"Section Break\"", + "fieldname": "collapsible", + "fieldtype": "Check", + "label": "Collapsible" + }, + { + "depends_on": "eval:doc.fieldtype==\"Section Break\"", + "fieldname": "collapsible_depends_on", + "fieldtype": "Code", + "label": "Collapsible Depends On" + }, + { + "fieldname": "default", + "fieldtype": "Text", + "label": "Default Value", + "oldfieldname": "default", + "oldfieldtype": "Text" + }, + { + "fieldname": "depends_on", + "fieldtype": "Code", + "label": "Depends On", + "length": 255 + }, + { + "fieldname": "description", + "fieldtype": "Text", + "label": "Field Description", + "oldfieldname": "description", + "oldfieldtype": "Text", + "print_width": "300px", + "width": "300px" + }, + { + "default": "0", + "fieldname": "permlevel", + "fieldtype": "Int", + "label": "Permission Level", + "oldfieldname": "permlevel", + "oldfieldtype": "Int" + }, + { + "fieldname": "width", + "fieldtype": "Data", + "label": "Width", + "oldfieldname": "width", + "oldfieldtype": "Data" + }, + { + "description": "Number of columns for a field in a List View or a Grid (Total Columns should be less than 11)", + "fieldname": "columns", + "fieldtype": "Int", + "label": "Columns" + }, + { + "fieldname": "properties", + "fieldtype": "Column Break", + "oldfieldtype": "Column Break", + "print_width": "50%", + "width": "50%" + }, + { + "default": "0", + "fieldname": "reqd", + "fieldtype": "Check", + "in_list_view": 1, + "label": "Is Mandatory Field", + "oldfieldname": "reqd", + "oldfieldtype": "Check" + }, + { + "default": "0", + "fieldname": "unique", + "fieldtype": "Check", + "label": "Unique" + }, + { + "default": "0", + "fieldname": "read_only", + "fieldtype": "Check", + "label": "Read Only" + }, + { + "default": "0", + "depends_on": "eval:doc.fieldtype===\"Link\"", + "fieldname": "ignore_user_permissions", + "fieldtype": "Check", + "label": "Ignore User Permissions" + }, + { + "default": "0", + "fieldname": "hidden", + "fieldtype": "Check", + "label": "Hidden" + }, + { + "default": "0", + "fieldname": "print_hide", + "fieldtype": "Check", + "label": "Print Hide", + "oldfieldname": "print_hide", + "oldfieldtype": "Check" + }, + { + "default": "0", + "depends_on": "eval:[\"Int\", \"Float\", \"Currency\", \"Percent\"].indexOf(doc.fieldtype)!==-1", + "fieldname": "print_hide_if_no_value", + "fieldtype": "Check", + "label": "Print Hide If No Value" + }, + { + "fieldname": "print_width", + "fieldtype": "Data", + "hidden": 1, + "label": "Print Width", + "no_copy": 1, + "print_hide": 1 + }, + { + "default": "0", + "fieldname": "no_copy", + "fieldtype": "Check", + "label": "No Copy", + "oldfieldname": "no_copy", + "oldfieldtype": "Check" + }, + { + "default": "0", + "fieldname": "allow_on_submit", + "fieldtype": "Check", + "label": "Allow on Submit", + "oldfieldname": "allow_on_submit", + "oldfieldtype": "Check" + }, + { + "default": "0", + "fieldname": "in_list_view", + "fieldtype": "Check", + "label": "In List View" + }, + { + "default": "0", + "fieldname": "in_standard_filter", + "fieldtype": "Check", + "label": "In Standard Filter" + }, + { + "default": "0", + "depends_on": "eval:([\"Data\", \"Select\", \"Table\", \"Text\", \"Text Editor\", \"Link\", \"Small Text\", \"Long Text\", \"Read Only\", \"Heading\", \"Dynamic Link\"].indexOf(doc.fieldtype) !== -1)", + "fieldname": "in_global_search", + "fieldtype": "Check", + "label": "In Global Search" + }, + { + "default": "0", + "fieldname": "bold", + "fieldtype": "Check", + "label": "Bold" + }, + { + "default": "0", + "fieldname": "report_hide", + "fieldtype": "Check", + "label": "Report Hide", + "oldfieldname": "report_hide", + "oldfieldtype": "Check" + }, + { + "default": "0", + "fieldname": "search_index", + "fieldtype": "Check", + "hidden": 1, + "label": "Index", + "no_copy": 1, + "print_hide": 1 + }, + { + "default": "0", + "description": "Don't HTML Encode HTML tags like <script> or just characters like < or >, as they could be intentionally used in this field", + "fieldname": "ignore_xss_filter", + "fieldtype": "Check", + "label": "Ignore XSS Filter" + }, + { + "default": "1", + "depends_on": "eval:['Data', 'Select', 'Text', 'Small Text', 'Text Editor'].includes(doc.fieldtype)", + "fieldname": "translatable", + "fieldtype": "Check", + "label": "Translatable" + }, + { + "depends_on": "eval:in_list(['Data', 'Link', 'Dynamic Link', 'Password', 'Select', 'Read Only', 'Attach', 'Attach Image', 'Int'], doc.fieldtype)", + "fieldname": "length", + "fieldtype": "Int", + "label": "Length" + }, + { + "fieldname": "mandatory_depends_on", + "fieldtype": "Code", + "label": "Mandatory Depends On", + "length": 255 + }, + { + "fieldname": "read_only_depends_on", + "fieldtype": "Code", + "label": "Read Only Depends On", + "length": 255 + }, + { + "default": "0", + "fieldname": "allow_in_quick_entry", + "fieldtype": "Check", + "label": "Allow in Quick Entry" + }, + { + "default": "0", + "depends_on": "eval:!in_list(['Table', 'Table MultiSelect'], doc.fieldtype);", + "fieldname": "in_preview", + "fieldtype": "Check", + "label": "In Preview" + }, + { + "default": "0", + "depends_on": "eval:doc.fieldtype=='Duration'", + "fieldname": "hide_seconds", + "fieldtype": "Check", + "label": "Hide Seconds" + }, + { + "default": "0", + "depends_on": "eval:doc.fieldtype=='Duration'", + "fieldname": "hide_days", + "fieldtype": "Check", + "label": "Hide Days" + }, + { + "default": "0", + "depends_on": "eval:doc.fieldtype=='Section Break'", + "fieldname": "hide_border", + "fieldtype": "Check", + "label": "Hide Border" + }, + { + "default": "0", + "depends_on": "eval:in_list([\"Int\", \"Float\", \"Currency\"], doc.fieldtype)", + "fieldname": "non_negative", + "fieldtype": "Check", + "label": "Non Negative" + }, + { + "fieldname": "module", + "fieldtype": "Link", + "label": "Module (for export)", + "options": "Module Def" + } + ], + "icon": "fa fa-glass", + "idx": 1, + "index_web_pages_for_search": 1, + "links": [], + "modified": "2022-01-29 15:42:21.885999", + "modified_by": "Administrator", + "module": "Custom", + "name": "Custom Field", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Administrator", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "search_fields": "dt,label,fieldtype,options", + "sort_field": "modified", + "sort_order": "ASC", + "states": [], + "track_changes": 1 } \ No newline at end of file diff --git a/frappe/custom/doctype/customize_form_field/customize_form_field.json b/frappe/custom/doctype/customize_form_field/customize_form_field.json index a545cd9fe1..93d1de0e63 100644 --- a/frappe/custom/doctype/customize_form_field/customize_form_field.json +++ b/frappe/custom/doctype/customize_form_field/customize_form_field.json @@ -1,7 +1,7 @@ { "actions": [], "autoname": "hash", - "creation": "2013-02-22 01:27:32", + "creation": "2022-01-25 14:14:07.063385", "doctype": "DocType", "document_type": "Setup", "editable_grid": 1, @@ -83,7 +83,7 @@ "label": "Type", "oldfieldname": "fieldtype", "oldfieldtype": "Select", - "options": "Attach\nAttach Image\nBarcode\nButton\nCheck\nCode\nColor\nColumn Break\nCurrency\nData\nDate\nDatetime\nDuration\nDynamic Link\nFloat\nFold\nGeolocation\nHeading\nHTML\nHTML Editor\nIcon\nImage\nInt\nLink\nLong Text\nMarkdown Editor\nPassword\nPercent\nRating\nRead Only\nSection Break\nSelect\nSignature\nSmall Text\nTab Break\nTable\nTable MultiSelect\nText\nText Editor\nTime", + "options": "Autocomplete\nAttach\nAttach Image\nBarcode\nButton\nCheck\nCode\nColor\nColumn Break\nCurrency\nData\nDate\nDatetime\nDuration\nDynamic Link\nFloat\nFold\nGeolocation\nHeading\nHTML\nHTML Editor\nIcon\nImage\nInt\nLink\nLong Text\nMarkdown Editor\nPassword\nPercent\nRating\nRead Only\nSection Break\nSelect\nSignature\nSmall Text\nTab Break\nTable\nTable MultiSelect\nText\nText Editor\nTime", "reqd": 1, "search_index": 1 }, @@ -436,7 +436,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2022-01-03 14:50:32.035768", + "modified": "2022-01-29 15:43:05.540546", "modified_by": "Administrator", "module": "Custom", "name": "Customize Form Field", diff --git a/frappe/database/postgres/database.py b/frappe/database/postgres/database.py index d5495c6879..504bebf5cd 100644 --- a/frappe/database/postgres/database.py +++ b/frappe/database/postgres/database.py @@ -62,7 +62,8 @@ class PostgresDatabase(Database): 'Barcode': ('text', ''), 'Geolocation': ('text', ''), 'Duration': ('decimal', '21,9'), - 'Icon': ('varchar', self.VARCHAR_LEN) + 'Icon': ('varchar', self.VARCHAR_LEN), + 'Autocomplete': ('varchar', self.VARCHAR_LEN), } def get_connection(self): diff --git a/frappe/model/__init__.py b/frappe/model/__init__.py index 232ffbe771..be038f3a95 100644 --- a/frappe/model/__init__.py +++ b/frappe/model/__init__.py @@ -36,7 +36,7 @@ data_fieldtypes = ( 'Geolocation', 'Duration', 'Icon', - 'Autocomplete' + 'Autocomplete', ) attachment_fieldtypes = ( From 07b70c2bd9c55ea5fc2ab5ba0348874a24191182 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Wed, 9 Feb 2022 11:35:15 +0530 Subject: [PATCH 05/15] test(ui): autocomplete control --- cypress/integration/control_autocomplete.js | 38 +++++++++++++++++++ .../js/frappe/form/controls/autocomplete.js | 4 +- 2 files changed, 40 insertions(+), 2 deletions(-) create mode 100644 cypress/integration/control_autocomplete.js diff --git a/cypress/integration/control_autocomplete.js b/cypress/integration/control_autocomplete.js new file mode 100644 index 0000000000..b5effb6965 --- /dev/null +++ b/cypress/integration/control_autocomplete.js @@ -0,0 +1,38 @@ +context('Control Autocomplete', () => { + before(() => { + cy.login(); + cy.visit('/app/website'); + }); + + function get_dialog_with_autocomplete() { + cy.visit('/app/website'); + return cy.dialog({ + title: 'Autocomplete', + fields: [ + { + 'label': 'Select an option', + 'fieldname': 'autocomplete', + 'fieldtype': 'Autocomplete', + 'options': ['Option 1', 'Option 2', 'Option 3'], + } + ] + }); + } + + it('should set the valid value', () => { + get_dialog_with_autocomplete().as('dialog'); + + cy.get('.frappe-control[data-fieldname=autocomplete] input').focus().as('input'); + cy.wait(1000); + cy.get('@input').type('2', { delay: 300 }); + cy.get('.frappe-control[data-fieldname=autocomplete]').findByRole('listbox').should('be.visible'); + cy.get('.frappe-control[data-fieldname=autocomplete] input').type('{enter}', { delay: 300 }); + cy.get('.frappe-control[data-fieldname=autocomplete] input').blur(); + cy.get('@dialog').then(dialog => { + let value = dialog.get_value('autocomplete'); + expect(value).to.eq('Option 2'); + dialog.clear(); + }); + }); + +}); diff --git a/frappe/public/js/frappe/form/controls/autocomplete.js b/frappe/public/js/frappe/form/controls/autocomplete.js index 3a2706b51d..45aa10a2fc 100644 --- a/frappe/public/js/frappe/form/controls/autocomplete.js +++ b/frappe/public/js/frappe/form/controls/autocomplete.js @@ -11,7 +11,7 @@ frappe.ui.form.ControlAutocomplete = class ControlAutoComplete extends frappe.ui set_options() { if (this.df.options) { let options = this.df.options || []; - this._data = this.parse_options(options); + this.set_data(options); } } @@ -102,7 +102,7 @@ frappe.ui.form.ControlAutocomplete = class ControlAutoComplete extends frappe.ui this.$input.on('input', frappe.utils.debounce((e) => { const cached_options = this.$input.cache[this.doctype][this.df.fieldname][e.target.value]; - if (cached_options) { + if (cached_options && cached_options.length) { this.set_data(cached_options); } else if (this.get_query || this.df.get_query) { this.execute_query_if_exists(e.target.value); From 436543d2128bf8ff29dc1073cb64becf3a691aec Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Sat, 12 Feb 2022 18:22:48 +0530 Subject: [PATCH 06/15] fix: autocomplete input value if label & value varies --- cypress/integration/control_autocomplete.js | 23 +++++++++++++++++-- .../js/frappe/form/controls/autocomplete.js | 10 +++++++- 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/cypress/integration/control_autocomplete.js b/cypress/integration/control_autocomplete.js index b5effb6965..3bf3e829f9 100644 --- a/cypress/integration/control_autocomplete.js +++ b/cypress/integration/control_autocomplete.js @@ -4,7 +4,7 @@ context('Control Autocomplete', () => { cy.visit('/app/website'); }); - function get_dialog_with_autocomplete() { + function get_dialog_with_autocomplete(options) { cy.visit('/app/website'); return cy.dialog({ title: 'Autocomplete', @@ -13,7 +13,7 @@ context('Control Autocomplete', () => { 'label': 'Select an option', 'fieldname': 'autocomplete', 'fieldtype': 'Autocomplete', - 'options': ['Option 1', 'Option 2', 'Option 3'], + 'options': options || ['Option 1', 'Option 2', 'Option 3'], } ] }); @@ -35,4 +35,23 @@ context('Control Autocomplete', () => { }); }); + it('should set the valid value with different label', () => { + const options_with_label = [ + { label: "Option 1", value: "option_1" }, + { label: "Option 2", value: "option_2" } + ]; + get_dialog_with_autocomplete(options_with_label).as('dialog'); + + cy.get('.frappe-control[data-fieldname=autocomplete] input').focus().as('input'); + cy.get('.frappe-control[data-fieldname=autocomplete]').findByRole('listbox').should('be.visible'); + cy.get('@input').type('2', { delay: 300 }); + cy.get('.frappe-control[data-fieldname=autocomplete] input').type('{enter}', { delay: 300 }); + cy.get('.frappe-control[data-fieldname=autocomplete] input').blur(); + cy.get('@dialog').then(dialog => { + let value = dialog.get_value('autocomplete'); + expect(value).to.eq('option_2'); + dialog.clear(); + }); + }); + }); diff --git a/frappe/public/js/frappe/form/controls/autocomplete.js b/frappe/public/js/frappe/form/controls/autocomplete.js index 45aa10a2fc..4e66ed6642 100644 --- a/frappe/public/js/frappe/form/controls/autocomplete.js +++ b/frappe/public/js/frappe/form/controls/autocomplete.js @@ -26,6 +26,14 @@ frappe.ui.form.ControlAutocomplete = class ControlAutoComplete extends frappe.ui } } + get_input_value() { + if (this.$input) { + const label = this.$input.val(); + const item = this._data?.find(i => i.label == label); + return item ? item.value : label; + } + } + get_awesomplete_settings() { var me = this; return { @@ -34,7 +42,7 @@ frappe.ui.form.ControlAutocomplete = class ControlAutoComplete extends frappe.ui autoFirst: true, list: this.get_data(), data: function(item) { - if (!(item instanceof Object)) { + if (typeof item !== 'object') { var d = { value: item }; item = d; } From 6b671af1ec29ae1e95595b7e8dd2c8495eedaa6a Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Mon, 14 Feb 2022 12:29:23 +0530 Subject: [PATCH 07/15] Merge branch 'develop' into autocomplete-control --- frappe/core/doctype/docfield/docfield.json | 9 ++- frappe/core/doctype/doctype/doctype.py | 13 ++-- .../doctype/custom_field/custom_field.json | 11 +++- .../doctype/custom_field/custom_field.py | 4 +- .../doctype/customize_form/customize_form.py | 9 ++- .../customize_form_field.json | 9 ++- .../property_setter/property_setter.py | 55 ++++------------ frappe/database/schema.py | 5 +- frappe/model/base_document.py | 28 ++++++-- frappe/model/meta.py | 11 +++- .../js/frappe/form/controls/base_control.js | 5 +- frappe/public/js/frappe/form/quick_entry.js | 2 +- .../public/js/frappe/form/script_manager.js | 15 ++++- frappe/public/js/frappe/list/list_view.js | 3 +- frappe/public/js/frappe/utils/common.js | 13 ++-- .../js/frappe/views/reports/report_view.js | 3 +- frappe/public/js/frappe/views/treeview.js | 2 +- frappe/tests/test_document.py | 65 +++++++++++++++++-- frappe/website/doctype/web_form/web_form.js | 2 +- 19 files changed, 178 insertions(+), 86 deletions(-) diff --git a/frappe/core/doctype/docfield/docfield.json b/frappe/core/doctype/docfield/docfield.json index 69c8c3505f..3267429298 100644 --- a/frappe/core/doctype/docfield/docfield.json +++ b/frappe/core/doctype/docfield/docfield.json @@ -17,6 +17,7 @@ "hide_days", "hide_seconds", "reqd", + "is_virtual", "search_index", "column_break_18", "options", @@ -534,13 +535,19 @@ "fieldname": "show_dashboard", "fieldtype": "Check", "label": "Show Dashboard" + }, + { + "default": "0", + "fieldname": "is_virtual", + "fieldtype": "Check", + "label": "Virtual" } ], "idx": 1, "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2022-01-04 11:56:19.812863", + "modified": "2022-02-14 11:56:19.812863", "modified_by": "Administrator", "module": "Core", "name": "DocField", diff --git a/frappe/core/doctype/doctype/doctype.py b/frappe/core/doctype/doctype/doctype.py index aa731a686b..6d0409521e 100644 --- a/frappe/core/doctype/doctype/doctype.py +++ b/frappe/core/doctype/doctype/doctype.py @@ -1078,6 +1078,9 @@ def validate_fields(meta): field.fetch_from = field.fetch_from.strip('\n').strip() def validate_data_field_type(docfield): + if docfield.get("is_virtual"): + return + if docfield.fieldtype == "Data" and not (docfield.oldfieldtype and docfield.oldfieldtype != "Data"): if docfield.options and (docfield.options not in data_field_options): df_str = frappe.bold(_(docfield.label)) @@ -1323,10 +1326,9 @@ def make_module_and_roles(doc, perm_fieldname="permissions"): else: raise -def check_fieldname_conflicts(doctype, fieldname): +def check_fieldname_conflicts(docfield): """Checks if fieldname conflicts with methods or properties""" - - doc = frappe.get_doc({"doctype": doctype}) + doc = frappe.get_doc({"doctype": docfield.dt}) available_objects = [x for x in dir(doc) if isinstance(x, str)] property_list = [ x for x in available_objects if isinstance(getattr(type(doc), x, None), property) @@ -1334,9 +1336,10 @@ def check_fieldname_conflicts(doctype, fieldname): method_list = [ x for x in available_objects if x not in property_list and callable(getattr(doc, x)) ] + msg = _("Fieldname {0} conflicting with meta object").format(docfield.fieldname) - if fieldname in method_list + property_list: - frappe.throw(_("Fieldname {0} conflicting with meta object").format(fieldname)) + if docfield.fieldname in method_list + property_list: + frappe.msgprint(msg, raise_exception=not docfield.is_virtual) def clear_linked_doctype_cache(): frappe.cache().delete_value('linked_doctypes_without_ignore_user_permissions_enabled') diff --git a/frappe/custom/doctype/custom_field/custom_field.json b/frappe/custom/doctype/custom_field/custom_field.json index d8b12d13b7..f09829a688 100644 --- a/frappe/custom/doctype/custom_field/custom_field.json +++ b/frappe/custom/doctype/custom_field/custom_field.json @@ -1,7 +1,7 @@ { "actions": [], "allow_import": 1, - "creation": "2022-01-25 14:14:01.217507", + "creation": "2013-01-10 16:34:01", "description": "Adds a custom field to a DocType", "doctype": "DocType", "document_type": "Setup", @@ -34,6 +34,7 @@ "non_negative", "reqd", "unique", + "is_virtual", "read_only", "ignore_user_permissions", "hidden", @@ -240,6 +241,12 @@ "fieldtype": "Check", "label": "Unique" }, + { + "default": "0", + "fieldname": "is_virtual", + "fieldtype": "Check", + "label": "Is Virtual" + }, { "default": "0", "fieldname": "read_only", @@ -424,7 +431,7 @@ "idx": 1, "index_web_pages_for_search": 1, "links": [], - "modified": "2022-01-29 15:42:21.885999", + "modified": "2022-02-14 15:42:21.885999", "modified_by": "Administrator", "module": "Custom", "name": "Custom Field", diff --git a/frappe/custom/doctype/custom_field/custom_field.py b/frappe/custom/doctype/custom_field/custom_field.py index 8f7b21dd24..cb1ea2c54d 100644 --- a/frappe/custom/doctype/custom_field/custom_field.py +++ b/frappe/custom/doctype/custom_field/custom_field.py @@ -54,7 +54,7 @@ class CustomField(Document): old_fieldtype = self.db_get('fieldtype') is_fieldtype_changed = (not self.is_new()) and (old_fieldtype != self.fieldtype) - if is_fieldtype_changed and not CustomizeForm.allow_fieldtype_change(old_fieldtype, self.fieldtype): + if not self.is_virtual and is_fieldtype_changed and not CustomizeForm.allow_fieldtype_change(old_fieldtype, self.fieldtype): frappe.throw(_("Fieldtype cannot be changed from {0} to {1}").format(old_fieldtype, self.fieldtype)) if not self.fieldname: @@ -65,7 +65,7 @@ class CustomField(Document): if not self.flags.ignore_validate: from frappe.core.doctype.doctype.doctype import check_fieldname_conflicts - check_fieldname_conflicts(self.dt, self.fieldname) + check_fieldname_conflicts(self) def on_update(self): if not frappe.flags.in_setup_wizard: diff --git a/frappe/custom/doctype/customize_form/customize_form.py b/frappe/custom/doctype/customize_form/customize_form.py index 1593ed49a5..2ccfa87544 100644 --- a/frappe/custom/doctype/customize_form/customize_form.py +++ b/frappe/custom/doctype/customize_form/customize_form.py @@ -418,6 +418,9 @@ class CustomizeForm(Document): return property_value def validate_fieldtype_change(self, df, old_value, new_value): + if df.is_virtual: + return + allowed = self.allow_fieldtype_change(old_value, new_value) if allowed: old_value_length = cint(frappe.db.type_map.get(old_value)[1]) @@ -430,7 +433,8 @@ class CustomizeForm(Document): self.validate_fieldtype_length() else: self.flags.update_db = True - if not allowed: + + else: frappe.throw(_("Fieldtype cannot be changed from {0} to {1} in row {2}").format(old_value, new_value, df.idx)) def validate_fieldtype_length(self): @@ -558,7 +562,8 @@ docfield_properties = { 'allow_in_quick_entry': 'Check', 'hide_border': 'Check', 'hide_days': 'Check', - 'hide_seconds': 'Check' + 'hide_seconds': 'Check', + 'is_virtual': 'Check', } doctype_link_properties = { diff --git a/frappe/custom/doctype/customize_form_field/customize_form_field.json b/frappe/custom/doctype/customize_form_field/customize_form_field.json index 93d1de0e63..1d721c57ec 100644 --- a/frappe/custom/doctype/customize_form_field/customize_form_field.json +++ b/frappe/custom/doctype/customize_form_field/customize_form_field.json @@ -14,6 +14,7 @@ "non_negative", "reqd", "unique", + "is_virtual", "in_list_view", "in_standard_filter", "in_global_search", @@ -115,6 +116,12 @@ "fieldtype": "Check", "label": "Unique" }, + { + "default": "0", + "fieldname": "is_virtual", + "fieldtype": "Check", + "label": "Is Virtual" + }, { "default": "0", "fieldname": "in_list_view", @@ -436,7 +443,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2022-01-29 15:43:05.540546", + "modified": "2022-01-27 21:45:22.349776", "modified_by": "Administrator", "module": "Custom", "name": "Customize Form Field", diff --git a/frappe/custom/doctype/property_setter/property_setter.py b/frappe/custom/doctype/property_setter/property_setter.py index 0a65aa6f5d..a86cf5efd6 100644 --- a/frappe/custom/doctype/property_setter/property_setter.py +++ b/frappe/custom/doctype/property_setter/property_setter.py @@ -1,4 +1,4 @@ -# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors +# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors # License: MIT. See LICENSE import frappe @@ -18,53 +18,19 @@ class PropertySetter(Document): def validate(self): self.validate_fieldtype_change() + if self.is_new(): delete_property_setter(self.doc_type, self.property, self.field_name, self.row_name) - - # clear cache frappe.clear_cache(doctype = self.doc_type) def validate_fieldtype_change(self): - if self.field_name in not_allowed_fieldtype_change and \ - self.property == 'fieldtype': - frappe.throw(_("Field type cannot be changed for {0}").format(self.field_name)) - - def get_property_list(self, dt): - return frappe.db.get_all('DocField', - fields=['fieldname', 'label', 'fieldtype'], - filters={ - 'parent': dt, - 'fieldtype': ['not in', ('Section Break', 'Column Break', 'Tab Break', 'HTML', 'Read Only', 'Fold') + frappe.model.table_fields], - 'fieldname': ['!=', ''] - }, - order_by='label asc', - as_dict=1 - ) - - def get_setup_data(self): - return { - 'doctypes': frappe.get_all("DocType", pluck="name"), - 'dt_properties': self.get_property_list('DocType'), - 'df_properties': self.get_property_list('DocField') - } - - def get_field_ids(self): - return frappe.db.get_values( - "DocField", - filters={"parent": self.doc_type}, - fieldname=["name", "fieldtype", "label", "fieldname"], - as_dict=True, - ) - - def get_defaults(self): - if not self.field_name: - return frappe.get_all("DocType", filters={"name": self.doc_type}, fields="*")[0] - else: - return frappe.db.get_values( - "DocField", - filters={"fieldname": self.field_name, "parent": self.doc_type}, - fieldname="*", - )[0] + if ( + self.property == 'fieldtype' + and self.field_name in not_allowed_fieldtype_change + ): + frappe.throw( + _("Field type cannot be changed for {0}").format(self.field_name) + ) def on_update(self): if frappe.flags.in_patch: @@ -74,6 +40,7 @@ class PropertySetter(Document): from frappe.core.doctype.doctype.doctype import validate_fields_for_doctype validate_fields_for_doctype(self.doc_type) + def make_property_setter(doctype, fieldname, property, value, property_type, for_doctype = False, validate_fields_for_doctype=True): # WARNING: Ignores Permissions @@ -91,6 +58,7 @@ def make_property_setter(doctype, fieldname, property, value, property_type, for property_setter.insert() return property_setter + def delete_property_setter(doc_type, property, field_name=None, row_name=None): """delete other property setters on this, if this is new""" filters = dict(doc_type=doc_type, property=property) @@ -100,4 +68,3 @@ def delete_property_setter(doc_type, property, field_name=None, row_name=None): filters["row_name"] = row_name frappe.db.delete('Property Setter', filters) - diff --git a/frappe/database/schema.py b/frappe/database/schema.py index dd54385c83..7cab8d42b2 100644 --- a/frappe/database/schema.py +++ b/frappe/database/schema.py @@ -67,7 +67,7 @@ class DBTable: """ get columns from docfields and custom fields """ - fields = self.meta.get_fieldnames_with_value(True) + fields = self.meta.get_fieldnames_with_value(with_field_meta=True) # optional fields like _comments if not self.meta.get('istable'): @@ -85,6 +85,9 @@ class DBTable: }) for field in fields: + if field.get("is_virtual"): + continue + self.columns[field.get('fieldname')] = DbColumn( self, field.get('fieldname'), diff --git a/frappe/model/base_document.py b/frappe/model/base_document.py index 307d95e84b..8380161050 100644 --- a/frappe/model/base_document.py +++ b/frappe/model/base_document.py @@ -1,16 +1,14 @@ -# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors +# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors # License: MIT. See LICENSE +import datetime import frappe -import datetime from frappe import _ -from frappe.model import default_fields, table_fields, child_table_fields +from frappe.model import child_table_fields, default_fields, display_fieldtypes, table_fields from frappe.model.naming import set_new_name from frappe.model.utils.link_count import notify_link_count from frappe.modules import load_doctype_module -from frappe.model import display_fieldtypes -from frappe.utils import (cint, flt, now, cstr, strip_html, - sanitize_html, sanitize_email, cast_fieldtype) +from frappe.utils import cast_fieldtype, cint, cstr, flt, now, sanitize_html, strip_html from frappe.utils.html_utils import unescape_html from frappe.model.docstatus import DocStatus @@ -254,7 +252,22 @@ class BaseDocument(object): continue df = self.meta.get_field(fieldname) - if df: + + if df and df.get("is_virtual"): + from frappe.utils.safe_exec import get_safe_globals + + if d[fieldname] is None: + if df.get("options"): + d[fieldname] = frappe.safe_eval( + code=df.get("options"), + eval_globals=get_safe_globals(), + eval_locals={"doc": self}, + ) + else: + _val = getattr(self, fieldname, None) + if _val and not callable(_val): + d[fieldname] = _val + elif df: if df.fieldtype=="Check": d[fieldname] = 1 if cint(d[fieldname]) else 0 @@ -328,6 +341,7 @@ class BaseDocument(object): def as_dict(self, no_nulls=False, no_default_fields=False, convert_dates_to_str=False, no_child_table_fields=False): doc = self.get_valid_dict(convert_dates_to_str=convert_dates_to_str) doc["doctype"] = self.doctype + for df in self.meta.get_table_fields(): children = self.get(df.fieldname) or [] doc[df.fieldname] = [ diff --git a/frappe/model/meta.py b/frappe/model/meta.py index 372392f689..77d4de466f 100644 --- a/frappe/model/meta.py +++ b/frappe/model/meta.py @@ -444,9 +444,16 @@ class Meta(Document): self.permissions = [Document(d) for d in custom_perms] def get_fieldnames_with_value(self, with_field_meta=False): - return [df if with_field_meta else df.fieldname \ - for df in self.fields if df.fieldtype not in no_value_fields] + def is_value_field(docfield): + return not ( + docfield.get("is_virtual") + or docfield.fieldtype in no_value_fields + ) + + if with_field_meta: + return [df for df in self.fields if is_value_field(df)] + return [df.fieldname for df in self.fields if is_value_field(df)] def get_fields_to_check_permissions(self, user_permission_doctypes): fields = self.get("fields", { diff --git a/frappe/public/js/frappe/form/controls/base_control.js b/frappe/public/js/frappe/form/controls/base_control.js index ce871c50cb..4ee52d16b8 100644 --- a/frappe/public/js/frappe/form/controls/base_control.js +++ b/frappe/public/js/frappe/form/controls/base_control.js @@ -39,6 +39,9 @@ frappe.ui.form.Control = class BaseControl { if (this.df.get_status) { return this.df.get_status(this); } + if (this.df.is_virtual) { + return "Read"; + } if ((!this.doctype && !this.docname) || this.df.parenttype === 'Web Form' || this.df.is_web_form) { // like in case of a dialog box @@ -52,7 +55,7 @@ frappe.ui.form.Control = class BaseControl { if(explain) console.log("By Hidden Dependency: None"); // eslint-disable-line no-console return "None"; - } else if (cint(this.df.read_only)) { + } else if (cint(this.df.read_only || this.df.is_virtual)) { // eslint-disable-next-line if (explain) console.log("By Read Only: Read"); // eslint-disable-line no-console return "Read"; diff --git a/frappe/public/js/frappe/form/quick_entry.js b/frappe/public/js/frappe/form/quick_entry.js index e412b1dec8..86523d7088 100644 --- a/frappe/public/js/frappe/form/quick_entry.js +++ b/frappe/public/js/frappe/form/quick_entry.js @@ -55,7 +55,7 @@ frappe.ui.form.QuickEntryForm = class QuickEntryForm { // prepare a list of mandatory, bold and allow in quick entry fields this.mandatory = fields.filter(df => { - return ((df.reqd || df.bold || df.allow_in_quick_entry) && !df.read_only); + return ((df.reqd || df.bold || df.allow_in_quick_entry) && !df.read_only && !df.is_virtual); }); } diff --git a/frappe/public/js/frappe/form/script_manager.js b/frappe/public/js/frappe/form/script_manager.js index 6169fa75b8..29f1c86d17 100644 --- a/frappe/public/js/frappe/form/script_manager.js +++ b/frappe/public/js/frappe/form/script_manager.js @@ -192,9 +192,18 @@ frappe.ui.form.ScriptManager = class ScriptManager { } function setup_add_fetch(df) { - if ((['Data', 'Read Only', 'Text', 'Small Text', 'Currency', 'Check', 'Attach Image', - 'Text Editor', 'Code', 'Link', 'Float', 'Int', 'Date', 'Select', 'Duration'].includes(df.fieldtype) || df.read_only==1) - && df.fetch_from && df.fetch_from.indexOf(".")!=-1) { + let is_read_only_field = ( + ['Data', 'Read Only', 'Text', 'Small Text', 'Currency', 'Check', 'Text Editor', 'Attach Image', + 'Code', 'Link', 'Float', 'Int', 'Date', 'Select', 'Duration'].includes(df.fieldtype) + || df.read_only == 1 + || df.is_virtual == 1 + ) + + if ( + is_read_only_field + && df.fetch_from + && df.fetch_from.indexOf(".") != -1 + ) { var parts = df.fetch_from.split("."); me.frm.add_fetch(parts[0], parts[1], df.fieldname, df.parent); } diff --git a/frappe/public/js/frappe/list/list_view.js b/frappe/public/js/frappe/list/list_view.js index 3cde04313f..64960e0b09 100644 --- a/frappe/public/js/frappe/list/list_view.js +++ b/frappe/public/js/frappe/list/list_view.js @@ -1672,7 +1672,8 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { frappe.model.is_value_type(field_doc) && field_doc.fieldtype !== "Read Only" && !field_doc.hidden && - !field_doc.read_only + !field_doc.read_only && + !field_doc.is_virtual ); }; diff --git a/frappe/public/js/frappe/utils/common.js b/frappe/public/js/frappe/utils/common.js index b324cecd39..1f3558b367 100644 --- a/frappe/public/js/frappe/utils/common.js +++ b/frappe/public/js/frappe/utils/common.js @@ -259,8 +259,16 @@ frappe.utils.xss_sanitise = function (string, options) { '/': '/' }; const REGEX_SCRIPT = /)<[^<]*)*<\/script>/gi; // used in jQuery 1.7.2 src/ajax.js Line 14 + const REGEX_ALERT = /confirm\(.*\)|alert\(.*\)|prompt\(.*\)/gi; // captures alert, confirm, prompt options = Object.assign({}, DEFAULT_OPTIONS, options); // don't deep copy, immutable beauty. + // Rule 3 - TODO: Check event handlers? + // script and alert should be checked first or else it will be escaped + if (options.strategies.includes('js')) { + sanitised = sanitised.replace(REGEX_SCRIPT, ""); + sanitised = sanitised.replace(REGEX_ALERT, ""); + } + // Rule 1 if (options.strategies.includes('html')) { for (let char in HTML_ESCAPE_MAP) { @@ -270,11 +278,6 @@ frappe.utils.xss_sanitise = function (string, options) { } } - // Rule 3 - TODO: Check event handlers? - if (options.strategies.includes('js')) { - sanitised = sanitised.replace(REGEX_SCRIPT, ""); - } - return sanitised; } diff --git a/frappe/public/js/frappe/views/reports/report_view.js b/frappe/public/js/frappe/views/reports/report_view.js index 1291e63543..25c0c512ff 100644 --- a/frappe/public/js/frappe/views/reports/report_view.js +++ b/frappe/public/js/frappe/views/reports/report_view.js @@ -648,6 +648,7 @@ frappe.views.ReportView = class ReportView extends frappe.views.ListView { // not a cancelled doc && data.docstatus !== 2 && !df.read_only + && !df.is_virtual && !df.hidden // not a standard field i.e., owner, modified_by, etc. && !frappe.model.std_fields_list.includes(df.fieldname)) @@ -1029,7 +1030,7 @@ frappe.views.ReportView = class ReportView extends frappe.views.ListView { title += ` (${__(doctype)})`; } - const editable = frappe.model.is_non_std_field(fieldname) && !docfield.read_only; + const editable = frappe.model.is_non_std_field(fieldname) && !docfield.read_only && !docfield.is_virtual; const align = (() => { const is_numeric = frappe.model.is_numeric_field(docfield); diff --git a/frappe/public/js/frappe/views/treeview.js b/frappe/public/js/frappe/views/treeview.js index 7179e4ab56..d5c6fb5e80 100644 --- a/frappe/public/js/frappe/views/treeview.js +++ b/frappe/public/js/frappe/views/treeview.js @@ -343,7 +343,7 @@ frappe.views.TreeView = class TreeView { this.ignore_fields = this.opts.ignore_fields || []; var mandatory_fields = $.map(me.opts.meta.fields, function(d) { - return (d.reqd || d.bold && !d.read_only) ? d : null }); + return (d.reqd || d.bold && !d.read_only && !!d.is_virtual) ? d : null }); var opts_field_names = this.fields.map(function(d) { return d.fieldname diff --git a/frappe/tests/test_document.py b/frappe/tests/test_document.py index 34a1dd070c..a0c44c5c72 100644 --- a/frappe/tests/test_document.py +++ b/frappe/tests/test_document.py @@ -1,11 +1,20 @@ -# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors +# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors # License: MIT. See LICENSE -import os import unittest +from contextlib import contextmanager +from datetime import timedelta +from unittest.mock import patch import frappe -from frappe.utils import cint -from frappe.model.naming import revert_series_if_last, make_autoname, parse_naming_series +from frappe.desk.doctype.note.note import Note +from frappe.model.naming import make_autoname, parse_naming_series, revert_series_if_last +from frappe.utils import cint, now_datetime + + +class CustomTestNote(Note): + @property + def age(self): + return now_datetime() - self.creation class TestDocument(unittest.TestCase): @@ -256,4 +265,50 @@ class TestDocument(unittest.TestCase): def test_limit_for_get(self): doc = frappe.get_doc("DocType", "DocType") # assuming DocType has more that 3 Data fields - self.assertEquals(len(doc.get("fields", filters={"fieldtype": "Data"}, limit=3)), 3) \ No newline at end of file + self.assertEquals(len(doc.get("fields", filters={"fieldtype": "Data"}, limit=3)), 3) + + def test_virtual_fields(self): + """Virtual fields are accessible via API and Form views, whenever .as_dict is invoked + """ + frappe.db.delete("Custom Field", {"dt": "Note", "fieldname":"age"}) + + def patch_note(): + return patch("frappe.controllers", new={frappe.local.site: {'Note': CustomTestNote}}) + + @contextmanager + def customize_note(with_options=False): + options = "frappe.utils.now_datetime() - doc.creation" if with_options else "" + custom_field = frappe.get_doc({ + "doctype": "Custom Field", + "dt": "Note", + "fieldname": "age", + "fieldtype": "Data", + "read_only": True, + "is_virtual": True, + "options": options, + }) + + try: + yield custom_field.insert(ignore_if_duplicate=True) + finally: + custom_field.delete() + + with patch_note(): + doc = frappe.get_last_doc("Note") + self.assertIsInstance(doc, CustomTestNote) + self.assertIsInstance(doc.age, timedelta) + self.assertIsNone(doc.as_dict().get("age")) + self.assertIsNone(doc.get_valid_dict().get("age")) + + with customize_note(), patch_note(): + doc = frappe.get_last_doc("Note") + self.assertIsInstance(doc, CustomTestNote) + self.assertIsInstance(doc.age, timedelta) + self.assertIsInstance(doc.as_dict().get("age"), timedelta) + self.assertIsInstance(doc.get_valid_dict().get("age"), timedelta) + + with customize_note(with_options=True): + doc = frappe.get_last_doc("Note") + self.assertIsInstance(doc, Note) + self.assertIsInstance(doc.as_dict().get("age"), timedelta) + self.assertIsInstance(doc.get_valid_dict().get("age"), timedelta) diff --git a/frappe/website/doctype/web_form/web_form.js b/frappe/website/doctype/web_form/web_form.js index d69d21c64d..1f27b350be 100644 --- a/frappe/website/doctype/web_form/web_form.js +++ b/frappe/website/doctype/web_form/web_form.js @@ -60,7 +60,7 @@ frappe.ui.form.on("Web Form", { options: field.options, reqd: field.reqd, default: field.default, - read_only: field.read_only, + read_only: field.read_only || field.is_virtual, depends_on: field.depends_on, mandatory_depends_on: field.mandatory_depends_on, read_only_depends_on: field.read_only_depends_on, From f98be2e19fcd5c8958f555aff6b6473b4ce61ddd Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Fri, 25 Feb 2022 15:05:31 +0530 Subject: [PATCH 08/15] chore: undo unintentional creation timestamp change Co-authored-by: gavin --- .../doctype/customize_form_field/customize_form_field.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/custom/doctype/customize_form_field/customize_form_field.json b/frappe/custom/doctype/customize_form_field/customize_form_field.json index 1d721c57ec..e29a485663 100644 --- a/frappe/custom/doctype/customize_form_field/customize_form_field.json +++ b/frappe/custom/doctype/customize_form_field/customize_form_field.json @@ -1,7 +1,7 @@ { "actions": [], "autoname": "hash", - "creation": "2022-01-25 14:14:07.063385", + "creation": "2013-02-22 01:27:32", "doctype": "DocType", "document_type": "Setup", "editable_grid": 1, From 429e1420211ca25d117ffe41c0e6c7060ff99f98 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Fri, 25 Feb 2022 16:01:47 +0530 Subject: [PATCH 09/15] chore: add missing modified timestamp --- .../doctype/customize_form_field/customize_form_field.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frappe/custom/doctype/customize_form_field/customize_form_field.json b/frappe/custom/doctype/customize_form_field/customize_form_field.json index c87668b2cb..1cc4c9f623 100644 --- a/frappe/custom/doctype/customize_form_field/customize_form_field.json +++ b/frappe/custom/doctype/customize_form_field/customize_form_field.json @@ -450,7 +450,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2022-02-08 19:38:16.111199", + "modified": "2022-02-25 16:01:12.616736", "modified_by": "Administrator", "module": "Custom", "name": "Customize Form Field", @@ -460,4 +460,4 @@ "sort_field": "modified", "sort_order": "ASC", "states": [] -} +} \ No newline at end of file From c1f1bf406160bccdd861a8ba98b78dcacceb050b Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Fri, 25 Feb 2022 20:41:05 +0530 Subject: [PATCH 10/15] fix: Unable to open query report --- frappe/desk/query_report.py | 19 +++++-------------- .../js/frappe/views/reports/query_report.js | 3 ++- 2 files changed, 7 insertions(+), 15 deletions(-) diff --git a/frappe/desk/query_report.py b/frappe/desk/query_report.py index 9ed956e986..a0b0aec255 100644 --- a/frappe/desk/query_report.py +++ b/frappe/desk/query_report.py @@ -73,7 +73,7 @@ def get_report_result(report, filters): return res @frappe.read_only() -def generate_report_result(report, filters=None, user=None, custom_columns=None, report_settings=None): +def generate_report_result(report, filters=None, user=None, custom_columns=None, is_tree=False, parent_field=None): user = user or frappe.session.user filters = filters or [] @@ -108,7 +108,7 @@ def generate_report_result(report, filters=None, user=None, custom_columns=None, result = get_filtered_data(report.ref_doctype, columns, result, user) if cint(report.add_total_row) and result and not skip_total_row: - result = add_total_row(result, columns, report_settings=report_settings) + result = add_total_row(result, columns, is_tree=is_tree, parent_field=parent_field) return { "result": result, @@ -210,7 +210,7 @@ def get_script(report_name): @frappe.whitelist() @frappe.read_only() -def run(report_name, filters=None, user=None, ignore_prepared_report=False, custom_columns=None, report_settings=None): +def run(report_name, filters=None, user=None, ignore_prepared_report=False, custom_columns=None, is_tree=False, parent_field=None): report = get_report_doc(report_name) if not user: user = frappe.session.user @@ -238,7 +238,7 @@ def run(report_name, filters=None, user=None, ignore_prepared_report=False, cust dn = "" result = get_prepared_report_result(report, filters, dn, user) else: - result = generate_report_result(report, filters, user, custom_columns, report_settings) + result = generate_report_result(report, filters, user, custom_columns, is_tree, parent_field) result["add_total_row"] = report.add_total_row and not result.get( "skip_total_row", False @@ -435,18 +435,9 @@ def build_xlsx_data(columns, data, visible_idx, include_indentation, ignore_visi return result, column_widths -def add_total_row(result, columns, meta=None, report_settings=None): +def add_total_row(result, columns, meta=None, is_tree=False, parent_field=None): total_row = [""] * len(columns) has_percent = [] - is_tree = False - parent_field = '' - - if report_settings: - if isinstance(report_settings, (str,)): - report_settings = json.loads(report_settings) - - is_tree = report_settings.get('tree') - parent_field = report_settings.get('parent_field') for i, col in enumerate(columns): fieldtype, options, fieldname = None, None, None diff --git a/frappe/public/js/frappe/views/reports/query_report.js b/frappe/public/js/frappe/views/reports/query_report.js index c226a3a458..7c12809fcd 100644 --- a/frappe/public/js/frappe/views/reports/query_report.js +++ b/frappe/public/js/frappe/views/reports/query_report.js @@ -578,7 +578,8 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { args: { report_name: this.report_name, filters: filters, - report_settings: this.report_settings + is_tree: this.report_settings.tree, + parent_field: this.report_settings.parent_field }, callback: resolve, always: () => this.page.btn_secondary.prop('disabled', false) From e2a17c38fdeba48537097798db05b2201c4d3f91 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Sat, 26 Feb 2022 12:15:25 +0530 Subject: [PATCH 11/15] revert: ignore_if_duplicate in postgres breaking CI --- frappe/desk/page/setup_wizard/setup_wizard.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/desk/page/setup_wizard/setup_wizard.py b/frappe/desk/page/setup_wizard/setup_wizard.py index 74101a6e1f..0c32e886f4 100755 --- a/frappe/desk/page/setup_wizard/setup_wizard.py +++ b/frappe/desk/page/setup_wizard/setup_wizard.py @@ -392,7 +392,7 @@ def make_records(records, debug=False): doc.flags.ignore_mandatory = True try: - doc.insert(ignore_permissions=True, ignore_if_duplicate=True) + doc.insert(ignore_permissions=True) frappe.db.commit() except frappe.DuplicateEntryError as e: From dad3a5c44803ceb916f49ae48649362f66e53b08 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 28 Feb 2022 08:40:07 +0530 Subject: [PATCH 12/15] test: Update test case --- frappe/core/doctype/report/test_report.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/frappe/core/doctype/report/test_report.py b/frappe/core/doctype/report/test_report.py index bf63afa5c5..7d434ff166 100644 --- a/frappe/core/doctype/report/test_report.py +++ b/frappe/core/doctype/report/test_report.py @@ -330,7 +330,8 @@ result = [ } ] - result = add_total_row(result, columns, meta=None, report_settings=report_settings) + result = add_total_row(result, columns, meta=None, is_tree=report_settings['tree'], + parent_field=report_settings['parent_field']) self.assertEqual(result[-1][0], "Total") self.assertEqual(result[-1][1], 200) self.assertEqual(result[-1][2], 150.50) From a8a544cf4494605aa15f96d04385bba0ca9fa22a Mon Sep 17 00:00:00 2001 From: DaizyModi <54097382+DaizyModi@users.noreply.github.com> Date: Mon, 28 Feb 2022 11:23:45 +0530 Subject: [PATCH 13/15] fix(grid): Select All checkbox not visible (#15913) In Child Tables such as Stock Entry Detail, Sales Invoice Item (wherever `grid.template` applies) non-editable grid doesn't let users select all rows of the child table. ![image](https://user-images.githubusercontent.com/54097382/153049801-26f2ca38-4892-4aff-9fe5-238a3fa58f5e.png) This fix will let users select all rows using a checkbox in the header row of the child table. ## Screenshots **Before** ![image](https://user-images.githubusercontent.com/16315650/153127765-1e575096-b7da-41f1-bed4-e5d1c48cb98e.png) **After** ![image](https://user-images.githubusercontent.com/16315650/153127672-9054014d-fbc2-4fff-9c7b-6dd0c7c6cc1c.png) --- frappe/public/js/frappe/form/grid_row.js | 15 +++++++-------- frappe/public/scss/common/grid.scss | 13 ++++++++++++- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/frappe/public/js/frappe/form/grid_row.js b/frappe/public/js/frappe/form/grid_row.js index cd8bde1f57..2e9c6f970a 100644 --- a/frappe/public/js/frappe/form/grid_row.js +++ b/frappe/public/js/frappe/form/grid_row.js @@ -183,21 +183,20 @@ export default class GridRow { render_template() { this.set_row_index(); - if(this.row_display) { + if (this.row_display) { this.row_display.remove(); } // row index - if(this.doc) { - if(!this.row_index) { - this.row_index = $('
'+this.row_check_html+'
').appendTo(this.row); - } + if (!this.row_index) { + this.row_index = $(`
${this.row_check_html}
`).appendTo(this.row); + } + + if (this.doc) { this.row_index.find('span').html(this.doc.idx); } - this.row_display = $('
'+ - +'
').appendTo(this.row) + this.row_display = $('
').appendTo(this.row) .html(frappe.render(this.grid.template, { doc: this.doc ? frappe.get_format_helper(this.doc) : null, frm: this.frm, diff --git a/frappe/public/scss/common/grid.scss b/frappe/public/scss/common/grid.scss index bfce93dbcc..ab6bcfe8e8 100644 --- a/frappe/public/scss/common/grid.scss +++ b/frappe/public/scss/common/grid.scss @@ -54,7 +54,7 @@ } .form-grid .grid-heading-row .template-row { - margin-left: 20px; + margin-left: 8px; } .form-grid .template-row { @@ -88,6 +88,17 @@ margin-top: 2px; } +.template-row-index { + float: left; + margin-left: 15px; + margin-top: 8px; + margin-right: -20px; + + span { + margin-left: 5px; + } +} + .editable-form .grid-static-col.bold { font-weight: bold; } From 741ea13db228a702d63b58b370f144c8e09f3d1e Mon Sep 17 00:00:00 2001 From: Sagar Vora Date: Wed, 16 Feb 2022 14:04:41 +0530 Subject: [PATCH 14/15] fix: forget return value after `run_method` execution --- frappe/model/document.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/model/document.py b/frappe/model/document.py index cb36c18b47..37e70e8126 100644 --- a/frappe/model/document.py +++ b/frappe/model/document.py @@ -1154,7 +1154,7 @@ class Document(BaseDocument): for f in hooks: add_to_return_value(self, f(self, method, *args, **kwargs)) - return self._return_value + return self.__dict__.pop("_return_value", None) return runner From 2b48c083403d93690ecc255d26163204fde4e675 Mon Sep 17 00:00:00 2001 From: ChillarAnand Date: Mon, 28 Feb 2022 10:28:10 +0530 Subject: [PATCH 15/15] fix: Fix flaky tests --- frappe/desk/form/load.py | 5 +++++ frappe/social/doctype/energy_point_log/energy_point_log.py | 3 ++- frappe/tests/test_utils.py | 2 -- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/frappe/desk/form/load.py b/frappe/desk/form/load.py index 4d35ebf5e8..b5dfacb1d6 100644 --- a/frappe/desk/form/load.py +++ b/frappe/desk/form/load.py @@ -11,8 +11,10 @@ from frappe.model.utils.user_settings import get_user_settings from frappe.permissions import get_doc_permissions from frappe.desk.form.document_follow import is_document_followed from frappe import _ +from frappe import _dict from urllib.parse import quote + @frappe.whitelist() def getdoc(doctype, name, user=None): """ @@ -50,8 +52,11 @@ def getdoc(doctype, name, user=None): doc.add_seen() set_link_titles(doc) + if frappe.response.docs is None: + frappe.response = _dict({"docs": []}) frappe.response.docs.append(doc) + @frappe.whitelist() def getdoctype(doctype, with_parent=False, cached_timestamp=None): """load doctype""" diff --git a/frappe/social/doctype/energy_point_log/energy_point_log.py b/frappe/social/doctype/energy_point_log/energy_point_log.py index 86843302e9..5e6a9df16f 100644 --- a/frappe/social/doctype/energy_point_log/energy_point_log.py +++ b/frappe/social/doctype/energy_point_log/energy_point_log.py @@ -164,6 +164,7 @@ def get_alert_dict(doc): return alert_dict + def create_energy_points_log(ref_doctype, ref_name, doc, apply_only_once=False): doc = frappe._dict(doc) @@ -171,7 +172,7 @@ def create_energy_points_log(ref_doctype, ref_name, doc, apply_only_once=False): ref_name, doc.rule, None if apply_only_once else doc.user) if log_exists: - return + return frappe.get_doc('Energy Point Log', log_exists) new_log = frappe.new_doc('Energy Point Log') new_log.reference_doctype = ref_doctype diff --git a/frappe/tests/test_utils.py b/frappe/tests/test_utils.py index 15029e961a..18fca9de8c 100644 --- a/frappe/tests/test_utils.py +++ b/frappe/tests/test_utils.py @@ -510,5 +510,3 @@ class TestLinkTitle(unittest.TestCase): todo.delete() user.delete() prop_setter.delete() - -