diff --git a/frappe/__init__.py b/frappe/__init__.py index 9f03ff2187..0b171dc89b 100644 --- a/frappe/__init__.py +++ b/frappe/__init__.py @@ -16,6 +16,7 @@ from faker import Faker # public from .exceptions import * from .utils.jinja import (get_jenv, get_template, render_template, get_email_from_template, get_jloader) +from .utils.error import get_frame_locals # Hamless for Python 3 # For Python 2 set default encoding to utf-8 @@ -23,7 +24,7 @@ if sys.version[0] == '2': reload(sys) sys.setdefaultencoding("utf-8") -__version__ = '11.1.5' +__version__ = '11.1.6' __title__ = "Frappe Framework" local = Local() @@ -273,7 +274,7 @@ def errprint(msg): if not request or (not "cmd" in local.form_dict) or conf.developer_mode: print(msg) - error_log.append(msg) + error_log.append({"exc": msg, "locals": get_frame_locals()}) def log(msg): """Add to `debug_log`. diff --git a/frappe/core/doctype/communication/email.py b/frappe/core/doctype/communication/email.py index fbcd30a3f2..0799238432 100755 --- a/frappe/core/doctype/communication/email.py +++ b/frappe/core/doctype/communication/email.py @@ -307,7 +307,8 @@ def set_incoming_outgoing_accounts(doc): doc.outgoing_email_account = frappe.db.get_value("Email Account", {"append_to": doc.reference_doctype, "enable_outgoing": 1}, - ["email_id", "always_use_account_email_id_as_sender", "name"], as_dict=True) + ["email_id", "always_use_account_email_id_as_sender", "name", + "always_use_account_name_as_sender_name"], as_dict=True) if not doc.incoming_email_account: doc.incoming_email_account = frappe.db.get_value("Email Account", @@ -317,12 +318,14 @@ def set_incoming_outgoing_accounts(doc): # if from address is not the default email account doc.outgoing_email_account = frappe.db.get_value("Email Account", {"email_id": doc.sender, "enable_outgoing": 1}, - ["email_id", "always_use_account_email_id_as_sender", "name", "send_unsubscribe_message"], as_dict=True) or frappe._dict() + ["email_id", "always_use_account_email_id_as_sender", "name", + "send_unsubscribe_message", "always_use_account_name_as_sender_name"], as_dict=True) or frappe._dict() if not doc.outgoing_email_account: doc.outgoing_email_account = frappe.db.get_value("Email Account", {"default_outgoing": 1, "enable_outgoing": 1}, - ["email_id", "always_use_account_email_id_as_sender", "name", "send_unsubscribe_message"],as_dict=True) or frappe._dict() + ["email_id", "always_use_account_email_id_as_sender", "name", + "send_unsubscribe_message", "always_use_account_name_as_sender_name"],as_dict=True) or frappe._dict() if doc.sent_or_received == "Sent": doc.db_set("email_account", doc.outgoing_email_account.name) diff --git a/frappe/core/doctype/doctype/doctype.py b/frappe/core/doctype/doctype/doctype.py index ce252c2d70..4edf29d937 100644 --- a/frappe/core/doctype/doctype/doctype.py +++ b/frappe/core/doctype/doctype/doctype.py @@ -52,7 +52,6 @@ class DocType(Document): self.permissions = [] self.scrub_field_names() - self.scrub_options_in_select() self.set_default_in_list_view() self.set_default_translatable() self.validate_series() @@ -202,17 +201,6 @@ class DocType(Document): # unique is automatically an index if d.unique: d.search_index = 0 - def scrub_options_in_select(self): - """Strip options for whitespaces""" - for field in self.fields: - if field.fieldtype == "Select" and field.options is not None: - options_list = [] - for i, option in enumerate(field.options.split("\n")): - _option = option.strip() - if i==0 or _option: - options_list.append(_option) - field.options = '\n'.join(options_list) - def validate_series(self, autoname=None, name=None): """Validate if `autoname` property is correctly set.""" if not autoname: autoname = self.autoname @@ -705,6 +693,20 @@ def validate_fields(meta): frappe.throw(_('DocType {0} provided for the field {1} must have atleast one Link field') .format(doctype, docfield.fieldname), frappe.ValidationError) + def scrub_options_in_select(field): + """Strip options for whitespaces""" + + if field.fieldtype == "Select" and field.options is not None: + options_list = [] + for i, option in enumerate(field.options.split("\n")): + _option = option.strip() + if i==0 or _option: + options_list.append(_option) + field.options = '\n'.join(options_list) + + def scrub_fetch_from(field): + if hasattr(field, 'fetch_from') and getattr(field, 'fetch_from'): + field.fetch_from = field.fetch_from.strip('\n').strip() fields = meta.get("fields") fieldname_list = [d.fieldname for d in fields] @@ -734,6 +736,8 @@ def validate_fields(meta): check_unique_and_text(d) check_illegal_depends_on_conditions(d) check_table_multiselect_option(d) + scrub_options_in_select(d) + scrub_fetch_from(d) check_fold(fields) check_search_fields(meta, fields) diff --git a/frappe/core/doctype/language/language.py b/frappe/core/doctype/language/language.py index 8c7e01cb62..fb18abdf5e 100644 --- a/frappe/core/doctype/language/language.py +++ b/frappe/core/doctype/language/language.py @@ -3,11 +3,22 @@ # For license information, please see license.txt from __future__ import unicode_literals -import frappe, json +import frappe, json, re +from frappe import _ from frappe.model.document import Document class Language(Document): - pass + def validate(self): + validate_with_regex(self.language_code, "Language Code") + + def before_rename(self, old, new, merge=False): + validate_with_regex(new, "Name") + +def validate_with_regex(name, label): + pattern = re.compile("^[a-zA-Z]+[-_]*[a-zA-Z]+$") + if not pattern.match(name): + frappe.throw(_("""{0} must begin and end with a letter and can only contain letters, + hyphen or underscore.""").format(label)) def export_languages_json(): '''Export list of all languages''' diff --git a/frappe/core/doctype/user_permission/user_permission.json b/frappe/core/doctype/user_permission/user_permission.json index 9a38f8d953..c2ea05e731 100644 --- a/frappe/core/doctype/user_permission/user_permission.json +++ b/frappe/core/doctype/user_permission/user_permission.json @@ -41,7 +41,7 @@ "remember_last_selected_value": 0, "report_hide": 0, "reqd": 1, - "search_index": 0, + "search_index": 1, "set_only_once": 0, "translatable": 0, "unique": 0 @@ -222,7 +222,7 @@ "issingle": 0, "istable": 0, "max_attachments": 0, - "modified": "2018-11-12 16:26:12.362352", + "modified": "2019-02-13 22:58:27.428741", "modified_by": "Administrator", "module": "Core", "name": "User Permission", diff --git a/frappe/database/database.py b/frappe/database/database.py index 136e25cd93..c9b184cbd1 100644 --- a/frappe/database/database.py +++ b/frappe/database/database.py @@ -881,9 +881,14 @@ class Database(object): def get_descendants(self, doctype, name): '''Return descendants of the current record''' - lft, rgt = self.get_value(doctype, name, ('lft', 'rgt')) - return self.sql_list('''select name from `tab{doctype}` - where lft > {lft} and rgt < {rgt}'''.format(doctype=doctype, lft=lft, rgt=rgt)) + node_location_indexes = self.get_value(doctype, name, ('lft', 'rgt')) + if node_location_indexes: + lft, rgt = node_location_indexes + return self.sql_list('''select name from `tab{doctype}` + where lft > {lft} and rgt < {rgt}'''.format(doctype=doctype, lft=lft, rgt=rgt)) + else: + # when document does not exist + return [] def is_missing_table_or_column(self, e): return self.is_missing_column(e) or self.is_missing_table(e) diff --git a/frappe/desk/query_report.py b/frappe/desk/query_report.py index 8954a9d36c..82b16da891 100644 --- a/frappe/desk/query_report.py +++ b/frappe/desk/query_report.py @@ -190,30 +190,34 @@ def run(report_name, filters=None, user=None): def get_prepared_report_result(report, filters, dn="", user=None): latest_report_data = {} - # Only look for completed prepared reports with given filters. - doc_list = frappe.get_all("Prepared Report", - filters={"status": "Completed", "report_name": report.name, "filters": filters, "owner": user}) - doc = None - if len(doc_list): - if dn: - # Get specified dn - doc = frappe.get_doc("Prepared Report", dn) - else: + if dn: + # Get specified dn + doc = frappe.get_doc("Prepared Report", dn) + else: + # Only look for completed prepared reports with given filters. + doc_list = frappe.get_all("Prepared Report", filters={"status": "Completed", "filters": json.dumps(filters), "owner": user}) + if doc_list: # Get latest doc = frappe.get_doc("Prepared Report", doc_list[0]) - # Prepared Report data is stored in a GZip compressed JSON file - attached_file_name = frappe.db.get_value("File", {"attached_to_doctype": doc.doctype, "attached_to_name":doc.name}, "name") - attached_file = frappe.get_doc('File', attached_file_name) - compressed_content = attached_file.get_content() - uncompressed_content = gzip_decompress(compressed_content) - data = json.loads(uncompressed_content) - if data: - latest_report_data = { - "columns": json.loads(doc.columns) if doc.columns else data[0], - "result": data - } + if doc: + try: + # Prepared Report data is stored in a GZip compressed JSON file + attached_file_name = frappe.db.get_value("File", {"attached_to_doctype": doc.doctype, "attached_to_name":doc.name}, "name") + attached_file = frappe.get_doc('File', attached_file_name) + compressed_content = attached_file.get_content() + uncompressed_content = gzip_decompress(compressed_content) + data = json.loads(uncompressed_content) + if data: + latest_report_data = { + "columns": json.loads(doc.columns) if doc.columns else data[0], + "result": data + } + except Exception: + frappe.delete_doc("Prepared Report", doc.name) + frappe.db.commit() + doc = None latest_report_data.update({ "prepared_report": True, diff --git a/frappe/email/doctype/email_account/email_account.json b/frappe/email/doctype/email_account/email_account.json index 5ab3cd1630..ce9b0f30b0 100644 --- a/frappe/email/doctype/email_account/email_account.json +++ b/frappe/email/doctype/email_account/email_account.json @@ -1,5 +1,6 @@ { "allow_copy": 0, + "allow_events_in_timeline": 0, "allow_guest_to_view": 0, "allow_import": 0, "allow_rename": 1, @@ -1063,6 +1064,40 @@ "translatable": 0, "unique": 0 }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "depends_on": "enable_outgoing", + "description": "Uses the Email Address Name mentioned in this Account as the Sender's Name for all emails sent using this Account.", + "fieldname": "always_use_account_name_as_sender_name", + "fieldtype": "Check", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Always use Account's Name as Sender's Name", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, { "allow_bulk_edit": 0, "allow_in_quick_entry": 0, @@ -1563,7 +1598,7 @@ "issingle": 0, "istable": 0, "max_attachments": 0, - "modified": "2019-01-30 11:02:41.011412", + "modified": "2019-02-12 17:09:50.653403", "modified_by": "Administrator", "module": "Email", "name": "Email Account", diff --git a/frappe/email/email_body.py b/frappe/email/email_body.py index b4af93e61d..4904f60831 100755 --- a/frappe/email/email_body.py +++ b/frappe/email/email_body.py @@ -174,6 +174,7 @@ class EMail: self.reply_to = validate_email_add(strip(self.reply_to) or self.sender, True) self.replace_sender() + self.replace_sender_name() self.recipients = [strip(r) for r in self.recipients] self.cc = [strip(r) for r in self.cc] @@ -188,6 +189,12 @@ class EMail: sender_name, sender_email = parse_addr(self.sender) self.sender = email.utils.formataddr((str(Header(sender_name or self.email_account.name, 'utf-8')), self.email_account.email_id)) + def replace_sender_name(self): + if cint(self.email_account.always_use_account_name_as_sender_name): + self.set_header('X-Original-From', self.sender) + sender_name, sender_email = parse_addr(self.sender) + self.sender = email.utils.formataddr((str(Header(self.email_account.name, 'utf-8')), sender_email)) + def set_message_id(self, message_id, is_notification=False): if message_id: self.msg_root["Message-Id"] = '<' + message_id + '>' diff --git a/frappe/email/smtp.py b/frappe/email/smtp.py index 549b3f1d1e..99b5f94bf0 100644 --- a/frappe/email/smtp.py +++ b/frappe/email/smtp.py @@ -109,7 +109,8 @@ def get_default_outgoing_email_account(raise_exception_not_set=True): "mail_password": "Super.Secret.Password", "auto_email_id": "emails@example.com", "email_sender_name": "Example Notifications", - "always_use_account_email_id_as_sender": 0 + "always_use_account_email_id_as_sender": 0, + "always_use_account_name_as_sender_name": 0 } ''' email_account = _get_email_account({"enable_outgoing": 1, "default_outgoing": 1}) @@ -128,7 +129,8 @@ def get_default_outgoing_email_account(raise_exception_not_set=True): "login_id": frappe.conf.get("mail_login"), "email_id": frappe.conf.get("auto_email_id") or frappe.conf.get("mail_login") or 'notifications@example.com', "password": frappe.conf.get("mail_password"), - "always_use_account_email_id_as_sender": frappe.conf.get("always_use_account_email_id_as_sender", 0) + "always_use_account_email_id_as_sender": frappe.conf.get("always_use_account_email_id_as_sender", 0), + "always_use_account_name_as_sender_name": frappe.conf.get("always_use_account_name_as_sender_name", 0) }) email_account.from_site_config = True email_account.name = frappe.conf.get("email_sender_name") or "Frappe" @@ -182,6 +184,7 @@ class SMTPServer: self.use_tls = self.email_account.use_tls self.sender = self.email_account.email_id self.always_use_account_email_id_as_sender = cint(self.email_account.get("always_use_account_email_id_as_sender")) + self.always_use_account_name_as_sender_name = cint(self.email_account.get("always_use_account_name_as_sender_name")) @property def sess(self): diff --git a/frappe/public/js/frappe/misc/utils.js b/frappe/public/js/frappe/misc/utils.js index e72bbc5129..c89804ef1f 100644 --- a/frappe/public/js/frappe/misc/utils.js +++ b/frappe/public/js/frappe/misc/utils.js @@ -665,13 +665,15 @@ Object.assign(frappe.utils, { return `${route[0]} ${route[1]}`; } }, - report_total_accumulator: function(column, values, type) { - if (column.fieldtype == "Percent" || type === "mean") { - return values.reduce((a, b) => ({content: a.content + flt(b.content)})).content / values.length; - } else if (frappe.model.is_numeric_field(column.fieldtype)) { - return values.reduce((a, b) => ({content: a.content + flt(b.content)})).content; + report_column_total: function(values, column, type) { + if (column.column.fieldtype == "Percent" || type === "mean") { + return values.reduce((a, b) => a + flt(b)) / values.length; + } else if (column.column.fieldtype == "Int") { + return values.reduce((a, b) => a + cint(b)); + } else if (frappe.model.is_numeric_field(column.column.fieldtype)) { + return values.reduce((a, b) => a + flt(b)); } else { - return false; + return null; } } }); diff --git a/frappe/public/js/frappe/request.js b/frappe/public/js/frappe/request.js index 031ab6f0c1..fbea85578d 100644 --- a/frappe/public/js/frappe/request.js +++ b/frappe/public/js/frappe/request.js @@ -376,9 +376,12 @@ frappe.request.report_error = function(xhr, request_opts) { var data = JSON.parse(xhr.responseText); if (data.exc) { var exc = (JSON.parse(data.exc) || []).join("\n"); + var locals = (JSON.parse(data.locals) || []).join("\n"); delete data.exc; + delete data.locals; } else { var exc = ""; + locals = ""; } if (exc) { @@ -410,6 +413,9 @@ frappe.request.report_error = function(xhr, request_opts) { '
' + exc + '', '
' + locals + '', + '
' + JSON.stringify(request_opts, null, "\t") + '', '