diff --git a/.eslintrc b/.eslintrc
index 7e469f7672..e79571f556 100644
--- a/.eslintrc
+++ b/.eslintrc
@@ -78,6 +78,7 @@
"has_common": true,
"has_words": true,
"validate_email": true,
+ "validate_phone": true,
"get_number_format": true,
"format_number": true,
"format_currency": true,
diff --git a/CODEOWNERS b/CODEOWNERS
new file mode 100644
index 0000000000..2ff8752871
--- /dev/null
+++ b/CODEOWNERS
@@ -0,0 +1,17 @@
+# Each line is a file pattern followed by one or more owners.
+
+# These owners will be the default owners for everything in
+# the repo. Unless a later match takes precedence,
+
+* @surajshetty3416, @netchampfaris
+website/ @scmmishra
+templates/ @scmmishra
+www/ @scmmishra
+integrations/ @Mangesh-Khairnar
+patches/ @surajshetty3416 @sahil28297
+dashboard/ @prssanna
+email/ @Thunderbottom
+event_streaming/ @ruchamahabal
+data_import* @netchampfaris
+core/ @surajshetty3416
+requirements.txt @gavindsouza
\ No newline at end of file
diff --git a/frappe/api.py b/frappe/api.py
index 1b1861b78f..6655ebc4d8 100644
--- a/frappe/api.py
+++ b/frappe/api.py
@@ -81,10 +81,8 @@ def handle():
frappe.local.response.update({"data": doc})
if frappe.local.request.method=="PUT":
- if frappe.local.form_dict.data is None:
- data = json.loads(frappe.safe_decode(frappe.local.request.get_data()))
- else:
- data = json.loads(frappe.local.form_dict.data)
+ data = get_request_form_data()
+
doc = frappe.get_doc(doctype, name)
if "flags" in data:
@@ -123,10 +121,7 @@ def handle():
})
if frappe.local.request.method == "POST":
- if frappe.local.form_dict.data is None:
- data = json.loads(frappe.safe_decode(frappe.local.request.get_data()))
- else:
- data = json.loads(frappe.local.form_dict.data)
+ data = get_request_form_data()
data.update({
"doctype": doctype
})
@@ -142,6 +137,13 @@ def handle():
return build_response("json")
+def get_request_form_data():
+ if frappe.local.form_dict.data is None:
+ data = frappe.safe_decode(frappe.local.request.get_data())
+ else:
+ data = frappe.local.form_dict.data
+
+ return frappe.parse_json(data)
def validate_oauth():
""" authentication using oauth """
diff --git a/frappe/contacts/doctype/contact/contact.py b/frappe/contacts/doctype/contact/contact.py
index b9239dc1f6..82311b19d4 100644
--- a/frappe/contacts/doctype/contact/contact.py
+++ b/frappe/contacts/doctype/contact/contact.py
@@ -69,23 +69,25 @@ class Contact(Document):
return True
def add_email(self, email_id, is_primary=0, autosave=False):
- self.append("email_ids", {
- "email_id": email_id,
- "is_primary": is_primary
- })
+ if not frappe.db.exists("Contact Email", {"email_id": email_id, "parent": self.name}):
+ self.append("email_ids", {
+ "email_id": email_id,
+ "is_primary": is_primary
+ })
- if autosave:
- self.save(ignore_permissions=True)
+ if autosave:
+ self.save(ignore_permissions=True)
def add_phone(self, phone, is_primary_phone=0, is_primary_mobile_no=0, autosave=False):
- self.append("phone_nos", {
- "phone": phone,
- "is_primary_phone": is_primary_phone,
- "is_primary_mobile_no": is_primary_mobile_no
- })
+ if not frappe.db.exists("Contact Phone", {"phone": phone, "parent": self.name}):
+ self.append("phone_nos", {
+ "phone": phone,
+ "is_primary_phone": is_primary_phone,
+ "is_primary_mobile_no": is_primary_mobile_no
+ })
- if autosave:
- self.save(ignore_permissions=True)
+ if autosave:
+ self.save(ignore_permissions=True)
def set_primary_email(self):
if not self.email_ids:
diff --git a/frappe/core/doctype/communication/communication.json b/frappe/core/doctype/communication/communication.json
index 7add513b01..58adc6187c 100644
--- a/frappe/core/doctype/communication/communication.json
+++ b/frappe/core/doctype/communication/communication.json
@@ -1,9 +1,11 @@
{
+ "actions": [],
"allow_import": 1,
"creation": "2013-01-29 10:47:14",
"description": "Keeps track of all communications",
"doctype": "DocType",
"document_type": "Setup",
+ "email_append_to": 1,
"engine": "InnoDB",
"field_order": [
"subject",
@@ -384,7 +386,8 @@
],
"icon": "fa fa-comment",
"idx": 1,
- "modified": "2019-10-09 14:22:27.664645",
+ "links": [],
+ "modified": "2019-12-27 14:44:04.880373",
"modified_by": "Administrator",
"module": "Core",
"name": "Communication",
@@ -440,8 +443,10 @@
}
],
"search_fields": "subject",
+ "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/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 2c8cd240ee..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
@@ -94,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]:
@@ -108,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:
@@ -124,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:
@@ -169,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:
@@ -194,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:
@@ -204,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',
@@ -234,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
@@ -271,7 +263,6 @@ 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()
@@ -325,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
@@ -343,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`'''
@@ -363,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":
@@ -386,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."""
@@ -397,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)
@@ -414,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)))
@@ -440,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"})
@@ -460,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):
@@ -505,7 +487,6 @@ class DocType(Document):
except ValueError:
pass
-
@staticmethod
def prepare_for_import(docdict):
# set order of fields from field_order
@@ -528,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)
@@ -557,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:
@@ -573,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:
@@ -642,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
@@ -669,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
@@ -693,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:
@@ -734,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))
@@ -763,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'
@@ -772,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
@@ -799,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):
@@ -814,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:
@@ -831,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"):
@@ -858,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:
@@ -870,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
@@ -878,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
@@ -890,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:
@@ -903,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"]
@@ -913,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
@@ -926,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"""
@@ -938,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]
@@ -973,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)
@@ -982,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)
@@ -994,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)
@@ -1009,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:
@@ -1103,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:
@@ -1134,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))]
@@ -1142,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/report/report.py b/frappe/core/doctype/report/report.py
index 2d49915f59..967b28b8b2 100644
--- a/frappe/core/doctype/report/report.py
+++ b/frappe/core/doctype/report/report.py
@@ -6,7 +6,7 @@ import frappe
import json, datetime
from frappe import _, scrub
import frappe.desk.query_report
-from frappe.utils import cint
+from frappe.utils import cint, cstr
from frappe.model.document import Document
from frappe.modules.export_file import export_to_files
from frappe.modules import make_boilerplate
@@ -92,6 +92,18 @@ class Report(Document):
make_boilerplate("controller.py", self, {"name": self.name})
make_boilerplate("controller.js", self, {"name": self.name})
+ def execute_query_report(self, filters):
+ if not self.query:
+ frappe.throw(_("Must specify a Query to run"), title=_('Report Document Error'))
+
+ if not self.query.lower().startswith("select"):
+ frappe.throw(_("Query must be a SELECT"), title=_('Report Document Error'))
+
+ result = [list(t) for t in frappe.db.sql(self.query, filters)]
+ columns = [cstr(c[0]) for c in frappe.db.get_description()]
+
+ return [columns, result]
+
def execute_script_report(self, filters):
# save the timestamp to automatically set to prepared
threshold = 30
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/user/user.js b/frappe/core/doctype/user/user.js
index c4873ee40e..b17548d994 100644
--- a/frappe/core/doctype/user/user.js
+++ b/frappe/core/doctype/user/user.js
@@ -97,6 +97,48 @@ frappe.ui.form.on('User', {
});
}, __("Password"));
+ frappe.db.get_single_value("LDAP Settings", "enabled").then((value) => {
+ if (value === 1 && frm.doc.name != "Administrator") {
+ frm.add_custom_button(__("Reset LDAP Password"), function() {
+ const d = new frappe.ui.Dialog({
+ title: __("Reset LDAP Password"),
+ fields: [
+ {
+ label: __("New Password"),
+ fieldtype: "Password",
+ fieldname: "new_password",
+ reqd: 1
+ },
+ {
+ label: __("Confirm New Password"),
+ fieldtype: "Password",
+ fieldname: "confirm_password",
+ reqd: 1
+ },
+ {
+ label: __("Logout All Sessions"),
+ fieldtype: "Check",
+ fieldname: "logout_sessions"
+ }
+ ],
+ primary_action: (values) => {
+ d.hide();
+ if (values.new_password !== values.confirm_password) {
+ frappe.throw(__("Passwords do not match!"));
+ }
+ frappe.call(
+ "frappe.integrations.doctype.ldap_settings.ldap_settings.reset_password", {
+ user: frm.doc.email,
+ password: values.new_password,
+ logout: values.logout_sessions
+ });
+ }
+ });
+ d.show();
+ }, __("Password"));
+ }
+ });
+
frm.add_custom_button(__("Reset OTP Secret"), function() {
frappe.call({
method: "frappe.core.doctype.user.user.reset_otp_secret",
diff --git a/frappe/core/page/dashboard/dashboard.js b/frappe/core/page/dashboard/dashboard.js
index 511aac7010..88bfba9e84 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,30 +76,42 @@ class Dashboard {
}
refresh() {
- this.get_dashboard_doc().then((doc) => {
- this.dashboard_doc = doc;
- this.charts = this.dashboard_doc.charts
- .map(chart => {
- return {
- chart_name: chart.chart,
- label: chart.chart,
- ...chart
- }
- });
+ this.get_permitted_dashboard_charts().then(charts => {
+ if (!charts.length) {
+ frappe.msgprint(__('No Permitted Charts on this Dashboard'), __('No Permitted Charts'))
+ }
- this.chart_group = new frappe.widget.WidgetGroup({
- title: null,
- container: this.container,
- type: "chart",
- columns: 2,
- allow_sorting: false,
- widgets: this.charts,
- });
+ frappe.dashboard_utils.get_dashboard_settings().then((settings) => {
+ let chart_config = settings.chart_config? JSON.parse(settings.chart_config): {};
+ this.charts =
+ charts.map(chart => {
+ return {
+ chart_name: chart.chart,
+ label: chart.chart,
+ chart_settings: chart_config[chart.chart] || {},
+ ...chart
+ }
+ });
+ this.chart_group = new frappe.widget.WidgetGroup({
+ title: null,
+ container: this.container,
+ type: "chart",
+ columns: 2,
+ allow_sorting: false,
+ widgets: this.charts,
+ });
+ })
});
}
- 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/core/utils.py b/frappe/core/utils.py
index 55767ffe34..55cfbc34d7 100644
--- a/frappe/core/utils.py
+++ b/frappe/core/utils.py
@@ -67,3 +67,19 @@ def find_all(list_of_dict, match_function):
if match_function(entry):
found.append(entry)
return found
+
+def ljust_list(_list, length, fill_word=None):
+ """
+ Similar to ljust but for list.
+
+ Usage:
+ $ ljust_list([1, 2, 3], 5)
+ > [1, 2, 3, None, None]
+ """
+ # make a copy to avoid mutation of passed list
+ _list = list(_list)
+ fill_length = length - len(_list)
+ if fill_length > 0:
+ _list.extend([fill_word] * fill_length)
+
+ return _list
diff --git a/frappe/custom/doctype/customize_form/customize_form.js b/frappe/custom/doctype/customize_form/customize_form.js
index 5c9fa37ef5..b1743a96a5 100644
--- a/frappe/custom/doctype/customize_form/customize_form.js
+++ b/frappe/custom/doctype/customize_form/customize_form.js
@@ -143,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 3259085781..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 = {
@@ -170,6 +173,7 @@ class CustomizeForm(Document):
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)
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/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/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/dashboard_settings/__init__.py b/frappe/desk/doctype/dashboard_settings/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/frappe/desk/doctype/dashboard_settings/dashboard_settings.js b/frappe/desk/doctype/dashboard_settings/dashboard_settings.js
new file mode 100644
index 0000000000..8e7966366d
--- /dev/null
+++ b/frappe/desk/doctype/dashboard_settings/dashboard_settings.js
@@ -0,0 +1,8 @@
+// Copyright (c) 2020, Frappe Technologies and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('Dashboard Settings', {
+ // refresh: function(frm) {
+
+ // }
+});
diff --git a/frappe/desk/doctype/dashboard_settings/dashboard_settings.json b/frappe/desk/doctype/dashboard_settings/dashboard_settings.json
new file mode 100644
index 0000000000..e1eb75db47
--- /dev/null
+++ b/frappe/desk/doctype/dashboard_settings/dashboard_settings.json
@@ -0,0 +1,51 @@
+{
+ "actions": [],
+ "autoname": "Prompt",
+ "creation": "2020-03-31 19:41:45.785014",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "user",
+ "chart_config"
+ ],
+ "fields": [
+ {
+ "fieldname": "user",
+ "fieldtype": "Link",
+ "label": "User",
+ "options": "User",
+ "read_only": 1
+ },
+ {
+ "fieldname": "chart_config",
+ "fieldtype": "Code",
+ "label": "Chart Configuration",
+ "options": "JSON",
+ "read_only": 1
+ }
+ ],
+ "in_create": 1,
+ "links": [],
+ "modified": "2020-04-01 00:07:26.489561",
+ "modified_by": "Administrator",
+ "module": "Desk",
+ "name": "Dashboard Settings",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "All",
+ "share": 1,
+ "write": 1
+ }
+ ],
+ "read_only": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/frappe/desk/doctype/dashboard_settings/dashboard_settings.py b/frappe/desk/doctype/dashboard_settings/dashboard_settings.py
new file mode 100644
index 0000000000..4697d897fc
--- /dev/null
+++ b/frappe/desk/doctype/dashboard_settings/dashboard_settings.py
@@ -0,0 +1,43 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020, Frappe Technologies and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+# import frappe
+from frappe.model.document import Document
+import frappe
+import json
+
+class DashboardSettings(Document):
+ pass
+
+
+@frappe.whitelist()
+def create_dashboard_settings(user):
+ if not frappe.db.exists("Dashboard Settings", user):
+ doc = frappe.new_doc('Dashboard Settings')
+ doc.name = user
+ doc.insert(ignore_permissions=True)
+ frappe.db.commit()
+ return doc
+
+def get_permission_query_conditions(user):
+ if not user: user = frappe.session.user
+
+ return '''(`tabDashboard Settings`.name = '{user}')'''.format(user=user)
+
+@frappe.whitelist()
+def save_chart_config(reset, config, chart_name):
+ reset = frappe.parse_json(reset)
+ doc = frappe.get_doc('Dashboard Settings', frappe.session.user)
+ chart_config = frappe.parse_json(doc.chart_config) or {}
+
+ if reset:
+ chart_config[chart_name] = {}
+ else:
+ config = frappe.parse_json(config)
+ if not chart_name in chart_config:
+ chart_config[chart_name] = {}
+ chart_config[chart_name].update(config)
+
+ frappe.db.set_value('Dashboard Settings', frappe.session.user, 'chart_config', json.dumps(chart_config))
\ No newline at end of file
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 d210af02fd..aaf859e7fd 100644
--- a/frappe/desk/query_report.py
+++ b/frappe/desk/query_report.py
@@ -8,7 +8,7 @@ import os, json
from frappe import _
from frappe.modules import scrub, get_module_path
-from frappe.utils import flt, cint, get_html_format, cstr, get_url_to_form
+from frappe.utils import flt, cint, get_html_format, get_url_to_form
from frappe.model.utils import render_include
from frappe.translate import send_translations
import frappe.desk.reportview
@@ -16,6 +16,7 @@ from frappe.permissions import get_role_permissions
from six import string_types, iteritems
from datetime import timedelta
from frappe.utils import gzip_decompress
+from frappe.core.utils import ljust_list
def get_report_doc(report_name):
doc = frappe.get_doc("Report", report_name)
@@ -42,44 +43,32 @@ def get_report_doc(report_name):
return doc
-def generate_report_result(report, filters=None, user=None):
- status = None
- if not user:
- user = frappe.session.user
- if not filters:
- filters = []
+def generate_report_result(report, filters=None, user=None, custom_columns=None):
+ user = user or frappe.session.user
+ filters = filters or []
if filters and isinstance(filters, string_types):
filters = json.loads(filters)
- columns, result, message, chart, report_summary, skip_total_row = [], [], None, None, None, 0
+
+ res = []
+
if report.report_type == "Query Report":
- if not report.query:
- status = "error"
- frappe.msgprint(_("Must specify a Query to run"), raise_exception=True)
-
- if not report.query.lower().startswith("select"):
- status = "error"
- frappe.msgprint(_("Query must be a SELECT"), raise_exception=True)
-
- result = [list(t) for t in frappe.db.sql(report.query, filters)]
- columns = [cstr(c[0]) for c in frappe.db.get_description()]
+ res = report.execute_query_report(filters)
elif report.report_type == 'Script Report':
res = report.execute_script_report(filters)
- columns, result = res[0], res[1]
- if len(res) > 2:
- message = res[2]
- if len(res) > 3:
- chart = res[3]
- if len(res) > 4:
- report_summary = res[4]
- if len(res) > 5:
- skip_total_row = cint(res[5])
+ columns, result, message, chart, report_summary, skip_total_row = \
+ ljust_list(res, 6)
if report.custom_columns:
columns = json.loads(report.custom_columns)
result = add_data_to_custom_columns(columns, result)
+ if custom_columns:
+ result = add_data_to_custom_columns(custom_columns, result)
+
+ for custom_column in custom_columns:
+ columns.insert(custom_column['insert_after_index'] + 1, custom_column)
if result:
result = get_filtered_data(report.ref_doctype, columns, result, user)
@@ -93,8 +82,8 @@ def generate_report_result(report, filters=None, user=None):
"message": message,
"chart": chart,
"report_summary": report_summary,
- "skip_total_row": skip_total_row,
- "status": status,
+ "skip_total_row": skip_total_row or 0,
+ "status": None,
"execution_time": frappe.cache().hget('report_execution_time', report.name) or 0
}
@@ -161,7 +150,7 @@ def get_script(report_name):
@frappe.whitelist()
@frappe.read_only()
-def run(report_name, filters=None, user=None, ignore_prepared_report=False):
+def run(report_name, filters=None, user=None, ignore_prepared_report=False, custom_columns=None):
report = get_report_doc(report_name)
if not user:
@@ -183,7 +172,7 @@ def run(report_name, filters=None, user=None, ignore_prepared_report=False):
dn = ""
result = get_prepared_report_result(report, filters, dn, user)
else:
- result = generate_report_result(report, filters, user)
+ result = generate_report_result(report, filters, user, custom_columns)
result["add_total_row"] = report.add_total_row and not result.get('skip_total_row', False)
@@ -294,6 +283,8 @@ def export_query():
if isinstance(data.get("file_format_type"), string_types):
file_format_type = data["file_format_type"]
+ custom_columns = frappe.parse_json(data["custom_columns"])
+
include_indentation = data["include_indentation"]
if isinstance(data.get("visible_idx"), string_types):
visible_idx = json.loads(data.get("visible_idx"))
@@ -301,7 +292,7 @@ def export_query():
visible_idx = None
if file_format_type == "Excel":
- data = run(report_name, filters)
+ data = run(report_name, filters, custom_columns=custom_columns)
data = frappe._dict(data)
if not data.columns:
frappe.respond_as_web_page(_("No data to export"),
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..4f65303be9 100644
--- a/frappe/hooks.py
+++ b/frappe/hooks.py
@@ -86,7 +86,9 @@ permission_query_conditions = {
"Event": "frappe.desk.doctype.event.event.get_permission_query_conditions",
"ToDo": "frappe.desk.doctype.todo.todo.get_permission_query_conditions",
"User": "frappe.core.doctype.user.user.get_permission_query_conditions",
+ "Dashboard Settings": "frappe.desk.doctype.dashboard_settings.dashboard_settings.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 +103,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/integrations/doctype/ldap_settings/ldap_settings.py b/frappe/integrations/doctype/ldap_settings/ldap_settings.py
index c0f12df04a..558f7117c0 100644
--- a/frappe/integrations/doctype/ldap_settings/ldap_settings.py
+++ b/frappe/integrations/doctype/ldap_settings/ldap_settings.py
@@ -4,7 +4,7 @@
from __future__ import unicode_literals
import frappe
-from frappe import _
+from frappe import _, safe_encode
from frappe.model.document import Document
@@ -19,7 +19,7 @@ class LDAPSettings(Document):
else:
frappe.throw(_("LDAP Search String needs to end with a placeholder, eg sAMAccountName={0}"))
- def connect_to_ldap(self, base_dn, password):
+ def connect_to_ldap(self, base_dn, password, read_only=True):
try:
import ldap3
import ssl
@@ -44,7 +44,7 @@ class LDAPSettings(Document):
user=base_dn,
password=password,
auto_bind=bind_type,
- read_only=True,
+ read_only=read_only,
raise_exceptions=True)
return conn
@@ -170,6 +170,36 @@ class LDAPSettings(Document):
else:
frappe.throw(_("Invalid username or password"))
+ def reset_password(self, user, password, logout_sessions=False):
+ from ldap3 import HASHED_SALTED_SHA, MODIFY_REPLACE
+ from ldap3.utils.hashed import hashed
+
+ search_filter = "({0}={1})".format(self.ldap_email_field, user)
+
+ conn = self.connect_to_ldap(self.base_dn, self.get_password(raise_exception=False),
+ read_only=False)
+
+ if conn.search(
+ search_base=self.organizational_unit,
+ search_filter=search_filter,
+ attributes=self.get_ldap_attributes()
+ ):
+ if conn.entries and conn.entries[0]:
+ entry_dn = conn.entries[0].entry_dn
+ hashed_password = hashed(HASHED_SALTED_SHA, safe_encode(password))
+ changes = {'userPassword': [(MODIFY_REPLACE, [hashed_password])]}
+ if conn.modify(entry_dn, changes=changes):
+ if logout_sessions:
+ from frappe.sessions import clear_sessions
+ clear_sessions(user=user, force=True)
+ frappe.msgprint(_("Password changed successfully."))
+ else:
+ frappe.throw(_("Failed to change password."))
+ else:
+ frappe.throw(_("No Entry for the User {0} found within LDAP!").format(user))
+ else:
+ frappe.throw(_("No LDAP User found for email: {0}").format(user))
+
def convert_ldap_entry_to_dict(self, user_entry):
# support multiple email values
@@ -211,3 +241,11 @@ def login():
# because of a GET request!
frappe.db.commit()
+
+
+@frappe.whitelist()
+def reset_password(user, password, logout):
+ ldap = frappe.get_doc("LDAP Settings")
+ if not ldap.enabled:
+ frappe.throw(_("LDAP is not enabled."))
+ ldap.reset_password(user, password, logout_sessions=int(logout))
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 66dd7e3c58..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()
diff --git a/frappe/model/meta.py b/frappe/model/meta.py
index 5065684311..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'):
+ 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 + \
diff --git a/frappe/model/workflow.py b/frappe/model/workflow.py
index b134f2f8dc..4384e7c8f5 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'''
@@ -185,7 +194,7 @@ def bulk_workflow_approval(docnames, doctype, action):
from collections import defaultdict
# dictionaries for logging
- errored_transactions = defaultdict(list)
+ failed_transactions = defaultdict(list)
successful_transactions = defaultdict(list)
# WARN: message log is cleared
@@ -206,7 +215,7 @@ def bulk_workflow_approval(docnames, doctype, action):
if e.args:
message += " : {0}".format(e.args[0])
message_dict = {"docname": docname, "message": message}
- errored_transactions[docname].append(message_dict)
+ failed_transactions[docname].append(message_dict)
frappe.db.rollback()
frappe.log_error(frappe.get_traceback(), "Workflow {0} threw an error for {1} {2}".format(action, doctype, docname))
@@ -219,20 +228,20 @@ def bulk_workflow_approval(docnames, doctype, action):
message_dict = {"docname": docname, "message": message.get("message")}
if message.get("raise_exception", False):
- errored_transactions[docname].append(message_dict)
+ failed_transactions[docname].append(message_dict)
else:
successful_transactions[docname].append(message_dict)
else:
successful_transactions[docname].append({"docname": docname, "message": None})
- if errored_transactions and successful_transactions:
+ if failed_transactions and successful_transactions:
indicator = "orange"
- elif errored_transactions:
+ elif failed_transactions:
indicator = "red"
else:
indicator = "green"
- print_workflow_log(errored_transactions, _("Errored Transactions"), doctype, indicator)
+ print_workflow_log(failed_transactions, _("Failed Transactions"), doctype, indicator)
print_workflow_log(successful_transactions, _("Successful Transactions"), doctype, indicator)
def print_workflow_log(messages, title, doctype, indicator):
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(`