diff --git a/frappe/__init__.py b/frappe/__init__.py
index c5f13f2295..fac0927428 100644
--- a/frappe/__init__.py
+++ b/frappe/__init__.py
@@ -348,7 +348,7 @@ def msgprint(msg, title=None, raise_exception=0, as_table=False, as_list=False,
if as_table and type(msg) in (list, tuple):
out.as_table = 1
-
+
if as_list and type(msg) in (list, tuple) and len(msg) > 1:
out.as_list = 1
@@ -796,11 +796,17 @@ def get_doc(*args, **kwargs):
return doc
-def get_last_doc(doctype):
+def get_last_doc(doctype, filters=None, order_by="creation desc"):
"""Get last created document of this type."""
- d = get_all(doctype, ["name"], order_by="creation desc", limit_page_length=1)
+ d = get_all(
+ doctype,
+ filters=filters,
+ limit_page_length=1,
+ order_by=order_by,
+ pluck="name"
+ )
if d:
- return get_doc(doctype, d[0].name)
+ return get_doc(doctype, d[0])
else:
raise DoesNotExistError
diff --git a/frappe/automation/doctype/assignment_rule/assignment_rule.js b/frappe/automation/doctype/assignment_rule/assignment_rule.js
index 774befc15e..ee1a076465 100644
--- a/frappe/automation/doctype/assignment_rule/assignment_rule.js
+++ b/frappe/automation/doctype/assignment_rule/assignment_rule.js
@@ -57,7 +57,8 @@ frappe.ui.form.on('Assignment Rule', {
frm.set_fields_as_options(
'field',
doctype,
- (df) => df.fieldtype == 'Link' && df.options == 'User',
+ (df) => ['Dynamic Link', 'Data'].includes(df.fieldtype)
+ || (df.fieldtype == 'Link' && df.options == 'User'),
[{ label: 'Owner', value: 'owner' }]
);
if (doctype) {
diff --git a/frappe/automation/doctype/assignment_rule/assignment_rule.py b/frappe/automation/doctype/assignment_rule/assignment_rule.py
index c85cb149ea..d20398d564 100644
--- a/frappe/automation/doctype/assignment_rule/assignment_rule.py
+++ b/frappe/automation/doctype/assignment_rule/assignment_rule.py
@@ -82,7 +82,7 @@ class AssignmentRule(Document):
elif self.rule == 'Load Balancing':
return self.get_user_load_balancing()
elif self.rule == 'Based on Field':
- return doc.get(self.field)
+ return self.get_user_based_on_field(doc)
def get_user_round_robin(self):
'''
@@ -119,6 +119,11 @@ class AssignmentRule(Document):
# pick the first user
return sorted_counts[0].get('user')
+ def get_user_based_on_field(self, doc):
+ val = doc.get(self.field)
+ if frappe.db.exists('User', val):
+ return val
+
def safe_eval(self, fieldname, doc):
try:
if self.get(fieldname):
diff --git a/frappe/commands/site.py b/frappe/commands/site.py
index 38e70534a5..51c352a931 100755
--- a/frappe/commands/site.py
+++ b/frappe/commands/site.py
@@ -103,11 +103,11 @@ def _new_site(db_name, site, mariadb_root_username=None, mariadb_root_password=N
@click.option('--install-app', multiple=True, help='Install app after installation')
@click.option('--with-public-files', help='Restores the public files of the site, given path to its tar file')
@click.option('--with-private-files', help='Restores the private files of the site, given path to its tar file')
-@click.option('--force', is_flag=True, default=False, help='Ignore the site downgrade warning, if applicable')
+@click.option('--force', is_flag=True, default=False, help='Ignore the validations and downgrade warnings. This action is not recommended')
@pass_context
def restore(context, sql_file_path, mariadb_root_username=None, mariadb_root_password=None, db_name=None, verbose=None, install_app=None, admin_password=None, force=None, with_public_files=None, with_private_files=None):
"Restore site database from an sql file"
- from frappe.installer import extract_sql_gzip, extract_files, is_downgrade
+ from frappe.installer import extract_sql_gzip, extract_files, is_downgrade, validate_database_sql
force = context.force or force
# Extract the gzip file if user has passed *.sql.gz file instead of *.sql file
@@ -127,6 +127,7 @@ def restore(context, sql_file_path, mariadb_root_username=None, mariadb_root_pas
else:
decompressed_file_name = sql_file_path
+ validate_database_sql(decompressed_file_name, _raise=not force)
site = get_site(context)
frappe.init(site=site)
@@ -222,15 +223,51 @@ def install_app(context, apps):
sys.exit(exit_code)
-@click.command('list-apps')
+@click.command("list-apps")
@pass_context
def list_apps(context):
"List apps in site"
- site = get_site(context)
- frappe.init(site=site)
- frappe.connect()
- print("\n".join(frappe.get_installed_apps()))
- frappe.destroy()
+
+ def fix_whitespaces(text):
+ if site == context.sites[-1]:
+ text = text.rstrip()
+ if len(context.sites) == 1:
+ text = text.lstrip()
+ return text
+
+ for site in context.sites:
+ frappe.init(site=site)
+ frappe.connect()
+ site_title = (
+ click.style(f"{site}", fg="green") if len(context.sites) > 1 else ""
+ )
+ apps = frappe.get_single("Installed Applications").installed_applications
+
+ if apps:
+ name_len, ver_len = [
+ max([len(x.get(y)) for x in apps])
+ for y in ["app_name", "app_version"]
+ ]
+ template = "{{0:{0}}} {{1:{1}}} {{2}}".format(name_len, ver_len)
+
+ installed_applications = [
+ template.format(app.app_name, app.app_version, app.git_branch)
+ for app in apps
+ ]
+ applications_summary = "\n".join(installed_applications)
+ summary = f"{site_title}\n{applications_summary}\n"
+
+ else:
+ applications_summary = "\n".join(frappe.get_installed_apps())
+ summary = f"{site_title}\n{applications_summary}\n"
+
+ summary = fix_whitespaces(summary)
+
+ if applications_summary and summary:
+ print(summary)
+
+ frappe.destroy()
+
@click.command('add-system-manager')
@click.argument('email')
@@ -265,14 +302,12 @@ def disable_user(context, email):
user.save(ignore_permissions=True)
frappe.db.commit()
-
@click.command('migrate')
@click.option('--skip-failing', is_flag=True, help="Skip patches that fail to run")
@click.option('--skip-search-index', is_flag=True, help="Skip search indexing for web documents")
@pass_context
def migrate(context, skip_failing=False, skip_search_index=False):
"Run patches, sync schema and rebuild files/translations"
- import compileall
import re
from frappe.migrate import migrate
@@ -291,9 +326,6 @@ def migrate(context, skip_failing=False, skip_search_index=False):
if not context.sites:
raise SiteNotSpecifiedError
- print("Compiling Python files...")
- compileall.compile_dir('../apps', quiet=1, rx=re.compile('.*node_modules.*'))
-
@click.command('migrate-to')
@click.argument('frappe_provider')
@pass_context
@@ -310,15 +342,16 @@ def migrate_to(context, frappe_provider):
@click.command('run-patch')
@click.argument('module')
+@click.option('--force', is_flag=True)
@pass_context
-def run_patch(context, module):
+def run_patch(context, module, force):
"Run a particular patch"
import frappe.modules.patch_handler
for site in context.sites:
frappe.init(site=site)
try:
frappe.connect()
- frappe.modules.patch_handler.run_single(module, force=context.force)
+ frappe.modules.patch_handler.run_single(module, force=force or context.force)
finally:
frappe.destroy()
if not context.sites:
diff --git a/frappe/config/customization.py b/frappe/config/customization.py
index 3d587e6839..95fa5d355c 100644
--- a/frappe/config/customization.py
+++ b/frappe/config/customization.py
@@ -54,12 +54,6 @@ def get_data():
"label": _("Custom Translations"),
"name": "Translation",
"description": _("Add your own translations")
- },
- {
- "type": "doctype",
- "label": _("Package"),
- "name": "Package",
- "description": _("Import and Export Packages.")
}
]
}
diff --git a/frappe/core/doctype/docfield/docfield.json b/frappe/core/doctype/docfield/docfield.json
index e420d3b775..ca134665b8 100644
--- a/frappe/core/doctype/docfield/docfield.json
+++ b/frappe/core/doctype/docfield/docfield.json
@@ -13,6 +13,7 @@
"fieldname",
"precision",
"length",
+ "non_negative",
"hide_days",
"hide_seconds",
"reqd",
@@ -473,13 +474,20 @@
"fieldname": "hide_border",
"fieldtype": "Check",
"label": "Hide Border"
+ },
+ {
+ "default": "0",
+ "depends_on": "eval:in_list([\"Int\", \"Float\", \"Currency\"], doc.fieldtype)",
+ "fieldname": "non_negative",
+ "fieldtype": "Check",
+ "label": "Non Negative"
}
],
"idx": 1,
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2020-08-28 11:28:21.252853",
+ "modified": "2020-10-29 06:09:26.454990",
"modified_by": "Administrator",
"module": "Core",
"name": "DocField",
diff --git a/frappe/core/doctype/doctype/doctype.py b/frappe/core/doctype/doctype/doctype.py
index 8a9c130fbe..fd0cb1917d 100644
--- a/frappe/core/doctype/doctype/doctype.py
+++ b/frappe/core/doctype/doctype/doctype.py
@@ -56,7 +56,8 @@ class DocType(Document):
- Check fieldnames (duplication etc)
- Clear permission table for child tables
- Add `amended_from` and `amended_by` if Amendable
- - Add custom field `auto_repeat` if Repeatable"""
+ - Add custom field `auto_repeat` if Repeatable
+ - Check if links point to valid fieldnames"""
self.check_developer_mode()
@@ -88,6 +89,7 @@ class DocType(Document):
self.make_repeatable()
self.validate_nestedset()
self.validate_website()
+ self.validate_links_table_fieldnames()
if not self.is_new():
self.before_update = frappe.get_doc('DocType', self.name)
@@ -656,6 +658,19 @@ class DocType(Document):
if not re.match("^(?![\W])[^\d_\s][\w ]+$", name, **flags):
frappe.throw(_("DocType's name should start with a letter and it can only consist of letters, numbers, spaces and underscores"), frappe.NameError)
+ def validate_links_table_fieldnames(self):
+ """Validate fieldnames in Links table"""
+ if frappe.flags.in_patch: return
+ if frappe.flags.in_fixtures: return
+ if not self.links: return
+
+ for index, link in enumerate(self.links):
+ meta = frappe.get_meta(link.link_doctype)
+ if not meta.get_field(link.link_fieldname):
+ message = _("Row #{0}: Could not find field {1} in {2} DocType").format(index+1, frappe.bold(link.link_fieldname), frappe.bold(link.link_doctype))
+ frappe.throw(message, InvalidFieldNameError, _("Invalid Fieldname"))
+
+
def validate_fields_for_doctype(doctype):
doc = frappe.get_doc("DocType", doctype)
diff --git a/frappe/core/doctype/doctype/test_doctype.py b/frappe/core/doctype/doctype/test_doctype.py
index 6f4a400577..10169073e5 100644
--- a/frappe/core/doctype/doctype/test_doctype.py
+++ b/frappe/core/doctype/doctype/test_doctype.py
@@ -451,6 +451,33 @@ class TestDocType(unittest.TestCase):
test_doc_1.delete()
frappe.db.commit()
+ def test_links_table_fieldname_validation(self):
+ doc = new_doctype("Test Links Table Validation")
+
+ # check valid data
+ doc.append("links", {
+ 'link_doctype': "User",
+ 'link_fieldname': "first_name"
+ })
+ doc.validate_links_table_fieldnames() # no error
+ doc.links = [] # reset links table
+
+ # check invalid doctype
+ doc.append("links", {
+ 'link_doctype': "User2",
+ 'link_fieldname': "first_name"
+ })
+ self.assertRaises(frappe.DoesNotExistError, doc.validate_links_table_fieldnames)
+ doc.links = [] # reset links table
+
+ # check invalid fieldname
+ doc.append("links", {
+ 'link_doctype': "User",
+ 'link_fieldname': "a_field_that_does_not_exists"
+ })
+ self.assertRaises(InvalidFieldNameError, doc.validate_links_table_fieldnames)
+
+
def new_doctype(name, unique=0, depends_on='', fields=None):
doc = frappe.get_doc({
"doctype": "DocType",
diff --git a/frappe/core/doctype/report/report.py b/frappe/core/doctype/report/report.py
index 9d30409a2a..01c32bcb57 100644
--- a/frappe/core/doctype/report/report.py
+++ b/frappe/core/doctype/report/report.py
@@ -61,8 +61,9 @@ class Report(Document):
def set_doctype_roles(self):
if not self.get('roles') and self.is_standard == 'No':
meta = frappe.get_meta(self.ref_doctype)
- roles = [{'role': d.role} for d in meta.permissions if d.permlevel==0]
- self.set('roles', roles)
+ if not meta.istable:
+ roles = [{'role': d.role} for d in meta.permissions if d.permlevel==0]
+ self.set('roles', roles)
def is_permitted(self):
"""Returns true if Has Role is not set or the user is allowed."""
diff --git a/frappe/core/doctype/role/role.py b/frappe/core/doctype/role/role.py
index e458b401e4..1920189f78 100644
--- a/frappe/core/doctype/role/role.py
+++ b/frappe/core/doctype/role/role.py
@@ -37,7 +37,7 @@ class Role(Document):
def get_info_based_on_role(role, field='email'):
''' Get information of all users that have been assigned this role '''
users = frappe.get_list("Has Role", filters={"role": role, "parenttype": "User"},
- fields=["parent"])
+ fields=["parent as user_name"])
return get_user_info(users, field)
@@ -45,7 +45,7 @@ def get_user_info(users, field='email'):
''' Fetch details about users for the specified field '''
info_list = []
for user in users:
- user_info, enabled = frappe.db.get_value("User", user.parent, [field, "enabled"])
+ user_info, enabled = frappe.db.get_value("User", user.get("user_name"), [field, "enabled"])
if enabled and user_info not in ["admin@example.com", "guest@example.com"]:
info_list.append(user_info)
return info_list
diff --git a/frappe/core/doctype/server_script/server_script.json b/frappe/core/doctype/server_script/server_script.json
index cc3995ad1d..420f96ec2f 100644
--- a/frappe/core/doctype/server_script/server_script.json
+++ b/frappe/core/doctype/server_script/server_script.json
@@ -31,6 +31,7 @@
"fieldname": "script",
"fieldtype": "Code",
"label": "Script",
+ "options": "Python",
"reqd": 1
},
{
@@ -87,7 +88,7 @@
],
"index_web_pages_for_search": 1,
"links": [],
- "modified": "2020-08-24 16:44:41.060350",
+ "modified": "2020-11-11 12:39:41.391052",
"modified_by": "Administrator",
"module": "Core",
"name": "Server Script",
diff --git a/frappe/custom/doctype/custom_field/custom_field.json b/frappe/custom/doctype/custom_field/custom_field.json
index 3946568bb6..2f0819ab68 100644
--- a/frappe/custom/doctype/custom_field/custom_field.json
+++ b/frappe/custom/doctype/custom_field/custom_field.json
@@ -30,6 +30,7 @@
"mandatory_depends_on",
"read_only_depends_on",
"properties",
+ "non_negative",
"reqd",
"unique",
"read_only",
@@ -403,13 +404,20 @@
"fieldname": "hide_border",
"fieldtype": "Check",
"label": "Hide Border"
+ },
+ {
+ "default": "0",
+ "depends_on": "eval:in_list([\"Int\", \"Float\", \"Currency\"], doc.fieldtype)",
+ "fieldname": "non_negative",
+ "fieldtype": "Check",
+ "label": "Non Negative"
}
],
"icon": "fa fa-glass",
"idx": 1,
"index_web_pages_for_search": 1,
"links": [],
- "modified": "2020-08-28 11:28:44.377753",
+ "modified": "2020-10-29 06:14:43.073329",
"modified_by": "Administrator",
"module": "Custom",
"name": "Custom Field",
diff --git a/frappe/custom/doctype/customize_form/customize_form.py b/frappe/custom/doctype/customize_form/customize_form.py
index 61ecdd88b9..9ce602906c 100644
--- a/frappe/custom/doctype/customize_form/customize_form.py
+++ b/frappe/custom/doctype/customize_form/customize_form.py
@@ -488,6 +488,7 @@ docfield_properties = {
'permlevel': 'Int',
'width': 'Data',
'print_width': 'Data',
+ 'non_negative': 'Check',
'reqd': 'Check',
'unique': 'Check',
'ignore_user_permissions': 'Check',
diff --git a/frappe/custom/doctype/customize_form_field/customize_form_field.json b/frappe/custom/doctype/customize_form_field/customize_form_field.json
index 1d71e1d1e3..227114137c 100644
--- a/frappe/custom/doctype/customize_form_field/customize_form_field.json
+++ b/frappe/custom/doctype/customize_form_field/customize_form_field.json
@@ -11,6 +11,7 @@
"label",
"fieldtype",
"fieldname",
+ "non_negative",
"reqd",
"unique",
"in_list_view",
@@ -414,13 +415,20 @@
"fieldname": "hide_border",
"fieldtype": "Check",
"label": "Hide Border"
+ },
+ {
+ "default": "0",
+ "depends_on": "eval:in_list([\"Int\", \"Float\", \"Currency\"], doc.fieldtype)",
+ "fieldname": "non_negative",
+ "fieldtype": "Check",
+ "label": "Non Negative"
}
],
"idx": 1,
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2020-09-24 14:05:31.093927",
+ "modified": "2020-10-29 06:11:57.661039",
"modified_by": "Administrator",
"module": "Custom",
"name": "Customize Form Field",
diff --git a/frappe/custom/doctype/package_document_type/package_document_type.json b/frappe/custom/doctype/package_document_type/package_document_type.json
deleted file mode 100644
index 6d011bd4e4..0000000000
--- a/frappe/custom/doctype/package_document_type/package_document_type.json
+++ /dev/null
@@ -1,65 +0,0 @@
-{
- "actions": [],
- "creation": "2020-05-14 16:45:47.196395",
- "doctype": "DocType",
- "editable_grid": 1,
- "engine": "InnoDB",
- "field_order": [
- "document_type",
- "column_break_2",
- "attachments",
- "overwrite",
- "section_break_4",
- "filters_json"
- ],
- "fields": [
- {
- "fieldname": "document_type",
- "fieldtype": "Link",
- "in_list_view": 1,
- "label": "Document Type",
- "options": "DocType",
- "reqd": 1
- },
- {
- "fieldname": "column_break_2",
- "fieldtype": "Column Break"
- },
- {
- "default": "0",
- "fieldname": "attachments",
- "fieldtype": "Check",
- "in_list_view": 1,
- "label": "Include Attachments"
- },
- {
- "default": "0",
- "fieldname": "overwrite",
- "fieldtype": "Check",
- "in_list_view": 1,
- "label": "Overwrite"
- },
- {
- "fieldname": "section_break_4",
- "fieldtype": "Section Break"
- },
- {
- "fieldname": "filters_json",
- "fieldtype": "Code",
- "label": "Filters",
- "options": "JSON"
- }
- ],
- "istable": 1,
- "links": [],
- "modified": "2020-05-14 16:45:47.196395",
- "modified_by": "Administrator",
- "module": "Custom",
- "name": "Package Document Type",
- "owner": "Administrator",
- "permissions": [],
- "quick_entry": 1,
- "sort_field": "modified",
- "sort_order": "DESC",
- "track_changes": 1
-}
\ No newline at end of file
diff --git a/frappe/custom/doctype/package_document_type/package_document_type.py b/frappe/custom/doctype/package_document_type/package_document_type.py
deleted file mode 100644
index 6e166eecbd..0000000000
--- a/frappe/custom/doctype/package_document_type/package_document_type.py
+++ /dev/null
@@ -1,10 +0,0 @@
-# -*- 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
-
-class PackageDocumentType(Document):
- pass
diff --git a/frappe/custom/doctype/package_publish_target/__init__.py b/frappe/custom/doctype/package_publish_target/__init__.py
deleted file mode 100644
index e69de29bb2..0000000000
diff --git a/frappe/custom/doctype/package_publish_target/package_publish_target.json b/frappe/custom/doctype/package_publish_target/package_publish_target.json
deleted file mode 100644
index baeb7cb8bc..0000000000
--- a/frappe/custom/doctype/package_publish_target/package_publish_target.json
+++ /dev/null
@@ -1,47 +0,0 @@
-{
- "actions": [],
- "creation": "2020-05-13 16:04:32.724663",
- "doctype": "DocType",
- "editable_grid": 1,
- "engine": "InnoDB",
- "field_order": [
- "instance_url",
- "username",
- "password"
- ],
- "fields": [
- {
- "fieldname": "instance_url",
- "fieldtype": "Data",
- "in_list_view": 1,
- "label": "Site URL",
- "reqd": 1
- },
- {
- "fieldname": "username",
- "fieldtype": "Data",
- "in_list_view": 1,
- "label": "Username",
- "reqd": 1
- },
- {
- "fieldname": "password",
- "fieldtype": "Password",
- "in_list_view": 1,
- "label": "Password",
- "reqd": 1
- }
- ],
- "istable": 1,
- "links": [],
- "modified": "2020-05-15 17:35:16.282235",
- "modified_by": "Administrator",
- "module": "Custom",
- "name": "Package Publish Target",
- "owner": "Administrator",
- "permissions": [],
- "quick_entry": 1,
- "sort_field": "modified",
- "sort_order": "DESC",
- "track_changes": 1
-}
\ No newline at end of file
diff --git a/frappe/custom/doctype/package_publish_tool/__init__.py b/frappe/custom/doctype/package_publish_tool/__init__.py
deleted file mode 100644
index e69de29bb2..0000000000
diff --git a/frappe/custom/doctype/package_publish_tool/package_publish_tool.js b/frappe/custom/doctype/package_publish_tool/package_publish_tool.js
deleted file mode 100644
index a0190a8d8c..0000000000
--- a/frappe/custom/doctype/package_publish_tool/package_publish_tool.js
+++ /dev/null
@@ -1,159 +0,0 @@
-// Copyright (c) 2020, Frappe Technologies and contributors
-// For license information, please see license.txt
-
-frappe.ui.form.on('Package Publish Tool', {
- refresh: function(frm) {
- frm.set_query("document_type", "package_details", function () {
- return {
- filters: {
- "istable": 0,
- }
- };
- });
-
- frappe.realtime.on("package", (data) => {
- frm.dashboard.show_progress(data.prefix, data.progress / data.total * 100, __("{0}", [data.message]));
- if ((data.progress+1) != data.total) {
- frm.dashboard.show_progress(data.prefix, data.progress / data.total * 100, __("{0}", [data.message]));
- } else {
- frm.dashboard.hide_progress();
- }
- });
-
- frm.trigger("show_instructions");
- frm.trigger("last_deployed_on");
- frm.trigger("set_dirty_trigger");
- frm.trigger("set_deploy_primary_action");
- },
- last_deployed_on: function(frm) {
- if (frm.doc.last_deployed_on) {
- frm.trigger("show_indicator");
- }
- },
- show_indicator: function(frm) {
- let pretty_date = frappe.datetime.prettyDate(frm.doc.last_deployed_on);
- frm.page.set_indicator(__("Last published {0}", [pretty_date]), "blue");
- },
- set_dirty_trigger: function(frm) {
- $(frm.wrapper).on("dirty", function() {
- frm.page.set_primary_action(__('Save'), () => frm.save());
- });
- },
- set_deploy_primary_action: function(frm) {
- if (frm.doc.package_details.length && frm.doc.instances.length) {
- frm.page.set_primary_action(__("Publish"), function () {
- frappe.show_alert({
- message: __("Publishing documents..."),
- indicator: "green"
- });
-
- frappe.call({
- method: "frappe.custom.doctype.package_publish_tool.package_publish_tool.deploy_package",
- callback: function() {
- frm.reload_doc();
- frappe.msgprint(__("Documents have been published."));
- }
- });
- });
- }
- },
- show_instructions: function(frm) {
- let field = frm.get_field("html_info");
- field.html(`
-
- Package Publish Tool let's you copy documents from your site to any other remote site.
- Follow the steps below to publish.
-
-
- - Add Document Types that you want to copy from the table below. You can also add filters by expanding the row.
- - Add the Sites URL where you want to copy these documents, and enter the Username and Password.
- - Click on Save. Now, you can click on Publish and the documents will be copied.
-
- `);
- }
-});
-
-frappe.ui.form.on('Package Document Type', {
- form_render: function (frm, cdt, cdn) {
- function _show_filters(filters, table) {
- table.find('tbody').empty();
-
- if (filters.length > 0) {
- filters.forEach(filter => {
- const filter_row =
- $(`
- | ${filter[1]} |
- ${filter[2] || ""} |
- ${filter[3]} |
-
`);
-
- table.find('tbody').append(filter_row);
- });
- } else {
- const filter_row = $(`|
- ${__("Click to Set Filters")} |
`);
- table.find('tbody').append(filter_row);
- }
- }
-
- let row = frappe.get_doc(cdt, cdn);
-
- let wrapper = $(`[data-fieldname="filters_json"]`).empty();
- let table = $(`
-
-
- | ${__('Filter')} |
- ${__('Condition')} |
- ${__('Value')} |
-
-
-
-
-
`).appendTo(wrapper);
- $(`${__("Click table to edit")}
`).appendTo(wrapper);
-
- let filters = JSON.parse(row.filters_json || '[]');
- _show_filters(filters, table);
-
- table.on('click', () => {
- if (!row.document_type) {
- frappe.msgprint(__("Select Document Type."));
- return;
- }
-
- frappe.model.with_doctype(row.document_type, function() {
- let dialog = new frappe.ui.Dialog({
- title: __('Set Filters'),
- fields: [
- {
- fieldtype: 'HTML',
- label: 'Filters',
- fieldname: 'filter_area',
- }
- ],
- primary_action: function() {
- let values = filter_group.get_filters();
- let flt = [];
- if (values) {
- values.forEach(function(value) {
- flt.push([value[0], value[1], value[2], value[3]]);
- });
- }
- row.filters_json = JSON.stringify(flt);
- _show_filters(flt, table);
- dialog.hide();
- },
- primary_action_label: "Set"
- });
-
- let filter_group = new frappe.ui.FilterGroup({
- parent: dialog.get_field('filter_area').$wrapper,
- doctype: row.document_type,
- on_change: () => {},
- });
- filter_group.add_filters_to_filter_group(filters);
- dialog.show();
- });
- });
- },
-});
diff --git a/frappe/custom/doctype/package_publish_tool/package_publish_tool.json b/frappe/custom/doctype/package_publish_tool/package_publish_tool.json
deleted file mode 100644
index 0f85ae0348..0000000000
--- a/frappe/custom/doctype/package_publish_tool/package_publish_tool.json
+++ /dev/null
@@ -1,84 +0,0 @@
-{
- "actions": [],
- "creation": "2020-05-13 15:54:38.082657",
- "doctype": "DocType",
- "editable_grid": 1,
- "engine": "InnoDB",
- "field_order": [
- "html_info",
- "sb_00",
- "package_details",
- "sb_01",
- "instances",
- "last_deployed_on"
- ],
- "fields": [
- {
- "description": "Click on the row for accessing filters.",
- "fieldname": "package_details",
- "fieldtype": "Table",
- "label": "Document Types",
- "options": "Package Document Type",
- "reqd": 1
- },
- {
- "fieldname": "instances",
- "fieldtype": "Table",
- "label": "Sites",
- "options": "Package Publish Target",
- "reqd": 1
- },
- {
- "fieldname": "html_info",
- "fieldtype": "HTML"
- },
- {
- "fieldname": "last_deployed_on",
- "fieldtype": "Datetime",
- "hidden": 1,
- "label": "Last Deployed On",
- "read_only": 1
- },
- {
- "fieldname": "sb_00",
- "fieldtype": "Section Break"
- },
- {
- "fieldname": "sb_01",
- "fieldtype": "Section Break"
- }
- ],
- "issingle": 1,
- "links": [],
- "modified": "2020-05-15 17:31:37.060199",
- "modified_by": "Administrator",
- "module": "Custom",
- "name": "Package Publish Tool",
- "owner": "Administrator",
- "permissions": [
- {
- "create": 1,
- "delete": 1,
- "email": 1,
- "print": 1,
- "read": 1,
- "role": "System Manager",
- "share": 1,
- "write": 1
- },
- {
- "create": 1,
- "delete": 1,
- "email": 1,
- "print": 1,
- "read": 1,
- "role": "All",
- "share": 1,
- "write": 1
- }
- ],
- "quick_entry": 1,
- "sort_field": "modified",
- "sort_order": "DESC",
- "track_changes": 1
-}
\ No newline at end of file
diff --git a/frappe/custom/doctype/package_publish_tool/package_publish_tool.py b/frappe/custom/doctype/package_publish_tool/package_publish_tool.py
deleted file mode 100644
index b73f93a628..0000000000
--- a/frappe/custom/doctype/package_publish_tool/package_publish_tool.py
+++ /dev/null
@@ -1,178 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright (c) 2020, Frappe Technologies and contributors
-# For license information, please see license.txt
-
-from __future__ import unicode_literals
-import frappe
-import json
-import datetime
-import base64
-from frappe.model.document import Document
-from frappe.utils.file_manager import save_file, get_file
-from frappe import _
-from six import string_types
-from frappe.frappeclient import FrappeClient
-from frappe.utils import get_datetime_str, get_datetime
-from frappe.utils.password import get_decrypted_password
-
-class PackagePublishTool(Document):
- pass
-
-@frappe.whitelist()
-def deploy_package():
- package, doc = export_package()
-
- file_name = "Package-" + get_datetime_str(get_datetime())
-
- length = len(doc.instances)
- for idx, instance in enumerate(doc.instances):
- frappe.publish_realtime("package", {"progress": idx, "total": length, "message": instance.instance_url, "prefix": _("Deploying")},
- user=frappe.session.user)
-
- install_package_to_remote(package, instance)
-
- frappe.db.set_value("Package Publish Tool", "Package Publish Tool", "last_deployed_on", frappe.utils.now_datetime())
-
-def install_package_to_remote(package, instance):
- try:
- connection = FrappeClient(instance.instance_url, instance.username, get_decrypted_password(instance.doctype, instance.name))
- except Exception:
- frappe.log_error(frappe.get_traceback())
- frappe.throw(_("Couldn't connect to site {0}. Please check Error Logs.").format(instance.instance_url))
-
- try:
- connection.post_request({
- "cmd": "frappe.custom.doctype.package_publish_tool.package_publish_tool.import_package",
- "package": json.dumps(package)
- })
- except Exception:
- frappe.log_error(frappe.get_traceback())
- frappe.throw(_("Error while installing package to site {0}. Please check Error Logs.").format(instance.instance_url))
-
-@frappe.whitelist()
-def export_package():
- """Export package as JSON."""
- package_doc = frappe.get_single("Package Publish Tool")
- package = []
-
- for doctype in package_doc.package_details:
- filters = []
-
- if doctype.get("filters_json"):
- filters = json.loads(doctype.get("filters_json"))
-
- docs = frappe.get_all(doctype.get("document_type"), filters=filters)
- length = len(docs)
-
- for idx, doc in enumerate(docs):
- frappe.publish_realtime("package", {
- "progress":idx, "total":length,
- "message":doctype.get("document_type"),
- "prefix": _("Exporting")
- },
- user=frappe.session.user)
-
- document = frappe.get_doc(doctype.get("document_type"), doc.name).as_dict()
- attachments = []
-
- if doctype.attachments:
- filters = {
- "attached_to_doctype": document.get("doctype"),
- "attached_to_name": document.get("name")
- }
-
- for f in frappe.get_list("File", filters=filters):
- fname, fcontents = get_file(f.name)
- attachments.append({
- "fname": fname,
- "content": base64.b64encode(fcontents).decode('ascii')
- })
-
- document.update({
- "__attachments": attachments,
- "__overwrite": True if doctype.overwrite else False
- })
-
- package.append(document)
-
- return post_process(package), package_doc
-
-@frappe.whitelist()
-def import_package(package=None):
- """Import package from JSON."""
- frappe.only_for("System Manager")
- if isinstance(package, string_types):
- package = json.loads(package)
-
- for doc in package:
- modified = doc.pop("modified")
- overwrite = doc.pop("__overwrite")
- attachments = doc.pop("__attachments")
- exists = frappe.db.exists(doc.get("doctype"), doc.get("name"))
-
- if not exists:
- d = frappe.get_doc(doc).insert(ignore_permissions=True, ignore_if_duplicate=True)
- if attachments:
- add_attachment(attachments, d)
- else:
- docname = doc.pop("name")
- document = frappe.get_doc(doc.get("doctype"), docname)
-
- if overwrite:
- update_document(document, doc, attachments)
-
- else:
- if frappe.utils.get_datetime(document.modified) < frappe.utils.get_datetime(modified):
- update_document(document, doc, attachments)
-
-def update_document(document, doc, attachments):
- document.update(doc)
- document.save()
- if attachments:
- add_attachment(attachments, document)
-
-def add_attachment(attachments, doc):
- for attachment in attachments:
- save_file(attachment.get("fname"), base64.b64decode(attachment.get("content")), doc.get("doctype"), doc.get("name"))
-
-def post_process(package):
- """Remove the keys from Document and Child Document. Convert datetime, date, time to str."""
- del_keys = ('modified_by', 'creation', 'owner', 'idx', 'docstatus')
- child_del_keys = ('modified_by', 'creation', 'owner', 'idx', 'docstatus', 'name')
-
- for doc in package:
- for key in del_keys:
- if key in doc:
- del doc[key]
-
- for key, value in doc.items():
- stringified_value = get_stringified_value(value)
- if stringified_value:
- doc[key] = stringified_value
-
- if not isinstance(value, list):
- continue
-
- for child in value:
- for child_key in child_del_keys:
- if child_key in child:
- del child[child_key]
-
- for child_key, child_value in child.items():
- stringified_value = get_stringified_value(child_value)
- if stringified_value:
- child[child_key] = stringified_value
-
- return package
-
-def get_stringified_value(value):
- if isinstance(value, datetime.datetime):
- return frappe.utils.get_datetime_str(value)
-
- if isinstance(value, datetime.date):
- return frappe.utils.get_date_str(value)
-
- if isinstance(value, datetime.timedelta):
- return frappe.utils.get_time_str(value)
-
- return None
diff --git a/frappe/custom/doctype/package_publish_tool/test_package_publish_tool.py b/frappe/custom/doctype/package_publish_tool/test_package_publish_tool.py
deleted file mode 100644
index 8332240543..0000000000
--- a/frappe/custom/doctype/package_publish_tool/test_package_publish_tool.py
+++ /dev/null
@@ -1,10 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright (c) 2020, Frappe Technologies and Contributors
-# See license.txt
-from __future__ import unicode_literals
-
-# import frappe
-import unittest
-
-class TestPackagePublishTool(unittest.TestCase):
- pass
diff --git a/frappe/email/doctype/notification/notification.js b/frappe/email/doctype/notification/notification.js
index 2cc027acd6..27fcd0e453 100644
--- a/frappe/email/doctype/notification/notification.js
+++ b/frappe/email/doctype/notification/notification.js
@@ -97,14 +97,7 @@ frappe.notification = {
},
setup_example_message: function(frm) {
let template = '';
- if (frm.doc.channel === 'WhatsApp') {
- template = `Warning:
Only Use Pre-Approved WhatsApp for Business Template
-Message Example
-
-
-Your appointment is coming up on {{ doc.date }} at {{ doc.time }}
-`;
- } else if (frm.doc.channel === 'Email') {
+ if (frm.doc.channel === 'Email') {
template = `Message Example
<h3>Order Overdue</h3>
@@ -124,7 +117,7 @@ Last comment: {{ comments[-1].comment }} by {{ comments[-1].by }}
</ul>
`;
- } else {
+ } else if (in_list(['Slack', 'System Notification', 'SMS'], frm.doc.channel)) {
template = `Message Example
*Order Overdue*
@@ -142,7 +135,9 @@ Last comment: {{ comments[-1].comment }} by {{ comments[-1].by }}
• Amount: {{ doc.grand_total }}
`;
}
- frm.set_df_property('message_examples', 'options', template);
+ if (template) {
+ frm.set_df_property('message_examples', 'options', template);
+ }
}
};
diff --git a/frappe/email/doctype/notification/notification.json b/frappe/email/doctype/notification/notification.json
index 2a8ee1aeb1..73a84e1d3e 100644
--- a/frappe/email/doctype/notification/notification.json
+++ b/frappe/email/doctype/notification/notification.json
@@ -10,7 +10,6 @@
"enabled",
"column_break_2",
"channel",
- "twilio_number",
"slack_webhook_url",
"filters",
"subject",
@@ -61,7 +60,7 @@
"fieldname": "channel",
"fieldtype": "Select",
"label": "Channel",
- "options": "Email\nSlack\nSystem Notification\nWhatsApp\nSMS",
+ "options": "Email\nSlack\nSystem Notification\nSMS",
"reqd": 1,
"set_only_once": 1
},
@@ -80,14 +79,14 @@
"label": "Filters"
},
{
- "depends_on": "eval: !in_list(['SMS', 'WhatsApp'], doc.channel)",
+ "depends_on": "eval: in_list(['Email', 'Slack', 'System Notification'], doc.channel)",
"description": "To add dynamic subject, use jinja tags like\n\n",
"fieldname": "subject",
"fieldtype": "Data",
"ignore_xss_filter": 1,
"in_list_view": 1,
"label": "Subject",
- "mandatory_depends_on": "eval:!in_list(['SMS', 'WhatsApp'], doc.channel)"
+ "mandatory_depends_on": "eval: in_list(['Email', 'Slack', 'System Notification'], doc.channel)"
},
{
"fieldname": "document_type",
@@ -208,7 +207,7 @@
"label": "Value To Be Set"
},
{
- "depends_on": "eval:in_list(['Email', 'SMS', 'WhatsApp'], doc.channel)",
+ "depends_on": "eval:in_list(['Email', 'SMS'], doc.channel)",
"fieldname": "column_break_5",
"fieldtype": "Section Break",
"label": "Recipients"
@@ -263,15 +262,6 @@
"label": "Print Format",
"options": "Print Format"
},
- {
- "depends_on": "eval: doc.channel==='WhatsApp'",
- "description": "To use WhatsApp for Business, initialize Twilio Settings.",
- "fieldname": "twilio_number",
- "fieldtype": "Link",
- "label": "Twilio Number",
- "mandatory_depends_on": "eval: doc.channel==='WhatsApp'",
- "options": "Twilio Number Group"
- },
{
"default": "0",
"depends_on": "eval: doc.channel !== 'System Notification'",
@@ -291,7 +281,7 @@
"icon": "fa fa-envelope",
"index_web_pages_for_search": 1,
"links": [],
- "modified": "2020-09-03 10:33:23.084590",
+ "modified": "2020-10-28 11:04:54.955567",
"modified_by": "Administrator",
"module": "Email",
"name": "Notification",
diff --git a/frappe/email/doctype/notification/notification.py b/frappe/email/doctype/notification/notification.py
index 62be313b82..75281d427e 100644
--- a/frappe/email/doctype/notification/notification.py
+++ b/frappe/email/doctype/notification/notification.py
@@ -14,7 +14,6 @@ from frappe.utils.safe_exec import get_safe_globals
from frappe.modules.utils import export_module_json, get_doc_module
from six import string_types
from frappe.integrations.doctype.slack_webhook_url.slack_webhook_url import send_slack_message
-from frappe.integrations.doctype.twilio_settings.twilio_settings import send_whatsapp_message
from frappe.core.doctype.sms_settings.sms_settings import send_sms
from frappe.desk.doctype.notification_log.notification_log import enqueue_create_notification
@@ -29,7 +28,7 @@ class Notification(Document):
self.name = self.subject
def validate(self):
- if self.channel not in ('WhatsApp', 'SMS'):
+ if self.channel in ("Email", "Slack", "System Notification"):
validate_template(self.subject)
validate_template(self.message)
@@ -43,7 +42,6 @@ class Notification(Document):
self.validate_forbidden_types()
self.validate_condition()
self.validate_standard()
- self.validate_twilio_settings()
frappe.cache().hdel('notifications', self.document_type)
def on_update(self):
@@ -70,11 +68,6 @@ def get_context(context):
if self.is_standard and not frappe.conf.developer_mode:
frappe.throw(_('Cannot edit Standard Notification. To edit, please disable this and duplicate it'))
- def validate_twilio_settings(self):
- if self.enabled and self.channel == "WhatsApp" \
- and not frappe.db.get_single_value("Twilio Settings", "enabled"):
- frappe.throw(_("Please enable Twilio settings to send WhatsApp messages"))
-
def validate_condition(self):
temp_doc = frappe.new_doc(self.document_type)
if self.condition:
@@ -137,9 +130,6 @@ def get_context(context):
if self.channel == 'Slack':
self.send_a_slack_msg(doc, context)
- if self.channel == 'WhatsApp':
- self.send_whatsapp_msg(doc, context)
-
if self.channel == 'SMS':
self.send_sms(doc, context)
@@ -230,13 +220,6 @@ def get_context(context):
reference_doctype=doc.doctype,
reference_name=doc.name)
- def send_whatsapp_msg(self, doc, context):
- send_whatsapp_message(
- sender=self.twilio_number,
- receiver_list=self.get_receiver_list(doc, context),
- message=frappe.render_template(self.message, context),
- )
-
def send_sms(self, doc, context):
send_sms(
receiver_list=self.get_receiver_list(doc, context),
@@ -302,7 +285,7 @@ def get_context(context):
# For sending messages to the owner's mobile phone number
if recipient.receiver_by_document_field == 'owner':
- receiver_list.append(get_user_info(doc.get('owner'), 'mobile_no'))
+ receiver_list += get_user_info([dict(user_name=doc.get('owner'))], 'mobile_no')
# For sending messages to the number specified in the receiver field
elif recipient.receiver_by_document_field:
receiver_list.append(doc.get(recipient.receiver_by_document_field))
diff --git a/frappe/event_streaming/doctype/event_consumer/event_consumer.py b/frappe/event_streaming/doctype/event_consumer/event_consumer.py
index 1505c3a05d..5789e09e74 100644
--- a/frappe/event_streaming/doctype/event_consumer/event_consumer.py
+++ b/frappe/event_streaming/doctype/event_consumer/event_consumer.py
@@ -31,10 +31,12 @@ class EventConsumer(Document):
self.update_consumer_status()
else:
frappe.db.set_value(self.doctype, self.name, 'incoming_change', 0)
-
+
frappe.cache().delete_value('event_consumer_document_type_map')
def on_trash(self):
+ for i in frappe.get_all('Event Update Log Consumer', {'consumer': self.name}):
+ frappe.delete_doc('Event Update Log Consumer', i.name)
frappe.cache().delete_value('event_consumer_document_type_map')
def update_consumer_status(self):
@@ -88,8 +90,9 @@ def register_consumer(data):
for entry in consumer_doctypes:
consumer.append('consumer_doctypes', {
- 'ref_doctype': entry,
- 'status': 'Pending'
+ 'ref_doctype': entry.get('doctype'),
+ 'status': 'Pending',
+ 'condition': entry.get('condition')
})
consumer.insert()
@@ -153,3 +156,53 @@ def notify(consumer):
jobs = get_jobs()
if not jobs or enqueued_method not in jobs[frappe.local.site] and not consumer.flags.notifed:
frappe.enqueue(enqueued_method, queue='long', enqueue_after_commit=True, **{'consumer': consumer})
+
+
+def has_consumer_access(consumer, update_log):
+ """Checks if consumer has completely satisfied all the conditions on the doc"""
+
+ if isinstance(consumer, str):
+ consumer = frappe.get_doc('Event Consumer', consumer)
+
+ if not frappe.db.exists(update_log.ref_doctype, update_log.docname):
+ # Delete Log
+ # Check if the last Update Log of this document was read by this consumer
+ last_update_log = frappe.get_all(
+ 'Event Update Log',
+ filters={
+ 'ref_doctype': update_log.ref_doctype,
+ 'docname': update_log.docname,
+ 'creation': ['<', update_log.creation]
+ },
+ order_by='creation desc',
+ limit_page_length=1
+ )
+ if not len(last_update_log):
+ return False
+
+ last_update_log = frappe.get_doc('Event Update Log', last_update_log[0].name)
+ return len([x for x in last_update_log.consumers if x.consumer == consumer.name])
+
+ doc = frappe.get_doc(update_log.ref_doctype, update_log.docname)
+ try:
+ for dt_entry in consumer.consumer_doctypes:
+ if dt_entry.ref_doctype != update_log.ref_doctype:
+ continue
+
+ if not dt_entry.condition:
+ return True
+
+ condition: str = dt_entry.condition
+ if condition.startswith('cmd:'):
+ cmd = condition.split('cmd:')[1].strip()
+ args = {
+ 'consumer': consumer,
+ 'doc': doc,
+ 'update_log': update_log
+ }
+ return frappe.call(cmd, **args)
+ else:
+ return frappe.safe_eval(condition, frappe._dict(doc=doc))
+ except Exception as e:
+ frappe.log_error(title='has_consumer_access error', message=e)
+ return False
\ No newline at end of file
diff --git a/frappe/event_streaming/doctype/event_consumer_document_type/event_consumer_document_type.json b/frappe/event_streaming/doctype/event_consumer_document_type/event_consumer_document_type.json
index 71dcc63127..c243334a09 100644
--- a/frappe/event_streaming/doctype/event_consumer_document_type/event_consumer_document_type.json
+++ b/frappe/event_streaming/doctype/event_consumer_document_type/event_consumer_document_type.json
@@ -7,7 +7,8 @@
"field_order": [
"ref_doctype",
"status",
- "unsubscribed"
+ "unsubscribed",
+ "condition"
],
"fields": [
{
@@ -37,11 +38,17 @@
"in_list_view": 1,
"label": "Unsubscribed",
"read_only": 1
+ },
+ {
+ "fieldname": "condition",
+ "fieldtype": "Code",
+ "label": "Condition",
+ "read_only": 1
}
],
"istable": 1,
"links": [],
- "modified": "2020-08-14 12:38:40.918620",
+ "modified": "2020-11-07 09:26:49.894294",
"modified_by": "Administrator",
"module": "Event Streaming",
"name": "Event Consumer Document Type",
diff --git a/frappe/event_streaming/doctype/event_producer/event_producer.py b/frappe/event_streaming/doctype/event_producer/event_producer.py
index d458f3c24b..d8a6a55510 100644
--- a/frappe/event_streaming/doctype/event_producer/event_producer.py
+++ b/frappe/event_streaming/doctype/event_producer/event_producer.py
@@ -102,9 +102,13 @@ class EventProducer(Document):
for entry in self.producer_doctypes:
if entry.has_mapping:
# if mapping, subscribe to remote doctype on consumer's site
- consumer_doctypes.append(frappe.db.get_value('Document Type Mapping', entry.mapping, 'remote_doctype'))
+ dt = frappe.db.get_value('Document Type Mapping', entry.mapping, 'remote_doctype')
else:
- consumer_doctypes.append(entry.ref_doctype)
+ dt = entry.ref_doctype
+ consumer_doctypes.append({
+ "doctype": dt,
+ "condition": entry.condition
+ })
user_key = frappe.db.get_value('User', self.user, 'api_key')
user_secret = get_decrypted_password('User', self.user, 'api_secret')
@@ -145,7 +149,8 @@ class EventProducer(Document):
event_consumer.consumer_doctypes.append({
'ref_doctype': ref_doctype,
'status': get_approval_status(config, ref_doctype),
- 'unsubscribed': entry.unsubscribe
+ 'unsubscribed': entry.unsubscribe,
+ 'condition': entry.condition
})
event_consumer.user = self.user
event_consumer.incoming_change = True
@@ -347,13 +352,13 @@ def set_delete(update):
def get_updates(producer_site, last_update, doctypes):
"""Get all updates generated after the last update timestamp"""
- docs = producer_site.get_list(
- doctype='Event Update Log',
- filters={'ref_doctype': ('in', doctypes), 'creation': ('>', last_update)},
- fields=['update_type', 'ref_doctype', 'docname', 'data', 'name', 'creation']
- )
- docs.reverse()
- return [frappe._dict(d) for d in docs]
+ docs = producer_site.post_request({
+ 'cmd': 'frappe.event_streaming.doctype.event_update_log.event_update_log.get_update_logs_for_consumer',
+ 'event_consumer': get_url(),
+ 'doctypes': frappe.as_json(doctypes),
+ 'last_update': last_update
+ })
+ return [frappe._dict(d) for d in (docs or [])]
def get_local_doc(update):
diff --git a/frappe/event_streaming/doctype/event_producer/test_event_producer.py b/frappe/event_streaming/doctype/event_producer/test_event_producer.py
index fa2461a9d8..4c259c3729 100644
--- a/frappe/event_streaming/doctype/event_producer/test_event_producer.py
+++ b/frappe/event_streaming/doctype/event_producer/test_event_producer.py
@@ -152,6 +152,82 @@ class TestEventProducer(unittest.TestCase):
reset_configuration(producer_url)
+ def test_conditional_events(self):
+ producer = get_remote_site()
+
+ # Add Condition
+ event_producer = frappe.get_doc('Event Producer', producer_url)
+ note_producer_entry = [
+ x for x in event_producer.producer_doctypes if x.ref_doctype == 'Note'
+ ][0]
+ note_producer_entry.condition = 'doc.public == 1'
+ event_producer.save()
+
+ # Make test doc
+ producer_note1 = frappe._dict(doctype='Note', public=0, title='test conditional sync')
+ delete_on_remote_if_exists(producer, 'Note', {'title': producer_note1['title']})
+ producer_note1 = producer.insert(producer_note1)
+
+ # Make Update
+ producer_note1['content'] = 'Test Conditional Sync Content'
+ producer_note1 = producer.update(producer_note1)
+
+ self.pull_producer_data()
+
+ # Check if synced here
+ self.assertFalse(frappe.db.exists('Note', producer_note1.name))
+
+ # Lets satisfy the condition
+ producer_note1['public'] = 1
+ producer_note1 = producer.update(producer_note1)
+
+ self.pull_producer_data()
+
+ # it should sync now
+ self.assertTrue(frappe.db.exists('Note', producer_note1.name))
+ local_note = frappe.get_doc('Note', producer_note1.name)
+ self.assertEqual(local_note.content, producer_note1.content)
+
+ reset_configuration(producer_url)
+
+ def test_conditional_events_with_cmd(self):
+ producer = get_remote_site()
+
+ # Add Condition
+ event_producer = frappe.get_doc('Event Producer', producer_url)
+ note_producer_entry = [
+ x for x in event_producer.producer_doctypes if x.ref_doctype == 'Note'
+ ][0]
+ note_producer_entry.condition = 'cmd: frappe.event_streaming.doctype.event_producer.test_event_producer.can_sync_note'
+ event_producer.save()
+
+ # Make test doc
+ producer_note1 = frappe._dict(doctype='Note', public=0, title='test conditional sync cmd')
+ delete_on_remote_if_exists(producer, 'Note', {'title': producer_note1['title']})
+ producer_note1 = producer.insert(producer_note1)
+
+ # Make Update
+ producer_note1['content'] = 'Test Conditional Sync Content'
+ producer_note1 = producer.update(producer_note1)
+
+ self.pull_producer_data()
+
+ # Check if synced here
+ self.assertFalse(frappe.db.exists('Note', producer_note1.name))
+
+ # Lets satisfy the condition
+ producer_note1['public'] = 1
+ producer_note1 = producer.update(producer_note1)
+
+ self.pull_producer_data()
+
+ # it should sync now
+ self.assertTrue(frappe.db.exists('Note', producer_note1.name))
+ local_note = frappe.get_doc('Note', producer_note1.name)
+ self.assertEqual(local_note.content, producer_note1.content)
+
+ reset_configuration(producer_url)
+
def test_update_log(self):
producer = get_remote_site()
producer_doc = insert_into_producer(producer, 'test update log')
@@ -221,6 +297,8 @@ class TestEventProducer(unittest.TestCase):
reset_configuration(producer_url)
+def can_sync_note(consumer, doc, update_log):
+ return doc.public == 1
def setup_event_producer_for_inner_mapping():
event_producer = frappe.get_doc('Event Producer', producer_url, for_update=True)
@@ -322,6 +400,7 @@ def create_event_producer(producer_url):
def reset_configuration(producer_url):
event_producer = frappe.get_doc('Event Producer', producer_url, for_update=True)
event_producer.producer_doctypes = []
+ event_producer.conditions = []
event_producer.producer_url = producer_url
event_producer.append('producer_doctypes', {
'ref_doctype': 'ToDo',
diff --git a/frappe/event_streaming/doctype/event_producer_document_type/event_producer_document_type.json b/frappe/event_streaming/doctype/event_producer_document_type/event_producer_document_type.json
index e5fe9497f8..17fd51d12d 100644
--- a/frappe/event_streaming/doctype/event_producer_document_type/event_producer_document_type.json
+++ b/frappe/event_streaming/doctype/event_producer_document_type/event_producer_document_type.json
@@ -10,7 +10,8 @@
"use_same_name",
"unsubscribe",
"has_mapping",
- "mapping"
+ "mapping",
+ "condition"
],
"fields": [
{
@@ -63,11 +64,16 @@
"fieldtype": "Check",
"in_list_view": 1,
"label": "Unsubscribe"
+ },
+ {
+ "fieldname": "condition",
+ "fieldtype": "Code",
+ "label": "Condition"
}
],
"istable": 1,
"links": [],
- "modified": "2020-08-14 11:38:01.278996",
+ "modified": "2020-11-07 09:26:58.463868",
"modified_by": "Administrator",
"module": "Event Streaming",
"name": "Event Producer Document Type",
diff --git a/frappe/event_streaming/doctype/event_update_log/event_update_log.json b/frappe/event_streaming/doctype/event_update_log/event_update_log.json
index 452a656b8b..a42bc7ec87 100644
--- a/frappe/event_streaming/doctype/event_update_log/event_update_log.json
+++ b/frappe/event_streaming/doctype/event_update_log/event_update_log.json
@@ -1,4 +1,5 @@
{
+ "actions": [],
"creation": "2019-07-30 15:31:26.352527",
"doctype": "DocType",
"editable_grid": 1,
@@ -7,7 +8,8 @@
"update_type",
"ref_doctype",
"docname",
- "data"
+ "data",
+ "consumers"
],
"fields": [
{
@@ -31,7 +33,6 @@
"fieldtype": "Data",
"in_list_view": 1,
"label": "Document Name",
- "options": "ref_doctype",
"read_only": 1
},
{
@@ -39,10 +40,18 @@
"fieldtype": "Code",
"label": "Data",
"read_only": 1
+ },
+ {
+ "fieldname": "consumers",
+ "fieldtype": "Table MultiSelect",
+ "label": "Consumers",
+ "options": "Event Update Log Consumer",
+ "read_only": 1
}
],
"in_create": 1,
- "modified": "2019-09-24 23:16:07.207707",
+ "links": [],
+ "modified": "2020-09-04 07:31:52.599804",
"modified_by": "Administrator",
"module": "Event Streaming",
"name": "Event Update Log",
diff --git a/frappe/event_streaming/doctype/event_update_log/event_update_log.py b/frappe/event_streaming/doctype/event_update_log/event_update_log.py
index 646331a02c..1c31718c2b 100644
--- a/frappe/event_streaming/doctype/event_update_log/event_update_log.py
+++ b/frappe/event_streaming/doctype/event_update_log/event_update_log.py
@@ -140,3 +140,137 @@ def check_docstatus(out, old, new, for_child):
if not for_child and old.docstatus != new.docstatus:
out.changed['docstatus'] = new.docstatus
return out
+
+
+def is_consumer_uptodate(update_log, consumer):
+ """
+ Checks if Consumer has read all the UpdateLogs before the specified update_log
+ :param update_log: The UpdateLog Doc in context
+ :param consumer: The EventConsumer doc
+ """
+ if update_log.update_type == 'Create':
+ # consumer is obviously up to date
+ return True
+
+ prev_logs = frappe.get_all(
+ 'Event Update Log',
+ filters={
+ 'ref_doctype': update_log.ref_doctype,
+ 'docname': update_log.docname,
+ 'creation': ['<', update_log.creation]
+ },
+ order_by='creation desc',
+ limit_page_length=1
+ )
+
+ if not len(prev_logs):
+ return False
+
+ prev_log_consumers = frappe.get_all(
+ 'Event Update Log Consumer',
+ fields=['consumer'],
+ filters={
+ 'parent': prev_logs[0].name,
+ 'parenttype': 'Event Update Log',
+ 'consumer': consumer.name
+ }
+ )
+
+ return len(prev_log_consumers) > 0
+
+
+def mark_consumer_read(update_log_name, consumer_name):
+ """
+ This function appends the Consumer to the list of Consumers that has 'read' an Update Log
+ """
+ update_log = frappe.get_doc('Event Update Log', update_log_name)
+ if len([x for x in update_log.consumers if x.consumer == consumer_name]):
+ return
+
+ frappe.get_doc(frappe._dict(
+ doctype='Event Update Log Consumer',
+ consumer=consumer_name,
+ parent=update_log_name,
+ parenttype='Event Update Log',
+ parentfield='consumers'
+ )).insert(ignore_permissions=True)
+
+
+def get_unread_update_logs(consumer_name, dt, dn):
+ """
+ Get old logs unread by the consumer on a particular document
+ """
+ already_consumed = [x[0] for x in frappe.db.sql("""
+ SELECT
+ update_log.name
+ FROM `tabEvent Update Log` update_log
+ JOIN `tabEvent Update Log Consumer` consumer ON consumer.parent = update_log.name
+ WHERE
+ consumer.consumer = %(consumer)s
+ AND update_log.ref_doctype = %(dt)s
+ AND update_log.docname = %(dn)s
+ """, {'consumer': consumer_name, "dt": dt, "dn": dn}, as_dict=0)]
+
+ logs = frappe.get_all(
+ 'Event Update Log',
+ fields=['update_type', 'ref_doctype',
+ 'docname', 'data', 'name', 'creation'],
+ filters={
+ 'ref_doctype': dt,
+ 'docname': dn,
+ 'name': ['not in', already_consumed]
+ },
+ order_by='creation'
+ )
+
+ return logs
+
+
+@frappe.whitelist()
+def get_update_logs_for_consumer(event_consumer, doctypes, last_update):
+ """
+ Fetches all the UpdateLogs for the consumer
+ It will inject old un-consumed Update Logs if a doc was just found to be accessible to the Consumer
+ """
+
+ if isinstance(doctypes, str):
+ doctypes = frappe.parse_json(doctypes)
+
+ from frappe.event_streaming.doctype.event_consumer.event_consumer import has_consumer_access
+
+ consumer = frappe.get_doc('Event Consumer', event_consumer)
+ docs = frappe.get_list(
+ doctype='Event Update Log',
+ filters={'ref_doctype': ('in', doctypes),
+ 'creation': ('>', last_update)},
+ fields=['update_type', 'ref_doctype',
+ 'docname', 'data', 'name', 'creation'],
+ order_by='creation desc'
+ )
+
+ result = []
+ to_update_history = []
+ for d in docs:
+ if (d.ref_doctype, d.docname) in to_update_history:
+ # will be notified by background jobs
+ continue
+
+ if not has_consumer_access(consumer=consumer, update_log=d):
+ continue
+
+ if not is_consumer_uptodate(d, consumer):
+ to_update_history.append((d.ref_doctype, d.docname))
+ # get_unread_update_logs will have the current log
+ old_logs = get_unread_update_logs(consumer.name, d.ref_doctype, d.docname)
+ if old_logs:
+ old_logs.reverse()
+ result.extend(old_logs)
+ else:
+ result.append(d)
+
+
+ for d in result:
+ mark_consumer_read(update_log_name=d.name, consumer_name=consumer.name)
+
+ result.reverse()
+ return result
\ No newline at end of file
diff --git a/frappe/custom/doctype/package_document_type/__init__.py b/frappe/event_streaming/doctype/event_update_log_consumer/__init__.py
similarity index 100%
rename from frappe/custom/doctype/package_document_type/__init__.py
rename to frappe/event_streaming/doctype/event_update_log_consumer/__init__.py
diff --git a/frappe/event_streaming/doctype/event_update_log_consumer/event_update_log_consumer.json b/frappe/event_streaming/doctype/event_update_log_consumer/event_update_log_consumer.json
new file mode 100644
index 0000000000..b3484c6481
--- /dev/null
+++ b/frappe/event_streaming/doctype/event_update_log_consumer/event_update_log_consumer.json
@@ -0,0 +1,32 @@
+{
+ "actions": [],
+ "creation": "2020-06-30 10:54:53.301787",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "consumer"
+ ],
+ "fields": [
+ {
+ "fieldname": "consumer",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Consumer",
+ "options": "Event Consumer",
+ "reqd": 1
+ }
+ ],
+ "istable": 1,
+ "links": [],
+ "modified": "2020-06-30 10:54:53.301787",
+ "modified_by": "Administrator",
+ "module": "Event Streaming",
+ "name": "Event Update Log Consumer",
+ "owner": "Administrator",
+ "permissions": [],
+ "quick_entry": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/frappe/custom/doctype/package_publish_target/package_publish_target.py b/frappe/event_streaming/doctype/event_update_log_consumer/event_update_log_consumer.py
similarity index 85%
rename from frappe/custom/doctype/package_publish_target/package_publish_target.py
rename to frappe/event_streaming/doctype/event_update_log_consumer/event_update_log_consumer.py
index 34eee02562..ee6d5d8ca9 100644
--- a/frappe/custom/doctype/package_publish_target/package_publish_target.py
+++ b/frappe/event_streaming/doctype/event_update_log_consumer/event_update_log_consumer.py
@@ -6,5 +6,5 @@ from __future__ import unicode_literals
# import frappe
from frappe.model.document import Document
-class PackagePublishTarget(Document):
+class EventUpdateLogConsumer(Document):
pass
diff --git a/frappe/exceptions.py b/frappe/exceptions.py
index 88428b875c..267f5410af 100644
--- a/frappe/exceptions.py
+++ b/frappe/exceptions.py
@@ -76,6 +76,7 @@ class UnknownDomainError(Exception): pass
class MappingMismatchError(ValidationError): pass
class InvalidStatusError(ValidationError): pass
class MandatoryError(ValidationError): pass
+class NonNegativeError(ValidationError): pass
class InvalidSignatureError(ValidationError): pass
class RateLimitExceededError(ValidationError): pass
class CannotChangeConstantError(ValidationError): pass
@@ -109,3 +110,4 @@ class DocumentAlreadyRestored(Exception): pass
class InvalidAuthorizationHeader(CSRFTokenError): pass
class InvalidAuthorizationPrefix(CSRFTokenError): pass
class InvalidAuthorizationToken(CSRFTokenError): pass
+class InvalidDatabaseFile(ValidationError): pass
\ No newline at end of file
diff --git a/frappe/geo/doctype/currency/currency.json b/frappe/geo/doctype/currency/currency.json
index bb9abb7ce8..db3fa5a19f 100644
--- a/frappe/geo/doctype/currency/currency.json
+++ b/frappe/geo/doctype/currency/currency.json
@@ -1,345 +1,113 @@
{
- "allow_copy": 0,
- "allow_guest_to_view": 0,
+ "actions": [],
"allow_import": 1,
"allow_rename": 1,
"autoname": "field:currency_name",
- "beta": 0,
"creation": "2013-01-28 10:06:02",
- "custom": 0,
"description": "**Currency** Master",
- "docstatus": 0,
"doctype": "DocType",
"document_type": "Setup",
- "editable_grid": 0,
+ "engine": "InnoDB",
+ "field_order": [
+ "currency_name",
+ "enabled",
+ "fraction",
+ "fraction_units",
+ "smallest_currency_fraction_value",
+ "symbol",
+ "number_format"
+ ],
"fields": [
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"fieldname": "currency_name",
"fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
"label": "Currency Name",
- "length": 0,
- "no_copy": 0,
"oldfieldname": "currency_name",
"oldfieldtype": "Data",
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
"reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
"unique": 1
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
+ "default": "0",
"fieldname": "enabled",
"fieldtype": "Check",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
"in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Enabled",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "label": "Enabled"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"description": "Sub-currency. For e.g. \"Cent\"",
"fieldname": "fraction",
"fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
"in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Fraction",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "label": "Fraction"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"description": "1 Currency = [?] Fraction\nFor e.g. 1 USD = 100 Cent",
"fieldname": "fraction_units",
"fieldtype": "Int",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
"in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Fraction Units",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "label": "Fraction Units"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"description": "Smallest circulating fraction unit (coin). For e.g. 1 cent for USD and it should be entered as 0.01",
"fieldname": "smallest_currency_fraction_value",
"fieldtype": "Currency",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
"label": "Smallest Currency Fraction Value",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "non_negative": 1
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"description": "A symbol for this currency. For e.g. $",
"fieldname": "symbol",
"fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
"in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Symbol",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "label": "Symbol"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"description": "How should this currency be formatted? If not set, will use system defaults",
"fieldname": "number_format",
"fieldtype": "Select",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
"in_list_view": 1,
- "in_standard_filter": 0,
"label": "Number Format",
- "length": 0,
- "no_copy": 0,
- "options": "\n#,###.##\n#.###,##\n# ###.##\n# ###,##\n#'###.##\n#, ###.##\n#,##,###.##\n#,###.###\n#.###\n#,###",
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "options": "\n#,###.##\n#.###,##\n# ###.##\n# ###,##\n#'###.##\n#, ###.##\n#,##,###.##\n#,###.###\n#.###\n#,###"
}
],
- "has_web_view": 0,
- "hide_heading": 0,
- "hide_toolbar": 0,
"icon": "fa fa-bitcoin",
"idx": 1,
- "image_view": 0,
- "in_create": 0,
- "is_submittable": 0,
- "issingle": 0,
- "istable": 0,
- "max_attachments": 0,
- "modified": "2018-08-29 06:37:19.908254",
+ "index_web_pages_for_search": 1,
+ "links": [],
+ "modified": "2020-10-29 06:33:12.879978",
"modified_by": "Administrator",
"module": "Geo",
"name": "Currency",
"owner": "Administrator",
"permissions": [
{
- "amend": 0,
- "cancel": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
- "if_owner": 0,
"import": 1,
- "permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
- "set_user_permissions": 0,
"share": 1,
- "submit": 0,
"write": 1
},
{
- "amend": 0,
- "cancel": 0,
- "create": 0,
- "delete": 0,
- "email": 0,
- "export": 0,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
- "print": 0,
"read": 1,
- "report": 0,
- "role": "Accounts User",
- "set_user_permissions": 0,
- "share": 0,
- "submit": 0,
- "write": 0
+ "role": "Accounts User"
},
{
- "amend": 0,
- "cancel": 0,
- "create": 0,
- "delete": 0,
- "email": 0,
- "export": 0,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
- "print": 0,
"read": 1,
- "report": 0,
- "role": "Sales User",
- "set_user_permissions": 0,
- "share": 0,
- "submit": 0,
- "write": 0
+ "role": "Sales User"
},
{
- "amend": 0,
- "cancel": 0,
- "create": 0,
- "delete": 0,
- "email": 0,
- "export": 0,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
- "print": 0,
"read": 1,
- "report": 0,
- "role": "Purchase User",
- "set_user_permissions": 0,
- "share": 0,
- "submit": 0,
- "write": 0
+ "role": "Purchase User"
}
],
- "quick_entry": 0,
- "read_only": 0,
- "read_only_onload": 0,
- "show_name_in_global_search": 0,
+ "sort_field": "modified",
"sort_order": "DESC",
- "track_changes": 1,
- "track_seen": 0,
- "track_views": 0
+ "track_changes": 1
}
\ No newline at end of file
diff --git a/frappe/installer.py b/frappe/installer.py
index 27fca9088a..be9b04d453 100755
--- a/frappe/installer.py
+++ b/frappe/installer.py
@@ -3,7 +3,7 @@
import json
import os
-
+from frappe.defaults import _clear_cache
import frappe
@@ -111,8 +111,8 @@ def remove_from_installed_apps(app_name):
installed_apps = frappe.get_installed_apps()
if app_name in installed_apps:
installed_apps.remove(app_name)
- frappe.db.set_global("installed_apps", json.dumps(installed_apps))
- frappe.get_single("Installed Applications").update_versions()
+ frappe.db.set_value("DefaultValue", {"defkey": "installed_apps"}, "defvalue", json.dumps(installed_apps))
+ _clear_cache("__global")
frappe.db.commit()
if frappe.flags.in_install:
post_install()
@@ -122,64 +122,80 @@ def remove_app(app_name, dry_run=False, yes=False, no_backup=False, force=False)
"""Remove app and all linked to the app's module with the app from a site."""
import click
+ site = frappe.local.site
+
# dont allow uninstall app if not installed unless forced
if not force:
if app_name not in frappe.get_installed_apps():
- click.secho("App {0} not installed on Site {1}".format(app_name, frappe.local.site), fg="yellow")
+ click.secho(f"App {app_name} not installed on Site {site}", fg="yellow")
return
- print("Uninstalling App {0} from Site {1}...".format(app_name, frappe.local.site))
+ print(f"Uninstalling App {app_name} from Site {site}...")
if not dry_run and not yes:
- confirm = click.confirm("All doctypes (including custom), modules related to this app will be deleted. Are you sure you want to continue?")
+ confirm = click.confirm(
+ "All doctypes (including custom), modules related to this app will be"
+ " deleted. Are you sure you want to continue?"
+ )
if not confirm:
return
- if not no_backup:
+ if not (dry_run or no_backup):
from frappe.utils.backups import scheduled_backup
+
print("Backing up...")
scheduled_backup(ignore_files=True)
frappe.flags.in_uninstall = True
drop_doctypes = []
- modules = (x.name for x in frappe.get_all("Module Def", filters={"app_name": app_name}))
+ modules = frappe.get_all("Module Def", filters={"app_name": app_name}, pluck="name")
for module_name in modules:
- print("Deleting Module '{0}'".format(module_name))
+ print(f"Deleting Module '{module_name}'")
- for doctype in frappe.get_list("DocType", filters={"module": module_name}, fields=["name", "issingle"]):
- print("* removing DocType '{0}'...".format(doctype.name))
+ for doctype in frappe.get_all(
+ "DocType", filters={"module": module_name}, fields=["name", "issingle"]
+ ):
+ print(f"* removing DocType '{doctype.name}'...")
if not dry_run:
- frappe.delete_doc("DocType", doctype.name)
+ frappe.delete_doc("DocType", doctype.name, ignore_on_trash=True)
if not doctype.issingle:
drop_doctypes.append(doctype.name)
- linked_doctypes = frappe.get_all("DocField", filters={"fieldtype": "Link", "options": "Module Def"}, fields=['parent'])
+ linked_doctypes = frappe.get_all(
+ "DocField", filters={"fieldtype": "Link", "options": "Module Def"}, fields=["parent"]
+ )
ordered_doctypes = ["Desk Page", "Report", "Page", "Web Form"]
- doctypes_with_linked_modules = ordered_doctypes + [doctype.parent for doctype in linked_doctypes if doctype.parent not in ordered_doctypes]
-
+ all_doctypes_with_linked_modules = ordered_doctypes + [
+ doctype.parent
+ for doctype in linked_doctypes
+ if doctype.parent not in ordered_doctypes
+ ]
+ doctypes_with_linked_modules = [
+ x for x in all_doctypes_with_linked_modules if frappe.db.exists("DocType", x)
+ ]
for doctype in doctypes_with_linked_modules:
- for record in frappe.get_list(doctype, filters={"module": module_name}):
- print("* removing {0} '{1}'...".format(doctype, record.name))
+ for record in frappe.get_all(doctype, filters={"module": module_name}, pluck="name"):
+ print(f"* removing {doctype} '{record}'...")
if not dry_run:
- frappe.delete_doc(doctype, record.name)
+ frappe.delete_doc(doctype, record, ignore_on_trash=True)
- print("* removing Module Def '{0}'...".format(module_name))
+ print(f"* removing Module Def '{module_name}'...")
if not dry_run:
- frappe.delete_doc("Module Def", module_name)
+ frappe.delete_doc("Module Def", module_name, ignore_on_trash=True)
+
+ for doctype in set(drop_doctypes):
+ print(f"* dropping Table for '{doctype}'...")
+ if not dry_run:
+ frappe.db.sql_ddl(f"drop table `tab{doctype}`")
if not dry_run:
remove_from_installed_apps(app_name)
-
- for doctype in set(drop_doctypes):
- print("* dropping Table for '{0}'...".format(doctype))
- frappe.db.sql_ddl("drop table `tab{0}`".format(doctype))
-
frappe.db.commit()
- click.secho("Uninstalled App {0} from Site {1}".format(app_name, frappe.local.site), fg="green")
+ click.secho(f"Uninstalled App {app_name} from Site {site}", fg="green")
frappe.flags.in_uninstall = False
@@ -406,3 +422,32 @@ def is_downgrade(sql_file_path, verbose=False):
print("Your site will be downgraded from Frappe {0} to {1}".format(current_version, backup_version))
return downgrade
+
+
+def validate_database_sql(path, _raise=True):
+ """Check if file has contents and if DefaultValue table exists
+
+ Args:
+ path (str): Path of the decompressed SQL file
+ _raise (bool, optional): Raise exception if invalid file. Defaults to True.
+ """
+ to_raise = False
+ error_message = ""
+
+ if not os.path.getsize(path):
+ error_message = f"{path} is an empty file!"
+ to_raise = True
+
+ if not _raise:
+ with open(path, "r") as f:
+ for line in f:
+ if 'tabDefaultValue' in line:
+ error_message = "Table `tabDefaultValue` not found in file."
+ to_raise = True
+
+ if error_message:
+ import click
+ click.secho(error_message, fg="red")
+
+ if _raise and to_raise:
+ raise frappe.InvalidDatabaseFile
diff --git a/frappe/integrations/desk_page/integrations/integrations.json b/frappe/integrations/desk_page/integrations/integrations.json
index cbf7c9c085..1acf4e6c4a 100644
--- a/frappe/integrations/desk_page/integrations/integrations.json
+++ b/frappe/integrations/desk_page/integrations/integrations.json
@@ -23,7 +23,7 @@
{
"hidden": 0,
"label": "Settings",
- "links": "[\n {\n \"description\": \"Webhooks calling API requests into web apps\",\n \"label\": \"Webhook\",\n \"name\": \"Webhook\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Slack Webhooks for internal integration\",\n \"label\": \"Slack Webhook URL\",\n \"name\": \"Slack Webhook URL\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Twilio Settings for WhatsApp integration\",\n \"label\": \"Twilio Settings\",\n \"name\": \"Twilio Settings\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"SMS Settings for sending sms\",\n \"label\": \"SMS Settings\",\n \"name\": \"SMS Settings\",\n \"type\": \"doctype\"\n }\n]"
+ "links": "[\n {\n \"description\": \"Webhooks calling API requests into web apps\",\n \"label\": \"Webhook\",\n \"name\": \"Webhook\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Slack Webhooks for internal integration\",\n \"label\": \"Slack Webhook URL\",\n \"name\": \"Slack Webhook URL\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"SMS Settings for sending sms\",\n \"label\": \"SMS Settings\",\n \"name\": \"SMS Settings\",\n \"type\": \"doctype\"\n }\n]"
}
],
"category": "Administration",
@@ -38,7 +38,7 @@
"idx": 0,
"is_standard": 1,
"label": "Integrations",
- "modified": "2020-08-20 23:04:04.528572",
+ "modified": "2020-10-28 10:25:54.792363",
"modified_by": "Administrator",
"module": "Integrations",
"name": "Integrations",
diff --git a/frappe/integrations/doctype/twilio_number_group/__init__.py b/frappe/integrations/doctype/twilio_number_group/__init__.py
deleted file mode 100644
index e69de29bb2..0000000000
diff --git a/frappe/integrations/doctype/twilio_number_group/twilio_number_group.json b/frappe/integrations/doctype/twilio_number_group/twilio_number_group.json
deleted file mode 100644
index 9d51e4b452..0000000000
--- a/frappe/integrations/doctype/twilio_number_group/twilio_number_group.json
+++ /dev/null
@@ -1,36 +0,0 @@
-{
- "actions": [],
- "autoname": "field:phone_number",
- "creation": "2020-02-24 13:58:58.036914",
- "doctype": "DocType",
- "editable_grid": 1,
- "engine": "InnoDB",
- "field_order": [
- "phone_number"
- ],
- "fields": [
- {
- "fieldname": "phone_number",
- "fieldtype": "Data",
- "in_list_view": 1,
- "label": "Phone Number",
- "options": "Phone",
- "show_days": 1,
- "show_seconds": 1,
- "unique": 1
- }
- ],
- "index_web_pages_for_search": 1,
- "istable": 1,
- "links": [],
- "modified": "2020-08-20 22:48:57.166791",
- "modified_by": "Administrator",
- "module": "Integrations",
- "name": "Twilio Number Group",
- "owner": "Administrator",
- "permissions": [],
- "quick_entry": 1,
- "sort_field": "modified",
- "sort_order": "DESC",
- "track_changes": 1
-}
\ No newline at end of file
diff --git a/frappe/integrations/doctype/twilio_number_group/twilio_number_group.py b/frappe/integrations/doctype/twilio_number_group/twilio_number_group.py
deleted file mode 100644
index 04cb9ae146..0000000000
--- a/frappe/integrations/doctype/twilio_number_group/twilio_number_group.py
+++ /dev/null
@@ -1,10 +0,0 @@
-# -*- 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
-
-class TwilioNumberGroup(Document):
- pass
diff --git a/frappe/integrations/doctype/twilio_settings/__init__.py b/frappe/integrations/doctype/twilio_settings/__init__.py
deleted file mode 100644
index e69de29bb2..0000000000
diff --git a/frappe/integrations/doctype/twilio_settings/test_twilio_settings.py b/frappe/integrations/doctype/twilio_settings/test_twilio_settings.py
deleted file mode 100644
index bcb1368d68..0000000000
--- a/frappe/integrations/doctype/twilio_settings/test_twilio_settings.py
+++ /dev/null
@@ -1,10 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright (c) 2020, Frappe Technologies and Contributors
-# See license.txt
-from __future__ import unicode_literals
-
-# import frappe
-import unittest
-
-class TestTwilioSettings(unittest.TestCase):
- pass
diff --git a/frappe/integrations/doctype/twilio_settings/twilio_settings.js b/frappe/integrations/doctype/twilio_settings/twilio_settings.js
deleted file mode 100644
index 59ebcf2e7d..0000000000
--- a/frappe/integrations/doctype/twilio_settings/twilio_settings.js
+++ /dev/null
@@ -1,8 +0,0 @@
-// Copyright (c) 2020, Frappe Technologies and contributors
-// For license information, please see license.txt
-
-frappe.ui.form.on('Twilio Settings', {
- refresh: function(frm) {
- frm.dashboard.set_headline(__("For more information, {0}.", [`${__('Click here')}`]));
- }
-});
diff --git a/frappe/integrations/doctype/twilio_settings/twilio_settings.json b/frappe/integrations/doctype/twilio_settings/twilio_settings.json
deleted file mode 100644
index 9eb2c0c512..0000000000
--- a/frappe/integrations/doctype/twilio_settings/twilio_settings.json
+++ /dev/null
@@ -1,67 +0,0 @@
-{
- "actions": [],
- "creation": "2020-01-28 15:21:44.457163",
- "doctype": "DocType",
- "editable_grid": 1,
- "engine": "InnoDB",
- "field_order": [
- "enabled",
- "account_sid",
- "auth_token",
- "column_break_2",
- "twilio_number"
- ],
- "fields": [
- {
- "fieldname": "account_sid",
- "fieldtype": "Data",
- "label": "Account SID",
- "mandatory_depends_on": "eval: doc.enabled"
- },
- {
- "fieldname": "auth_token",
- "fieldtype": "Password",
- "label": "Auth Token",
- "mandatory_depends_on": "eval: doc.enabled"
- },
- {
- "fieldname": "column_break_2",
- "fieldtype": "Column Break"
- },
- {
- "fieldname": "twilio_number",
- "fieldtype": "Table",
- "label": "Twilio Number",
- "options": "Twilio Number Group"
- },
- {
- "default": "0",
- "fieldname": "enabled",
- "fieldtype": "Check",
- "label": "Enabled"
- }
- ],
- "index_web_pages_for_search": 1,
- "issingle": 1,
- "links": [],
- "modified": "2020-09-03 10:17:21.318743",
- "modified_by": "Administrator",
- "module": "Integrations",
- "name": "Twilio Settings",
- "owner": "Administrator",
- "permissions": [
- {
- "create": 1,
- "delete": 1,
- "email": 1,
- "print": 1,
- "read": 1,
- "role": "System Manager",
- "share": 1,
- "write": 1
- }
- ],
- "sort_field": "modified",
- "sort_order": "DESC",
- "track_changes": 1
-}
\ No newline at end of file
diff --git a/frappe/integrations/doctype/twilio_settings/twilio_settings.py b/frappe/integrations/doctype/twilio_settings/twilio_settings.py
deleted file mode 100644
index b8f991e829..0000000000
--- a/frappe/integrations/doctype/twilio_settings/twilio_settings.py
+++ /dev/null
@@ -1,63 +0,0 @@
-# -*- 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
-from frappe import _
-from frappe.utils.password import get_decrypted_password
-from twilio.rest import Client
-from six import string_types
-from json import loads
-
-class TwilioSettings(Document):
- def on_update(self):
- if self.enabled:
- self.validate_twilio_credentials()
-
- def validate_twilio_credentials(self):
- try:
- auth_token = get_decrypted_password("Twilio Settings", "Twilio Settings", 'auth_token')
- client = Client(self.account_sid, auth_token)
- client.api.accounts(self.account_sid).fetch()
- except Exception:
- frappe.throw(_("Invalid Account SID or Auth Token."))
-
-def send_whatsapp_message(sender, receiver_list, message):
- twilio_settings = frappe.get_doc("Twilio Settings")
- if not twilio_settings.enabled:
- frappe.throw(_("Please enable twilio settings before sending WhatsApp messages"))
-
- if isinstance(receiver_list, string_types):
- receiver_list = loads(receiver_list)
- if not isinstance(receiver_list, list):
- receiver_list = [receiver_list]
-
- auth_token = get_decrypted_password("Twilio Settings", "Twilio Settings", 'auth_token')
- client = Client(twilio_settings.account_sid, auth_token)
- args = {
- "from_": 'whatsapp:+{}'.format(sender),
- "body": message
- }
-
- failed_delivery = []
-
- for rec in receiver_list:
- args.update({"to": 'whatsapp:{}'.format(rec)})
- resp = _send_whatsapp(args, client)
- if not resp or resp.error_message:
- failed_delivery.append(rec)
-
- if failed_delivery:
- frappe.log_error(_("The message wasn't correctly delivered to: {}".format(", ".join(failed_delivery))), _('Delivery Failed'))
-
-
-def _send_whatsapp(message_dict, client):
- response = frappe._dict()
- try:
- response = client.messages.create(**message_dict)
- except Exception as e:
- frappe.log_error(e, title = _('Twilio WhatsApp Message Error'))
-
- return response
\ No newline at end of file
diff --git a/frappe/model/delete_doc.py b/frappe/model/delete_doc.py
index a38470e3f5..862abe375c 100644
--- a/frappe/model/delete_doc.py
+++ b/frappe/model/delete_doc.py
@@ -335,19 +335,25 @@ def clear_timeline_references(link_doctype, link_name):
WHERE `tabCommunication Link`.link_doctype=%s AND `tabCommunication Link`.link_name=%s""", (link_doctype, link_name))
def insert_feed(doc):
- from frappe.utils import get_fullname
-
- if frappe.flags.in_install or frappe.flags.in_import or getattr(doc, "no_feed_on_delete", False):
+ if (
+ frappe.flags.in_install
+ or frappe.flags.in_uninstall
+ or frappe.flags.in_import
+ or getattr(doc, "no_feed_on_delete", False)
+ ):
return
+ from frappe.utils import get_fullname
+
frappe.get_doc({
"doctype": "Comment",
"comment_type": "Deleted",
"reference_doctype": doc.doctype,
"subject": "{0} {1}".format(_(doc.doctype), doc.name),
- "full_name": get_fullname(doc.owner)
+ "full_name": get_fullname(doc.owner),
}).insert(ignore_permissions=True)
+
def delete_controllers(doctype, module):
"""
Delete controller code in the doctype folder
diff --git a/frappe/model/document.py b/frappe/model/document.py
index 53fcd99f78..3789e20b19 100644
--- a/frappe/model/document.py
+++ b/frappe/model/document.py
@@ -493,6 +493,7 @@ class Document(BaseDocument):
self._validate_mandatory()
self._validate_data_fields()
self._validate_selects()
+ self._validate_non_negative()
self._validate_length()
self._extract_images_from_text_editor()
self._sanitize_content()
@@ -503,6 +504,7 @@ class Document(BaseDocument):
for d in children:
d._validate_data_fields()
d._validate_selects()
+ d._validate_non_negative()
d._validate_length()
d._extract_images_from_text_editor()
d._sanitize_content()
@@ -514,6 +516,21 @@ class Document(BaseDocument):
else:
self.validate_set_only_once()
+ def _validate_non_negative(self):
+ def get_msg(df):
+ if self.parentfield:
+ return "{} {} #{}: {} {}".format(frappe.bold(_(self.doctype)),
+ _("Row"), self.idx, _("Value cannot be negative for"), frappe.bold(_(df.label)))
+ else:
+ return _("Value cannot be negative for {0}: {1}").format(_(df.parent), frappe.bold(_(df.label)))
+
+ for df in self.meta.get('fields', {'non_negative': ('=', 1),
+ 'fieldtype': ('in', ['Int', 'Float', 'Currency'])}):
+
+ if flt(self.get(df.fieldname)) < 0:
+ msg = get_msg(df)
+ frappe.throw(msg, frappe.NonNegativeError, title=_("Negative Value"))
+
def validate_workflow(self):
"""Validate if the workflow transition is valid"""
if frappe.flags.in_install == 'frappe': return
diff --git a/frappe/patches.txt b/frappe/patches.txt
index 2a6ba321d5..0daf29e001 100644
--- a/frappe/patches.txt
+++ b/frappe/patches.txt
@@ -314,3 +314,5 @@ execute:frappe.db.set_value('Website Settings', 'Website Settings', {'navbar_tem
frappe.patches.v13_0.delete_event_producer_and_consumer_keys
frappe.patches.v13_0.web_template_set_module #2020-10-05
frappe.patches.v13_0.remove_custom_link
+execute:frappe.delete_doc("DocType", "Footer Item")
+frappe.patches.v13_0.replace_field_target_with_open_in_new_tab
diff --git a/frappe/patches/v13_0/replace_field_target_with_open_in_new_tab.py b/frappe/patches/v13_0/replace_field_target_with_open_in_new_tab.py
new file mode 100644
index 0000000000..21b2d8ef03
--- /dev/null
+++ b/frappe/patches/v13_0/replace_field_target_with_open_in_new_tab.py
@@ -0,0 +1,11 @@
+import frappe
+
+
+def execute():
+ doctype = "Top Bar Item"
+ if not frappe.db.table_exists(doctype) \
+ or not frappe.db.has_column(doctype, "target"):
+ return
+
+ frappe.reload_doc("website", "doctype", "top_bar_item")
+ frappe.db.set_value(doctype, {"target": 'target = "_blank"'}, 'open_in_new_tab', 1)
diff --git a/frappe/public/build.json b/frappe/public/build.json
index 242cf0160a..a3622499d5 100755
--- a/frappe/public/build.json
+++ b/frappe/public/build.json
@@ -245,7 +245,9 @@
"public/js/frappe/ui/chart.js",
"public/js/frappe/ui/datatable.js",
"public/js/frappe/ui/driver.js",
- "public/js/frappe/barcode_scanner/index.js"
+ "public/js/frappe/barcode_scanner/index.js",
+
+ "public/js/frappe/widgets/utils.js"
],
"css/form.min.css": [
"public/less/form_grid.less"
diff --git a/frappe/public/js/frappe/form/controls/base_input.js b/frappe/public/js/frappe/form/controls/base_input.js
index f3f04ec4d8..2f051a4701 100644
--- a/frappe/public/js/frappe/form/controls/base_input.js
+++ b/frappe/public/js/frappe/form/controls/base_input.js
@@ -133,18 +133,6 @@ frappe.ui.form.ControlInput = frappe.ui.form.Control.extend({
me.parse_validate_and_set_in_model(me.get_input_value(), e);
});
},
- bind_focusout: function() {
- // on touchscreen devices, scroll to top
- // so that static navbar and page head don't overlap the input
- if (frappe.dom.is_touchscreen()) {
- var me = this;
- this.$input && this.$input.on("focusout", function() {
- if (frappe.dom.is_touchscreen()) {
- frappe.utils.scroll_to(me.$wrapper);
- }
- });
- }
- },
set_label: function(label) {
if(label) this.df.label = label;
diff --git a/frappe/public/js/frappe/form/controls/code.js b/frappe/public/js/frappe/form/controls/code.js
index a547cfcf32..f3c51e0232 100644
--- a/frappe/public/js/frappe/form/controls/code.js
+++ b/frappe/public/js/frappe/form/controls/code.js
@@ -66,6 +66,7 @@ frappe.ui.form.ControlCode = frappe.ui.form.ControlText.extend({
const ace_language_mode = language_map[language] || '';
this.editor.session.setMode(ace_language_mode);
+ this.editor.setKeyboardHandler('ace/keyboard/vscode');
},
parse(value) {
diff --git a/frappe/public/js/frappe/form/controls/data.js b/frappe/public/js/frappe/form/controls/data.js
index bbf9a89072..4db2553bd1 100644
--- a/frappe/public/js/frappe/form/controls/data.js
+++ b/frappe/public/js/frappe/form/controls/data.js
@@ -21,7 +21,6 @@ frappe.ui.form.ControlData = frappe.ui.form.ControlInput.extend({
this.input = this.$input.get(0);
this.has_input = true;
this.bind_change_event();
- this.bind_focusout();
this.setup_autoname_check();
if (this.df.options == 'Phone') {
this.setup_phone();
diff --git a/frappe/public/js/frappe/form/dashboard.js b/frappe/public/js/frappe/form/dashboard.js
index 3b6ccd9a5c..b6daabc616 100644
--- a/frappe/public/js/frappe/form/dashboard.js
+++ b/frappe/public/js/frappe/form/dashboard.js
@@ -35,6 +35,7 @@ frappe.ui.form.Dashboard = Class.extend({
// clear custom
this.wrapper.find('.custom').remove();
+ this.hide();
},
set_headline: function(html, color) {
this.frm.layout.show_message(html, color);
@@ -171,7 +172,7 @@ frappe.ui.form.Dashboard = Class.extend({
if(this.data.graph) {
this.setup_graph();
- show = true;
+ // show = true;
}
if(show) {
@@ -494,6 +495,9 @@ frappe.ui.form.Dashboard = Class.extend({
callback: function(r) {
if(r.message) {
me.render_graph(r.message);
+ me.show();
+ } else {
+ me.hide();
}
}
});
diff --git a/frappe/public/js/frappe/form/layout.js b/frappe/public/js/frappe/form/layout.js
index 2195568317..3505cf4857 100644
--- a/frappe/public/js/frappe/form/layout.js
+++ b/frappe/public/js/frappe/form/layout.js
@@ -113,7 +113,7 @@ frappe.ui.form.Layout = Class.extend({
label: __('Dashboard'),
cssClass: 'form-dashboard',
collapsible: 1,
- //hidden: 1
+ // hidden: 1
});
},
diff --git a/frappe/public/js/frappe/form/templates/form_sidebar.html b/frappe/public/js/frappe/form/templates/form_sidebar.html
index 67e674b1c1..481d0159c3 100644
--- a/frappe/public/js/frappe/form/templates/form_sidebar.html
+++ b/frappe/public/js/frappe/form/templates/form_sidebar.html
@@ -60,7 +60,7 @@