-
\ No newline at end of file
+
diff --git a/frappe/core/doctype/doctype/doctype.js b/frappe/core/doctype/doctype/doctype.js
index 9a19185cfc..b3469abf29 100644
--- a/frappe/core/doctype/doctype/doctype.js
+++ b/frappe/core/doctype/doctype/doctype.js
@@ -53,7 +53,7 @@ frappe.ui.form.on('DocType', {
frm.events.autoname(frm);
},
- autoname(frm) {
+ autoname: function(frm) {
frm.set_df_property('fields', 'reqd', frm.doc.autoname !== 'Prompt');
}
})
diff --git a/frappe/core/doctype/doctype/doctype.json b/frappe/core/doctype/doctype/doctype.json
index 4e3f2fd84a..379ea227cb 100644
--- a/frappe/core/doctype/doctype/doctype.json
+++ b/frappe/core/doctype/doctype/doctype.json
@@ -54,6 +54,10 @@
"color",
"show_preview_popup",
"show_name_in_global_search",
+ "email_settings_sb",
+ "email_append_to",
+ "sender_field",
+ "subject_field",
"sb2",
"permissions",
"restrict_to_domain",
@@ -488,11 +492,37 @@
"fieldtype": "Table",
"label": "Links",
"options": "DocType Link"
+ },
+ {
+ "depends_on": "email_append_to",
+ "fieldname": "subject_field",
+ "fieldtype": "Data",
+ "label": "Subject Field"
+ },
+ {
+ "depends_on": "email_append_to",
+ "fieldname": "sender_field",
+ "fieldtype": "Data",
+ "label": "Sender Field",
+ "mandatory_depends_on": "email_append_to"
+ },
+ {
+ "default": "0",
+ "fieldname": "email_append_to",
+ "fieldtype": "Check",
+ "label": "Allow document creation via Email"
+ },
+ {
+ "collapsible": 1,
+ "fieldname": "email_settings_sb",
+ "fieldtype": "Section Break",
+ "label": "Email Settings"
}
],
"icon": "fa fa-bolt",
"idx": 6,
- "modified": "2019-11-25 17:24:03.690192",
+ "links": [],
+ "modified": "2020-03-27 14:51:44.581128",
"modified_by": "Administrator",
"module": "Core",
"name": "DocType",
diff --git a/frappe/core/doctype/doctype/doctype.py b/frappe/core/doctype/doctype/doctype.py
index da1b184cc1..f970f51419 100644
--- a/frappe/core/doctype/doctype/doctype.py
+++ b/frappe/core/doctype/doctype/doctype.py
@@ -15,7 +15,7 @@ import frappe
import frappe.website.render
from frappe import _
from frappe.utils import now, cint
-from frappe.model import no_value_fields, default_fields, data_fieldtypes, table_fields
+from frappe.model import no_value_fields, default_fields, data_fieldtypes, table_fields, data_field_options
from frappe.model.document import Document
from frappe.custom.doctype.property_setter.property_setter import make_property_setter
from frappe.custom.doctype.custom_field.custom_field import create_custom_field
@@ -24,6 +24,7 @@ from frappe.modules import make_boilerplate, get_doc_path
from frappe.database.schema import validate_column_name, validate_column_length
from frappe.model.docfield import supports_translation
from frappe.modules.import_file import get_file_path
+from frappe.model.meta import Meta
class InvalidFieldNameError(frappe.ValidationError): pass
@@ -93,10 +94,11 @@ class DocType(Document):
if not self.is_new():
self.setup_fields_to_fetch()
+ check_email_append_to(self)
+
if self.default_print_format and not self.custom:
frappe.throw(_('Standard DocType cannot have default print format, use Customize Form'))
-
def set_default_in_list_view(self):
'''Set default in-list-view for first 4 mandatory fields'''
if not [d.fieldname for d in self.fields if d.in_list_view]:
@@ -107,14 +109,12 @@ class DocType(Document):
cnt += 1
if cnt == 4: break
-
def set_default_translatable(self):
'''Ensure that non-translatable never will be translatable'''
for d in self.fields:
if d.translatable and not supports_translation(d.fieldtype):
d.translatable = 0
-
def check_developer_mode(self):
"""Throw exception if not developer mode or via patch"""
if frappe.flags.in_patch or frappe.flags.in_test:
@@ -123,7 +123,6 @@ class DocType(Document):
if not frappe.conf.get("developer_mode") and not self.custom:
frappe.throw(_("Not in Developer Mode! Set in site_config.json or make 'Custom' DocType."), CannotCreateStandardDoctypeError)
-
def setup_fields_to_fetch(self):
'''Setup query to update values for newly set fetch values'''
try:
@@ -168,21 +167,18 @@ class DocType(Document):
)
)
-
def update_fields_to_fetch(self):
'''Update fetch values based on queries setup'''
if self.flags.update_fields_to_fetch_queries and not self.issingle:
for query in self.flags.update_fields_to_fetch_queries:
frappe.db.sql(query)
-
def validate_document_type(self):
if self.document_type=="Transaction":
self.document_type = "Document"
if self.document_type=="Master":
self.document_type = "Setup"
-
def validate_website(self):
"""Ensure that website generator has field 'route'"""
if self.has_web_view:
@@ -193,7 +189,6 @@ class DocType(Document):
# clear website cache
frappe.website.render.clear_cache()
-
def change_modified_of_parent(self):
"""Change the timestamp of parent DocType if the current one is a child to clear caches."""
if frappe.flags.in_import:
@@ -203,7 +198,6 @@ class DocType(Document):
for p in parent_list:
frappe.db.sql('UPDATE `tabDocType` SET modified=%s WHERE `name`=%s', (now(), p.parent))
-
def scrub_field_names(self):
"""Sluggify fieldnames if not set from Label."""
restricted = ('name','parent','creation','modified','modified_by',
@@ -233,7 +227,6 @@ class DocType(Document):
# unique is automatically an index
if d.unique: d.search_index = 0
-
def validate_series(self, autoname=None, name=None):
"""Validate if `autoname` property is correctly set."""
if not autoname: autoname = self.autoname
@@ -270,12 +263,11 @@ class DocType(Document):
if used_in:
frappe.throw(_("Series {0} already used in {1}").format(prefix, used_in[0][0]))
-
def on_update(self):
"""Update database schema, make controller templates if `custom` is not set and clear cache."""
self.delete_duplicate_custom_fields()
try:
- frappe.db.updatedb(self.name, self)
+ frappe.db.updatedb(self.name, Meta(self))
except Exception as e:
print("\n\nThere was an issue while migrating the DocType: {}\n".format(self.name))
raise e
@@ -324,7 +316,6 @@ class DocType(Document):
dt = {0} and fieldname in ({1})
'''.format('%s', ', '.join(['%s'] * len(fields))), tuple([self.name] + fields), as_dict=True)
-
def sync_global_search(self):
'''If global search settings are changed, rebuild search properties for this table'''
global_search_fields_before_update = [d.fieldname for d in
@@ -342,7 +333,6 @@ class DocType(Document):
frappe.enqueue('frappe.utils.global_search.rebuild_for_doctype',
now=now, doctype=self.name)
-
def set_base_class_for_controller(self):
'''Updates the controller class to subclass from `WebsiteGenertor`,
if it is a subclass of `Document`'''
@@ -362,14 +352,12 @@ class DocType(Document):
with open(controller_path, 'w') as f:
f.write(code)
-
def run_module_method(self, method):
from frappe.modules import load_doctype_module
module = load_doctype_module(self.name, self.module)
if hasattr(module, method):
getattr(module, method)()
-
def before_rename(self, old, new, merge=False):
"""Throw exception if merge. DocTypes cannot be merged."""
if not self.custom and frappe.session.user != "Administrator":
@@ -385,7 +373,6 @@ class DocType(Document):
if not self.custom and not frappe.flags.in_test and not frappe.flags.in_patch:
self.rename_files_and_folders(old, new)
-
def after_rename(self, old, new, merge=False):
"""Change table name using `RENAME TABLE` if table exists. Or update
`doctype` property for Single type."""
@@ -396,7 +383,6 @@ class DocType(Document):
else:
frappe.db.sql("rename table `tab%s` to `tab%s`" % (old, new))
-
def rename_files_and_folders(self, old, new):
# move files
new_path = get_doc_path(self.module, 'doctype', new)
@@ -413,7 +399,6 @@ class DocType(Document):
self.rename_inside_controller(new, old, new_path)
frappe.msgprint(_('Renamed files and replaced code in controllers, please check!'))
-
def rename_inside_controller(self, new, old, new_path):
for fname in ('{}.js', '{}.py', '{}_list.js', '{}_calendar.js', 'test_{}.py', 'test_{}.js'):
fname = os.path.join(new_path, fname.format(frappe.scrub(new)))
@@ -439,7 +424,6 @@ class DocType(Document):
if not (self.issingle and self.istable):
self.preserve_naming_series_options_in_property_setter()
-
def preserve_naming_series_options_in_property_setter(self):
"""Preserve naming_series as property setter if it does not exist"""
naming_series = self.get("fields", {"fieldname": "naming_series"})
@@ -459,7 +443,6 @@ class DocType(Document):
if naming_series[0].default:
make_property_setter(self.name, "naming_series", "default", naming_series[0].default, "Text", validate_fields_for_doctype=False)
-
def before_export(self, docdict):
# remove null and empty fields
def remove_null_fields(o):
@@ -504,7 +487,6 @@ class DocType(Document):
except ValueError:
pass
-
@staticmethod
def prepare_for_import(docdict):
# set order of fields from field_order
@@ -527,19 +509,16 @@ class DocType(Document):
if "field_order" in docdict:
del docdict["field_order"]
-
def export_doc(self):
"""Export to standard folder `[module]/doctype/[name]/[name].json`."""
from frappe.modules.export_file import export_to_files
export_to_files(record_list=[['DocType', self.name]], create_init=True)
-
def import_doc(self):
"""Import from standard folder `[module]/doctype/[name]/[name].json`."""
from frappe.modules.import_module import import_from_files
import_from_files(record_list=[[self.module, 'doctype', self.name]])
-
def make_controller_template(self):
"""Make boilerplate controller template."""
make_boilerplate("controller._py", self)
@@ -556,7 +535,6 @@ class DocType(Document):
make_boilerplate('templates/controller.html', self.as_dict())
make_boilerplate('templates/controller_row.html', self.as_dict())
-
def make_amendable(self):
"""If is_submittable is set, add amended_from docfields."""
if self.is_submittable:
@@ -572,7 +550,6 @@ class DocType(Document):
"no_copy": 1
})
-
def make_repeatable(self):
"""If allow_auto_repeat is set, add auto_repeat custom field."""
if self.allow_auto_repeat:
@@ -641,14 +618,12 @@ class DocType(Document):
})
self.nsm_parent_field = parent_field_name
-
def get_max_idx(self):
"""Returns the highest `idx`"""
max_idx = frappe.db.sql("""select max(idx) from `tabDocField` where parent = %s""",
self.name)
return max_idx and max_idx[0][0] or 0
-
def validate_name(self, name=None):
if not name:
name = self.name
@@ -668,7 +643,6 @@ def validate_fields_for_doctype(doctype):
doc.delete_duplicate_custom_fields()
validate_fields(frappe.get_meta(doctype, cached=False))
-
# this is separate because it is also called via custom field
def validate_fields(meta):
"""Validate doctype fields. Checks
@@ -692,29 +666,24 @@ def validate_fields(meta):
def check_illegal_characters(fieldname):
validate_column_name(fieldname)
-
def check_invalid_fieldnames(docname, fieldname):
invalid_fields = ('doctype',)
if fieldname in invalid_fields:
frappe.throw(_("{0}: Fieldname cannot be one of {1}")
.format(docname, ", ".join([frappe.bold(d) for d in invalid_fields])))
-
def check_unique_fieldname(docname, fieldname):
duplicates = list(filter(None, map(lambda df: df.fieldname==fieldname and str(df.idx) or None, fields)))
if len(duplicates) > 1:
frappe.throw(_("{0}: Fieldname {1} appears multiple times in rows {2}").format(docname, fieldname, ", ".join(duplicates)), UniqueFieldnameError)
-
def check_fieldname_length(fieldname):
validate_column_length(fieldname)
-
def check_illegal_mandatory(docname, d):
if (d.fieldtype in no_value_fields) and d.fieldtype not in table_fields and d.reqd:
frappe.throw(_("{0}: Field {1} of type {2} cannot be mandatory").format(docname, d.label, d.fieldtype), IllegalMandatoryError)
-
def check_link_table_options(docname, d):
if frappe.flags.in_patch: return
if d.fieldtype in ("Link",) + table_fields:
@@ -733,28 +702,23 @@ def validate_fields(meta):
# fix case
d.options = options
-
def check_hidden_and_mandatory(docname, d):
if d.hidden and d.reqd and not d.default:
frappe.throw(_("{0}: Field {1} in row {2} cannot be hidden and mandatory without default").format(docname, d.label, d.idx), HiddenAndMandatoryWithoutDefaultError)
-
def check_width(d):
if d.fieldtype == "Currency" and cint(d.width) < 100:
frappe.throw(_("Max width for type Currency is 100px in row {0}").format(d.idx))
-
def check_in_list_view(d):
if d.in_list_view and (d.fieldtype in not_allowed_in_list_view):
frappe.throw(_("'In List View' not allowed for type {0} in row {1}").format(d.fieldtype, d.idx))
-
def check_in_global_search(d):
if d.in_global_search and d.fieldtype in no_value_fields:
frappe.throw(_("'In Global Search' not allowed for type {0} in row {1}")
.format(d.fieldtype, d.idx))
-
def check_dynamic_link_options(d):
if d.fieldtype=="Dynamic Link":
doctype_pointer = list(filter(lambda df: df.fieldname==d.options, fields))
@@ -762,7 +726,6 @@ def validate_fields(meta):
or (doctype_pointer[0].fieldtype=="Link" and doctype_pointer[0].options!="DocType"):
frappe.throw(_("Options 'Dynamic Link' type of field must point to another Link Field with options as 'DocType'"))
-
def check_illegal_default(d):
if d.fieldtype == "Check" and not d.default:
d.default = '0'
@@ -771,12 +734,10 @@ def validate_fields(meta):
if d.fieldtype == "Select" and d.default and (d.default not in d.options.split("\n")):
frappe.throw(_("Default for {0} must be an option").format(d.fieldname))
-
def check_precision(d):
if d.fieldtype in ("Currency", "Float", "Percent") and d.precision is not None and not (1 <= cint(d.precision) <= 6):
frappe.throw(_("Precision should be between 1 and 6"))
-
def check_unique_and_text(docname, d):
if meta.issingle:
d.unique = 0
@@ -798,7 +759,6 @@ def validate_fields(meta):
if d.search_index and d.fieldtype in ("Text", "Long Text", "Small Text", "Code", "Text Editor"):
frappe.throw(_("{0}:Fieldtype {1} for {2} cannot be indexed").format(docname, d.fieldtype, d.label), CannotIndexedError)
-
def check_fold(fields):
fold_exists = False
for i, f in enumerate(fields):
@@ -813,7 +773,6 @@ def validate_fields(meta):
else:
frappe.throw(_("Fold can not be at the end of the form"))
-
def check_search_fields(meta, fields):
"""Throw exception if `search_fields` don't contain valid fields."""
if not meta.search_fields:
@@ -830,7 +789,6 @@ def validate_fields(meta):
(fieldname not in fieldname_list):
frappe.throw(_("Search field {0} is not valid").format(fieldname))
-
def check_title_field(meta):
"""Throw exception if `title_field` isn't a valid fieldname."""
if not meta.get("title_field"):
@@ -857,7 +815,6 @@ def validate_fields(meta):
_validate_title_field_pattern(df.options)
_validate_title_field_pattern(df.default)
-
def check_image_field(meta):
'''check image_field exists and is of type "Attach Image"'''
if not meta.image_field:
@@ -869,7 +826,6 @@ def validate_fields(meta):
if df[0].fieldtype != 'Attach Image':
frappe.throw(_("Image field must be of type Attach Image"), InvalidFieldNameError)
-
def check_is_published_field(meta):
if not meta.is_published_field:
return
@@ -877,7 +833,6 @@ def validate_fields(meta):
if meta.is_published_field not in fieldname_list:
frappe.throw(_("Is Published Field must be a valid fieldname"), InvalidFieldNameError)
-
def check_timeline_field(meta):
if not meta.timeline_field:
return
@@ -889,7 +844,6 @@ def validate_fields(meta):
if df.fieldtype not in ("Link", "Dynamic Link"):
frappe.throw(_("Timeline field must be a Link or Dynamic Link"), InvalidFieldNameError)
-
def check_sort_field(meta):
'''Validate that sort_field(s) is a valid field'''
if meta.sort_field:
@@ -902,7 +856,6 @@ def validate_fields(meta):
frappe.throw(_("Sort field {0} must be a valid fieldname").format(fieldname),
InvalidFieldNameError)
-
def check_illegal_depends_on_conditions(docfield):
''' assignment operation should not be allowed in the depends on condition.'''
depends_on_fields = ["depends_on", "collapsible_depends_on", "mandatory_depends_on", "read_only_depends_on"]
@@ -912,7 +865,6 @@ def validate_fields(meta):
re.match("""[\w\.:_]+\s*={1}\s*[\w\.@'"]+""", depends_on):
frappe.throw(_("Invalid {0} condition").format(frappe.unscrub(field)), frappe.ValidationError)
-
def check_table_multiselect_option(docfield):
'''check if the doctype provided in Option has atleast 1 Link field'''
if not docfield.fieldtype == 'Table MultiSelect': return
@@ -925,7 +877,6 @@ 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"""
@@ -937,11 +888,20 @@ def validate_fields(meta):
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()
+ def validate_data_field_type(docfield):
+ if docfield.fieldtype == "Data":
+ if docfield.options and (docfield.options not in data_field_options):
+ df_str = frappe.bold(_(docfield.label))
+ text_str = _("{0} is an invalid Data field.").format(df_str) + " " * 2 + _("Only Options allowed for Data field are:") + " "
+ df_options_str = "
" + "
".join([_(x) for x in data_field_options]) + "
"
+
+ frappe.msgprint(text_str + df_options_str, title="Invalid Data Field", raise_exception=True)
+
+
fields = meta.get("fields")
fieldname_list = [d.fieldname for d in fields]
@@ -972,6 +932,7 @@ def validate_fields(meta):
check_table_multiselect_option(d)
scrub_options_in_select(d)
scrub_fetch_from(d)
+ validate_data_field_type(d)
check_fold(fields)
check_search_fields(meta, fields)
@@ -981,7 +942,6 @@ def validate_fields(meta):
check_sort_field(meta)
check_image_field(meta)
-
def validate_permissions_for_doctype(doctype, for_remove=False):
"""Validates if permissions are set correctly."""
doctype = frappe.get_doc("DocType", doctype)
@@ -993,7 +953,6 @@ def validate_permissions_for_doctype(doctype, for_remove=False):
clear_permissions_cache(doctype.name)
-
def clear_permissions_cache(doctype):
frappe.clear_cache(doctype=doctype)
delete_notification_count_for(doctype)
@@ -1008,7 +967,6 @@ def clear_permissions_cache(doctype):
""", doctype):
frappe.clear_cache(user=user)
-
def validate_permissions(doctype, for_remove=False):
permissions = doctype.get("permissions")
if not permissions:
@@ -1102,7 +1060,6 @@ def validate_permissions(doctype, for_remove=False):
check_level_zero_is_set(d)
remove_rights_for_single(d)
-
def make_module_and_roles(doc, perm_fieldname="permissions"):
"""Make `Module Def` and `Role` records if already not made. Called while installing."""
try:
@@ -1133,7 +1090,6 @@ def make_module_and_roles(doc, perm_fieldname="permissions"):
else:
raise
-
def check_if_fieldname_conflicts_with_methods(doctype, fieldname):
doc = frappe.get_doc({"doctype": doctype})
method_list = [method for method in dir(doc) if isinstance(method, str) and callable(getattr(doc, method))]
@@ -1141,6 +1097,38 @@ def check_if_fieldname_conflicts_with_methods(doctype, fieldname):
if fieldname in method_list:
frappe.throw(_("Fieldname {0} conflicting with meta object").format(fieldname))
-
def clear_linked_doctype_cache():
frappe.cache().delete_value('linked_doctypes_without_ignore_user_permissions_enabled')
+
+def check_email_append_to(doc):
+ if not hasattr(doc, "email_append_to") or not doc.email_append_to:
+ return
+
+ # Subject Field
+ doc.subject_field = doc.subject_field.strip() if doc.subject_field else None
+ subject_field = get_field(doc, doc.subject_field)
+
+ if doc.subject_field and not subject_field:
+ frappe.throw(_("Select a valid Subject field for creating documents from Email"))
+
+ if subject_field and subject_field.fieldtype not in ["Data", "Text", "Long Text", "Small Text", "Text Editor"]:
+ frappe.throw(_("Subject Field type should be Data, Text, Long Text, Small Text, Text Editor"))
+
+ # Sender Field is mandatory
+ doc.sender_field = doc.sender_field.strip() if doc.sender_field else None
+ sender_field = get_field(doc, doc.sender_field)
+
+ if doc.sender_field and not sender_field:
+ frappe.throw(_("Select a valid Sender Field for creating documents from Email"))
+
+ if not sender_field.options == "Email":
+ frappe.throw(_("Sender Field should have Email in options"))
+
+
+def get_field(doc, fieldname):
+ if not (doc or fieldname):
+ return
+
+ for field in doc.fields:
+ if field.fieldname == fieldname:
+ return field
diff --git a/frappe/core/doctype/doctype/test_doctype.py b/frappe/core/doctype/doctype/test_doctype.py
index 969a71ab7d..fe9f88b9b3 100644
--- a/frappe/core/doctype/doctype/test_doctype.py
+++ b/frappe/core/doctype/doctype/test_doctype.py
@@ -113,6 +113,32 @@ class TestDocType(unittest.TestCase):
if condition:
self.assertFalse(re.match(pattern, condition))
+ def test_data_field_options(self):
+ doctype_name = "Test Data Fields"
+ valid_data_field_options = frappe.model.data_field_options + ("",)
+ invalid_data_field_options = ("Invalid Option 1", frappe.utils.random_string(5))
+
+ for field_option in (valid_data_field_options + invalid_data_field_options):
+ test_doctype = frappe.get_doc({
+ "doctype": "DocType",
+ "name": doctype_name,
+ "module": "Core",
+ "custom": 1,
+ "fields": [{
+ "fieldname": "{0}_field".format(field_option),
+ "fieldtype": "Data",
+ "options": field_option
+ }]
+ })
+
+ if field_option in invalid_data_field_options:
+ # assert that only data options in frappe.model.data_field_options are valid
+ self.assertRaises(frappe.ValidationError, test_doctype.insert)
+ else:
+ test_doctype.insert()
+ self.assertEqual(test_doctype.name, doctype_name)
+ test_doctype.delete()
+
def test_sync_field_order(self):
from frappe.modules.import_file import get_file_path
import os
@@ -349,4 +375,4 @@ class TestDocType(unittest.TestCase):
# delete doctype
link_doc.delete()
doc.delete()
- frappe.db.commit()
\ No newline at end of file
+ frappe.db.commit()
diff --git a/frappe/core/doctype/role_profile/role_profile.js b/frappe/core/doctype/role_profile/role_profile.js
index 09aead670a..d31618cc4a 100644
--- a/frappe/core/doctype/role_profile/role_profile.js
+++ b/frappe/core/doctype/role_profile/role_profile.js
@@ -2,7 +2,7 @@
// For license information, please see license.txt
frappe.ui.form.on('Role Profile', {
- setup: function(frm) {
+ refresh: function(frm) {
if(has_common(frappe.user_roles, ["Administrator", "System Manager"])) {
if(!frm.roles_editor) {
var role_area = $('
')
diff --git a/frappe/core/doctype/system_settings/system_settings.json b/frappe/core/doctype/system_settings/system_settings.json
index eb20802624..f2c62ad1a3 100644
--- a/frappe/core/doctype/system_settings/system_settings.json
+++ b/frappe/core/doctype/system_settings/system_settings.json
@@ -37,6 +37,7 @@
"allow_login_using_user_name",
"allow_error_traceback",
"password_settings",
+ "logout_on_password_reset",
"force_user_to_reset_password",
"column_break_31",
"enable_password_policy",
@@ -407,6 +408,12 @@
"fieldname": "dormant_days",
"fieldtype": "Int",
"label": "Run Jobs only Daily if Inactive For (Days)"
+ },
+ {
+ "default": "1",
+ "fieldname": "logout_on_password_reset",
+ "fieldtype": "Check",
+ "label": "Logout All Sessions on Password Reset"
}
],
"icon": "fa fa-cog",
diff --git a/frappe/core/doctype/user/test_user.py b/frappe/core/doctype/user/test_user.py
index 0d981c9e9e..d4c0fa98ed 100644
--- a/frappe/core/doctype/user/test_user.py
+++ b/frappe/core/doctype/user/test_user.py
@@ -224,3 +224,4 @@ class TestUser(unittest.TestCase):
def delete_contact(user):
frappe.db.sql("DELETE FROM `tabContact` WHERE `email_id`= %s", user)
+ frappe.db.sql("DELETE FROM `tabContact Email` WHERE `email_id`= %s", user)
diff --git a/frappe/core/doctype/user/user.json b/frappe/core/doctype/user/user.json
index b71c9555e1..5ebde7e7bd 100644
--- a/frappe/core/doctype/user/user.json
+++ b/frappe/core/doctype/user/user.json
@@ -1,4 +1,5 @@
{
+ "actions": [],
"allow_import": 1,
"allow_rename": 1,
"creation": "2014-03-11 14:55:00",
@@ -178,7 +179,7 @@
{
"fieldname": "time_zone",
"fieldtype": "Select",
- "label": "Timezone"
+ "label": "Time Zone"
},
{
"description": "Get your globally recognized avatar from Gravatar.com",
@@ -302,7 +303,7 @@
"default": "0",
"fieldname": "logout_all_sessions",
"fieldtype": "Check",
- "label": "Logout from all devices while changing Password"
+ "label": "Logout From All Devices After Changing Password"
},
{
"fieldname": "reset_password_key",
@@ -338,7 +339,7 @@
"default": "0",
"fieldname": "document_follow_notify",
"fieldtype": "Check",
- "label": "Send Notifications for documents followed by me"
+ "label": "Send Notifications For Documents Followed By Me"
},
{
"default": "Daily",
@@ -359,7 +360,7 @@
"default": "1",
"fieldname": "thread_notify",
"fieldtype": "Check",
- "label": "Send Notifications for Email threads"
+ "label": "Send Notifications For Email Threads"
},
{
"default": "0",
@@ -496,7 +497,7 @@
"description": "If enabled, user can login from any IP Address using Two Factor Auth, this can also be set for all users in System Settings",
"fieldname": "bypass_restrict_ip_check_if_2fa_enabled",
"fieldtype": "Check",
- "label": "Bypass restricted IP Address check If Two Factor Auth Enabled"
+ "label": "Bypass Restricted IP Address Check If Two Factor Auth Enabled"
},
{
"fieldname": "column_break1",
@@ -585,8 +586,9 @@
"icon": "fa fa-user",
"idx": 413,
"image_field": "user_image",
+ "links": [],
"max_attachments": 5,
- "modified": "2019-10-22 14:16:34.810223",
+ "modified": "2020-03-23 22:59:26.154985",
"modified_by": "Administrator",
"module": "Core",
"name": "User",
diff --git a/frappe/core/doctype/user/user.py b/frappe/core/doctype/user/user.py
index 5740485e16..ddad3a91fb 100644
--- a/frappe/core/doctype/user/user.py
+++ b/frappe/core/doctype/user/user.py
@@ -555,7 +555,8 @@ def update_password(new_password, logout_all_sessions=0, key=None, old_password=
else:
user = res['user']
- _update_password(user, new_password, logout_all_sessions=int(logout_all_sessions))
+ logout_all_sessions = cint(logout_all_sessions) or frappe.db.get_single_value("System Settings", "logout_on_password_reset")
+ _update_password(user, new_password, logout_all_sessions=cint(logout_all_sessions))
user_doc, redirect_url = reset_user_data(user)
diff --git a/frappe/core/page/dashboard/dashboard.js b/frappe/core/page/dashboard/dashboard.js
index 511aac7010..8705804014 100644
--- a/frappe/core/page/dashboard/dashboard.js
+++ b/frappe/core/page/dashboard/dashboard.js
@@ -59,7 +59,7 @@ class Dashboard {
}
show_dashboard(current_dashboard_name) {
- if(this.dashboard_name !== current_dashboard_name) {
+ if (this.dashboard_name !== current_dashboard_name) {
this.dashboard_name = current_dashboard_name;
let title = this.dashboard_name;
if (!this.dashboard_name.toLowerCase().includes(__('dashboard'))) {
@@ -76,9 +76,11 @@ class Dashboard {
}
refresh() {
- this.get_dashboard_doc().then((doc) => {
- this.dashboard_doc = doc;
- this.charts = this.dashboard_doc.charts
+ this.get_permitted_dashboard_charts().then(charts => {
+ if (!charts.length) {
+ frappe.msgprint(__('No Permitted Charts on this Dashboard'), __('No Permitted Charts'))
+ }
+ this.charts = charts
.map(chart => {
return {
chart_name: chart.chart,
@@ -98,8 +100,14 @@ class Dashboard {
});
}
- get_dashboard_doc() {
- return frappe.model.with_doc('Dashboard', this.dashboard_name);
+ get_permitted_dashboard_charts() {
+ return frappe.xcall(
+ 'frappe.desk.doctype.dashboard.dashboard.get_permitted_charts',
+ {
+ dashboard_name: this.dashboard_name
+ }).then(charts => {
+ return charts;
+ });
}
set_dropdown() {
diff --git a/frappe/core/page/dashboard/dashboard.json b/frappe/core/page/dashboard/dashboard.json
index 891dcb26f8..58fda5a34c 100644
--- a/frappe/core/page/dashboard/dashboard.json
+++ b/frappe/core/page/dashboard/dashboard.json
@@ -4,7 +4,7 @@
"docstatus": 0,
"doctype": "Page",
"idx": 0,
- "modified": "2019-01-08 19:19:48.073410",
+ "modified": "2020-03-26 13:30:44.603948",
"modified_by": "Administrator",
"module": "Core",
"name": "dashboard",
diff --git a/frappe/core/page/permission_manager/permission_manager.js b/frappe/core/page/permission_manager/permission_manager.js
index 710bb51680..ed3b0d17db 100644
--- a/frappe/core/page/permission_manager/permission_manager.js
+++ b/frappe/core/page/permission_manager/permission_manager.js
@@ -217,6 +217,7 @@ frappe.PermissionEngine = Class.extend({
me.rights.forEach(r => {
if (!d.is_submittable && ['submit', 'cancel', 'amend'].includes(r)) return;
+ if (d.in_create && ['create', 'write', 'delete'].includes(r)) return;
me.add_check(perm_container, d, r);
});
diff --git a/frappe/core/page/permission_manager/permission_manager.py b/frappe/core/page/permission_manager/permission_manager.py
index 1afd7bb423..637b526d5c 100644
--- a/frappe/core/page/permission_manager/permission_manager.py
+++ b/frappe/core/page/permission_manager/permission_manager.py
@@ -66,6 +66,7 @@ def get_permissions(doctype=None, role=None):
meta = frappe.get_meta(d.parent)
if meta:
d.is_submittable = meta.is_submittable
+ d.in_create = meta.in_create
return out
diff --git a/frappe/custom/doctype/customize_form/customize_form.js b/frappe/custom/doctype/customize_form/customize_form.js
index 63b094615a..b1743a96a5 100644
--- a/frappe/custom/doctype/customize_form/customize_form.js
+++ b/frappe/custom/doctype/customize_form/customize_form.js
@@ -85,6 +85,10 @@ frappe.ui.form.on("Customize Form", {
if(frm.doc.doc_type) {
frappe.customize_form.set_primary_action(frm);
+ frm.add_custom_button(__('Go to {0} List', [frm.doc.doc_type]), function() {
+ frappe.set_route('List', frm.doc.doc_type);
+ });
+
frm.add_custom_button(__('Refresh Form'), function() {
frm.script_manager.trigger("doc_type");
}, "fa fa-refresh", "btn-default");
@@ -139,8 +143,7 @@ frappe.ui.form.on("Customize Form", {
}, 1000);
}
- },
-
+ }
});
frappe.ui.form.on("Customize Form Field", {
diff --git a/frappe/custom/doctype/customize_form/customize_form.json b/frappe/custom/doctype/customize_form/customize_form.json
index 0b1df62f9d..51a5c0b85f 100644
--- a/frappe/custom/doctype/customize_form/customize_form.json
+++ b/frappe/custom/doctype/customize_form/customize_form.json
@@ -1,4 +1,5 @@
{
+ "actions": [],
"autoname": "DL.####",
"creation": "2013-01-29 17:55:08",
"doctype": "DocType",
@@ -28,6 +29,10 @@
"sort_field",
"column_break_10",
"sort_order",
+ "section_break_23",
+ "email_append_to",
+ "sender_field",
+ "subject_field",
"fields_section_break",
"fields"
],
@@ -174,13 +179,38 @@
"fieldname": "allow_import",
"fieldtype": "Check",
"label": "Allow Import (via Data Import Tool)"
+ },
+ {
+ "depends_on": "email_append_to",
+ "fieldname": "subject_field",
+ "fieldtype": "Data",
+ "label": "Subject Field"
+ },
+ {
+ "depends_on": "email_append_to",
+ "fieldname": "sender_field",
+ "fieldtype": "Data",
+ "label": "Sender Field",
+ "mandatory_depends_on": "email_append_to"
+ },
+ {
+ "default": "0",
+ "fieldname": "email_append_to",
+ "fieldtype": "Check",
+ "label": "Allow document creation via Email"
+ },
+ {
+ "depends_on": "doc_type",
+ "fieldname": "section_break_23",
+ "fieldtype": "Section Break"
}
],
"hide_toolbar": 1,
"icon": "fa fa-glass",
"idx": 1,
"issingle": 1,
- "modified": "2019-10-08 11:16:36.698006",
+ "links": [],
+ "modified": "2020-03-27 15:06:35.443861",
"modified_by": "Administrator",
"module": "Custom",
"name": "Customize Form",
diff --git a/frappe/custom/doctype/customize_form/customize_form.py b/frappe/custom/doctype/customize_form/customize_form.py
index 8d47a075ba..68848d26f6 100644
--- a/frappe/custom/doctype/customize_form/customize_form.py
+++ b/frappe/custom/doctype/customize_form/customize_form.py
@@ -12,7 +12,7 @@ from frappe import _
from frappe.utils import cint
from frappe.model.document import Document
from frappe.model import no_value_fields, core_doctypes_list
-from frappe.core.doctype.doctype.doctype import validate_fields_for_doctype
+from frappe.core.doctype.doctype.doctype import validate_fields_for_doctype, check_email_append_to
from frappe.custom.doctype.custom_field.custom_field import create_custom_field
from frappe.model.docfield import supports_translation
@@ -31,7 +31,10 @@ doctype_properties = {
'track_changes': 'Check',
'track_views': 'Check',
'allow_auto_repeat': 'Check',
- 'allow_import': 'Check'
+ 'allow_import': 'Check',
+ 'email_append_to': 'Check',
+ 'subject_field': 'Data',
+ 'sender_field': 'Data'
}
docfield_properties = {
@@ -166,11 +169,11 @@ class CustomizeForm(Document):
self.flags.update_db = False
self.flags.rebuild_doctype_for_global_search = False
-
self.set_property_setters()
self.update_custom_fields()
self.set_name_translation()
validate_fields_for_doctype(self.doc_type)
+ check_email_append_to(self)
if self.flags.update_db:
frappe.db.updatedb(self.doc_type)
@@ -362,13 +365,49 @@ class CustomizeForm(Document):
def validate_fieldtype_change(self, df, old_value, new_value):
allowed = False
+ self.check_length_for_fieldtypes = []
for allowed_changes in allowed_fieldtype_change:
if (old_value in allowed_changes and new_value in allowed_changes):
allowed = True
+ if frappe.db.type_map.get(old_value)[1] > frappe.db.type_map.get(new_value)[1]:
+ self.check_length_for_fieldtypes.append({'df': df, 'old_value': old_value})
+ self.validate_fieldtype_length()
+ else:
+ self.flags.update_db = True
break
if not allowed:
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):
+ for field in self.check_length_for_fieldtypes:
+ df = field.get('df')
+ max_length = frappe.db.type_map.get(df.fieldtype)[1]
+ fieldname = df.fieldname
+ docs = frappe.db.sql('''
+ SELECT name, {fieldname}, LENGTH({fieldname}) AS len
+ FROM `tab{doctype}`
+ WHERE LENGTH({fieldname}) > {max_length}
+ '''.format(
+ fieldname=fieldname,
+ doctype=self.doc_type,
+ max_length=max_length
+ ), as_dict=True)
+ links = []
+ label = df.label
+ for doc in docs:
+ links.append(frappe.utils.get_link_to_form(self.doc_type, doc.name))
+ links_str = ', '.join(links)
+
+ if docs:
+ frappe.throw(_('Value for field {0} is too long in {1}. Length should be lesser than {2} characters')
+ .format(
+ frappe.bold(label),
+ links_str,
+ frappe.bold(max_length)
+ ), title=_('Data Too Long'), is_minimizable=len(docs) > 1)
+
+ self.flags.update_db = True
+
def reset_to_defaults(self):
if not self.doc_type:
return
diff --git a/frappe/custom/doctype/customize_form/test_customize_form.py b/frappe/custom/doctype/customize_form/test_customize_form.py
index 1cd71ea05d..cace25a03d 100644
--- a/frappe/custom/doctype/customize_form/test_customize_form.py
+++ b/frappe/custom/doctype/customize_form/test_customize_form.py
@@ -46,7 +46,7 @@ class TestCustomizeForm(unittest.TestCase):
d = self.get_customize_form("Event")
self.assertEquals(d.doc_type, "Event")
- self.assertEquals(len(d.get("fields")), 35)
+ self.assertEquals(len(d.get("fields")), 36)
d = self.get_customize_form("Event")
self.assertEquals(d.doc_type, "Event")
diff --git a/frappe/database/mariadb/framework_mariadb.sql b/frappe/database/mariadb/framework_mariadb.sql
index dbe53df4e4..46940cc846 100644
--- a/frappe/database/mariadb/framework_mariadb.sql
+++ b/frappe/database/mariadb/framework_mariadb.sql
@@ -217,6 +217,9 @@ CREATE TABLE `tabDocType` (
`allow_guest_to_view` int(1) NOT NULL DEFAULT 0,
`route` varchar(255) DEFAULT NULL,
`is_published_field` varchar(255) DEFAULT NULL,
+ `email_append_to` int(1) NOT NULL DEFAULT 0,
+ `subject_field` varchar(255) DEFAULT NULL,
+ `sender_field` varchar(255) DEFAULT NULL,
PRIMARY KEY (`name`),
KEY `parent` (`parent`)
) ENGINE=InnoDB ROW_FORMAT=COMPRESSED CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
diff --git a/frappe/database/postgres/database.py b/frappe/database/postgres/database.py
index 243d0f934e..e30ef3293f 100644
--- a/frappe/database/postgres/database.py
+++ b/frappe/database/postgres/database.py
@@ -92,7 +92,7 @@ class PostgresDatabase(Database):
# pylint: disable=W0221
def sql(self, *args, **kwargs):
- if len(args):
+ if args:
# since tuple is immutable
args = list(args)
args[0] = modify_query(args[0])
@@ -276,13 +276,13 @@ class PostgresDatabase(Database):
# pylint: disable=W1401
return self.sql('''
SELECT a.column_name AS name,
- CASE a.data_type
+ CASE LOWER(a.data_type)
WHEN 'character varying' THEN CONCAT('varchar(', a.character_maximum_length ,')')
- WHEN 'timestamp without TIME zone' THEN 'timestamp'
+ WHEN 'timestamp without time zone' THEN 'timestamp'
ELSE a.data_type
END AS type,
COUNT(b.indexdef) AS Index,
- COALESCE(a.column_default, NULL) AS default,
+ SPLIT_PART(COALESCE(a.column_default, NULL), '::', 1) AS default,
BOOL_OR(b.unique) AS unique
FROM information_schema.columns a
LEFT JOIN
diff --git a/frappe/database/postgres/framework_postgres.sql b/frappe/database/postgres/framework_postgres.sql
index 457f6c906a..26760dbcc9 100644
--- a/frappe/database/postgres/framework_postgres.sql
+++ b/frappe/database/postgres/framework_postgres.sql
@@ -222,6 +222,9 @@ CREATE TABLE "tabDocType" (
"allow_guest_to_view" smallint NOT NULL DEFAULT 0,
"route" varchar(255) DEFAULT NULL,
"is_published_field" varchar(255) DEFAULT NULL,
+ "email_append_to" smallint NOT NULL DEFAULT 0,
+ "subject_field" varchar(255) DEFAULT NULL,
+ "sender_field" varchar(255) DEFAULT NULL,
PRIMARY KEY ("name")
) ;
diff --git a/frappe/database/schema.py b/frappe/database/schema.py
index 88cda9340b..28e055f382 100644
--- a/frappe/database/schema.py
+++ b/frappe/database/schema.py
@@ -13,7 +13,7 @@ class DBTable:
def __init__(self, doctype, meta=None):
self.doctype = doctype
self.table_name = 'tab{}'.format(doctype)
- self.meta = meta or frappe.get_meta(doctype)
+ self.meta = meta or frappe.get_meta(doctype, False)
self.columns = {}
self.current_columns = {}
@@ -65,64 +65,35 @@ class DBTable:
"""
get columns from docfields and custom fields
"""
- fl = frappe.db.sql("SELECT * FROM `tabDocField` WHERE parent = %s", self.doctype, as_dict = 1)
- lengths = {}
- precisions = {}
- uniques = {}
+ fields = self.meta.get_fieldnames_with_value(True)
# optional fields like _comments
- if not self.meta.istable:
+ if not self.meta.get('istable'):
for fieldname in frappe.db.OPTIONAL_COLUMNS:
- fl.append({
+ fields.append({
"fieldname": fieldname,
"fieldtype": "Text"
})
# add _seen column if track_seen
- if getattr(self.meta, 'track_seen', False):
- fl.append({
+ if self.meta.get('track_seen'):
+ fields.append({
'fieldname': '_seen',
'fieldtype': 'Text'
})
- if (not frappe.flags.in_install_db
- and (frappe.flags.in_install != "frappe"
- or frappe.flags.ignore_in_install)):
- custom_fl = frappe.db.sql("""
- SELECT * FROM `tabCustom Field`
- WHERE dt = %s AND docstatus < 2
- """, (self.doctype,), as_dict=1)
- if custom_fl: fl += custom_fl
-
- # apply length, precision and unique from property setters
- for ps in frappe.get_all("Property Setter",
- fields=["field_name", "property", "value"],
- filters={
- "doc_type": self.doctype,
- "doctype_or_field": "DocField",
- "property": ["in", ["precision", "length", "unique"]]
- }):
-
- if ps.property=="length":
- lengths[ps.field_name] = cint(ps.value)
-
- elif ps.property=="precision":
- precisions[ps.field_name] = cint(ps.value)
-
- elif ps.property=="unique":
- uniques[ps.field_name] = cint(ps.value)
-
- for f in fl:
- self.columns[f['fieldname']] = DbColumn(self,
- f['fieldname'],
- f['fieldtype'],
- lengths.get(f["fieldname"]) or f.get('length'),
- f.get('default'),
- f.get('search_index'),
- f.get('options'),
- uniques.get(f["fieldname"],
- f.get('unique')),
- precisions.get(f['fieldname']) or f.get('precision'))
+ for field in fields:
+ self.columns[field.get('fieldname')] = DbColumn(
+ self,
+ field.get('fieldname'),
+ field.get('fieldtype'),
+ field.get('length'),
+ field.get('default'),
+ field.get('search_index'),
+ field.get('options'),
+ field.get('unique'),
+ field.get('precision')
+ )
def validate(self):
"""Check if change in varchar length isn't truncating the columns"""
diff --git a/frappe/desk/desktop.py b/frappe/desk/desktop.py
index ef84114745..1cb03355c6 100644
--- a/frappe/desk/desktop.py
+++ b/frappe/desk/desktop.py
@@ -146,8 +146,9 @@ class Workspace:
charts = charts + self.extended_charts
for chart in charts:
- chart.label = chart.label if chart.label else chart.chart_name
- all_charts.append(chart)
+ if frappe.has_permission('Dashboard Chart', doc=chart.chart_name):
+ chart.label = chart.label if chart.label else chart.chart_name
+ all_charts.append(chart)
return all_charts
diff --git a/frappe/desk/doctype/dashboard/dashboard.json b/frappe/desk/doctype/dashboard/dashboard.json
index 239f35bea8..c177ee70ac 100644
--- a/frappe/desk/doctype/dashboard/dashboard.json
+++ b/frappe/desk/doctype/dashboard/dashboard.json
@@ -34,7 +34,7 @@
}
],
"links": [],
- "modified": "2020-01-26 20:00:10.069817",
+ "modified": "2020-03-25 21:09:37.080132",
"modified_by": "Administrator",
"module": "Desk",
"name": "Dashboard",
@@ -51,6 +51,27 @@
"role": "System Manager",
"share": 1,
"write": 1
+ },
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Dashboard Manager",
+ "share": 1,
+ "write": 1
+ },
+ {
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "All",
+ "share": 1
}
],
"quick_entry": 1,
diff --git a/frappe/desk/doctype/dashboard/dashboard.py b/frappe/desk/doctype/dashboard/dashboard.py
index c8f22d29b9..5c344956bf 100644
--- a/frappe/desk/doctype/dashboard/dashboard.py
+++ b/frappe/desk/doctype/dashboard/dashboard.py
@@ -12,3 +12,12 @@ class Dashboard(Document):
# make all other dashboards non-default
frappe.db.sql('''update
tabDashboard set is_default = 0 where name != %s''', self.name)
+
+@frappe.whitelist()
+def get_permitted_charts(dashboard_name):
+ permitted_charts = []
+ dashboard = frappe.get_doc('Dashboard', dashboard_name)
+ for chart in dashboard.charts:
+ if frappe.has_permission('Dashboard Chart', doc=chart.chart):
+ permitted_charts.append(chart)
+ return permitted_charts
diff --git a/frappe/desk/doctype/dashboard_chart/dashboard_chart.json b/frappe/desk/doctype/dashboard_chart/dashboard_chart.json
index 0a017a0de2..9652ae3945 100644
--- a/frappe/desk/doctype/dashboard_chart/dashboard_chart.json
+++ b/frappe/desk/doctype/dashboard_chart/dashboard_chart.json
@@ -215,7 +215,7 @@
}
],
"links": [],
- "modified": "2020-03-13 19:19:37.162771",
+ "modified": "2020-03-31 16:00:01.987059",
"modified_by": "Administrator",
"module": "Desk",
"name": "Dashboard Chart",
@@ -232,6 +232,27 @@
"role": "System Manager",
"share": 1,
"write": 1
+ },
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Dashboard Manager",
+ "share": 1,
+ "write": 1
+ },
+ {
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "All",
+ "share": 1
}
],
"sort_field": "modified",
diff --git a/frappe/desk/doctype/dashboard_chart/dashboard_chart.py b/frappe/desk/doctype/dashboard_chart/dashboard_chart.py
index f01c976b9c..b2a6f0a0ff 100644
--- a/frappe/desk/doctype/dashboard_chart/dashboard_chart.py
+++ b/frappe/desk/doctype/dashboard_chart/dashboard_chart.py
@@ -10,8 +10,51 @@ import json
from frappe.core.page.dashboard.dashboard import cache_source, get_from_date_from_timespan
from frappe.utils import nowdate, add_to_date, getdate, get_last_day, formatdate, get_datetime
from frappe.model.naming import append_number_if_name_exists
+from frappe.boot import get_allowed_reports
from frappe.model.document import Document
+
+def get_permission_query_conditions(user):
+
+ if not user:
+ user = frappe.session.user
+
+ if user == 'Administrator':
+ return
+
+ roles = frappe.get_roles(user)
+ if "System Manager" in roles:
+ return None
+
+ allowed_doctypes = tuple(frappe.permissions.get_doctypes_with_read())
+ allowed_reports = tuple([key.encode('UTF8') for key in get_allowed_reports()])
+
+ return '''
+ `tabDashboard Chart`.`document_type` in {allowed_doctypes}
+ or `tabDashboard Chart`.`report_name` in {allowed_reports}
+ '''.format(
+ allowed_doctypes=allowed_doctypes,
+ allowed_reports=allowed_reports
+ )
+
+
+def has_permission(doc, ptype, user):
+ roles = frappe.get_roles(user)
+ if "System Manager" in roles:
+ return True
+
+
+ if doc.chart_type == 'Report':
+ allowed_reports = tuple([key.encode('UTF8') for key in get_allowed_reports()])
+ if doc.report_name in allowed_reports:
+ return True
+ else:
+ allowed_doctypes = tuple(frappe.permissions.get_doctypes_with_read())
+ if doc.document_type in allowed_doctypes:
+ return True
+
+ return False
+
@frappe.whitelist()
@cache_source
def get(chart_name = None, chart = None, no_cache = None, filters = None, from_date = None,
diff --git a/frappe/desk/doctype/event/event.json b/frappe/desk/doctype/event/event.json
index 032030ddef..5768f00f32 100644
--- a/frappe/desk/doctype/event/event.json
+++ b/frappe/desk/doctype/event/event.json
@@ -1,9 +1,11 @@
{
+ "actions": [],
"allow_import": 1,
"autoname": "EV.#####",
"creation": "2013-06-10 13:17:47",
"doctype": "DocType",
"document_type": "Document",
+ "email_append_to": 1,
"engine": "InnoDB",
"field_order": [
"details",
@@ -17,6 +19,7 @@
"starts_on",
"ends_on",
"status",
+ "sender",
"all_day",
"sync_with_google_calendar",
"sb_00",
@@ -262,11 +265,19 @@
"fieldtype": "Check",
"label": "Pulled from Google Calendar",
"read_only": 1
+ },
+ {
+ "fieldname": "sender",
+ "fieldtype": "Data",
+ "label": "Sender",
+ "options": "Email",
+ "read_only": 1
}
],
"icon": "fa fa-calendar",
"idx": 1,
- "modified": "2019-08-08 16:01:19.489396",
+ "links": [],
+ "modified": "2020-01-14 21:47:15.825287",
"modified_by": "Administrator",
"module": "Desk",
"name": "Event",
@@ -297,8 +308,10 @@
}
],
"read_only": 1,
+ "sender_field": "sender",
"sort_field": "modified",
"sort_order": "DESC",
+ "subject_field": "subject",
"title_field": "subject",
"track_changes": 1,
"track_seen": 1,
diff --git a/frappe/desk/doctype/todo/todo.json b/frappe/desk/doctype/todo/todo.json
index 508720a488..15e0e4abe1 100644
--- a/frappe/desk/doctype/todo/todo.json
+++ b/frappe/desk/doctype/todo/todo.json
@@ -1,8 +1,10 @@
{
+ "actions": [],
"autoname": "hash",
"creation": "2012-07-03 13:30:35",
"doctype": "DocType",
"document_type": "Setup",
+ "email_append_to": 1,
"engine": "InnoDB",
"field_order": [
"description_and_status",
@@ -142,7 +144,8 @@
"fieldname": "sender",
"fieldtype": "Data",
"hidden": 1,
- "label": "Sender"
+ "label": "Sender",
+ "options": "Email"
},
{
"fieldname": "assignment_rule",
@@ -154,7 +157,8 @@
],
"icon": "fa fa-check",
"idx": 2,
- "modified": "2019-09-10 14:34:59.161750",
+ "links": [],
+ "modified": "2020-01-14 17:04:36.971002",
"modified_by": "Administrator",
"module": "Desk",
"name": "ToDo",
@@ -185,9 +189,11 @@
],
"quick_entry": 1,
"search_fields": "description, reference_type, reference_name",
+ "sender_field": "sender",
"sort_field": "modified",
"sort_order": "DESC",
+ "subject_field": "description",
"title_field": "description",
"track_changes": 1,
"track_seen": 1
-}
+}
\ No newline at end of file
diff --git a/frappe/desk/doctype/todo/todo.py b/frappe/desk/doctype/todo/todo.py
index 6cd7c68368..8e8102d093 100644
--- a/frappe/desk/doctype/todo/todo.py
+++ b/frappe/desk/doctype/todo/todo.py
@@ -8,8 +8,6 @@ import json
from frappe.model.document import Document
from frappe.utils import get_fullname
-subject_field = "description"
-sender_field = "sender"
exclude_from_linked_with = True
class ToDo(Document):
diff --git a/frappe/desk/form/utils.py b/frappe/desk/form/utils.py
index 498ab50645..4c3bab2e23 100644
--- a/frappe/desk/form/utils.py
+++ b/frappe/desk/form/utils.py
@@ -56,18 +56,20 @@ def validate_link():
frappe.response['valid_value'] = valid_value
frappe.response['message'] = 'Ok'
+
@frappe.whitelist()
-def add_comment(reference_doctype, reference_name, content, comment_email):
+def add_comment(reference_doctype, reference_name, content, comment_email, comment_by):
"""allow any logged user to post a comment"""
doc = frappe.get_doc(dict(
- doctype = 'Comment',
- reference_doctype = reference_doctype,
- reference_name = reference_name,
- comment_email = comment_email,
- comment_type = 'Comment'
+ doctype='Comment',
+ reference_doctype=reference_doctype,
+ reference_name=reference_name,
+ comment_email=comment_email,
+ comment_type='Comment',
+ comment_by=comment_by
))
doc.content = extract_images_from_html(doc, content)
- doc.insert(ignore_permissions = True)
+ doc.insert(ignore_permissions=True)
follow_document(doc.reference_doctype, doc.reference_name, frappe.session.user)
return doc.as_dict()
diff --git a/frappe/desk/query_report.py b/frappe/desk/query_report.py
index 301c37cc21..d210af02fd 100644
--- a/frappe/desk/query_report.py
+++ b/frappe/desk/query_report.py
@@ -77,9 +77,9 @@ def generate_report_result(report, filters=None, user=None):
if len(res) > 5:
skip_total_row = cint(res[5])
- if report.custom_columns:
- columns = json.loads(report.custom_columns)
- result = add_data_to_custom_columns(columns, result)
+ if report.custom_columns:
+ columns = json.loads(report.custom_columns)
+ result = add_data_to_custom_columns(columns, result)
if result:
result = get_filtered_data(report.ref_doctype, columns, result, user)
diff --git a/frappe/email/doctype/email_account/email_account.py b/frappe/email/doctype/email_account/email_account.py
index f3eb2188b7..c0a198f5e5 100755
--- a/frappe/email/doctype/email_account/email_account.py
+++ b/frappe/email/doctype/email_account/email_account.py
@@ -452,16 +452,15 @@ class EmailAccount(Document):
def set_sender_field_and_subject_field(self):
'''Identify the sender and subject fields from the `append_to` DocType'''
# set subject_field and sender_field
- meta_module = frappe.get_meta_module(self.append_to)
meta = frappe.get_meta(self.append_to)
+ self.subject_field = None
+ self.sender_field = None
- self.subject_field = getattr(meta_module, "subject_field", "subject")
- if not meta.get_field(self.subject_field):
- self.subject_field = None
+ if hasattr(meta, "subject_field"):
+ self.subject_field = meta.subject_field
- self.sender_field = getattr(meta_module, "sender_field", "sender")
- if not meta.get_field(self.sender_field):
- self.sender_field = None
+ if hasattr(meta, "sender_field"):
+ self.sender_field = meta.sender_field
def find_parent_based_on_subject_and_sender(self, communication, email):
'''Find parent document based on subject and sender match'''
@@ -675,8 +674,21 @@ class EmailAccount(Document):
@frappe.whitelist()
def get_append_to(doctype=None, txt=None, searchfield=None, start=None, page_len=None, filters=None):
- if not txt: txt = ""
- return [[d] for d in frappe.get_hooks("email_append_to") if txt in d]
+ txt = txt if txt else ""
+ email_append_to_list = []
+
+ # Set Email Append To DocTypes via DocType
+ filters = {"istable": 0, "issingle": 0, "email_append_to": 1}
+ for dt in frappe.get_all("DocType", filters=filters, fields=["name", "email_append_to"]):
+ email_append_to_list.append(dt.name)
+
+ # Set Email Append To DocTypes set via Customize Form
+ for dt in frappe.get_list("Property Setter", filters={"property": "email_append_to", "value": 1}, fields=["doc_type"]):
+ email_append_to_list.append(dt.doc_type)
+
+ email_append_to = [[d] for d in set(email_append_to_list) if txt in d]
+
+ return email_append_to
def test_internet(host="8.8.8.8", port=53, timeout=3):
"""Returns True if internet is connected
diff --git a/frappe/exceptions.py b/frappe/exceptions.py
index 3d63f4b2b4..732fc39e9a 100644
--- a/frappe/exceptions.py
+++ b/frappe/exceptions.py
@@ -78,6 +78,7 @@ class TimestampMismatchError(ValidationError): pass
class EmptyTableError(ValidationError): pass
class LinkExistsError(ValidationError): pass
class InvalidEmailAddressError(ValidationError): pass
+class InvalidPhoneNumberError(ValidationError): pass
class TemplateNotFoundError(ValidationError): pass
class UniqueValidationError(ValidationError): pass
class AppNotInstalledError(ValidationError): pass
diff --git a/frappe/hooks.py b/frappe/hooks.py
index c44c05fdf4..733cec7a08 100644
--- a/frappe/hooks.py
+++ b/frappe/hooks.py
@@ -87,6 +87,7 @@ permission_query_conditions = {
"ToDo": "frappe.desk.doctype.todo.todo.get_permission_query_conditions",
"User": "frappe.core.doctype.user.user.get_permission_query_conditions",
"Notification Log": "frappe.desk.doctype.notification_log.notification_log.get_permission_query_conditions",
+ "Dashboard Chart": "frappe.desk.doctype.dashboard_chart.dashboard_chart.get_permission_query_conditions",
"Notification Settings": "frappe.desk.doctype.notification_settings.notification_settings.get_permission_query_conditions",
"Note": "frappe.desk.doctype.note.note.get_permission_query_conditions",
"Kanban Board": "frappe.desk.doctype.kanban_board.kanban_board.get_permission_query_conditions",
@@ -101,6 +102,7 @@ has_permission = {
"ToDo": "frappe.desk.doctype.todo.todo.has_permission",
"User": "frappe.core.doctype.user.user.has_permission",
"Note": "frappe.desk.doctype.note.note.has_permission",
+ "Dashboard Chart": "frappe.desk.doctype.dashboard_chart.dashboard_chart.has_permission",
"Kanban Board": "frappe.desk.doctype.kanban_board.kanban_board.has_permission",
"Contact": "frappe.contacts.address_and_contact.has_permission",
"Address": "frappe.contacts.address_and_contact.has_permission",
diff --git a/frappe/model/__init__.py b/frappe/model/__init__.py
index 1fe92d7a67..7af987f4bc 100644
--- a/frappe/model/__init__.py
+++ b/frappe/model/__init__.py
@@ -48,6 +48,7 @@ table_fields = ('Table', 'Table MultiSelect')
core_doctypes_list = ('DocType', 'DocField', 'DocPerm', 'DocType Action', 'DocType Link', 'User', 'Role', 'Has Role',
'Page', 'Module Def', 'Print Format', 'Report', 'Customize Form',
'Customize Form Field', 'Property Setter', 'Custom Field', 'Custom Script')
+data_field_options = ('Email', 'Phone')
def copytables(srctype, src, srcfield, tartype, tar, tarfield, srcfields, tarfields=[]):
if not tarfields:
diff --git a/frappe/model/base_document.py b/frappe/model/base_document.py
index 569cea9d5f..4af502f844 100644
--- a/frappe/model/base_document.py
+++ b/frappe/model/base_document.py
@@ -544,6 +544,23 @@ class BaseDocument(object):
frappe.throw(_('{0} {1} cannot be "{2}". It should be one of "{3}"').format(prefix, label,
value, comma_options))
+ def _validate_data_fields(self):
+ from frappe.core.doctype.user.user import STANDARD_USERS
+
+ # data_field options defined in frappe.model.data_field_options
+ for data_field in self.meta.get_data_fields():
+ data = self.get(data_field.fieldname)
+ data_field_options = data_field.get("options")
+
+ if data_field_options == "Email":
+ if (self.owner in STANDARD_USERS) and (data in STANDARD_USERS):
+ return
+ for email_address in frappe.utils.split_emails(data):
+ frappe.utils.validate_email_address(email_address, throw=True)
+
+ if data_field_options == "Phone":
+ frappe.utils.validate_phone_number(data, throw=True)
+
def _validate_constants(self):
if frappe.flags.in_import or self.is_new() or self.flags.ignore_validate_constants:
return
diff --git a/frappe/model/document.py b/frappe/model/document.py
index 41f946efd9..03b21ea667 100644
--- a/frappe/model/document.py
+++ b/frappe/model/document.py
@@ -468,6 +468,7 @@ class Document(BaseDocument):
def _validate(self):
self._validate_mandatory()
+ self._validate_data_fields()
self._validate_selects()
self._validate_length()
self._extract_images_from_text_editor()
@@ -477,6 +478,7 @@ class Document(BaseDocument):
children = self.get_all_children()
for d in children:
+ d._validate_data_fields()
d._validate_selects()
d._validate_length()
d._extract_images_from_text_editor()
@@ -978,7 +980,7 @@ class Document(BaseDocument):
def reset_seen(self):
"""Clear _seen property and set current user as seen"""
if getattr(self.meta, 'track_seen', False):
- self.db_set('_seen', json.dumps([frappe.session.user]), update_modified=False)
+ frappe.db.set_value(self.doctype, self.name, "_seen", json.dumps([frappe.session.user]), update_modified=False)
def notify_update(self):
"""Publish realtime that the current document is modified"""
diff --git a/frappe/model/meta.py b/frappe/model/meta.py
index 1938a4a96c..9c71f8c0b1 100644
--- a/frappe/model/meta.py
+++ b/frappe/model/meta.py
@@ -68,7 +68,7 @@ def load_doctype_from_file(doctype):
class Meta(Document):
_metaclass = True
default_fields = list(default_fields)[1:]
- special_doctypes = ("DocField", "DocPerm", "Role", "DocType", "Module Def")
+ special_doctypes = ("DocField", "DocPerm", "Role", "DocType", "Module Def", 'DocType Action', 'DocType Link')
def __init__(self, doctype):
self._fields = {}
@@ -128,6 +128,9 @@ class Meta(Document):
def get_link_fields(self):
return self.get("fields", {"fieldtype": "Link", "options":["!=", "[Select]"]})
+ def get_data_fields(self):
+ return self.get("fields", {"fieldtype": "Data"})
+
def get_dynamic_link_fields(self):
if not hasattr(self, '_dynamic_link_fields'):
self._dynamic_link_fields = self.get("fields", {"fieldtype": "Dynamic Link"})
@@ -165,7 +168,8 @@ class Meta(Document):
def get_valid_columns(self):
if not hasattr(self, "_valid_columns"):
- if self.name in ("DocType", "DocField", "DocPerm", 'DocType Action', 'DocType Link', "Property Setter"):
+ table_exists = frappe.db.table_exists(self.name)
+ if self.name in self.special_doctypes and table_exists:
self._valid_columns = get_table_columns(self.name)
else:
self._valid_columns = self.default_fields + \
@@ -290,17 +294,20 @@ class Meta(Document):
return get_workflow_name(self.name)
def add_custom_fields(self):
- try:
- self.extend("fields", frappe.db.sql("""SELECT * FROM `tabCustom Field`
- WHERE dt = %s AND docstatus < 2""", (self.name,), as_dict=1,
- update={"is_custom_field": 1}))
- except Exception as e:
- if frappe.db.is_table_missing(e):
- return
- else:
- raise
+ if not frappe.db.table_exists('Custom Field'):
+ return
+
+ custom_fields = frappe.db.sql("""
+ SELECT * FROM `tabCustom Field`
+ WHERE dt = %s AND docstatus < 2
+ """, (self.name,), as_dict=1, update={"is_custom_field": 1})
+
+ self.extend("fields", custom_fields)
def apply_property_setters(self):
+ if not frappe.db.table_exists('Property Setter'):
+ return
+
property_setters = frappe.db.sql("""select * from `tabProperty Setter` where
doc_type=%s""", (self.name,), as_dict=1)
@@ -378,8 +385,9 @@ class Meta(Document):
if custom_perms:
self.permissions = [Document(d) for d in custom_perms]
- def get_fieldnames_with_value(self):
- return [df.fieldname for df in self.fields if df.fieldtype not in no_value_fields]
+ 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 get_fields_to_check_permissions(self, user_permission_doctypes):
diff --git a/frappe/model/workflow.py b/frappe/model/workflow.py
index b134f2f8dc..3f3711af9d 100644
--- a/frappe/model/workflow.py
+++ b/frappe/model/workflow.py
@@ -45,20 +45,29 @@ def get_transitions(doc, workflow = None, raise_exception=False):
transitions = []
for transition in workflow.transitions:
if transition.state == current_state and transition.allowed in roles:
- if transition.condition:
- # if condition, evaluate
- # access to frappe.db.get_value and frappe.db.get_list
- success = frappe.safe_eval(transition.condition,
- dict(frappe = frappe._dict(
- db = frappe._dict(get_value = frappe.db.get_value, get_list=frappe.db.get_list),
- session = frappe.session
- )),
- dict(doc = doc))
- if not success:
- continue
+ if not is_transition_condition_satisfied(transition, doc):
+ continue
transitions.append(transition.as_dict())
return transitions
+def get_workflow_safe_globals():
+ # access to frappe.db.get_value and frappe.db.get_list
+ return dict(
+ frappe=frappe._dict(
+ db=frappe._dict(
+ get_value=frappe.db.get_value,
+ get_list=frappe.db.get_list
+ ),
+ session=frappe.session
+ )
+ )
+
+def is_transition_condition_satisfied(transition, doc):
+ if not transition.condition:
+ return True
+ else:
+ return frappe.safe_eval(transition.condition, get_workflow_safe_globals(), dict(doc=doc.as_dict()))
+
@frappe.whitelist()
def apply_workflow(doc, action):
'''Allow workflow action on the current doc'''
diff --git a/frappe/patches.txt b/frappe/patches.txt
index a33b4d68b0..fc4f3ae998 100644
--- a/frappe/patches.txt
+++ b/frappe/patches.txt
@@ -270,3 +270,4 @@ execute:frappe.delete_doc_if_exists('DocType', 'GSuite Settings')
execute:frappe.delete_doc_if_exists('DocType', 'GSuite Templates')
execute:frappe.delete_doc_if_exists('DocType', 'GCalendar Account')
execute:frappe.delete_doc_if_exists('DocType', 'GCalendar Settings')
+frappe.patches.v12_0.remove_parent_and_parenttype_from_print_formats
diff --git a/frappe/patches/v12_0/move_email_and_phone_to_child_table.py b/frappe/patches/v12_0/move_email_and_phone_to_child_table.py
index b18a7487f3..4388d3c849 100644
--- a/frappe/patches/v12_0/move_email_and_phone_to_child_table.py
+++ b/frappe/patches/v12_0/move_email_and_phone_to_child_table.py
@@ -5,7 +5,11 @@ def execute():
SELECT
`name`, `email_id`, `phone`, `mobile_no`, `modified_by`, `creation`, `modified`
FROM `tabContact`
+ where not exists (select * from `tabContact Email`
+ where `tabContact Email`.parent=`tabContact`.name
+ and `tabContact Email`.email_id=`tabContact`.email_id)
""", as_dict=True)
+
frappe.reload_doc("contacts", "doctype", "contact_email")
frappe.reload_doc("contacts", "doctype", "contact_phone")
frappe.reload_doc("contacts", "doctype", "contact")
@@ -15,7 +19,6 @@ def execute():
for count, contact_detail in enumerate(contact_details):
phone_counter = 1
is_primary = 1
-
if contact_detail.email_id:
email_values.append((
1,
diff --git a/frappe/patches/v12_0/remove_parent_and_parenttype_from_print_formats.py b/frappe/patches/v12_0/remove_parent_and_parenttype_from_print_formats.py
new file mode 100644
index 0000000000..1a3c56da59
--- /dev/null
+++ b/frappe/patches/v12_0/remove_parent_and_parenttype_from_print_formats.py
@@ -0,0 +1,14 @@
+import frappe
+
+def execute():
+ frappe.db.sql("""
+ UPDATE
+ `tabPrint Format`
+ SET
+ `tabPrint Format`.`parent`='',
+ `tabPrint Format`.`parenttype`='',
+ `tabPrint Format`.parentfield=''
+ WHERE
+ `tabPrint Format`.parent != ''
+ OR `tabPrint Format`.parenttype != ''
+ """)
\ No newline at end of file
diff --git a/frappe/permissions.py b/frappe/permissions.py
index a0d1677fac..0d766aec8d 100644
--- a/frappe/permissions.py
+++ b/frappe/permissions.py
@@ -307,7 +307,7 @@ def has_controller_permissions(doc, ptype, user=None):
return None
def get_doctypes_with_read():
- return list(set([p.parent for p in get_valid_perms()]))
+ return list(set([p.parent if type(p.parent) == str else p.parent.encode('UTF8') for p in get_valid_perms()]))
def get_valid_perms(doctype=None, user=None):
'''Get valid permissions for the current user from DocPerm and Custom DocPerm'''
diff --git a/frappe/public/js/frappe/form/controls/base_control.js b/frappe/public/js/frappe/form/controls/base_control.js
index 2adb5435e3..c1ba41ab16 100644
--- a/frappe/public/js/frappe/form/controls/base_control.js
+++ b/frappe/public/js/frappe/form/controls/base_control.js
@@ -152,6 +152,7 @@ frappe.ui.form.Control = Class.extend({
() => me.set_model_value(value),
() => {
me.set_mandatory && me.set_mandatory(value);
+ me.set_invalid && me.set_invalid();
if(me.df.change || me.df.onchange) {
// onchange event specified in df
diff --git a/frappe/public/js/frappe/form/controls/base_input.js b/frappe/public/js/frappe/form/controls/base_input.js
index 8a8ac271c7..0dbaaeb63c 100644
--- a/frappe/public/js/frappe/form/controls/base_input.js
+++ b/frappe/public/js/frappe/form/controls/base_input.js
@@ -179,6 +179,9 @@ frappe.ui.form.ControlInput = frappe.ui.form.Control.extend({
set_mandatory: function(value) {
this.$wrapper.toggleClass("has-error", (this.df.reqd && is_null(value)) ? true : false);
},
+ set_invalid: function () {
+ this.$wrapper.toggleClass("has-error", (this.df.invalid ? true : false));
+ },
set_bold: function() {
if(this.$input) {
this.$input.toggleClass("bold", !!(this.df.bold || this.df.reqd));
diff --git a/frappe/public/js/frappe/form/controls/data.js b/frappe/public/js/frappe/form/controls/data.js
index 6dc8c3d387..a7f0050d65 100644
--- a/frappe/public/js/frappe/form/controls/data.js
+++ b/frappe/public/js/frappe/form/controls/data.js
@@ -87,56 +87,29 @@ frappe.ui.form.ControlData = frappe.ui.form.ControlInput.extend({
return val==null ? "" : val;
},
validate: function(v) {
+ if (!v) {
+ return '';
+ }
if(this.df.is_filter) {
return v;
}
if(this.df.options == 'Phone') {
- if(v+''=='') {
- return '';
- }
- var v1 = '';
- // phone may start with + and must only have numbers later, '-' and ' ' are stripped
- v = v.replace(/ /g, '').replace(/-/g, '').replace(/\(/g, '').replace(/\)/g, '');
-
- // allow initial +,0,00
- if(v && v.substr(0,1)=='+') {
- v1 = '+'; v = v.substr(1);
- }
- if(v && v.substr(0,2)=='00') {
- v1 += '00'; v = v.substr(2);
- }
- if(v && v.substr(0,1)=='0') {
- v1 += '0'; v = v.substr(1);
- }
- v1 += cint(v) + '';
- return v1;
+ this.df.invalid = !validate_phone(v);
+ return v;
} else if(this.df.options == 'Email') {
- if(v+''=='') {
- return '';
- }
-
var email_list = frappe.utils.split_emails(v);
if (!email_list) {
- // invalid email
return '';
} else {
- var invalid_email = false;
+ let email_invalid = false;
email_list.forEach(function(email) {
if (!validate_email(email)) {
- frappe.msgprint(__("Invalid Email: {0}", [email]));
- invalid_email = true;
+ email_invalid = true;
}
});
-
- if (invalid_email) {
- // at least 1 invalid email
- return '';
- } else {
- // all good
- return v;
- }
+ this.df.invalid = email_invalid;
+ return v;
}
-
} else {
return v;
}
diff --git a/frappe/public/js/frappe/form/controls/markdown_editor.js b/frappe/public/js/frappe/form/controls/markdown_editor.js
index ee00fef0f7..81e47a0924 100644
--- a/frappe/public/js/frappe/form/controls/markdown_editor.js
+++ b/frappe/public/js/frappe/form/controls/markdown_editor.js
@@ -6,6 +6,8 @@ frappe.ui.form.ControlMarkdownEditor = frappe.ui.form.ControlCode.extend({
this.ace_editor_target.wrap(`
@@ -85,9 +88,10 @@ frappe.views.ListGroupBy = class ListGroupBy {
this.$wrapper.on('click', '.group-by-field', (e)=> {
let dropdown = $(e.currentTarget).find('.group-by-dropdown');
let fieldname = $(e.currentTarget).find('a').attr('data-fieldname');
+ let fieldtype = $(e.currentTarget).find('a').attr('data-fieldtype');
this.get_group_by_count(fieldname).then(field_count_list => {
if (field_count_list.length) {
- this.render_dropdown_items(field_count_list, dropdown);
+ this.render_dropdown_items(field_count_list, fieldtype, dropdown);
this.sidebar.setup_dropdown_search(dropdown, '.group-by-value');
} else {
dropdown.find('.group-by-loading').html(`${__("No filters found")}`);
@@ -98,7 +102,7 @@ frappe.views.ListGroupBy = class ListGroupBy {
get_group_by_dropdown_fields() {
let group_by_fields = [];
- let fields = this.list_view.meta.fields.filter((f)=> ["Select", "Link"].includes(f.fieldtype));
+ let fields = this.list_view.meta.fields.filter((f)=> ["Select", "Link", "Data", "Int", "Check"].includes(f.fieldtype));
group_by_fields.push({
label: __(this.doctype),
fieldname: 'group_by_fields',
@@ -118,7 +122,8 @@ frappe.views.ListGroupBy = class ListGroupBy {
let current_filters = this.list_view.get_filters_for_args();
// remove filter of the current field
- current_filters = current_filters.filter((f_arr) => !f_arr.includes(field === 'assigned_to' ? '_assign': field));
+ current_filters =
+ current_filters.filter((f_arr) => !f_arr.includes(field === 'assigned_to' ? '_assign': field));
let args = {
doctype: this.doctype,
@@ -138,11 +143,13 @@ frappe.views.ListGroupBy = class ListGroupBy {
});
}
- render_dropdown_items(fields, dropdown) {
+ render_dropdown_items(fields, fieldtype, dropdown) {
let get_dropdown_html = (field) => {
let label = field.name == null ? __('Not Specified') : field.name;
if (label === frappe.session.user) {
label = __('Me');
+ } else if (fieldtype && fieldtype == 'Check') {
+ label = label == '0'? __('No'): __('Yes');
}
let value = field.name == null ? '' : encodeURIComponent(field.name);
@@ -167,7 +174,9 @@ frappe.views.ListGroupBy = class ListGroupBy {
this.$wrapper.on('click', '.group-by-item', (e) => {
let $target = $(e.currentTarget);
let fieldname = $target.parents('.group-by-field').find('a').data('fieldname');
- let value = decodeURIComponent($target.data('value').trim());
+ let value = typeof $target.data('value') === 'string'
+ ? decodeURIComponent($target.data('value').trim())
+ : $target.data('value');
fieldname = fieldname === 'assigned_to' ? '_assign': fieldname;
return this.list_view.filter_area.remove(fieldname)
diff --git a/frappe/public/js/frappe/model/create_new.js b/frappe/public/js/frappe/model/create_new.js
index ef559a2e99..1a118be5db 100644
--- a/frappe/public/js/frappe/model/create_new.js
+++ b/frappe/public/js/frappe/model/create_new.js
@@ -161,6 +161,10 @@ $.extend(frappe.model, {
user_default = frappe.boot.user.last_selected_values[df.options];
}
+ if (!user_default && default_doc) {
+ user_default = default_doc;
+ }
+
var is_allowed_user_default = user_default &&
(!has_user_permissions || allowed_records.includes(user_default));
diff --git a/frappe/public/js/frappe/ui/group_by/group_by.js b/frappe/public/js/frappe/ui/group_by/group_by.js
index 6141d5a89a..0214339ee7 100644
--- a/frappe/public/js/frappe/ui/group_by/group_by.js
+++ b/frappe/public/js/frappe/ui/group_by/group_by.js
@@ -252,7 +252,7 @@ frappe.ui.GroupBy = class {
this.group_by_fields = {};
this.all_fields = {};
- let fields = this.report_view.meta.fields.filter(f => ["Select", "Link", "Data", "Int"].includes(f.fieldtype));
+ let fields = this.report_view.meta.fields.filter(f => ["Select", "Link", "Data", "Int", "Check"].includes(f.fieldtype));
this.group_by_fields[this.doctype] = fields;
this.all_fields[this.doctype] = this.report_view.meta.fields;
diff --git a/frappe/public/js/frappe/utils/datatype.js b/frappe/public/js/frappe/utils/datatype.js
index 0f73526e04..16f87b872f 100644
--- a/frappe/public/js/frappe/utils/datatype.js
+++ b/frappe/public/js/frappe/utils/datatype.js
@@ -44,6 +44,10 @@ window.validate_email = function(txt) {
return frappe.utils.validate_type(txt, "email");
};
+window.validate_phone = function(txt) {
+ return frappe.utils.validate_type(txt, "phone");
+};
+
window.nth = function(number) {
number = cint(number);
var s = 'th';
diff --git a/frappe/public/js/frappe/utils/utils.js b/frappe/public/js/frappe/utils/utils.js
index 278c80897e..1af37c1f60 100644
--- a/frappe/public/js/frappe/utils/utils.js
+++ b/frappe/public/js/frappe/utils/utils.js
@@ -232,6 +232,9 @@ Object.assign(frappe.utils, {
var regExp;
switch ( type ) {
+ case "phone":
+ regExp = /^([0-9\ \+\_\-\,\.\*\#\(\)]){1,20}$/;
+ break;
case "number":
regExp = /^-?(?:\d+|\d{1,3}(?:,\d{3})+)?(?:\.\d+)?$/;
break;
diff --git a/frappe/public/js/frappe/views/reports/query_report.js b/frappe/public/js/frappe/views/reports/query_report.js
index 851800f4e9..1d7065e70d 100644
--- a/frappe/public/js/frappe/views/reports/query_report.js
+++ b/frappe/public/js/frappe/views/reports/query_report.js
@@ -298,6 +298,55 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
}, 1000);
}
+ refresh_filters_dependency() {
+ this.filters.forEach(filter => {
+ filter.guardian_has_value = true;
+
+ if (filter.df.depends_on) {
+ filter.guardian_has_value =
+ this.evaluate_depends_on_value(filter.df.depends_on, filter.df.label);
+
+ if (filter.guardian_has_value) {
+ if (filter.df.hidden_due_to_dependency) {
+ filter.df.hidden_due_to_dependency = false;
+ this.toggle_filter_display(filter.df.fieldname, false);
+ }
+ } else {
+ if (!filter.df.hidden_due_to_dependency) {
+ filter.df.hidden_due_to_dependency = true;
+ this.toggle_filter_display(filter.df.fieldname, true);
+ filter.set_value(filter.df.default || null);
+ }
+ }
+ }
+
+ });
+ }
+
+ evaluate_depends_on_value(expression, filter_label) {
+ let out = null;
+ let filters = this.get_filter_values();
+ if (filters) {
+ if (typeof expression === 'boolean') {
+ out = expression;
+ } else if (expression.substr(0, 5) == 'eval:') {
+ try {
+ out = eval(expression.substr(5));
+ } catch (e) {
+ frappe.throw(__(`Invalid "depends_on" expression set in filter ${filter_label}`));
+ }
+ } else {
+ var value = filters[expression];
+ if ($.isArray(value)) {
+ out = !!value.length;
+ } else {
+ out = !!value;
+ }
+ }
+ }
+ return out;
+ }
+
setup_filters() {
this.clear_filters();
const { filters = [] } = this.report_settings;
@@ -315,6 +364,8 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
if (df.on_change) f.on_change = df.on_change;
df.onchange = () => {
+ this.refresh_filters_dependency();
+
let current_filters = this.get_filter_value();
if (this.previous_filters
&& (JSON.stringify(this.previous_filters) === JSON.stringify(current_filters))) {
@@ -344,6 +395,7 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
}).filter(Boolean);
+ this.refresh_filters_dependency();
if (this.filters.length === 0) {
// hide page form if no filters
this.page.hide_form();
@@ -1472,8 +1524,8 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
}
}
- toggle_filter(fieldname, flag) {
- $(`div[data-fieldname=${fieldname}]`).toggleClass('hide-control', flag);
+ toggle_filter_display(fieldname, flag) {
+ this.$page.find(`div[data-fieldname=${fieldname}]`).toggleClass('hide-control', flag);
}
toggle_report(flag) {
diff --git a/frappe/templates/includes/macros.html b/frappe/templates/includes/macros.html
index f8605b4372..3e822b8bf3 100644
--- a/frappe/templates/includes/macros.html
+++ b/frappe/templates/includes/macros.html
@@ -17,3 +17,9 @@
{% endif %}
{% endmacro %}
+
+{%- macro inspect(var, render=True) -%}
+{%- if render -%}
+
{{ var | pprint | e }}
+{%- endif -%}
+{%- endmacro %}
diff --git a/frappe/templates/styles/card_style.css b/frappe/templates/styles/card_style.css
index f5b002a17f..595d974011 100644
--- a/frappe/templates/styles/card_style.css
+++ b/frappe/templates/styles/card_style.css
@@ -28,4 +28,12 @@
}
.page-card p {
font-size: 14px;
-}
\ No newline at end of file
+}
+
+.ellipsis {
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ max-width: 100%;
+ vertical-align: middle;
+}
diff --git a/frappe/tests/test_db_update.py b/frappe/tests/test_db_update.py
new file mode 100644
index 0000000000..34378de3af
--- /dev/null
+++ b/frappe/tests/test_db_update.py
@@ -0,0 +1,70 @@
+import unittest
+import frappe
+
+from frappe.core.utils import find
+from frappe.custom.doctype.property_setter.property_setter import make_property_setter
+
+
+class TestDBUpdate(unittest.TestCase):
+ def test_db_update(self):
+ doctype = 'User'
+ frappe.reload_doctype('User', force=True)
+ frappe.model.meta.trim_tables('User')
+ make_property_setter(doctype, 'bio', 'fieldtype', 'Text', 'Data')
+ make_property_setter(doctype, 'enabled', 'default', '1', 'Int')
+
+ frappe.db.updatedb(doctype)
+
+ field_defs = get_field_defs(doctype)
+ table_columns = frappe.db.get_table_columns_description('tab{}'.format(doctype))
+
+ self.assertEqual(len(field_defs), len(table_columns))
+
+ for field_def in field_defs:
+ fieldname = field_def.get('fieldname')
+ table_column = find(table_columns, lambda d: d.get('name') == fieldname)
+
+ fieldtype = get_fieldtype_from_def(field_def)
+
+ fallback_default = '0' if field_def.get('fieldtype') in frappe.model.numeric_fieldtypes else 'NULL'
+ default = field_def.default if field_def.default is not None else fallback_default
+
+ self.assertEqual(fieldtype, table_column.type)
+ self.assertIn(table_column.default or 'NULL', [default, "'{}'".format(default)])
+
+def get_fieldtype_from_def(field_def):
+ fieldtuple = frappe.db.type_map.get(field_def.fieldtype, ('', 0))
+ fieldtype = fieldtuple[0]
+ if fieldtype in ('varchar', 'datetime', 'int'):
+ fieldtype += '({})'.format(field_def.length or fieldtuple[1])
+ return fieldtype
+
+def get_field_defs(doctype):
+ meta = frappe.get_meta(doctype, cached=False)
+ field_defs = meta.get_fieldnames_with_value(True)
+ field_defs += get_other_fields_meta(meta)
+ return field_defs
+
+def get_other_fields_meta(meta):
+ default_fields_map = {
+ 'name': ('Data', 0),
+ 'owner': ('Data', 0),
+ 'parent': ('Data', 0),
+ 'parentfield': ('Data', 0),
+ 'modified_by': ('Data', 0),
+ 'parenttype': ('Data', 0),
+ 'creation': ('Datetime', 0),
+ 'modified': ('Datetime', 0),
+ 'idx': ('Int', 8),
+ 'docstatus': ('Check', 0)
+ }
+
+ optional_fields = frappe.db.OPTIONAL_COLUMNS
+ if meta.track_seen:
+ optional_fields.append('_seen')
+
+ optional_fields_map = {field: ('Text', 0) for field in optional_fields}
+ fields = dict(default_fields_map, **optional_fields_map)
+ field_map = [frappe._dict({'fieldname': field, 'fieldtype': _type, 'length': _length}) for field, (_type, _length) in fields.items()]
+
+ return field_map
\ No newline at end of file
diff --git a/frappe/utils/__init__.py b/frappe/utils/__init__.py
index 82e6ea1b45..649d3bf72c 100644
--- a/frappe/utils/__init__.py
+++ b/frappe/utils/__init__.py
@@ -75,11 +75,18 @@ def extract_email_id(email):
email_id = email_id.decode("utf-8", "ignore")
return email_id
-def validate_email_add(email_str, throw=False):
- """
- validate_email_add will be renamed to the validate_email_address in v12
- """
- return validate_email_address(email_str, throw=False)
+def validate_phone_number(phone_number, throw=False):
+ """Returns True if valid phone number"""
+ if not phone_number:
+ return False
+
+ phone_number = phone_number.strip()
+ match = re.match("([0-9\ \+\_\-\,\.\*\#\(\)]){1,20}$", phone_number)
+
+ if not match and throw:
+ frappe.throw(frappe._("{0} is not a valid Phone Number").format(phone_number), frappe.InvalidPhoneNumberError)
+
+ return bool(match)
def validate_email_address(email_str, throw=False):
"""Validates the email string"""
@@ -98,15 +105,15 @@ def validate_email_address(email_str, throw=False):
_valid = False
else:
- e = extract_email_id(e)
- match = re.match("[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?", e.lower()) if e else None
+ email_id = extract_email_id(e)
+ match = re.match("[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?", email_id.lower()) if email_id else None
if not match:
_valid = False
else:
matched = match.group(0)
if match:
- match = matched==e.lower()
+ match = matched==email_id.lower()
if not _valid:
if throw:
@@ -691,4 +698,4 @@ def get_html_for_route(route):
set_request(method='GET', path=route)
response = render.render()
html = frappe.safe_decode(response.get_data())
- return html
\ No newline at end of file
+ return html
diff --git a/frappe/utils/boilerplate.py b/frappe/utils/boilerplate.py
index b81d802a07..e65fa44253 100755
--- a/frappe/utils/boilerplate.py
+++ b/frappe/utils/boilerplate.py
@@ -151,6 +151,10 @@ app_license = "{app_license}"
# web_include_css = "/assets/{app_name}/css/{app_name}.css"
# web_include_js = "/assets/{app_name}/js/{app_name}.js"
+# include js, css files in header of web form
+# webform_include_js = {"doctype": "public/js/doctype.js"}
+# webform_include_css = {"doctype": "public/css/doctype.css"}
+
# include js in page
# page_js = {{"page" : "public/js/file.js"}}
diff --git a/frappe/utils/html_utils.py b/frappe/utils/html_utils.py
index 67a01296c1..3de647894b 100644
--- a/frappe/utils/html_utils.py
+++ b/frappe/utils/html_utils.py
@@ -1,7 +1,9 @@
from __future__ import unicode_literals
import frappe
-import json, re
-import bleach, bleach_whitelist.bleach_whitelist as bleach_whitelist
+import json
+import re
+import bleach
+import bleach_whitelist.bleach_whitelist as bleach_whitelist
from six import string_types
from bs4 import BeautifulSoup
@@ -47,7 +49,7 @@ def clean_script_and_style(html):
def sanitize_html(html, linkify=False):
"""
Sanitize HTML tags, attributes and style to prevent XSS attacks
- Based on bleach clean, bleach whitelist and HTML5lib's Sanitizer defaults
+ Based on bleach clean, bleach whitelist and html5lib's Sanitizer defaults
Does not sanitize JSON, as it could lead to future problems
"""
diff --git a/frappe/utils/pdf.py b/frappe/utils/pdf.py
index bc1ad6cef9..58275a5c6c 100644
--- a/frappe/utils/pdf.py
+++ b/frappe/utils/pdf.py
@@ -16,6 +16,7 @@ import frappe
from frappe import _
from frappe.utils import get_wkhtmltopdf_version, scrub_urls
+
PDF_CONTENT_ERRORS = ["ContentNotFoundError", "ContentOperationNotPermittedError",
"UnknownContentError", "RemoteHostClosedError"]
diff --git a/frappe/website/context.py b/frappe/website/context.py
index 20762f7454..dcef22af43 100644
--- a/frappe/website/context.py
+++ b/frappe/website/context.py
@@ -31,6 +31,11 @@ def get_context(path, args=None):
if hasattr(frappe.local, 'response') and frappe.local.response.get('context'):
context.update(frappe.local.response.context)
+ # to be able to inspect the context in development
+ # Use the macro "inspect" from macros.html
+ if frappe.conf.developer_mode:
+ context._context_dict = context
+
return context
def update_controller_context(context, controller):
diff --git a/frappe/website/doctype/web_form/web_form.py b/frappe/website/doctype/web_form/web_form.py
index ae2c27c420..a188503ea3 100644
--- a/frappe/website/doctype/web_form/web_form.py
+++ b/frappe/website/doctype/web_form/web_form.py
@@ -2,18 +2,23 @@
# For license information, please see license.txt
from __future__ import unicode_literals
-import frappe, json, os
-from frappe.website.website_generator import WebsiteGenerator
+
+import json
+import os
+
+from six import iteritems
+from six.moves.urllib.parse import urlencode
+
+import frappe
from frappe import _, scrub
+from frappe.core.doctype.file.file import get_max_file_size, remove_file_by_url
+from frappe.custom.doctype.customize_form.customize_form import docfield_properties
+from frappe.desk.form.meta import get_code_files_via_hooks
+from frappe.integrations.utils import get_payment_gateway_controller
+from frappe.modules.utils import export_module_json, get_doc_module
from frappe.utils import cstr
from frappe.website.utils import get_comment_list
-from frappe.custom.doctype.customize_form.customize_form import docfield_properties
-from frappe.core.doctype.file.file import get_max_file_size
-from frappe.core.doctype.file.file import remove_file_by_url
-from frappe.modules.utils import export_module_json, get_doc_module
-from six.moves.urllib.parse import urlencode
-from frappe.integrations.utils import get_payment_gateway_controller
-from six import iteritems
+from frappe.website.website_generator import WebsiteGenerator
class WebForm(WebsiteGenerator):
@@ -237,11 +242,23 @@ def get_context(context):
js_path = os.path.join(os.path.dirname(self.web_form_module.__file__), scrub(self.name) + '.js')
if os.path.exists(js_path):
- context.script = frappe.render_template(open(js_path, 'r').read(), context)
+ script = frappe.render_template(open(js_path, 'r').read(), context)
+
+ for path in get_code_files_via_hooks("webform_include_js", context.doc_type):
+ custom_js = frappe.render_template(open(path, 'r').read(), context)
+ script = "\n\n".join([script, custom_js])
+
+ context.script = script
css_path = os.path.join(os.path.dirname(self.web_form_module.__file__), scrub(self.name) + '.css')
if os.path.exists(css_path):
- context.style = open(css_path, 'r').read()
+ style = open(css_path, 'r').read()
+
+ for path in get_code_files_via_hooks("webform_include_css", context.doc_type):
+ custom_css = open(path, 'r').read()
+ style = "\n\n".join([style, custom_css])
+
+ context.style = style
def get_layout(self):
layout = []
@@ -589,4 +606,3 @@ def get_link_options(web_form_name, doctype, allow_read_on_all_link_options=Fals
else:
raise frappe.PermissionError('Not Allowed, {0}'.format(doctype))
-
diff --git a/frappe/website/utils.py b/frappe/website/utils.py
index e392e9d0f6..f5e35976eb 100644
--- a/frappe/website/utils.py
+++ b/frappe/website/utils.py
@@ -3,11 +3,15 @@
from __future__ import unicode_literals
import functools
-import frappe, re, os
+import re
+import os
+import frappe
+
from six import iteritems
from past.builtins import cmp
from frappe.utils import markdown
+
def delete_page_cache(path):
cache = frappe.cache()
cache.delete_value('full_index')
@@ -20,7 +24,7 @@ def delete_page_cache(path):
cache.delete_key(name)
def find_first_image(html):
- m = re.finditer("""]*src\s?=\s?['"]([^'"]*)['"]""", html)
+ m = re.finditer(r"""]*src\s?=\s?['"]([^'"]*)['"]""", html)
try:
return next(m).groups()[0]
except StopIteration:
@@ -33,16 +37,34 @@ def can_cache(no_cache=False):
return False
return not no_cache
+
def get_comment_list(doctype, name):
- return frappe.get_all('Comment',
- fields = ['name', 'creation', 'owner', 'comment_email', 'comment_by', 'content'],
- filters = dict(
- reference_doctype = doctype,
- reference_name = name,
- comment_type = 'Comment',
- published = 1
- ),
- order_by = 'creation asc')
+ comments = frappe.get_all('Comment',
+ fields=['name', 'creation', 'owner',
+ 'comment_email', 'comment_by', 'content'],
+ filters=dict(
+ reference_doctype=doctype,
+ reference_name=name,
+ comment_type='Comment',
+ ),
+ or_filters=[
+ ['owner', '=', frappe.session.user],
+ ['published', '=', 1]])
+
+ communications = frappe.get_all("Communication",
+ fields=['name', 'creation', 'owner', 'owner as comment_email',
+ 'sender_full_name as comment_by', 'content', 'recipients'],
+ filters=dict(
+ reference_doctype=doctype,
+ reference_name=name,
+ ),
+ or_filters=[
+ ['recipients', 'like', '%{0}%'.format(frappe.session.user)],
+ ['cc', 'like', '%{0}%'.format(frappe.session.user)],
+ ['bcc', 'like', '%{0}%'.format(frappe.session.user)]])
+
+ return sorted((comments + communications), key=lambda comment: comment['creation'], reverse=True)
+
def get_home_page():
if frappe.local.flags.home_page:
@@ -92,7 +114,7 @@ def cleanup_page_name(title):
return ''
name = title.lower()
- name = re.sub('[~!@#$%^&*+()<>,."\'\?]', '', name)
+ name = re.sub(r'[~!@#$%^&*+()<>,."\'\?]', '', name)
name = re.sub('[:/]', '-', name)
name = '-'.join(name.split())
@@ -194,7 +216,6 @@ def abs_url(path):
def get_toc(route, url_prefix=None, app=None):
'''Insert full index (table of contents) for {index} tag'''
- from frappe.website.utils import get_full_index
full_index = get_full_index(app=app)
@@ -287,7 +308,9 @@ def extract_title(source, path):
if not title and "
" in source:
# extract title from h1
match = re.findall('
([^<]*)', source)
- title = match[0].strip()[:300]
+ title_content = match[0].strip()[:300]
+ if '{{' not in title_content:
+ title = title_content
if not title:
# make title from name
diff --git a/frappe/workflow/doctype/workflow_action/workflow_action.py b/frappe/workflow/doctype/workflow_action/workflow_action.py
index 3d4c106f29..68ee7cd2ef 100644
--- a/frappe/workflow/doctype/workflow_action/workflow_action.py
+++ b/frappe/workflow/doctype/workflow_action/workflow_action.py
@@ -9,8 +9,8 @@ from frappe.utils import get_url, get_datetime
from frappe.desk.form.utils import get_pdf_link
from frappe.utils.verified_command import get_signed_params, verify_request
from frappe import _
-from frappe.model.workflow import apply_workflow, get_workflow_name, \
- has_approval_access, get_workflow_state_field, send_email_alert, get_workflow_field_value
+from frappe.model.workflow import apply_workflow, get_workflow_name, has_approval_access, \
+ get_workflow_state_field, send_email_alert, get_workflow_field_value, is_transition_condition_satisfied
from frappe.desk.notifications import clear_doctype_notifications
from frappe.utils.user import get_users_with_role
@@ -155,10 +155,10 @@ def get_next_possible_transitions(workflow_name, state, doc=None):
for transition in transitions:
is_next_state_optional = get_state_optional_field_value(workflow_name, transition.next_state)
# skip transition if next state of the transition is optional
- if transition.condition and not frappe.safe_eval(transition.condition, None, {'doc': doc.as_dict()}):
- continue
if is_next_state_optional:
continue
+ if not is_transition_condition_satisfied(transition, doc):
+ continue
transitions_to_return.append(transition)
return transitions_to_return
diff --git a/frappe/www/message.html b/frappe/www/message.html
index 2919592901..87d311d9cc 100644
--- a/frappe/www/message.html
+++ b/frappe/www/message.html
@@ -24,7 +24,7 @@ html, body {
{{ title or _("Message") }}
-