From 002e27e8a5f04bcc1d616e1dcb4737f9cf892e63 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Fri, 12 May 2017 11:13:09 +0530 Subject: [PATCH 01/20] Fixes for showing version changes for grid values (#3273) --- frappe/public/js/frappe/form/footer/timeline.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/frappe/public/js/frappe/form/footer/timeline.js b/frappe/public/js/frappe/form/footer/timeline.js index a296762426..54a9c44b96 100644 --- a/frappe/public/js/frappe/form/footer/timeline.js +++ b/frappe/public/js/frappe/form/footer/timeline.js @@ -447,8 +447,9 @@ frappe.ui.form.Timeline = Class.extend({ var parts = [], count = 0; data.row_changed.every(function(row) { row[3].every(function(p) { - var df = frappe.meta.get_docfield(me.frm.fields_dict[row[0]].grid.doctype, - p[0], me.frm.docname); + var df = me.frm.fields_dict[row[0]] && + frappe.meta.get_docfield(me.frm.fields_dict[row[0]].grid.doctype, + p[0], me.frm.docname); if(df && !df.hidden) { field_display_status = frappe.perm.get_field_display_status(df, From b8f1fdc92e2ce81e6f4a466a963f1937e0b910b6 Mon Sep 17 00:00:00 2001 From: Makarand Bauskar Date: Fri, 12 May 2017 11:14:30 +0530 Subject: [PATCH 02/20] [minor] fixed the set_query for the doctype field in Custom Field (#3275) --- frappe/custom/doctype/custom_field/custom_field.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/frappe/custom/doctype/custom_field/custom_field.js b/frappe/custom/doctype/custom_field/custom_field.js index 3d3f03ef9c..30a5bcef5f 100644 --- a/frappe/custom/doctype/custom_field/custom_field.js +++ b/frappe/custom/doctype/custom_field/custom_field.js @@ -13,7 +13,9 @@ frappe.ui.form.on('Custom Field', { if(user!=="Administrator") { filters.push(['DocType', 'module', '!=', 'Core']) } - return filters + return { + "filters": filters + } }); }, refresh: function(frm) { From 33e1b346bc7cf6e54bf00b7cef9f50f257a5ee8e Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Fri, 12 May 2017 13:29:02 +0530 Subject: [PATCH 03/20] Updated modified date in system settings --- frappe/core/doctype/system_settings/system_settings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/core/doctype/system_settings/system_settings.json b/frappe/core/doctype/system_settings/system_settings.json index d742681469..0a84b1a791 100644 --- a/frappe/core/doctype/system_settings/system_settings.json +++ b/frappe/core/doctype/system_settings/system_settings.json @@ -902,7 +902,7 @@ "issingle": 1, "istable": 0, "max_attachments": 0, - "modified": "2017-05-01 15:27:11.079447", + "modified": "2017-05-11 15:27:11.079447", "modified_by": "Administrator", "module": "Core", "name": "System Settings", From 1a63f988c71ff42423fe03e51eca063ddbcf20c0 Mon Sep 17 00:00:00 2001 From: Sagar Vora Date: Fri, 12 May 2017 15:20:22 +0530 Subject: [PATCH 04/20] [minor] Fail travis build immediately, if python tests fail. (#3269) --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 7e2c0d5e1a..e9e5c0e183 100644 --- a/.travis.yml +++ b/.travis.yml @@ -24,6 +24,7 @@ install: - cp -r $TRAVIS_BUILD_DIR/test_sites/test_site ~/frappe-bench/sites/ script: + - set -e - bench --verbose run-tests - bench reinstall --yes - testcafe chrome apps/frappe/frappe/tests/testcafe/ From 931ef1f2442b33f406ca81be00f8a9b912fd156a Mon Sep 17 00:00:00 2001 From: Makarand Bauskar Date: Fri, 12 May 2017 15:20:47 +0530 Subject: [PATCH 05/20] [minor] new line after install & reinstallation of new-site (#3262) --- frappe/utils/install.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/utils/install.py b/frappe/utils/install.py index 066cb7b4ce..a557c48ff3 100644 --- a/frappe/utils/install.py +++ b/frappe/utils/install.py @@ -105,7 +105,7 @@ def import_country_and_currency(): country = frappe._dict(data[name]) add_country_and_currency(name, country) - print + print() # enable frequently used currencies for currency in ("INR", "USD", "GBP", "EUR", "AED", "AUD", "JPY", "CNY", "CHF"): From 77ef3e8a272eedac2cc1b8f992776a17bcb649a9 Mon Sep 17 00:00:00 2001 From: Saurabh Date: Fri, 12 May 2017 15:22:01 +0530 Subject: [PATCH 06/20] [fix] list stripe payment gateway under integrations (#3270) * [fix] list stripe payment gateway under integrations * Update integrations.py --- frappe/config/integrations.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/frappe/config/integrations.py b/frappe/config/integrations.py index 53a942a32d..5eae544c75 100644 --- a/frappe/config/integrations.py +++ b/frappe/config/integrations.py @@ -7,6 +7,11 @@ def get_data(): "label": _("Payments"), "icon": "fa fa-star", "items": [ + { + "type": "doctype", + "name": "Stripe Settings", + "description": _("Stripe payment gateway settings"), + }, { "type": "doctype", "name": "PayPal Settings", From bfb181333c1bc9afb0b129df0854ad212a3b57a2 Mon Sep 17 00:00:00 2001 From: Manas Solanki Date: Fri, 12 May 2017 15:23:11 +0530 Subject: [PATCH 07/20] handle the escape sequence in the html2text library (#3272) --- frappe/utils/xlsxutils.py | 31 ++++++++++++++++++++++--------- 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/frappe/utils/xlsxutils.py b/frappe/utils/xlsxutils.py index 01feb84497..6728222535 100644 --- a/frappe/utils/xlsxutils.py +++ b/frappe/utils/xlsxutils.py @@ -9,7 +9,6 @@ import openpyxl from cStringIO import StringIO from openpyxl.styles import Font -import html2text # return xlsx file object def make_xlsx(data, sheet_name): @@ -24,19 +23,33 @@ def make_xlsx(data, sheet_name): clean_row = [] for item in row: if isinstance(item, basestring): - obj = html2text.HTML2Text() - obj.ignore_links = True - obj.body_width = 0 - obj = obj.handle(unicode(item or "")) - obj = obj.rsplit('\n', 1) - value = obj[0] + value = handle_html(item) else: value = item - clean_row.append(value) ws.append(clean_row) xlsx_file = StringIO() wb.save(xlsx_file) - return xlsx_file \ No newline at end of file + return xlsx_file + + +def handle_html(data): + # import html2text + from html2text import unescape, HTML2Text + + h = HTML2Text() + h.unicode_snob = True + h = h.unescape(data or "") + + obj = HTML2Text() + obj.ignore_links = True + obj.body_width = 0 + value = obj.handle(h) + value = value.split('\n', 1) + value = value[0].split('# ',1) + if len(value) < 2: + return value[0] + else: + return value[1] From 8f684444d017feaea5be52e6c8880ba24c744a51 Mon Sep 17 00:00:00 2001 From: Charles-Henri Decultot Date: Fri, 12 May 2017 11:53:51 +0200 Subject: [PATCH 08/20] Correction of doctype translations in modal dialog (#3277) --- frappe/public/js/frappe/form/quick_entry.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/public/js/frappe/form/quick_entry.js b/frappe/public/js/frappe/form/quick_entry.js index b3040cc1bd..2e830ce54d 100644 --- a/frappe/public/js/frappe/form/quick_entry.js +++ b/frappe/public/js/frappe/form/quick_entry.js @@ -31,7 +31,7 @@ frappe.ui.form.quick_entry = function(doctype, success) { } var dialog = new frappe.ui.Dialog({ - title: __("New {0}", [doctype]), + title: __("New {0}", [__(doctype)]), fields: mandatory, }); From 355a494641c610fa30ca7509898232502eb8d8a9 Mon Sep 17 00:00:00 2001 From: Faris Ansari Date: Fri, 12 May 2017 15:25:02 +0530 Subject: [PATCH 09/20] Kanban fixes (#3278) - redirect back to list when board is not found - fix no default columns when custom field is used --- frappe/desk/doctype/kanban_board/kanban_board.py | 13 ++++++++----- frappe/public/js/frappe/list/list_renderer.js | 2 +- .../public/js/frappe/views/kanban/kanban_board.js | 9 +++++++++ 3 files changed, 18 insertions(+), 6 deletions(-) diff --git a/frappe/desk/doctype/kanban_board/kanban_board.py b/frappe/desk/doctype/kanban_board/kanban_board.py index 4d9072e9e4..062b72590c 100644 --- a/frappe/desk/doctype/kanban_board/kanban_board.py +++ b/frappe/desk/doctype/kanban_board/kanban_board.py @@ -118,10 +118,13 @@ def update_order(board_name, order): def quick_kanban_board(doctype, board_name, field_name): '''Create new KanbanBoard quickly with default options''' doc = frappe.new_doc('Kanban Board') - options = frappe.get_value('DocField', dict( - parent=doctype, - fieldname=field_name - ), 'options') + + meta = frappe.get_meta(doctype) + + options = '' + for field in meta.fields: + if field.fieldname == field_name: + options = field.options columns = [] if options: @@ -198,4 +201,4 @@ def set_indicator(board_name, column_name, indicator): def save_filters(board_name, filters): '''Save filters silently''' frappe.db.set_value('Kanban Board', board_name, 'filters', - filters, update_modified=False) + filters, update_modified=False) diff --git a/frappe/public/js/frappe/list/list_renderer.js b/frappe/public/js/frappe/list/list_renderer.js index b2368bf5ec..1f0209f74b 100644 --- a/frappe/public/js/frappe/list/list_renderer.js +++ b/frappe/public/js/frappe/list/list_renderer.js @@ -144,7 +144,7 @@ frappe.views.ListRenderer = Class.extend({ } // kanban column fields if (me.meta.__kanban_column_fields) { - me.fields = me.fields.concat(me.meta.__kanban_column_fields); + me.meta.__kanban_column_fields.map(add_field); } }, set_columns: function () { diff --git a/frappe/public/js/frappe/views/kanban/kanban_board.js b/frappe/public/js/frappe/views/kanban/kanban_board.js index 71b2221f87..fa3695baf9 100644 --- a/frappe/public/js/frappe/views/kanban/kanban_board.js +++ b/frappe/public/js/frappe/views/kanban/kanban_board.js @@ -41,6 +41,12 @@ frappe.provide("frappe.views"); columns: columns, cur_list: opts.cur_list }); + }) + .fail(function() { + // redirect back to List + setTimeout(() => { + frappe.set_route('List', opts.doctype, 'List'); + }, 2000); }); }, update_cards: function (updater, cards) { @@ -1038,6 +1044,9 @@ frappe.provide("frappe.views"); function is_filters_modified(board, cur_list) { return new Promise(function(resolve, reject) { setTimeout(function() { + // sometimes the filter_list is not initiated, so early return + if(!cur_list.filter_list) resolve(false); + var list_filters = JSON.stringify(cur_list.filter_list.get_filters()); resolve(list_filters !== board.filters); }, 2000); From 6cb0d6da77d6abf9ff7d23948fc893c714571433 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Fri, 12 May 2017 15:25:31 +0530 Subject: [PATCH 10/20] Re-run global search patch (#3271) --- frappe/patches.txt | 2 +- frappe/patches/v8_0/update_records_in_global_search.py | 2 ++ frappe/utils/global_search.py | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/frappe/patches.txt b/frappe/patches.txt index 285c96f4aa..1b2dab39a2 100644 --- a/frappe/patches.txt +++ b/frappe/patches.txt @@ -12,7 +12,7 @@ frappe.patches.v8_0.drop_in_dialog execute:frappe.reload_doc('core', 'doctype', 'docfield', force=True) #2017-03-03 execute:frappe.reload_doc('core', 'doctype', 'docperm') #2017-03-03 frappe.patches.v8_0.drop_is_custom_from_docperm -frappe.patches.v8_0.update_records_in_global_search +frappe.patches.v8_0.update_records_in_global_search #11-05-2017 frappe.patches.v8_0.update_published_in_global_search execute:frappe.reload_doc('core', 'doctype', 'custom_docperm') execute:frappe.reload_doc('core', 'doctype', 'deleted_document') diff --git a/frappe/patches/v8_0/update_records_in_global_search.py b/frappe/patches/v8_0/update_records_in_global_search.py index 397128c64f..8ee8f36de8 100644 --- a/frappe/patches/v8_0/update_records_in_global_search.py +++ b/frappe/patches/v8_0/update_records_in_global_search.py @@ -1,5 +1,7 @@ +import frappe from frappe.utils.global_search import get_doctypes_with_global_search, rebuild_for_doctype def execute(): + frappe.cache().delete_value('doctypes_with_global_search') for doctype in get_doctypes_with_global_search(with_child_tables=False): rebuild_for_doctype(doctype) diff --git a/frappe/utils/global_search.py b/frappe/utils/global_search.py index c7f8c5ecc9..8d481a814b 100644 --- a/frappe/utils/global_search.py +++ b/frappe/utils/global_search.py @@ -54,7 +54,7 @@ def rebuild_for_doctype(doctype): :param doctype: Doctype ''' def _get_filters(): - filters = frappe._dict({ "docstatus": ["!=", 1] }) + filters = frappe._dict({ "docstatus": ["!=", 2] }) if meta.has_field("enabled"): filters.enabled = 1 if meta.has_field("disabled"): From eb9551fca7aa3ee3447aa3676ffad306170ea220 Mon Sep 17 00:00:00 2001 From: Saurabh Date: Fri, 12 May 2017 15:41:26 +0530 Subject: [PATCH 11/20] [fix] check all custom perms before adding permissions from standard docperm (#3280) --- frappe/permissions.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/frappe/permissions.py b/frappe/permissions.py index 2455f8428a..22342d9811 100644 --- a/frappe/permissions.py +++ b/frappe/permissions.py @@ -330,7 +330,8 @@ def get_all_perms(role): '''Returns valid permissions for a given role''' perms = frappe.get_all('DocPerm', fields='*', filters=dict(role=role)) custom_perms = frappe.get_all('Custom DocPerm', fields='*', filters=dict(role=role)) - doctypes_with_custom_perms = list(set(p.parent for p in custom_perms)) + doctypes_with_custom_perms = frappe.db.sql_list("""select distinct parent + from `tabCustom DocPerm`""") for p in perms: if p.parent not in doctypes_with_custom_perms: From b23aa1446b0841ca670921b2199b37f4545cbeab Mon Sep 17 00:00:00 2001 From: Prateeksha Singh Date: Mon, 15 May 2017 11:29:41 +0530 Subject: [PATCH 12/20] Multiselect dialog for getting items (#3255) * First approach by making a control * Implement multi select for child tables * Basic UI and items fetch in place * Multiselect with checkboxes * Functional modal with filters and new_doc * Map filter fields to target_doc * pass json arrays instead of strings * Get items from quotation (in SO) working * [minor] fix link route in list * [minor] cleanup * Add date, select first by default * map_docs test, default date field * [minor][fix] make new button bug * [minor] move map_docs to erpnext * [minor] format dates --- frappe/desk/search.py | 19 +- frappe/model/mapper.py | 23 +- frappe/public/build.json | 2 + frappe/public/css/desk.css | 7 + .../js/frappe/form/multi_select_dialog.js | 208 ++++++++++++++++++ frappe/public/js/frappe/model/model.js | 5 + frappe/public/js/frappe/ui/field_group.js | 1 - .../js/frappe/ui/toolbar/search_utils.js | 6 +- frappe/public/less/desk.less | 13 +- 9 files changed, 263 insertions(+), 21 deletions(-) create mode 100644 frappe/public/js/frappe/form/multi_select_dialog.js diff --git a/frappe/desk/search.py b/frappe/desk/search.py index 0bb6ac0a0b..734da033a3 100644 --- a/frappe/desk/search.py +++ b/frappe/desk/search.py @@ -3,7 +3,7 @@ # Search from __future__ import unicode_literals -import frappe +import frappe, json from frappe.utils import cstr, unique # this is called by the Link Field @@ -16,7 +16,7 @@ def search_link(doctype, txt, query=None, filters=None, page_len=20, searchfield # this is called by the search box @frappe.whitelist() def search_widget(doctype, txt, query=None, searchfield=None, start=0, - page_len=10, filters=None, as_dict=False): + page_len=10, filters=None, filter_fields=None, as_dict=False): if isinstance(filters, basestring): import json filters = json.loads(filters) @@ -76,20 +76,24 @@ def search_widget(doctype, txt, query=None, searchfield=None, start=0, if meta.get("fields", {"fieldname":"disabled", "fieldtype":"Check"}): filters.append([doctype, "disabled", "!=", 1]) + # format a list of fields combining search fields and filter fields fields = get_std_fields_list(meta, searchfield or "name") + if filter_fields: + fields = list(set(fields + json.loads(filter_fields))) + formatted_fields = ['`tab%s`.`%s`' % (meta.name, f.strip()) for f in fields] # find relevance as location of search term from the beginning of string `name`. used for sorting results. - fields.append("""locate("{_txt}", `tab{doctype}`.`name`) as `_relevance`""".format( + formatted_fields.append("""locate("{_txt}", `tab{doctype}`.`name`) as `_relevance`""".format( _txt=frappe.db.escape((txt or "").replace("%", "")), doctype=frappe.db.escape(doctype))) - + # In order_by, `idx` gets second priority, because it stores link count from frappe.model.db_query import get_order_by order_by_based_on_meta = get_order_by(doctype, meta) order_by = "if(_relevance, _relevance, 99999), idx desc, {0}".format(order_by_based_on_meta) - + values = frappe.get_list(doctype, - filters=filters, fields=fields, + filters=filters, fields=formatted_fields, or_filters = or_filters, limit_start = start, limit_page_length=page_len, order_by=order_by, @@ -97,6 +101,7 @@ def search_widget(doctype, txt, query=None, searchfield=None, start=0, as_list=not as_dict) # remove _relevance from results + frappe.response["fields"] = fields frappe.response["values"] = [r[:-1] for r in values] def get_std_fields_list(meta, key): @@ -107,7 +112,7 @@ def get_std_fields_list(meta, key): if not key in sflist: sflist = sflist + [key] - return ['`tab%s`.`%s`' % (meta.name, f.strip()) for f in sflist] + return sflist def build_for_autosuggest(res): results = [] diff --git a/frappe/model/mapper.py b/frappe/model/mapper.py index d8384eee09..72a9a77e3c 100644 --- a/frappe/model/mapper.py +++ b/frappe/model/mapper.py @@ -25,16 +25,21 @@ def make_mapped_doc(method, source_name, selected_children=None): return method(source_name) +@frappe.whitelist() +def map_docs(method, source_names, target_doc): + '''Returns the mapped document calling the given mapper method + with each of the given source docs on the target doc''' + method = frappe.get_attr(method) + if method not in frappe.whitelisted: + raise frappe.PermissionError + + for src in json.loads(source_names): + target_doc = method(src, target_doc) + return target_doc def get_mapped_doc(from_doctype, from_docname, table_maps, target_doc=None, postprocess=None, ignore_permissions=False, ignore_child_tables=False): - source_doc = frappe.get_doc(from_doctype, from_docname) - - if not ignore_permissions: - if not source_doc.has_permission("read"): - source_doc.raise_no_permission_to("read") - # main if not target_doc: target_doc = frappe.new_doc(table_maps[from_doctype]["doctype"]) @@ -44,6 +49,12 @@ def get_mapped_doc(from_doctype, from_docname, table_maps, target_doc=None, if not ignore_permissions and not target_doc.has_permission("create"): target_doc.raise_no_permission_to("create") + source_doc = frappe.get_doc(from_doctype, from_docname) + + if not ignore_permissions: + if not source_doc.has_permission("read"): + source_doc.raise_no_permission_to("read") + map_doc(source_doc, target_doc, table_maps[source_doc.doctype]) row_exists_for_parentfield = {} diff --git a/frappe/public/build.json b/frappe/public/build.json index 11bd5ea34e..598b964574 100755 --- a/frappe/public/build.json +++ b/frappe/public/build.json @@ -30,6 +30,7 @@ "public/js/frappe/ui/field_group.js", "public/js/frappe/form/control.js", "public/js/frappe/form/link_selector.js", + "public/js/frappe/form/multi_select_dialog.js", "public/js/frappe/ui/dialog.js" ], "css/desk.min.css": [ @@ -104,6 +105,7 @@ "public/js/frappe/ui/field_group.js", "public/js/frappe/form/control.js", "public/js/frappe/form/link_selector.js", + "public/js/frappe/form/multi_select_dialog.js", "public/js/frappe/ui/dialog.js", "public/js/frappe/ui/app_icon.js", diff --git a/frappe/public/css/desk.css b/frappe/public/css/desk.css index 76d352a6b9..302fe38647 100644 --- a/frappe/public/css/desk.css +++ b/frappe/public/css/desk.css @@ -978,6 +978,13 @@ input[type="checkbox"]:checked:before { font-size: 13px; color: #3b99fc; } +.multiselect-empty-state { + min-height: 300px; + display: flex; + align-items: center; + justify-content: center; + height: 100%; +} @-moz-document url-prefix() { input[type="checkbox"] { visibility: visible; diff --git a/frappe/public/js/frappe/form/multi_select_dialog.js b/frappe/public/js/frappe/form/multi_select_dialog.js new file mode 100644 index 0000000000..dd4251bd1f --- /dev/null +++ b/frappe/public/js/frappe/form/multi_select_dialog.js @@ -0,0 +1,208 @@ +// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors +// MIT License. See license.txt + +frappe.ui.form.MultiSelectDialog = Class.extend({ + init: function(opts) { + /* Options: doctype, target, setters, get_query, action */ + $.extend(this, opts); + + var me = this; + if(this.doctype!="[Select]") { + frappe.model.with_doctype(this.doctype, function(r) { + me.make(); + }); + } else { + this.make(); + } + }, + make: function() { + let me = this; + + let fields = []; + let count = 0; + if(!this.date_field) { + this.date_field = "transaction_date"; + } + Object.keys(this.setters).forEach(function(setter) { + fields.push({ + fieldtype: me.target.fields_dict[setter].df.fieldtype, + label: me.target.fields_dict[setter].df.label, + fieldname: setter, + options: me.target.fields_dict[setter].df.options, + default: me.setters[setter] + }); + if (count++ < Object.keys(me.setters).length - 1) { + fields.push({fieldtype: "Column Break"}); + } + }); + + fields = fields.concat([ + { fieldtype: "Section Break" }, + { fieldtype: "HTML", fieldname: "results_area" }, + { fieldtype: "Button", fieldname: "make_new", label: __("Make a new " + me.doctype) } + ]); + + let doctype_plural = !this.doctype.endsWith('y') ? this.doctype + 's' + : this.doctype.slice(0, -1) + 'ies'; + + this.dialog = new frappe.ui.Dialog({ + title: __("Select {0}", [(this.doctype=='[Select]') ? __("value") : __(doctype_plural)]), + fields: fields, + primary_action_label: __("Get Items"), + primary_action: function() { + me.action(me.get_checked_values(), me.args); + } + }); + + this.$parent = $(this.dialog.body); + this.$wrapper = this.dialog.fields_dict.results_area.$wrapper.append(`
`); + this.$results = this.$wrapper.find('.results'); + this.$make_new_btn = this.dialog.fields_dict.make_new.$wrapper; + + this.$placeholder = $(`
+ + +

No ${this.doctype} found

+ +
+
`); + + this.args = {}; + + this.bind_events(); + this.get_results(); + this.dialog.show(); + }, + + bind_events: function() { + let me = this; + this.$results.on('click', '.list-item-container', function (e) { + if (!$(e.target).is(':checkbox') && !$(e.target).is('a')) { + $(this).find(':checkbox').trigger('click'); + } + }); + this.$results.on('click', '.list-item--head :checkbox', (e) => { + this.$results.find('.list-item-container .list-row-check') + .prop("checked", ($(e.target).is(':checked'))); + }); + + this.$parent.find('.input-with-feedback').on('change', (e) => { + this.get_results(); + }); + this.$parent.on('click', '.btn[data-fieldname="make_new"]', (e) => { + frappe.route_options = {}; + Object.keys(this.setters).forEach(function(setter) { + frappe.route_options[setter] = me.dialog.fields_dict[setter].get_value() || undefined; + }); + frappe.new_doc(this.doctype, true); + }); + }, + + get_checked_values: function() { + return this.$results.find('.list-item-container').map(function() { + if ($(this).find('.list-row-check:checkbox:checked').length > 0 ) { + return $(this).attr('data-item-name'); + } + }).get(); + }, + + make_list_row: function(result={}) { + var me = this; + // Make a head row by default (if result not passed) + let head = Object.keys(result).length === 0; + + let contents = ``; + let columns = (["name"].concat(Object.keys(this.setters))).concat("Date"); + columns.forEach(function(column) { + contents += `
+ ${ + head ? __(frappe.model.unscrub(column)) + + : (column !== "name" ? __(result[column]) + : ` + ${__(result[column])}`) + } +
`; + }) + + let $row = $(`
+
+ +
+ ${contents} +
`); + + head ? $row.addClass('list-item--head') + : $row = $(`
`).append($row); + return $row; + }, + + render_result_list: function(results) { + var me = this; + this.$results.empty(); + if(results.length === 0) { + this.$make_new_btn.addClass('hide'); + this.$results.append(me.$placeholder); + return; + } + this.$make_new_btn.removeClass('hide'); + + this.$results.append(this.make_list_row()); + results.forEach((result) => { + me.$results.append(me.make_list_row(result)); + }) + }, + + get_results: function() { + let me = this; + + let filters = this.get_query().filters; + Object.keys(this.setters).forEach(function(setter) { + filters[setter] = me.dialog.fields_dict[setter].get_value() || undefined; + me.args[setter] = filters[setter]; + }); + + let args = { + doctype: me.doctype, + txt: '', + filters: filters, + filter_fields: Object.keys(me.setters).concat([me.date_field]) + } + frappe.call({ + type: "GET", + method:'frappe.desk.search.search_widget', + no_spinner: true, + args: args, + callback: function(r) { + if(r.values) { + let results = []; + r.values.forEach(function(value_list) { + let result = {}; + value_list.forEach(function(value, index){ + if(r.fields[index] === me.date_field) { + result["Date"] = value; + } else { + result[r.fields[index]] = value; + } + }); + result.checked = 0; + result.parsed_date = Date.parse(result["Date"]); + results.push(result); + }); + let min_date = Math.min.apply( Math, results.map((result) => result.parsed_date) ); + + results.map( (result) => { + result["Date"] = frappe.format(result["Date"], {"fieldtype":"Date"}); + if(result.parsed_date === min_date) { + result.checked = 1; + } + }) + me.render_result_list(results); + } + } + }); + }, + +}); \ No newline at end of file diff --git a/frappe/public/js/frappe/model/model.js b/frappe/public/js/frappe/model/model.js index 543a47db1f..10a882db67 100644 --- a/frappe/public/js/frappe/model/model.js +++ b/frappe/public/js/frappe/model/model.js @@ -187,6 +187,11 @@ $.extend(frappe.model, { return txt.replace(/ /g, "_").toLowerCase(); }, + unscrub: function(txt) { + return __(txt || '').replace(/-|_/g, " ").replace(/\w*/g, + function(keywords){return keywords.charAt(0).toUpperCase() + keywords.substr(1).toLowerCase();}); + }, + can_create: function(doctype) { return frappe.boot.user.can_create.indexOf(doctype)!==-1; }, diff --git a/frappe/public/js/frappe/ui/field_group.js b/frappe/public/js/frappe/ui/field_group.js index e9d9c8e248..e40b6d87f5 100644 --- a/frappe/public/js/frappe/ui/field_group.js +++ b/frappe/public/js/frappe/ui/field_group.js @@ -67,7 +67,6 @@ frappe.ui.FieldGroup = frappe.ui.form.Layout.extend({ var f = this.fields_dict[key]; if(f.get_parsed_value) { var v = f.get_parsed_value(); - if(f.df.reqd && is_null(v)) errors.push(__(f.df.label)); diff --git a/frappe/public/js/frappe/ui/toolbar/search_utils.js b/frappe/public/js/frappe/ui/toolbar/search_utils.js index 37bca09203..a771ce9122 100644 --- a/frappe/public/js/frappe/ui/toolbar/search_utils.js +++ b/frappe/public/js/frappe/ui/toolbar/search_utils.js @@ -575,10 +575,6 @@ frappe.search.utils = { return rendered; } - }, + } - unscrub_and_titlecase: function(str) { - return __(str || '').replace(/-|_/g, " ").replace(/\w*/g, - function(keywords){return keywords.charAt(0).toUpperCase() + keywords.substr(1).toLowerCase();}); - }, } \ No newline at end of file diff --git a/frappe/public/less/desk.less b/frappe/public/less/desk.less index 2a9aeb4e64..6a0ad2bb77 100644 --- a/frappe/public/less/desk.less +++ b/frappe/public/less/desk.less @@ -431,7 +431,7 @@ textarea.form-control { flex: 0 0 36px; order: -1; justify-content: flex-end; - + input[type="checkbox"] { margin-right: 0; } @@ -894,8 +894,17 @@ input[type="checkbox"] { } } +// Will not be required after commonifying lists with empty state +.multiselect-empty-state{ + min-height: 300px; + display: flex; + align-items: center; + justify-content: center; + height: 100%; +} -// mozilla doesn't support pseudo elements on checkbox +// mozilla doesn't support +// pseudo elements on checkbox @-moz-document url-prefix() { input[type="checkbox"] { visibility: visible; From 41cc814d892e2babac64f3a47af90c3364769710 Mon Sep 17 00:00:00 2001 From: Makarand Bauskar Date: Mon, 15 May 2017 11:48:46 +0530 Subject: [PATCH 13/20] [minor] ignore user filters if frappe.route_options are passed (#3282) --- frappe/public/js/frappe/views/reports/reportview.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/public/js/frappe/views/reports/reportview.js b/frappe/public/js/frappe/views/reports/reportview.js index a614da842e..df7359b3d3 100644 --- a/frappe/public/js/frappe/views/reports/reportview.js +++ b/frappe/public/js/frappe/views/reports/reportview.js @@ -227,7 +227,7 @@ frappe.views.ReportView = frappe.ui.BaseList.extend({ set_route_filters: function(first_load) { var me = this; - if(frappe.route_options && !this.user_settings.filters) { + if(frappe.route_options) { this.set_filters_from_route_options(); return true; } else if(this.user_settings From 44359d058de61f8867e6c4215741af25ff24155a Mon Sep 17 00:00:00 2001 From: Prateeksha Singh Date: Mon, 15 May 2017 13:57:04 +0530 Subject: [PATCH 14/20] [minor] Oldest first order, preselect first, build bug fix (#3288) --- .../public/js/frappe/form/multi_select_dialog.js | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/frappe/public/js/frappe/form/multi_select_dialog.js b/frappe/public/js/frappe/form/multi_select_dialog.js index dd4251bd1f..d3548c19d6 100644 --- a/frappe/public/js/frappe/form/multi_select_dialog.js +++ b/frappe/public/js/frappe/form/multi_select_dialog.js @@ -129,7 +129,7 @@ frappe.ui.form.MultiSelectDialog = Class.extend({ let $row = $(`
- +
${contents}
`); @@ -191,14 +191,18 @@ frappe.ui.form.MultiSelectDialog = Class.extend({ result.parsed_date = Date.parse(result["Date"]); results.push(result); }); - let min_date = Math.min.apply( Math, results.map((result) => result.parsed_date) ); results.map( (result) => { result["Date"] = frappe.format(result["Date"], {"fieldtype":"Date"}); - if(result.parsed_date === min_date) { - result.checked = 1; - } }) + + results.sort((a, b) => { + return a.parsed_date - b.parsed_date; + }); + + // Preselect oldest entry + results[0].checked = 1 + me.render_result_list(results); } } From 5c6483df974e52c8e1306d6a4bd5f44ec7850fbe Mon Sep 17 00:00:00 2001 From: Faris Ansari Date: Mon, 15 May 2017 13:57:35 +0530 Subject: [PATCH 15/20] Add sort order field from Customize Form (#3284) * Add sort order field from Customize Form - fixes support issue WN-SUP25048 * Handle case when meta_sort_field is undefined --- frappe/public/js/frappe/ui/sort_selector.js | 43 ++++++++++++++------- 1 file changed, 30 insertions(+), 13 deletions(-) diff --git a/frappe/public/js/frappe/ui/sort_selector.js b/frappe/public/js/frappe/ui/sort_selector.js index 0906771dc2..681956b714 100644 --- a/frappe/public/js/frappe/ui/sort_selector.js +++ b/frappe/public/js/frappe/ui/sort_selector.js @@ -91,16 +91,12 @@ frappe.ui.SortSelector = Class.extend({ var me = this; var meta = frappe.get_meta(this.doctype); + var { meta_sort_field, meta_sort_order } = this.get_meta_sort_field(); + if(!this.args.sort_by) { - if(meta.sort_field) { - if(meta.sort_field.indexOf(',')!==-1) { - parts = meta.sort_field.split(',')[0].split(' '); - this.args.sort_by = parts[0]; - this.args.sort_order = parts[1]; - } else { - this.args.sort_by = meta.sort_field; - this.args.sort_order = meta.sort_order.toLowerCase(); - } + if(meta_sort_field) { + this.args.sort_by = meta_sort_field; + this.args.sort_order = meta_sort_order; } else { // default this.args.sort_by = 'modified'; @@ -115,7 +111,7 @@ frappe.ui.SortSelector = Class.extend({ if(!this.args.options) { // default options var _options = [ - {'fieldname': 'modified'}, + {'fieldname': 'modified'} ] // title field @@ -130,9 +126,15 @@ frappe.ui.SortSelector = Class.extend({ } }); - _options.push({'fieldname': 'name'}); - _options.push({'fieldname': 'creation'}); - _options.push({'fieldname': 'idx'}); + // meta sort field + if(meta_sort_field) _options.push({ 'fieldname': meta_sort_field }); + + // more default options + _options.push( + {'fieldname': 'name'}, + {'fieldname': 'creation'}, + {'fieldname': 'idx'} + ) // de-duplicate this.args.options = _options.uniqBy(function(obj) { @@ -151,6 +153,21 @@ frappe.ui.SortSelector = Class.extend({ this.sort_by = this.args.sort_by; this.sort_order = this.args.sort_order; }, + get_meta_sort_field: function() { + var meta = frappe.get_meta(this.doctype); + if(meta.sort_field && meta.sort_field.includes(',')) { + var parts = meta.sort_field.split(',')[0].split(' '); + return { + meta_sort_field: parts[0], + meta_sort_order: parts[1] + } + } else { + return { + meta_sort_field: meta.sort_field, + meta_sort_order: meta.sort_order.toLowerCase() + } + } + }, get_label: function(fieldname) { if(fieldname==='idx') { return __("Most Used"); From 665602522ba4f2e34b56104548a178522ebbfa33 Mon Sep 17 00:00:00 2001 From: Ayush Shukla Date: Tue, 16 May 2017 11:42:15 +0530 Subject: [PATCH 16/20] Close grid on escape key (#3292) * [minor] Escape fix * [minor] Method name changed --- frappe/public/js/frappe/ui/keyboard.js | 41 +++++++++++++++----------- 1 file changed, 23 insertions(+), 18 deletions(-) diff --git a/frappe/public/js/frappe/ui/keyboard.js b/frappe/public/js/frappe/ui/keyboard.js index 2a88667735..231d87bec0 100644 --- a/frappe/public/js/frappe/ui/keyboard.js +++ b/frappe/public/js/frappe/ui/keyboard.js @@ -8,7 +8,6 @@ frappe.ui.keys.setup = function() { for(var i=0, l = frappe.ui.keys.handlers[key].length; i down key = key.substr(5).toLowerCase(); } @@ -69,22 +67,13 @@ frappe.ui.keys.on('ctrl+b', function(e) { } }); -frappe.ui.keys.on('Escape', function(e) { - // close open grid row - var open_row = $(".grid-row-open"); - if(open_row.length) { - var grid_row = open_row.data("grid_row"); - grid_row.toggle_view(false); - return false; - } - - // close open dialog - if(cur_dialog && !cur_dialog.no_cancel_flag) { - cur_dialog.cancel(); - return false; - } +frappe.ui.keys.on('escape', function(e) { + close_grid_and_dialog(); }); +frappe.ui.keys.on('esc', function(e) { + close_grid_and_dialog(); +}); frappe.ui.keys.on('Enter', function(e) { if(cur_dialog && cur_dialog.confirm_dialog) { @@ -127,4 +116,20 @@ frappe.ui.keyCode = { ENTER: 13, TAB: 9, SPACE: 32 +} + +function close_grid_and_dialog() { + // close open grid row + var open_row = $(".grid-row-open"); + if (open_row.length) { + var grid_row = open_row.data("grid_row"); + grid_row.toggle_view(false); + return false; + } + + // close open dialog + if (cur_dialog && !cur_dialog.no_cancel_flag) { + cur_dialog.cancel(); + return false; + } } \ No newline at end of file From adfc6926688b742269578233ec2052ebfe24af32 Mon Sep 17 00:00:00 2001 From: Faris Ansari Date: Tue, 16 May 2017 12:44:39 +0530 Subject: [PATCH 17/20] Update field when editing via SlickGrid (Fixes frappe/erpnext#5064) (#3296) --- frappe/public/js/frappe/views/reports/reportview.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/frappe/public/js/frappe/views/reports/reportview.js b/frappe/public/js/frappe/views/reports/reportview.js index df7359b3d3..6ed7c61392 100644 --- a/frappe/public/js/frappe/views/reports/reportview.js +++ b/frappe/public/js/frappe/views/reports/reportview.js @@ -548,9 +548,12 @@ frappe.views.ReportView = frappe.ui.BaseList.extend({ $.each(frappe.model.get_all_docs(doc), function(i, d) { // find the document of the current updated record // from locals (which is synced in the response) - if(item[d.doctype + ":name"]===d.name) { - for(k in d) { - v = d[k]; + var name = item[d.doctype + ":name"]; + if(!name) name = item.name; + + if(name===d.name) { + for(var k in d) { + var v = d[k]; if(frappe.model.std_fields_list.indexOf(k)===-1 && item[k]!==undefined) { new_item[k] = v; From 0aa97b7aaac4d78e60e6125a39212904c1a144b8 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Tue, 16 May 2017 12:46:58 +0530 Subject: [PATCH 18/20] Made seprate method to parse the naming series (#3293) --- frappe/model/naming.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/frappe/model/naming.py b/frappe/model/naming.py index f6a947cf4b..dbe236f09e 100644 --- a/frappe/model/naming.py +++ b/frappe/model/naming.py @@ -94,12 +94,15 @@ def make_autoname(key='', doctype='', doc=''): elif not "." in key: frappe.throw(_("Invalid naming series (. missing)") + (_(" for {0}").format(doctype) if doctype else "")) + parts = key.split('.') + n = parse_naming_series(parts, doctype, doc) + return n + +def parse_naming_series(parts, doctype= '', doc = ''): n = '' - l = key.split('.') series_set = False today = now_datetime() - - for e in l: + for e in parts: part = '' if e.startswith('#'): if not series_set: @@ -120,6 +123,7 @@ def make_autoname(key='', doctype='', doc=''): if isinstance(part, basestring): n+=part + return n def getseries(key, digits, doctype=''): From 9abcaff48ac4e0daee8428ce347b6e1280961657 Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Tue, 16 May 2017 12:49:47 +0530 Subject: [PATCH 19/20] [fix] disable email alert on wrong field. fixes frappe/erpnext#3372 (#3290) --- .../email/doctype/email_alert/email_alert.py | 9 +++++- .../doctype/email_alert/test_email_alert.py | 30 +++++++++++++++++++ frappe/exceptions.py | 1 + 3 files changed, 39 insertions(+), 1 deletion(-) diff --git a/frappe/email/doctype/email_alert/email_alert.py b/frappe/email/doctype/email_alert/email_alert.py index 666ae25dbc..f9212cfe88 100755 --- a/frappe/email/doctype/email_alert/email_alert.py +++ b/frappe/email/doctype/email_alert/email_alert.py @@ -217,7 +217,14 @@ def evaluate_alert(doc, alert, event): return if event=="Value Change" and not doc.is_new(): - db_value = frappe.db.get_value(doc.doctype, doc.name, alert.value_changed) + try: + db_value = frappe.db.get_value(doc.doctype, doc.name, alert.value_changed) + except frappe.DatabaseOperationalError as e: + if e.args[0]==1054: + alert.db_set('enabled', 0) + frappe.log_error('Email Alert {0} has been disabled due to missing field'.format(alert.name)) + return + db_value = parse_val(db_value) if (doc.get(alert.value_changed) == db_value) or \ (not db_value and not doc.get(alert.value_changed)): diff --git a/frappe/email/doctype/email_alert/test_email_alert.py b/frappe/email/doctype/email_alert/test_email_alert.py index 57686dd984..1f7c457ae2 100755 --- a/frappe/email/doctype/email_alert/test_email_alert.py +++ b/frappe/email/doctype/email_alert/test_email_alert.py @@ -87,6 +87,36 @@ class TestEmailAlert(unittest.TestCase): self.assertTrue(frappe.db.get_value("Email Queue", {"reference_doctype": "Event", "reference_name": event.name, "status":"Not Sent"})) + def test_alert_disabled_on_wrong_field(self): + frappe.set_user('Administrator') + email_alert = frappe.get_doc({ + "doctype": "Email Alert", + "subject":"_Test Email Alert for wrong field", + "document_type": "Event", + "event": "Value Change", + "attach_print": 0, + "value_changed": "description1", + "message": "Description changed", + "recipients": [ + { "email_by_document_field": "owner" } + ] + }).insert() + + event = frappe.new_doc("Event") + event.subject = "test-2", + event.event_type = "Private" + event.starts_on = "2014-06-06 12:00:00" + event.insert() + event.subject = "test 1" + event.save() + + # verify that email_alert is disabled + email_alert.reload() + self.assertEqual(email_alert.enabled, 0) + email_alert.delete() + event.delete() + + def test_date_changed(self): event = frappe.new_doc("Event") event.subject = "test", diff --git a/frappe/exceptions.py b/frappe/exceptions.py index 1a5f6d688e..723c602496 100644 --- a/frappe/exceptions.py +++ b/frappe/exceptions.py @@ -7,6 +7,7 @@ from __future__ import unicode_literals from werkzeug.exceptions import NotFound from MySQLdb import ProgrammingError as SQLError, Error +from MySQLdb import OperationalError as DatabaseOperationalError class ValidationError(Exception): From 3fce6cb78bbaf3eda32bd00ad2525536c21bd624 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Tue, 16 May 2017 14:05:38 +0600 Subject: [PATCH 20/20] bumped to version 8.0.44 --- frappe/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/__init__.py b/frappe/__init__.py index 7697f14a56..3a1c6cc6b0 100644 --- a/frappe/__init__.py +++ b/frappe/__init__.py @@ -13,7 +13,7 @@ import os, sys, importlib, inspect, json from .exceptions import * from .utils.jinja import get_jenv, get_template, render_template -__version__ = '8.0.43' +__version__ = '8.0.44' __title__ = "Frappe Framework" local = Local()