diff --git a/frappe/commands/site.py b/frappe/commands/site.py
index 52994ccec3..d92c727dc7 100755
--- a/frappe/commands/site.py
+++ b/frappe/commands/site.py
@@ -256,6 +256,15 @@ def migrate(context, rebuild_website=False, skip_failing=False):
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
+def migrate_to(context, frappe_provider):
+ "Migrates site to the specified provider"
+ from frappe.integrations.frappe_providers import migrate_to
+ for site in context.sites:
+ migrate_to(site, frappe_provider)
+
@click.command('run-patch')
@click.argument('module')
@pass_context
@@ -322,18 +331,19 @@ def use(site, sites_path='.'):
@click.command('backup')
@click.option('--with-files', default=False, is_flag=True, help="Take backup with files")
+@click.option('--verbose', default=False, is_flag=True)
@pass_context
def backup(context, with_files=False, backup_path_db=None, backup_path_files=None,
- backup_path_private_files=None, quiet=False):
+ backup_path_private_files=None, quiet=False, verbose=False):
"Backup"
from frappe.utils.backups import scheduled_backup
- verbose = context.verbose
+ verbose = verbose or context.verbose
exit_code = 0
for site in context.sites:
try:
frappe.init(site=site)
frappe.connect()
- odb = scheduled_backup(ignore_files=not with_files, backup_path_db=backup_path_db, backup_path_files=backup_path_files, backup_path_private_files=backup_path_private_files, force=True)
+ odb = scheduled_backup(ignore_files=not with_files, backup_path_db=backup_path_db, backup_path_files=backup_path_files, backup_path_private_files=backup_path_private_files, force=True, verbose=verbose)
except Exception as e:
if verbose:
print("Backup failed for {0}. Database or site_config.json may be corrupted".format(site))
@@ -342,10 +352,12 @@ def backup(context, with_files=False, backup_path_db=None, backup_path_files=Non
if verbose:
from frappe.utils import now
- print("database backup taken -", odb.backup_path_db, "- on", now())
+ summary_title = "Backup Summary at {0}".format(now())
+ print(summary_title + "\n" + "-" * len(summary_title))
+ print("Database backup:", odb.backup_path_db)
if with_files:
- print("files backup taken -", odb.backup_path_files, "- on", now())
- print("private files backup taken -", odb.backup_path_private_files, "- on", now())
+ print("Public files: ", odb.backup_path_files)
+ print("Private files: ", odb.backup_path_private_files)
frappe.destroy()
sys.exit(exit_code)
@@ -559,6 +571,7 @@ commands = [
install_app,
list_apps,
migrate,
+ migrate_to,
new_site,
reinstall,
reload_doc,
diff --git a/frappe/config/customization.py b/frappe/config/customization.py
index 06eaa2ea00..3d587e6839 100644
--- a/frappe/config/customization.py
+++ b/frappe/config/customization.py
@@ -3,7 +3,7 @@ from frappe import _
def get_data():
return [
- {
+ {
"label": _("Form Customization"),
"icon": "fa fa-glass",
"items": [
@@ -57,9 +57,9 @@ def get_data():
},
{
"type": "doctype",
- "label": _("Custom Tags"),
- "name": "Tag Category",
- "description": _("Add your own Tag Categories")
+ "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 8ae2b740b5..83d3c18453 100644
--- a/frappe/core/doctype/docfield/docfield.json
+++ b/frappe/core/doctype/docfield/docfield.json
@@ -45,6 +45,7 @@
"report_hide",
"remember_last_selected_value",
"ignore_xss_filter",
+ "hide_border",
"property_depends_on_section",
"mandatory_depends_on",
"column_break_38",
@@ -464,12 +465,19 @@
"fieldname": "show_seconds",
"fieldtype": "Check",
"label": "Show Seconds"
+ },
+ {
+ "default": "0",
+ "depends_on": "eval:doc.fieldtype=='Section Break'",
+ "fieldname": "hide_border",
+ "fieldtype": "Check",
+ "label": "Hide Border"
}
],
"idx": 1,
"istable": 1,
"links": [],
- "modified": "2020-05-06 09:06:25.224411",
+ "modified": "2020-05-15 09:06:25.224411",
"modified_by": "Administrator",
"module": "Core",
"name": "DocField",
diff --git a/frappe/core/doctype/installed_application/__init__.py b/frappe/core/doctype/installed_application/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/frappe/core/doctype/installed_application/installed_application.json b/frappe/core/doctype/installed_application/installed_application.json
new file mode 100644
index 0000000000..1f32c557ce
--- /dev/null
+++ b/frappe/core/doctype/installed_application/installed_application.json
@@ -0,0 +1,49 @@
+{
+ "actions": [],
+ "creation": "2020-05-11 17:44:54.674657",
+ "doctype": "DocType",
+ "engine": "InnoDB",
+ "field_order": [
+ "app_name",
+ "app_version",
+ "git_branch"
+ ],
+ "fields": [
+ {
+ "fieldname": "git_branch",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Git Branch",
+ "read_only": 1,
+ "reqd": 1
+ },
+ {
+ "fieldname": "app_name",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Application Name",
+ "read_only": 1,
+ "reqd": 1
+ },
+ {
+ "fieldname": "app_version",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Application Version",
+ "read_only": 1,
+ "reqd": 1
+ }
+ ],
+ "istable": 1,
+ "links": [],
+ "modified": "2020-05-12 10:09:49.148087",
+ "modified_by": "Administrator",
+ "module": "Core",
+ "name": "Installed Application",
+ "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/core/doctype/installed_application/installed_application.py b/frappe/core/doctype/installed_application/installed_application.py
new file mode 100644
index 0000000000..6bb12afc49
--- /dev/null
+++ b/frappe/core/doctype/installed_application/installed_application.py
@@ -0,0 +1,10 @@
+# -*- 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 InstalledApplication(Document):
+ pass
diff --git a/frappe/core/doctype/installed_applications/__init__.py b/frappe/core/doctype/installed_applications/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/frappe/core/doctype/installed_applications/installed_applications.js b/frappe/core/doctype/installed_applications/installed_applications.js
new file mode 100644
index 0000000000..9a1fd5ac18
--- /dev/null
+++ b/frappe/core/doctype/installed_applications/installed_applications.js
@@ -0,0 +1,8 @@
+// Copyright (c) 2020, Frappe Technologies and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('Installed Applications', {
+ // refresh: function(frm) {
+
+ // }
+});
diff --git a/frappe/core/doctype/installed_applications/installed_applications.json b/frappe/core/doctype/installed_applications/installed_applications.json
new file mode 100644
index 0000000000..f2345e66b2
--- /dev/null
+++ b/frappe/core/doctype/installed_applications/installed_applications.json
@@ -0,0 +1,42 @@
+{
+ "actions": [],
+ "creation": "2020-05-11 17:45:41.587750",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "installed_applications"
+ ],
+ "fields": [
+ {
+ "fieldname": "installed_applications",
+ "fieldtype": "Table",
+ "label": "Installed Applications",
+ "options": "Installed Application",
+ "read_only": 1
+ }
+ ],
+ "issingle": 1,
+ "links": [],
+ "modified": "2020-05-12 10:09:14.310622",
+ "modified_by": "Administrator",
+ "module": "Core",
+ "name": "Installed Applications",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "print": 1,
+ "read": 1,
+ "role": "System Manager",
+ "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/core/doctype/installed_applications/installed_applications.py b/frappe/core/doctype/installed_applications/installed_applications.py
new file mode 100644
index 0000000000..aa0401f368
--- /dev/null
+++ b/frappe/core/doctype/installed_applications/installed_applications.py
@@ -0,0 +1,18 @@
+# -*- 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 InstalledApplications(Document):
+ def update_versions(self):
+ self.delete_key("installed_applications")
+ for app in frappe.utils.get_installed_apps_info():
+ self.append("installed_applications", {
+ "app_name": app.get("app_name"),
+ "app_version": app.get("version"),
+ "git_branch": app.get("branch")
+ })
+ self.save()
\ No newline at end of file
diff --git a/frappe/core/doctype/installed_applications/test_installed_applications.py b/frappe/core/doctype/installed_applications/test_installed_applications.py
new file mode 100644
index 0000000000..ab9b849fa1
--- /dev/null
+++ b/frappe/core/doctype/installed_applications/test_installed_applications.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020, Frappe Technologies and Contributors
+# See license.txt
+from __future__ import unicode_literals
+
+# import frappe
+import unittest
+
+class TestInstalledApplications(unittest.TestCase):
+ pass
diff --git a/frappe/core/page/dashboard/dashboard.js b/frappe/core/page/dashboard/dashboard.js
index 222a31a863..0d1337351e 100644
--- a/frappe/core/page/dashboard/dashboard.js
+++ b/frappe/core/page/dashboard/dashboard.js
@@ -6,7 +6,7 @@ frappe.provide('frappe.dashboards.chart_sources');
frappe.pages['dashboard'].on_page_load = function(wrapper) {
- var page = frappe.ui.make_app_page({
+ frappe.ui.make_app_page({
parent: wrapper,
title: __("Dashboard"),
single_column: true
@@ -21,7 +21,7 @@ frappe.pages['dashboard'].on_page_load = function(wrapper) {
class Dashboard {
constructor(wrapper) {
this.wrapper = $(wrapper);
- $(`
+ $(`
`).appendTo(this.wrapper.find(".page-content").empty());
this.container = this.wrapper.find(".dashboard-graph");
diff --git a/frappe/custom/doctype/custom_field/custom_field.json b/frappe/custom/doctype/custom_field/custom_field.json
index d220e448df..77490c8c43 100644
--- a/frappe/custom/doctype/custom_field/custom_field.json
+++ b/frappe/custom/doctype/custom_field/custom_field.json
@@ -50,6 +50,7 @@
"allow_in_quick_entry",
"ignore_xss_filter",
"translatable",
+ "hide_border",
"description",
"permlevel",
"width",
@@ -57,379 +58,386 @@
],
"fields": [
{
- "bold": 1,
- "fieldname": "dt",
- "fieldtype": "Link",
- "in_filter": 1,
- "in_list_view": 1,
- "label": "Document",
- "oldfieldname": "dt",
- "oldfieldtype": "Link",
- "options": "DocType",
- "reqd": 1,
- "search_index": 1
+ "bold": 1,
+ "fieldname": "dt",
+ "fieldtype": "Link",
+ "in_filter": 1,
+ "in_list_view": 1,
+ "label": "Document",
+ "oldfieldname": "dt",
+ "oldfieldtype": "Link",
+ "options": "DocType",
+ "reqd": 1,
+ "search_index": 1
},
{
- "bold": 1,
- "fieldname": "label",
- "fieldtype": "Data",
- "in_filter": 1,
- "label": "Label",
- "no_copy": 1,
- "oldfieldname": "label",
- "oldfieldtype": "Data"
+ "bold": 1,
+ "fieldname": "label",
+ "fieldtype": "Data",
+ "in_filter": 1,
+ "label": "Label",
+ "no_copy": 1,
+ "oldfieldname": "label",
+ "oldfieldtype": "Data"
},
{
- "fieldname": "label_help",
- "fieldtype": "HTML",
- "label": "Label Help",
- "oldfieldtype": "HTML"
+ "fieldname": "label_help",
+ "fieldtype": "HTML",
+ "label": "Label Help",
+ "oldfieldtype": "HTML"
},
{
- "fieldname": "fieldname",
- "fieldtype": "Data",
- "in_list_view": 1,
- "label": "Fieldname",
- "no_copy": 1,
- "oldfieldname": "fieldname",
- "oldfieldtype": "Data",
- "read_only": 1
+ "fieldname": "fieldname",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Fieldname",
+ "no_copy": 1,
+ "oldfieldname": "fieldname",
+ "oldfieldtype": "Data",
+ "read_only": 1
},
{
- "description": "Select the label after which you want to insert new field.",
- "fieldname": "insert_after",
- "fieldtype": "Select",
- "label": "Insert After",
- "no_copy": 1,
- "oldfieldname": "insert_after",
- "oldfieldtype": "Select"
+ "description": "Select the label after which you want to insert new field.",
+ "fieldname": "insert_after",
+ "fieldtype": "Select",
+ "label": "Insert After",
+ "no_copy": 1,
+ "oldfieldname": "insert_after",
+ "oldfieldtype": "Select"
},
{
- "fieldname": "column_break_6",
- "fieldtype": "Column Break"
+ "fieldname": "column_break_6",
+ "fieldtype": "Column Break"
},
{
- "bold": 1,
- "default": "Data",
- "fieldname": "fieldtype",
- "fieldtype": "Select",
- "in_filter": 1,
- "in_list_view": 1,
- "label": "Field Type",
- "oldfieldname": "fieldtype",
- "oldfieldtype": "Select",
- "options": "Attach\nAttach Image\nBarcode\nButton\nCheck\nCode\nColor\nColumn Break\nCurrency\nData\nDate\nDatetime\nDuration\nDynamic Link\nFloat\nGeolocation\nHTML\nImage\nInt\nLink\nLong Text\nMarkdown Editor\nPassword\nPercent\nRating\nRead Only\nSection Break\nSelect\nSmall Text\nTable\nTable MultiSelect\nText\nText Editor\nTime\nSignature",
- "reqd": 1
+ "bold": 1,
+ "default": "Data",
+ "fieldname": "fieldtype",
+ "fieldtype": "Select",
+ "in_filter": 1,
+ "in_list_view": 1,
+ "label": "Field Type",
+ "oldfieldname": "fieldtype",
+ "oldfieldtype": "Select",
+ "options": "Attach\nAttach Image\nBarcode\nButton\nCheck\nCode\nColor\nColumn Break\nCurrency\nData\nDate\nDatetime\nDuration\nDynamic Link\nFloat\nGeolocation\nHTML\nImage\nInt\nLink\nLong Text\nMarkdown Editor\nPassword\nPercent\nRating\nRead Only\nSection Break\nSelect\nSmall Text\nTable\nTable MultiSelect\nText\nText Editor\nTime\nSignature",
+ "reqd": 1
},
{
- "depends_on": "eval:in_list([\"Float\", \"Currency\", \"Percent\"], doc.fieldtype)",
- "description": "Set non-standard precision for a Float or Currency field",
- "fieldname": "precision",
- "fieldtype": "Select",
- "label": "Precision",
- "options": "\n0\n1\n2\n3\n4\n5\n6\n7\n8\n9"
+ "depends_on": "eval:in_list([\"Float\", \"Currency\", \"Percent\"], doc.fieldtype)",
+ "description": "Set non-standard precision for a Float or Currency field",
+ "fieldname": "precision",
+ "fieldtype": "Select",
+ "label": "Precision",
+ "options": "\n0\n1\n2\n3\n4\n5\n6\n7\n8\n9"
},
{
- "fieldname": "options",
- "fieldtype": "Small Text",
- "in_list_view": 1,
- "label": "Options",
- "oldfieldname": "options",
- "oldfieldtype": "Text"
+ "fieldname": "options",
+ "fieldtype": "Small Text",
+ "in_list_view": 1,
+ "label": "Options",
+ "oldfieldname": "options",
+ "oldfieldtype": "Text"
},
{
- "fieldname": "fetch_from",
- "fieldtype": "Small Text",
- "label": "Fetch From"
+ "fieldname": "fetch_from",
+ "fieldtype": "Small Text",
+ "label": "Fetch From"
},
{
- "default": "0",
- "description": "If checked, this field will be not overwritten based on Fetch From if a value already exists.",
- "fieldname": "fetch_if_empty",
- "fieldtype": "Check",
- "label": "Fetch If Empty"
+ "default": "0",
+ "description": "If checked, this field will be not overwritten based on Fetch From if a value already exists.",
+ "fieldname": "fetch_if_empty",
+ "fieldtype": "Check",
+ "label": "Fetch If Empty"
},
{
- "fieldname": "options_help",
- "fieldtype": "HTML",
- "label": "Options Help",
- "oldfieldtype": "HTML"
+ "fieldname": "options_help",
+ "fieldtype": "HTML",
+ "label": "Options Help",
+ "oldfieldtype": "HTML"
},
{
- "fieldname": "section_break_11",
- "fieldtype": "Section Break"
+ "fieldname": "section_break_11",
+ "fieldtype": "Section Break"
},
{
- "default": "0",
- "depends_on": "eval:doc.fieldtype==\"Section Break\"",
- "fieldname": "collapsible",
- "fieldtype": "Check",
- "label": "Collapsible"
+ "default": "0",
+ "depends_on": "eval:doc.fieldtype==\"Section Break\"",
+ "fieldname": "collapsible",
+ "fieldtype": "Check",
+ "label": "Collapsible"
},
{
- "depends_on": "eval:doc.fieldtype==\"Section Break\"",
- "fieldname": "collapsible_depends_on",
- "fieldtype": "Code",
- "label": "Collapsible Depends On"
+ "depends_on": "eval:doc.fieldtype==\"Section Break\"",
+ "fieldname": "collapsible_depends_on",
+ "fieldtype": "Code",
+ "label": "Collapsible Depends On"
},
{
- "fieldname": "default",
- "fieldtype": "Text",
- "label": "Default Value",
- "oldfieldname": "default",
- "oldfieldtype": "Text"
+ "fieldname": "default",
+ "fieldtype": "Text",
+ "label": "Default Value",
+ "oldfieldname": "default",
+ "oldfieldtype": "Text"
},
{
- "fieldname": "depends_on",
- "fieldtype": "Code",
- "label": "Depends On",
- "length": 255
+ "fieldname": "depends_on",
+ "fieldtype": "Code",
+ "label": "Depends On",
+ "length": 255
},
{
- "fieldname": "description",
- "fieldtype": "Text",
- "label": "Field Description",
- "oldfieldname": "description",
- "oldfieldtype": "Text",
- "print_width": "300px",
- "width": "300px"
+ "fieldname": "description",
+ "fieldtype": "Text",
+ "label": "Field Description",
+ "oldfieldname": "description",
+ "oldfieldtype": "Text",
+ "print_width": "300px",
+ "width": "300px"
},
{
- "default": "0",
- "fieldname": "permlevel",
- "fieldtype": "Int",
- "label": "Permission Level",
- "oldfieldname": "permlevel",
- "oldfieldtype": "Int"
+ "default": "0",
+ "fieldname": "permlevel",
+ "fieldtype": "Int",
+ "label": "Permission Level",
+ "oldfieldname": "permlevel",
+ "oldfieldtype": "Int"
},
{
- "fieldname": "width",
- "fieldtype": "Data",
- "label": "Width",
- "oldfieldname": "width",
- "oldfieldtype": "Data"
+ "fieldname": "width",
+ "fieldtype": "Data",
+ "label": "Width",
+ "oldfieldname": "width",
+ "oldfieldtype": "Data"
},
{
- "description": "Number of columns for a field in a List View or a Grid (Total Columns should be less than 11)",
- "fieldname": "columns",
- "fieldtype": "Int",
- "label": "Columns"
+ "description": "Number of columns for a field in a List View or a Grid (Total Columns should be less than 11)",
+ "fieldname": "columns",
+ "fieldtype": "Int",
+ "label": "Columns"
},
{
- "fieldname": "properties",
- "fieldtype": "Column Break",
- "oldfieldtype": "Column Break",
- "print_width": "50%",
- "width": "50%"
+ "fieldname": "properties",
+ "fieldtype": "Column Break",
+ "oldfieldtype": "Column Break",
+ "print_width": "50%",
+ "width": "50%"
},
{
- "default": "0",
- "fieldname": "reqd",
- "fieldtype": "Check",
- "in_list_view": 1,
- "label": "Is Mandatory Field",
- "oldfieldname": "reqd",
- "oldfieldtype": "Check"
+ "default": "0",
+ "fieldname": "reqd",
+ "fieldtype": "Check",
+ "in_list_view": 1,
+ "label": "Is Mandatory Field",
+ "oldfieldname": "reqd",
+ "oldfieldtype": "Check"
},
{
- "default": "0",
- "fieldname": "unique",
- "fieldtype": "Check",
- "label": "Unique"
+ "default": "0",
+ "fieldname": "unique",
+ "fieldtype": "Check",
+ "label": "Unique"
},
{
- "default": "0",
- "fieldname": "read_only",
- "fieldtype": "Check",
- "label": "Read Only"
+ "default": "0",
+ "fieldname": "read_only",
+ "fieldtype": "Check",
+ "label": "Read Only"
},
{
- "default": "0",
- "depends_on": "eval:doc.fieldtype===\"Link\"",
- "fieldname": "ignore_user_permissions",
- "fieldtype": "Check",
- "label": "Ignore User Permissions"
+ "default": "0",
+ "depends_on": "eval:doc.fieldtype===\"Link\"",
+ "fieldname": "ignore_user_permissions",
+ "fieldtype": "Check",
+ "label": "Ignore User Permissions"
},
{
- "default": "0",
- "fieldname": "hidden",
- "fieldtype": "Check",
- "label": "Hidden"
+ "default": "0",
+ "fieldname": "hidden",
+ "fieldtype": "Check",
+ "label": "Hidden"
},
{
- "default": "0",
- "fieldname": "print_hide",
- "fieldtype": "Check",
- "label": "Print Hide",
- "oldfieldname": "print_hide",
- "oldfieldtype": "Check"
+ "default": "0",
+ "fieldname": "print_hide",
+ "fieldtype": "Check",
+ "label": "Print Hide",
+ "oldfieldname": "print_hide",
+ "oldfieldtype": "Check"
},
{
- "default": "0",
- "depends_on": "eval:[\"Int\", \"Float\", \"Currency\", \"Percent\"].indexOf(doc.fieldtype)!==-1",
- "fieldname": "print_hide_if_no_value",
- "fieldtype": "Check",
- "label": "Print Hide If No Value"
+ "default": "0",
+ "depends_on": "eval:[\"Int\", \"Float\", \"Currency\", \"Percent\"].indexOf(doc.fieldtype)!==-1",
+ "fieldname": "print_hide_if_no_value",
+ "fieldtype": "Check",
+ "label": "Print Hide If No Value"
},
{
- "fieldname": "print_width",
- "fieldtype": "Data",
- "hidden": 1,
- "label": "Print Width",
- "no_copy": 1,
- "print_hide": 1
+ "fieldname": "print_width",
+ "fieldtype": "Data",
+ "hidden": 1,
+ "label": "Print Width",
+ "no_copy": 1,
+ "print_hide": 1
},
{
- "default": "0",
- "fieldname": "no_copy",
- "fieldtype": "Check",
- "label": "No Copy",
- "oldfieldname": "no_copy",
- "oldfieldtype": "Check"
+ "default": "0",
+ "fieldname": "no_copy",
+ "fieldtype": "Check",
+ "label": "No Copy",
+ "oldfieldname": "no_copy",
+ "oldfieldtype": "Check"
},
{
- "default": "0",
- "fieldname": "allow_on_submit",
- "fieldtype": "Check",
- "label": "Allow on Submit",
- "oldfieldname": "allow_on_submit",
- "oldfieldtype": "Check"
+ "default": "0",
+ "fieldname": "allow_on_submit",
+ "fieldtype": "Check",
+ "label": "Allow on Submit",
+ "oldfieldname": "allow_on_submit",
+ "oldfieldtype": "Check"
},
{
- "default": "0",
- "fieldname": "in_list_view",
- "fieldtype": "Check",
- "label": "In List View"
+ "default": "0",
+ "fieldname": "in_list_view",
+ "fieldtype": "Check",
+ "label": "In List View"
},
{
- "default": "0",
- "fieldname": "in_standard_filter",
- "fieldtype": "Check",
- "label": "In Standard Filter"
+ "default": "0",
+ "fieldname": "in_standard_filter",
+ "fieldtype": "Check",
+ "label": "In Standard Filter"
},
{
- "default": "0",
- "depends_on": "eval:([\"Data\", \"Select\", \"Table\", \"Text\", \"Text Editor\", \"Link\", \"Small Text\", \"Long Text\", \"Read Only\", \"Heading\", \"Dynamic Link\"].indexOf(doc.fieldtype) !== -1)",
- "fieldname": "in_global_search",
- "fieldtype": "Check",
- "label": "In Global Search"
+ "default": "0",
+ "depends_on": "eval:([\"Data\", \"Select\", \"Table\", \"Text\", \"Text Editor\", \"Link\", \"Small Text\", \"Long Text\", \"Read Only\", \"Heading\", \"Dynamic Link\"].indexOf(doc.fieldtype) !== -1)",
+ "fieldname": "in_global_search",
+ "fieldtype": "Check",
+ "label": "In Global Search"
},
{
- "default": "0",
- "fieldname": "bold",
- "fieldtype": "Check",
- "label": "Bold"
+ "default": "0",
+ "fieldname": "bold",
+ "fieldtype": "Check",
+ "label": "Bold"
},
{
- "default": "0",
- "fieldname": "report_hide",
- "fieldtype": "Check",
- "label": "Report Hide",
- "oldfieldname": "report_hide",
- "oldfieldtype": "Check"
+ "default": "0",
+ "fieldname": "report_hide",
+ "fieldtype": "Check",
+ "label": "Report Hide",
+ "oldfieldname": "report_hide",
+ "oldfieldtype": "Check"
},
{
- "default": "0",
- "fieldname": "search_index",
- "fieldtype": "Check",
- "hidden": 1,
- "label": "Index",
- "no_copy": 1,
- "print_hide": 1
+ "default": "0",
+ "fieldname": "search_index",
+ "fieldtype": "Check",
+ "hidden": 1,
+ "label": "Index",
+ "no_copy": 1,
+ "print_hide": 1
},
{
- "default": "0",
- "description": "Don't HTML Encode HTML tags like <script> or just characters like < or >, as they could be intentionally used in this field",
- "fieldname": "ignore_xss_filter",
- "fieldtype": "Check",
- "label": "Ignore XSS Filter"
+ "default": "0",
+ "description": "Don't HTML Encode HTML tags like <script> or just characters like < or >, as they could be intentionally used in this field",
+ "fieldname": "ignore_xss_filter",
+ "fieldtype": "Check",
+ "label": "Ignore XSS Filter"
},
{
- "default": "1",
- "depends_on": "eval:['Data', 'Select', 'Text', 'Small Text', 'Text Editor'].includes(doc.fieldtype)",
- "fieldname": "translatable",
- "fieldtype": "Check",
- "label": "Translatable"
+ "default": "1",
+ "depends_on": "eval:['Data', 'Select', 'Text', 'Small Text', 'Text Editor'].includes(doc.fieldtype)",
+ "fieldname": "translatable",
+ "fieldtype": "Check",
+ "label": "Translatable"
},
{
- "depends_on": "eval:in_list(['Data', 'Link', 'Dynamic Link', 'Password', 'Select', 'Read Only', 'Attach', 'Attach Image', 'Int'], doc.fieldtype)",
- "fieldname": "length",
- "fieldtype": "Int",
- "label": "Length"
+ "depends_on": "eval:in_list(['Data', 'Link', 'Dynamic Link', 'Password', 'Select', 'Read Only', 'Attach', 'Attach Image', 'Int'], doc.fieldtype)",
+ "fieldname": "length",
+ "fieldtype": "Int",
+ "label": "Length"
},
{
- "fieldname": "mandatory_depends_on",
- "fieldtype": "Code",
- "label": "Mandatory Depends On",
- "length": 255
+ "fieldname": "mandatory_depends_on",
+ "fieldtype": "Code",
+ "label": "Mandatory Depends On",
+ "length": 255
},
{
- "fieldname": "read_only_depends_on",
- "fieldtype": "Code",
- "label": "Read Only Depends On",
- "length": 255
+ "fieldname": "read_only_depends_on",
+ "fieldtype": "Code",
+ "label": "Read Only Depends On",
+ "length": 255
},
{
- "default": "0",
- "fieldname": "allow_in_quick_entry",
- "fieldtype": "Check",
- "label": "Allow in Quick Entry"
+ "default": "0",
+ "fieldname": "allow_in_quick_entry",
+ "fieldtype": "Check",
+ "label": "Allow in Quick Entry"
},
{
- "default": "0",
- "fieldname": "in_preview",
- "fieldtype": "Check",
- "label": "In Preview"
+ "default": "0",
+ "fieldname": "in_preview",
+ "fieldtype": "Check",
+ "label": "In Preview"
},
{
- "default": "1",
- "depends_on": "eval:doc.fieldtype === \"Duration\";",
- "fieldname": "show_seconds",
- "fieldtype": "Check",
- "label": "Show Seconds",
- "show_days": 1,
- "show_seconds": 1
+ "default": "1",
+ "depends_on": "eval:doc.fieldtype === \"Duration\";",
+ "fieldname": "show_seconds",
+ "fieldtype": "Check",
+ "label": "Show Seconds",
+ "show_days": 1,
+ "show_seconds": 1
},
{
- "default": "1",
- "depends_on": "eval:doc.fieldtype === \"Duration\";",
- "fieldname": "show_days",
- "fieldtype": "Check",
- "label": "Show Days",
- "show_days": 1,
- "show_seconds": 1
+ "default": "1",
+ "depends_on": "eval:doc.fieldtype === \"Duration\";",
+ "fieldname": "show_days",
+ "fieldtype": "Check",
+ "label": "Show Days",
+ "show_days": 1,
+ "show_seconds": 1
+ },
+ {
+ "default": "0",
+ "depends_on": "eval:doc.fieldtype=='Section Break'",
+ "fieldname": "hide_border",
+ "fieldtype": "Check",
+ "label": "Hide Border"
}
],
"icon": "fa fa-glass",
"idx": 1,
"links": [],
- "modified": "2020-05-14 23:43:00.123572",
+ "modified": "2020-05-15 23:43:00.123572",
"modified_by": "Administrator",
"module": "Custom",
"name": "Custom Field",
"owner": "Administrator",
"permissions": [
{
- "create": 1,
- "delete": 1,
- "email": 1,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "Administrator",
- "share": 1,
- "write": 1
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Administrator",
+ "share": 1,
+ "write": 1
},
{
- "create": 1,
- "delete": 1,
- "email": 1,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "System Manager",
- "share": 1,
- "write": 1
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "System Manager",
+ "share": 1,
+ "write": 1
}
],
"search_fields": "dt,label,fieldtype,options",
diff --git a/frappe/custom/doctype/custom_link/__init__.py b/frappe/custom/doctype/custom_link/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/frappe/custom/doctype/custom_link/custom_link.js b/frappe/custom/doctype/custom_link/custom_link.js
new file mode 100644
index 0000000000..8662724b1a
--- /dev/null
+++ b/frappe/custom/doctype/custom_link/custom_link.js
@@ -0,0 +1,20 @@
+// Copyright (c) 2020, Frappe Technologies and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('Custom Link', {
+ refresh: function(frm) {
+ frm.set_query("document_type", function () {
+ return {
+ filters: {
+ custom: 0,
+ istable: 0,
+ module: ['not in', ["Email", "Core", "Custom", "Event Streaming", "Social", "Data Migration", "Geo", "Desk"]]
+ }
+ };
+ });
+
+ frm.add_custom_button(__('Go to {0} List', [frm.doc.document_type]), function() {
+ frappe.set_route('List', frm.doc.document_type);
+ });
+ }
+});
diff --git a/frappe/custom/doctype/custom_link/custom_link.json b/frappe/custom/doctype/custom_link/custom_link.json
new file mode 100644
index 0000000000..350e6b1c2d
--- /dev/null
+++ b/frappe/custom/doctype/custom_link/custom_link.json
@@ -0,0 +1,52 @@
+{
+ "actions": [],
+ "autoname": "field:document_type",
+ "creation": "2020-04-08 15:16:44.342509",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "document_type",
+ "links"
+ ],
+ "fields": [
+ {
+ "fieldname": "document_type",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Document Type",
+ "options": "DocType",
+ "reqd": 1,
+ "unique": 1
+ },
+ {
+ "fieldname": "links",
+ "fieldtype": "Table",
+ "label": "Links",
+ "options": "DocType Link"
+ }
+ ],
+ "links": [],
+ "modified": "2020-04-08 16:42:59.402671",
+ "modified_by": "Administrator",
+ "module": "Custom",
+ "name": "Custom Link",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 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/custom/doctype/custom_link/custom_link.py b/frappe/custom/doctype/custom_link/custom_link.py
new file mode 100644
index 0000000000..11316d5751
--- /dev/null
+++ b/frappe/custom/doctype/custom_link/custom_link.py
@@ -0,0 +1,10 @@
+# -*- 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 CustomLink(Document):
+ pass
diff --git a/frappe/custom/doctype/custom_link/test_custom_link.py b/frappe/custom/doctype/custom_link/test_custom_link.py
new file mode 100644
index 0000000000..a292f73ad0
--- /dev/null
+++ b/frappe/custom/doctype/custom_link/test_custom_link.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020, Frappe Technologies and Contributors
+# See license.txt
+from __future__ import unicode_literals
+
+# import frappe
+import unittest
+
+class TestCustomLink(unittest.TestCase):
+ pass
diff --git a/frappe/custom/doctype/customize_form/customize_form.py b/frappe/custom/doctype/customize_form/customize_form.py
index ebf01d11b3..6a54d9c7e6 100644
--- a/frappe/custom/doctype/customize_form/customize_form.py
+++ b/frappe/custom/doctype/customize_form/customize_form.py
@@ -76,7 +76,8 @@ docfield_properties = {
'remember_last_selected_value': 'Check',
'allow_bulk_edit': 'Check',
'auto_repeat': 'Link',
- 'allow_in_quick_entry': 'Check'
+ 'allow_in_quick_entry': 'Check',
+ 'hide_border': 'Check'
}
allowed_fieldtype_change = (('Currency', 'Float', 'Percent'), ('Small Text', 'Data'),
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 5876666485..f422c36e61 100644
--- a/frappe/custom/doctype/customize_form_field/customize_form_field.json
+++ b/frappe/custom/doctype/customize_form_field/customize_form_field.json
@@ -41,6 +41,7 @@
"allow_on_submit",
"report_hide",
"remember_last_selected_value",
+ "hide_border",
"property_depends_on_section",
"mandatory_depends_on",
"column_break_33",
@@ -59,361 +60,368 @@
],
"fields": [
{
- "fieldname": "label_and_type",
- "fieldtype": "Section Break",
- "label": "Label and Type"
+ "fieldname": "label_and_type",
+ "fieldtype": "Section Break",
+ "label": "Label and Type"
},
{
- "fieldname": "label",
- "fieldtype": "Data",
- "in_list_view": 1,
- "label": "Label",
- "oldfieldname": "label",
- "oldfieldtype": "Data",
- "search_index": 1
+ "fieldname": "label",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Label",
+ "oldfieldname": "label",
+ "oldfieldtype": "Data",
+ "search_index": 1
},
{
- "default": "Data",
- "fieldname": "fieldtype",
- "fieldtype": "Select",
- "in_list_view": 1,
- "label": "Type",
- "oldfieldname": "fieldtype",
- "oldfieldtype": "Select",
- "options": "Attach\nAttach Image\nBarcode\nButton\nCheck\nCode\nColor\nColumn Break\nCurrency\nData\nDate\nDatetime\nDuration\nDynamic Link\nFloat\nFold\nGeolocation\nHeading\nHTML\nHTML Editor\nImage\nInt\nLink\nLong Text\nMarkdown Editor\nPassword\nPercent\nRating\nRead Only\nSection Break\nSelect\nSignature\nSmall Text\nTable\nTable MultiSelect\nText\nText Editor\nTime",
- "reqd": 1,
- "search_index": 1
+ "default": "Data",
+ "fieldname": "fieldtype",
+ "fieldtype": "Select",
+ "in_list_view": 1,
+ "label": "Type",
+ "oldfieldname": "fieldtype",
+ "oldfieldtype": "Select",
+ "options": "Attach\nAttach Image\nBarcode\nButton\nCheck\nCode\nColor\nColumn Break\nCurrency\nData\nDate\nDatetime\nDuration\nDynamic Link\nFloat\nFold\nGeolocation\nHeading\nHTML\nHTML Editor\nImage\nInt\nLink\nLong Text\nMarkdown Editor\nPassword\nPercent\nRating\nRead Only\nSection Break\nSelect\nSignature\nSmall Text\nTable\nTable MultiSelect\nText\nText Editor\nTime",
+ "reqd": 1,
+ "search_index": 1
},
{
- "fieldname": "fieldname",
- "fieldtype": "Data",
- "in_list_view": 1,
- "label": "Name",
- "oldfieldname": "fieldname",
- "oldfieldtype": "Data",
- "read_only": 1,
- "search_index": 1
+ "fieldname": "fieldname",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Name",
+ "oldfieldname": "fieldname",
+ "oldfieldtype": "Data",
+ "read_only": 1,
+ "search_index": 1
},
{
- "default": "0",
- "depends_on": "eval:!in_list([\"Section Break\", \"Column Break\", \"Button\", \"HTML\"], doc.fieldtype)",
- "fieldname": "reqd",
- "fieldtype": "Check",
- "label": "Mandatory",
- "oldfieldname": "reqd",
- "oldfieldtype": "Check",
- "print_width": "50px",
- "width": "50px"
+ "default": "0",
+ "depends_on": "eval:!in_list([\"Section Break\", \"Column Break\", \"Button\", \"HTML\"], doc.fieldtype)",
+ "fieldname": "reqd",
+ "fieldtype": "Check",
+ "label": "Mandatory",
+ "oldfieldname": "reqd",
+ "oldfieldtype": "Check",
+ "print_width": "50px",
+ "width": "50px"
},
{
- "default": "0",
- "fieldname": "unique",
- "fieldtype": "Check",
- "label": "Unique"
+ "default": "0",
+ "fieldname": "unique",
+ "fieldtype": "Check",
+ "label": "Unique"
},
{
- "default": "0",
- "fieldname": "in_list_view",
- "fieldtype": "Check",
- "label": "In List View"
+ "default": "0",
+ "fieldname": "in_list_view",
+ "fieldtype": "Check",
+ "label": "In List View"
},
{
- "default": "0",
- "fieldname": "in_standard_filter",
- "fieldtype": "Check",
- "label": "In Standard Filter"
+ "default": "0",
+ "fieldname": "in_standard_filter",
+ "fieldtype": "Check",
+ "label": "In Standard Filter"
},
{
- "default": "0",
- "depends_on": "eval:([\"Data\", \"Select\", \"Table\", \"Text\", \"Text Editor\", \"Link\", \"Small Text\", \"Long Text\", \"Read Only\", \"Heading\", \"Dynamic Link\"].indexOf(doc.fieldtype) !== -1)",
- "fieldname": "in_global_search",
- "fieldtype": "Check",
- "label": "In Global Search"
+ "default": "0",
+ "depends_on": "eval:([\"Data\", \"Select\", \"Table\", \"Text\", \"Text Editor\", \"Link\", \"Small Text\", \"Long Text\", \"Read Only\", \"Heading\", \"Dynamic Link\"].indexOf(doc.fieldtype) !== -1)",
+ "fieldname": "in_global_search",
+ "fieldtype": "Check",
+ "label": "In Global Search"
},
{
- "default": "0",
- "fieldname": "bold",
- "fieldtype": "Check",
- "label": "Bold"
+ "default": "0",
+ "fieldname": "bold",
+ "fieldtype": "Check",
+ "label": "Bold"
},
{
- "default": "1",
- "depends_on": "eval:['Data', 'Select', 'Text', 'Small Text', 'Text Editor'].includes(doc.fieldtype)",
- "fieldname": "translatable",
- "fieldtype": "Check",
- "label": "Translatable"
+ "default": "1",
+ "depends_on": "eval:['Data', 'Select', 'Text', 'Small Text', 'Text Editor'].includes(doc.fieldtype)",
+ "fieldname": "translatable",
+ "fieldtype": "Check",
+ "label": "Translatable"
},
{
- "fieldname": "column_break_7",
- "fieldtype": "Column Break"
+ "fieldname": "column_break_7",
+ "fieldtype": "Column Break"
},
{
- "depends_on": "eval:in_list([\"Float\", \"Currency\", \"Percent\"], doc.fieldtype)",
- "description": "Set non-standard precision for a Float or Currency field",
- "fieldname": "precision",
- "fieldtype": "Select",
- "label": "Precision",
- "options": "\n0\n1\n2\n3\n4\n5\n6\n7\n8\n9"
+ "depends_on": "eval:in_list([\"Float\", \"Currency\", \"Percent\"], doc.fieldtype)",
+ "description": "Set non-standard precision for a Float or Currency field",
+ "fieldname": "precision",
+ "fieldtype": "Select",
+ "label": "Precision",
+ "options": "\n0\n1\n2\n3\n4\n5\n6\n7\n8\n9"
},
{
- "depends_on": "eval:in_list(['Data', 'Link', 'Dynamic Link', 'Password', 'Select', 'Read Only', 'Attach', 'Attach Image'], doc.fieldtype)",
- "fieldname": "length",
- "fieldtype": "Int",
- "label": "Length"
+ "depends_on": "eval:in_list(['Data', 'Link', 'Dynamic Link', 'Password', 'Select', 'Read Only', 'Attach', 'Attach Image'], doc.fieldtype)",
+ "fieldname": "length",
+ "fieldtype": "Int",
+ "label": "Length"
},
{
- "description": "For Links, enter the DocType as range.\nFor Select, enter list of Options, each on a new line.",
- "fieldname": "options",
- "fieldtype": "Small Text",
- "in_list_view": 1,
- "label": "Options",
- "oldfieldname": "options",
- "oldfieldtype": "Text"
+ "description": "For Links, enter the DocType as range.\nFor Select, enter list of Options, each on a new line.",
+ "fieldname": "options",
+ "fieldtype": "Small Text",
+ "in_list_view": 1,
+ "label": "Options",
+ "oldfieldname": "options",
+ "oldfieldtype": "Text"
},
{
- "fieldname": "fetch_from",
- "fieldtype": "Small Text",
- "label": "Fetch From"
+ "fieldname": "fetch_from",
+ "fieldtype": "Small Text",
+ "label": "Fetch From"
},
{
- "default": "0",
- "description": "If checked, this field will be not overwritten based on Fetch From if a value already exists.",
- "fieldname": "fetch_if_empty",
- "fieldtype": "Check",
- "label": "Fetch If Empty"
+ "default": "0",
+ "description": "If checked, this field will be not overwritten based on Fetch From if a value already exists.",
+ "fieldname": "fetch_if_empty",
+ "fieldtype": "Check",
+ "label": "Fetch If Empty"
},
{
- "fieldname": "permissions",
- "fieldtype": "Section Break",
- "label": "Permissions"
+ "fieldname": "permissions",
+ "fieldtype": "Section Break",
+ "label": "Permissions"
},
{
- "description": "This field will appear only if the fieldname defined here has value OR the rules are true (examples): \nmyfield\neval:doc.myfield=='My Value'\neval:doc.age>18",
- "fieldname": "depends_on",
- "fieldtype": "Code",
- "label": "Depends On",
- "oldfieldname": "depends_on",
- "oldfieldtype": "Data",
- "options": "JS"
+ "description": "This field will appear only if the fieldname defined here has value OR the rules are true (examples): \nmyfield\neval:doc.myfield=='My Value'\neval:doc.age>18",
+ "fieldname": "depends_on",
+ "fieldtype": "Code",
+ "label": "Depends On",
+ "oldfieldname": "depends_on",
+ "oldfieldtype": "Data",
+ "options": "JS"
},
{
- "default": "0",
- "fieldname": "permlevel",
- "fieldtype": "Int",
- "in_list_view": 1,
- "label": "Perm Level",
- "oldfieldname": "permlevel",
- "oldfieldtype": "Int"
+ "default": "0",
+ "fieldname": "permlevel",
+ "fieldtype": "Int",
+ "in_list_view": 1,
+ "label": "Perm Level",
+ "oldfieldname": "permlevel",
+ "oldfieldtype": "Int"
},
{
- "default": "0",
- "fieldname": "hidden",
- "fieldtype": "Check",
- "label": "Hidden",
- "oldfieldname": "hidden",
- "oldfieldtype": "Check",
- "print_width": "50px",
- "width": "50px"
+ "default": "0",
+ "fieldname": "hidden",
+ "fieldtype": "Check",
+ "label": "Hidden",
+ "oldfieldname": "hidden",
+ "oldfieldtype": "Check",
+ "print_width": "50px",
+ "width": "50px"
},
{
- "default": "0",
- "fieldname": "read_only",
- "fieldtype": "Check",
- "label": "Read Only"
+ "default": "0",
+ "fieldname": "read_only",
+ "fieldtype": "Check",
+ "label": "Read Only"
},
{
- "default": "0",
- "depends_on": "eval:doc.fieldtype==\"Section Break\"",
- "fieldname": "collapsible",
- "fieldtype": "Check",
- "label": "Collapsible"
+ "default": "0",
+ "depends_on": "eval:doc.fieldtype==\"Section Break\"",
+ "fieldname": "collapsible",
+ "fieldtype": "Check",
+ "label": "Collapsible"
},
{
- "default": "0",
- "depends_on": "eval: doc.fieldtype == \"Table\"",
- "fieldname": "allow_bulk_edit",
- "fieldtype": "Check",
- "label": "Allow Bulk Edit"
+ "default": "0",
+ "depends_on": "eval: doc.fieldtype == \"Table\"",
+ "fieldname": "allow_bulk_edit",
+ "fieldtype": "Check",
+ "label": "Allow Bulk Edit"
},
{
- "depends_on": "eval:doc.fieldtype==\"Section Break\"",
- "fieldname": "collapsible_depends_on",
- "fieldtype": "Code",
- "label": "Collapsible Depends On",
- "options": "JS"
+ "depends_on": "eval:doc.fieldtype==\"Section Break\"",
+ "fieldname": "collapsible_depends_on",
+ "fieldtype": "Code",
+ "label": "Collapsible Depends On",
+ "options": "JS"
},
{
- "fieldname": "column_break_14",
- "fieldtype": "Column Break"
+ "fieldname": "column_break_14",
+ "fieldtype": "Column Break"
},
{
- "default": "0",
- "fieldname": "ignore_user_permissions",
- "fieldtype": "Check",
- "label": "Ignore User Permissions"
+ "default": "0",
+ "fieldname": "ignore_user_permissions",
+ "fieldtype": "Check",
+ "label": "Ignore User Permissions"
},
{
- "default": "0",
- "fieldname": "allow_on_submit",
- "fieldtype": "Check",
- "label": "Allow on Submit",
- "oldfieldname": "allow_on_submit",
- "oldfieldtype": "Check"
+ "default": "0",
+ "fieldname": "allow_on_submit",
+ "fieldtype": "Check",
+ "label": "Allow on Submit",
+ "oldfieldname": "allow_on_submit",
+ "oldfieldtype": "Check"
},
{
- "default": "0",
- "fieldname": "report_hide",
- "fieldtype": "Check",
- "label": "Report Hide",
- "oldfieldname": "report_hide",
- "oldfieldtype": "Check"
+ "default": "0",
+ "fieldname": "report_hide",
+ "fieldtype": "Check",
+ "label": "Report Hide",
+ "oldfieldname": "report_hide",
+ "oldfieldtype": "Check"
},
{
- "default": "0",
- "depends_on": "eval:(doc.fieldtype == 'Link')",
- "fieldname": "remember_last_selected_value",
- "fieldtype": "Check",
- "label": "Remember Last Selected Value"
+ "default": "0",
+ "depends_on": "eval:(doc.fieldtype == 'Link')",
+ "fieldname": "remember_last_selected_value",
+ "fieldtype": "Check",
+ "label": "Remember Last Selected Value"
},
{
- "fieldname": "display",
- "fieldtype": "Section Break",
- "label": "Display"
+ "fieldname": "display",
+ "fieldtype": "Section Break",
+ "label": "Display"
},
{
- "fieldname": "default",
- "fieldtype": "Text",
- "label": "Default",
- "oldfieldname": "default",
- "oldfieldtype": "Text"
+ "fieldname": "default",
+ "fieldtype": "Text",
+ "label": "Default",
+ "oldfieldname": "default",
+ "oldfieldtype": "Text"
},
{
- "default": "0",
- "fieldname": "in_filter",
- "fieldtype": "Check",
- "label": "In Filter",
- "oldfieldname": "in_filter",
- "oldfieldtype": "Check",
- "print_width": "50px",
- "width": "50px"
+ "default": "0",
+ "fieldname": "in_filter",
+ "fieldtype": "Check",
+ "label": "In Filter",
+ "oldfieldname": "in_filter",
+ "oldfieldtype": "Check",
+ "print_width": "50px",
+ "width": "50px"
},
{
- "fieldname": "column_break_21",
- "fieldtype": "Column Break"
+ "fieldname": "column_break_21",
+ "fieldtype": "Column Break"
},
{
- "fieldname": "description",
- "fieldtype": "Text",
- "label": "Description",
- "oldfieldname": "description",
- "oldfieldtype": "Text",
- "print_width": "300px",
- "width": "300px"
+ "fieldname": "description",
+ "fieldtype": "Text",
+ "label": "Description",
+ "oldfieldname": "description",
+ "oldfieldtype": "Text",
+ "print_width": "300px",
+ "width": "300px"
},
{
- "default": "0",
- "fieldname": "print_hide",
- "fieldtype": "Check",
- "label": "Print Hide",
- "oldfieldname": "print_hide",
- "oldfieldtype": "Check"
+ "default": "0",
+ "fieldname": "print_hide",
+ "fieldtype": "Check",
+ "label": "Print Hide",
+ "oldfieldname": "print_hide",
+ "oldfieldtype": "Check"
},
{
- "default": "0",
- "depends_on": "eval:[\"Int\", \"Float\", \"Currency\", \"Percent\"].indexOf(doc.fieldtype)!==-1",
- "fieldname": "print_hide_if_no_value",
- "fieldtype": "Check",
- "label": "Print Hide If No Value"
+ "default": "0",
+ "depends_on": "eval:[\"Int\", \"Float\", \"Currency\", \"Percent\"].indexOf(doc.fieldtype)!==-1",
+ "fieldname": "print_hide_if_no_value",
+ "fieldtype": "Check",
+ "label": "Print Hide If No Value"
},
{
- "description": "Print Width of the field, if the field is a column in a table",
- "fieldname": "print_width",
- "fieldtype": "Data",
- "label": "Print Width",
- "print_width": "50px",
- "width": "50px"
+ "description": "Print Width of the field, if the field is a column in a table",
+ "fieldname": "print_width",
+ "fieldtype": "Data",
+ "label": "Print Width",
+ "print_width": "50px",
+ "width": "50px"
},
{
- "depends_on": "eval:cur_frm.doc.istable",
- "description": "Number of columns for a field in a Grid (Total Columns in a grid should be less than 11)",
- "fieldname": "columns",
- "fieldtype": "Int",
- "label": "Columns"
+ "depends_on": "eval:cur_frm.doc.istable",
+ "description": "Number of columns for a field in a Grid (Total Columns in a grid should be less than 11)",
+ "fieldname": "columns",
+ "fieldtype": "Int",
+ "label": "Columns"
},
{
- "fieldname": "width",
- "fieldtype": "Data",
- "in_list_view": 1,
- "label": "Width",
- "oldfieldname": "width",
- "oldfieldtype": "Data",
- "print_width": "50px",
- "width": "50px"
+ "fieldname": "width",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Width",
+ "oldfieldname": "width",
+ "oldfieldtype": "Data",
+ "print_width": "50px",
+ "width": "50px"
},
{
- "default": "0",
- "fieldname": "is_custom_field",
- "fieldtype": "Check",
- "hidden": 1,
- "label": "Is Custom Field",
- "read_only": 1
+ "default": "0",
+ "fieldname": "is_custom_field",
+ "fieldtype": "Check",
+ "hidden": 1,
+ "label": "Is Custom Field",
+ "read_only": 1
},
{
- "default": "0",
- "fieldname": "allow_in_quick_entry",
- "fieldtype": "Check",
- "label": "Allow in Quick Entry"
+ "default": "0",
+ "fieldname": "allow_in_quick_entry",
+ "fieldtype": "Check",
+ "label": "Allow in Quick Entry"
},
{
- "fieldname": "property_depends_on_section",
- "fieldtype": "Section Break",
- "label": "Property Depends On"
+ "fieldname": "property_depends_on_section",
+ "fieldtype": "Section Break",
+ "label": "Property Depends On"
},
{
- "fieldname": "mandatory_depends_on",
- "fieldtype": "Code",
- "label": "Mandatory Depends On",
- "options": "JS"
+ "fieldname": "mandatory_depends_on",
+ "fieldtype": "Code",
+ "label": "Mandatory Depends On",
+ "options": "JS"
},
{
- "fieldname": "column_break_33",
- "fieldtype": "Column Break"
+ "fieldname": "column_break_33",
+ "fieldtype": "Column Break"
},
{
- "fieldname": "read_only_depends_on",
- "fieldtype": "Code",
- "label": "Read Only Depends On",
- "options": "JS"
+ "fieldname": "read_only_depends_on",
+ "fieldtype": "Code",
+ "label": "Read Only Depends On",
+ "options": "JS"
},
{
- "default": "0",
- "fieldname": "in_preview",
- "fieldtype": "Check",
- "label": "In Preview"
+ "default": "0",
+ "fieldname": "in_preview",
+ "fieldtype": "Check",
+ "label": "In Preview"
},
{
- "default": "1",
- "depends_on": "eval:doc.fieldtype === \"Duration\";",
- "fieldname": "show_seconds",
- "fieldtype": "Check",
- "label": "Show Seconds",
- "show_days": 1,
- "show_seconds": 1
+ "default": "1",
+ "depends_on": "eval:doc.fieldtype === \"Duration\";",
+ "fieldname": "show_seconds",
+ "fieldtype": "Check",
+ "label": "Show Seconds",
+ "show_days": 1,
+ "show_seconds": 1
},
{
- "default": "1",
- "depends_on": "eval:doc.fieldtype === \"Duration\";",
- "fieldname": "show_days",
- "fieldtype": "Check",
- "label": "Show Days",
- "show_days": 1,
- "show_seconds": 1
+ "default": "1",
+ "depends_on": "eval:doc.fieldtype === \"Duration\";",
+ "fieldname": "show_days",
+ "fieldtype": "Check",
+ "label": "Show Days",
+ "show_days": 1,
+ "show_seconds": 1
+ },
+ {
+ "default": "0",
+ "depends_on": "eval:doc.fieldtype=='Section Break'",
+ "fieldname": "hide_border",
+ "fieldtype": "Check",
+ "label": "Hide Border"
}
],
"idx": 1,
"istable": 1,
"links": [],
- "modified": "2020-05-14 23:45:46.810869",
+ "modified": "2020-05-15 23:45:46.810869",
"modified_by": "Administrator",
"module": "Custom",
"name": "Customize Form Field",
diff --git a/frappe/custom/doctype/package_document_type/__init__.py b/frappe/custom/doctype/package_document_type/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/frappe/custom/doctype/package_document_type/package_document_type.json b/frappe/custom/doctype/package_document_type/package_document_type.json
new file mode 100644
index 0000000000..6d011bd4e4
--- /dev/null
+++ b/frappe/custom/doctype/package_document_type/package_document_type.json
@@ -0,0 +1,65 @@
+{
+ "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
new file mode 100644
index 0000000000..6e166eecbd
--- /dev/null
+++ b/frappe/custom/doctype/package_document_type/package_document_type.py
@@ -0,0 +1,10 @@
+# -*- 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
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/frappe/custom/doctype/package_publish_target/package_publish_target.json b/frappe/custom/doctype/package_publish_target/package_publish_target.json
new file mode 100644
index 0000000000..baeb7cb8bc
--- /dev/null
+++ b/frappe/custom/doctype/package_publish_target/package_publish_target.json
@@ -0,0 +1,47 @@
+{
+ "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_target/package_publish_target.py b/frappe/custom/doctype/package_publish_target/package_publish_target.py
new file mode 100644
index 0000000000..34eee02562
--- /dev/null
+++ b/frappe/custom/doctype/package_publish_target/package_publish_target.py
@@ -0,0 +1,10 @@
+# -*- 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 PackagePublishTarget(Document):
+ pass
diff --git a/frappe/custom/doctype/package_publish_tool/__init__.py b/frappe/custom/doctype/package_publish_tool/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/frappe/custom/doctype/package_publish_tool/package_publish_tool.js b/frappe/custom/doctype/package_publish_tool/package_publish_tool.js
new file mode 100644
index 0000000000..a0190a8d8c
--- /dev/null
+++ b/frappe/custom/doctype/package_publish_tool/package_publish_tool.js
@@ -0,0 +1,159 @@
+// 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
new file mode 100644
index 0000000000..0f85ae0348
--- /dev/null
+++ b/frappe/custom/doctype/package_publish_tool/package_publish_tool.json
@@ -0,0 +1,84 @@
+{
+ "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
new file mode 100644
index 0000000000..a01dd0ba47
--- /dev/null
+++ b/frappe/custom/doctype/package_publish_tool/package_publish_tool.py
@@ -0,0 +1,177 @@
+# -*- 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."""
+ 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
new file mode 100644
index 0000000000..8332240543
--- /dev/null
+++ b/frappe/custom/doctype/package_publish_tool/test_package_publish_tool.py
@@ -0,0 +1,10 @@
+# -*- 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/database/mariadb/framework_mariadb.sql b/frappe/database/mariadb/framework_mariadb.sql
index 46940cc846..bd93069a3f 100644
--- a/frappe/database/mariadb/framework_mariadb.sql
+++ b/frappe/database/mariadb/framework_mariadb.sql
@@ -63,6 +63,7 @@ CREATE TABLE `tabDocField` (
`precision` varchar(255) DEFAULT NULL,
`length` int(11) NOT NULL DEFAULT 0,
`translatable` int(1) NOT NULL DEFAULT 0,
+ `hide_border` int(1) NOT NULL DEFAULT 0,
PRIMARY KEY (`name`),
KEY `parent` (`parent`),
KEY `label` (`label`),
diff --git a/frappe/database/postgres/framework_postgres.sql b/frappe/database/postgres/framework_postgres.sql
index 26760dbcc9..76309e7347 100644
--- a/frappe/database/postgres/framework_postgres.sql
+++ b/frappe/database/postgres/framework_postgres.sql
@@ -63,6 +63,7 @@ CREATE TABLE "tabDocField" (
"precision" varchar(255) DEFAULT NULL,
"length" bigint NOT NULL DEFAULT 0,
"translatable" smallint NOT NULL DEFAULT 0,
+ "hide_border" smallint NOT NULL DEFAULT 0,
PRIMARY KEY ("name")
) ;
diff --git a/frappe/desk/desktop.py b/frappe/desk/desktop.py
index ddcfb670d4..512b3f2890 100644
--- a/frappe/desk/desktop.py
+++ b/frappe/desk/desktop.py
@@ -127,6 +127,8 @@ class Workspace:
return name in self.allowed_reports
if item_type == "help":
return True
+ if item_type == "dashboard":
+ return True
return False
@@ -272,6 +274,8 @@ class Workspace:
for doc in self.onboarding_doc.get_steps():
step = doc.as_dict().copy()
step.label = _(doc.title)
+ if step.action == "Create Entry":
+ step.is_submittable = frappe.db.get_value("DocType", step.reference_document, 'is_submittable', cache=True)
steps.append(step)
return steps
diff --git a/frappe/desk/doctype/dashboard/dashboard.json b/frappe/desk/doctype/dashboard/dashboard.json
index c17bc3235c..c0e2bddcf8 100644
--- a/frappe/desk/doctype/dashboard/dashboard.json
+++ b/frappe/desk/doctype/dashboard/dashboard.json
@@ -9,6 +9,7 @@
"dashboard_name",
"is_default",
"charts",
+ "chart_options",
"cards"
],
"fields": [
@@ -33,6 +34,13 @@
"options": "Dashboard Chart Link",
"reqd": 1
},
+ {
+ "description": "Set Default Options for all charts on this Dashboard (Ex: \"colors\": [\"#d1d8dd\", \"#ff5858\"])",
+ "fieldname": "chart_options",
+ "fieldtype": "Code",
+ "label": "Chart Options",
+ "options": "JSON"
+ },
{
"fieldname": "cards",
"fieldtype": "Table",
@@ -41,7 +49,7 @@
}
],
"links": [],
- "modified": "2020-04-19 17:44:36.237163",
+ "modified": "2020-04-29 13:26:37.362482",
"modified_by": "Administrator",
"module": "Desk",
"name": "Dashboard",
diff --git a/frappe/desk/doctype/dashboard/dashboard.py b/frappe/desk/doctype/dashboard/dashboard.py
index b85e135071..af0c48d9c6 100644
--- a/frappe/desk/doctype/dashboard/dashboard.py
+++ b/frappe/desk/doctype/dashboard/dashboard.py
@@ -5,6 +5,8 @@
from __future__ import unicode_literals
from frappe.model.document import Document
import frappe
+from frappe import _
+import json
class Dashboard(Document):
def on_update(self):
@@ -13,13 +15,29 @@ class Dashboard(Document):
frappe.db.sql('''update
tabDashboard set is_default = 0 where name != %s''', self.name)
+ def validate(self):
+ self.validate_custom_options()
+
+ def validate_custom_options(self):
+ if self.chart_options:
+ try:
+ json.loads(self.chart_options)
+ except ValueError as error:
+ frappe.throw(_("Invalid json added in the custom options: {0}").format(error))
+
@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)
+ chart_dict = frappe._dict()
+ chart_dict.update(chart.as_dict())
+
+ if dashboard.get('chart_options'):
+ chart_dict.custom_options = dashboard.get('chart_options')
+ permitted_charts.append(chart_dict)
+
return permitted_charts
@frappe.whitelist()
diff --git a/frappe/desk/doctype/dashboard_chart/dashboard_chart.js b/frappe/desk/doctype/dashboard_chart/dashboard_chart.js
index f8d5886b26..2ec73cff42 100644
--- a/frappe/desk/doctype/dashboard_chart/dashboard_chart.js
+++ b/frappe/desk/doctype/dashboard_chart/dashboard_chart.js
@@ -49,6 +49,7 @@ frappe.ui.form.on('Dashboard Chart', {
});
frm.set_df_property("filters_section", "hidden", 1);
+ frm.trigger('set_time_series');
frm.set_query('document_type', function() {
return {
filters: {
@@ -57,6 +58,7 @@ frappe.ui.form.on('Dashboard Chart', {
}
});
frm.trigger('update_options');
+ frm.trigger('set_heatmap_year_options');
if (frm.doc.report_name) {
frm.trigger('set_chart_report_filters');
}
@@ -70,7 +72,17 @@ frappe.ui.form.on('Dashboard Chart', {
frm.trigger("show_filters");
},
+ set_heatmap_year_options: function(frm) {
+ if (frm.doc.type == 'Heatmap') {
+ frappe.db.get_doc('System Settings').then(doc => {
+ const creation_date = doc.creation;
+ frm.set_df_property('heatmap_year', 'options', frappe.dashboard_utils.get_years_since_creation(creation_date));
+ });
+ }
+ },
+
chart_type: function(frm) {
+ frm.trigger('set_time_series');
if (frm.doc.chart_type == 'Report') {
frm.set_query('report_name', () => {
return {
@@ -80,23 +92,25 @@ frappe.ui.form.on('Dashboard Chart', {
}
});
} else {
- // set timeseries based on chart type
- if (['Count', 'Average', 'Sum'].includes(frm.doc.chart_type)) {
- frm.set_value('timeseries', 1);
- } else {
- frm.set_value('timeseries', 0);
- }
-
if (frm.doc.chart_type == 'Group By') {
- frm.set_df_property('type', 'options', ['Line', 'Bar', 'Percentage', 'Pie']);
+ frm.set_df_property('type', 'options', ['Line', 'Bar', 'Percentage', 'Pie', 'Donut']);
} else {
- frm.set_df_property('type', 'options', ['Line', 'Bar']);
+ frm.set_df_property('type', 'options', ['Line', 'Bar', 'Heatmap']);
}
frm.set_value('document_type', '');
}
},
+ set_time_series: function(frm) {
+ // set timeseries based on chart type
+ if (['Count', 'Average', 'Sum'].includes(frm.doc.chart_type)) {
+ frm.set_value('timeseries', 1);
+ } else {
+ frm.set_value('timeseries', 0);
+ }
+ },
+
document_type: function(frm) {
// update `based_on` options based on date / datetime fields
frm.set_value('source', '');
@@ -283,17 +297,7 @@ frappe.ui.form.on('Dashboard Chart', {
});
}
} else if (frm.chart_filters.length) {
- fields = frm.chart_filters.filter(f => {
- if (f.on_change && !f.reqd) {
- return false;
- }
- if (f.get_query || f.get_data) {
- f.read_only = 1;
- }
-
- return f.fieldname;
- });
-
+ fields = frm.chart_filters.filter(f => f.fieldname);
fields.map( f => {
if (filters[f.fieldname]) {
let condition = '=';
@@ -353,10 +357,10 @@ frappe.ui.form.on('Dashboard Chart', {
}
dialog.show();
+ //Set query report object so that it can be used while fetching filter values in the report
+ frappe.query_report = new frappe.views.QueryReport({'filters': dialog.fields_list});
dialog.set_values(filters);
});
},
});
-
-
diff --git a/frappe/desk/doctype/dashboard_chart/dashboard_chart.json b/frappe/desk/doctype/dashboard_chart/dashboard_chart.json
index b5201a8b1f..72f5c43316 100644
--- a/frappe/desk/doctype/dashboard_chart/dashboard_chart.json
+++ b/frappe/desk/doctype/dashboard_chart/dashboard_chart.json
@@ -23,17 +23,18 @@
"number_of_groups",
"column_break_6",
"is_public",
+ "heatmap_year",
"timespan",
"from_date",
"to_date",
"time_interval",
"timeseries",
+ "type",
"filters_section",
"filters_json",
"chart_options_section",
- "type",
- "column_break_2",
"color",
+ "column_break_2",
"custom_options",
"section_break_10",
"last_synced_on"
@@ -85,14 +86,14 @@
"fieldtype": "Column Break"
},
{
- "depends_on": "timeseries",
+ "depends_on": "eval: doc.timeseries && doc.type !== 'Heatmap'",
"fieldname": "timespan",
"fieldtype": "Select",
"label": "Timespan",
"options": "Last Year\nLast Quarter\nLast Month\nLast Week\nSelect Date Range"
},
{
- "depends_on": "timeseries",
+ "depends_on": "eval: doc.timeseries && doc.type !== 'Heatmap'",
"fieldname": "time_interval",
"fieldtype": "Select",
"label": "Time Interval",
@@ -100,7 +101,7 @@
},
{
"default": "0",
- "depends_on": "eval: ['Count', 'Sum', 'Average'].includes(doc.chart_type)",
+ "depends_on": "eval: !['Group By', 'Report'].includes(doc.chart_type)\n",
"fieldname": "timeseries",
"fieldtype": "Check",
"label": "Time Series"
@@ -123,10 +124,11 @@
"label": "Chart Options"
},
{
+ "default": "Line",
"fieldname": "type",
"fieldtype": "Select",
"label": "Type",
- "options": "Line\nBar\nPercentage\nPie\nDonut",
+ "options": "Line\nBar\nHeatmap",
"reqd": 1
},
{
@@ -134,7 +136,7 @@
"fieldtype": "Column Break"
},
{
- "depends_on": "eval:doc.chart_type !== 'Report'",
+ "depends_on": "eval: doc.chart_type !== 'Report' && doc.type !== 'Heatmap'",
"fieldname": "color",
"fieldtype": "Color",
"label": "Color"
@@ -217,7 +219,7 @@
"options": "Dashboard Chart Field"
},
{
- "description": "Ex: \"colors\": [\"#d1d8dd\", \"#ff5858\"]",
+ "description": "Ex: \"colors\": [\"#d1d8dd\", \"#ff5858\"] (the options set here will override the chart options set in the Dashboard)",
"fieldname": "custom_options",
"fieldtype": "Code",
"label": "Custom Options"
@@ -228,10 +230,16 @@
"fieldname": "is_public",
"fieldtype": "Check",
"label": "Is Public"
+ },
+ {
+ "depends_on": "eval: doc.type == 'Heatmap'",
+ "fieldname": "heatmap_year",
+ "fieldtype": "Select",
+ "label": "Year"
}
],
"links": [],
- "modified": "2020-05-01 15:22:59.119341",
+ "modified": "2020-05-01 19:45:01.669384",
"modified_by": "Administrator",
"module": "Desk",
"name": "Dashboard Chart",
@@ -275,4 +283,4 @@
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
-}
\ No newline at end of file
+}
diff --git a/frappe/desk/doctype/dashboard_chart/dashboard_chart.py b/frappe/desk/doctype/dashboard_chart/dashboard_chart.py
index 417ef2ba82..7e375e835f 100644
--- a/frappe/desk/doctype/dashboard_chart/dashboard_chart.py
+++ b/frappe/desk/doctype/dashboard_chart/dashboard_chart.py
@@ -8,7 +8,7 @@ from frappe import _
import datetime
import json
from frappe.utils.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.utils import nowdate, add_to_date, getdate, get_last_day, formatdate, get_datetime, cint
from frappe.model.naming import append_number_if_name_exists
from frappe.boot import get_allowed_reports
from frappe.model.document import Document
@@ -58,13 +58,13 @@ def has_permission(doc, ptype, user):
@frappe.whitelist()
@cache_source
def get(chart_name = None, chart = None, no_cache = None, filters = None, from_date = None,
- to_date = None, timespan = None, time_interval = None, refresh = None):
+ to_date = None, timespan = None, time_interval = None, heatmap_year=None, refresh = None):
if chart_name:
chart = frappe.get_doc('Dashboard Chart', chart_name)
else:
chart = frappe._dict(frappe.parse_json(chart))
-
+ heatmap_year = heatmap_year or chart.heatmap_year
timespan = timespan or chart.timespan
if timespan == 'Select Date Range':
@@ -87,7 +87,10 @@ def get(chart_name = None, chart = None, no_cache = None, filters = None, from_d
if chart.chart_type == 'Group By':
chart_config = get_group_by_chart_config(chart, filters)
else:
- chart_config = get_chart_config(chart, filters, timespan, timegrain, from_date, to_date)
+ if chart.type == 'Heatmap':
+ chart_config = get_heatmap_chart_config(chart, filters, heatmap_year)
+ else:
+ chart_config = get_chart_config(chart, filters, timespan, timegrain, from_date, to_date)
return chart_config
@@ -174,6 +177,41 @@ def get_chart_config(chart, filters, timespan, timegrain, from_date, to_date):
return chart_config
+def get_heatmap_chart_config(chart, filters, heatmap_year):
+ aggregate_function = get_aggregate_function(chart.chart_type)
+ value_field = chart.value_based_on or '1'
+ doctype = chart.document_type
+ datefield = chart.based_on
+ year = cint(heatmap_year) if heatmap_year else getdate(nowdate()).year
+ year_start_date = datetime.date(year, 1, 1).strftime('%Y-%m-%d')
+ next_year_start_date = datetime.date(year + 1, 1, 1).strftime('%Y-%m-%d')
+
+ filters.append([doctype, datefield, '>', "{date}".format(date=year_start_date), False])
+ filters.append([doctype, datefield, '<', "{date}".format(date=next_year_start_date), False])
+
+ if frappe.db.db_type == 'mariadb':
+ timestamp_field = 'unix_timestamp({datefield})'.format(datefield=datefield)
+ else:
+ timestamp_field = 'extract(epoch from timestamp {datefield})'.format(datefield=datefield)
+
+ data = dict(frappe.db.get_all(
+ doctype,
+ fields = [
+ timestamp_field,
+ '{aggregate_function}({value_field})'.format(aggregate_function=aggregate_function, value_field=value_field),
+ ],
+ filters = filters,
+ group_by = 'date({datefield})'.format(datefield=datefield),
+ as_list = 1,
+ order_by = '{datefield} asc'.format(datefield=datefield),
+ ignore_ifnull = True
+ ))
+
+ chart_config = {
+ 'labels': [],
+ 'dataPoints': data,
+ }
+ return chart_config
def get_group_by_chart_config(chart, filters):
@@ -397,11 +435,11 @@ class DashboardChart(Document):
def check_document_type(self):
if frappe.get_meta(self.document_type).issingle:
- frappe.throw("You cannot create a dashboard chart from single DocTypes")
+ frappe.throw(_("You cannot create a dashboard chart from single DocTypes"))
def validate_custom_options(self):
if self.custom_options:
try:
json.loads(self.custom_options)
except ValueError as error:
- frappe.throw("Invalid json added in the custom options: %s" % error)
\ No newline at end of file
+ frappe.throw(_("Invalid json added in the custom options: {0}").format(error))
diff --git a/frappe/desk/doctype/desk_page/desk_page.js b/frappe/desk/doctype/desk_page/desk_page.js
index ec8eaaa60b..503859eb61 100644
--- a/frappe/desk/doctype/desk_page/desk_page.js
+++ b/frappe/desk/doctype/desk_page/desk_page.js
@@ -12,7 +12,7 @@ frappe.ui.form.on('Desk Page', {
frm.set_df_property("extends", "read_only", true);
}
- if (frm.doc.for_user || frm.doc.is_standard) {
+ if (frm.doc.for_user || (frm.doc.is_standard && !frappe.boot.developer_mode)) {
frm.trigger('disable_form');
}
},
diff --git a/frappe/desk/doctype/desk_shortcut/desk_shortcut.json b/frappe/desk/doctype/desk_shortcut/desk_shortcut.json
index 550ea609c8..f3fd546a77 100644
--- a/frappe/desk/doctype/desk_shortcut/desk_shortcut.json
+++ b/frappe/desk/doctype/desk_shortcut/desk_shortcut.json
@@ -23,7 +23,7 @@
"fieldtype": "Select",
"in_list_view": 1,
"label": "Type",
- "options": "DocType\nReport\nPage",
+ "options": "DocType\nReport\nPage\nDashboard",
"reqd": 1
},
{
@@ -88,7 +88,7 @@
],
"istable": 1,
"links": [],
- "modified": "2020-05-13 19:26:34.229669",
+ "modified": "2020-05-14 16:02:15.420993",
"modified_by": "Administrator",
"module": "Desk",
"name": "Desk Shortcut",
diff --git a/frappe/desk/doctype/onboarding_step/onboarding_step.json b/frappe/desk/doctype/onboarding_step/onboarding_step.json
index 3bc9d5f286..37d1d63dbe 100644
--- a/frappe/desk/doctype/onboarding_step/onboarding_step.json
+++ b/frappe/desk/doctype/onboarding_step/onboarding_step.json
@@ -15,11 +15,16 @@
"action",
"column_break_7",
"reference_document",
+ "show_full_form",
"is_single",
"reference_report",
"report_reference_doctype",
"report_type",
"report_description",
+ "path",
+ "callback_title",
+ "callback_message",
+ "validate_action",
"field",
"value_to_validate",
"video_url"
@@ -58,7 +63,7 @@
"fieldname": "action",
"fieldtype": "Select",
"label": "Action",
- "options": "Create Entry\nUpdate Settings\nShow Form Tour\nView Report\nWatch Video",
+ "options": "Create Entry\nUpdate Settings\nShow Form Tour\nView Report\nGo to Page\nWatch Video",
"reqd": 1
},
{
@@ -70,6 +75,7 @@
"fieldname": "reference_document",
"fieldtype": "Link",
"label": "Reference Document",
+ "mandatory_depends_on": "eval:doc.action == \"Create Entry\" || doc.action == \"Update Settings\" || doc.action == \"Create Entry\" || doc.action == \"Show Form Tour\"",
"options": "DocType"
},
{
@@ -84,7 +90,8 @@
"depends_on": "eval:doc.action == \"Watch Video\"",
"fieldname": "video_url",
"fieldtype": "Data",
- "label": "Video URL"
+ "label": "Video URL",
+ "mandatory_depends_on": "eval:doc.action == \"Watch Video\""
},
{
"depends_on": "eval:doc.action == \"View Report\"",
@@ -102,17 +109,19 @@
"label": "Is Skipped"
},
{
- "depends_on": "eval:doc.action == \"Update Settings\"",
+ "depends_on": "eval:doc.action == \"Update Settings\" && doc.validate_action",
"fieldname": "field",
"fieldtype": "Select",
- "label": "Field"
+ "label": "Field",
+ "mandatory_depends_on": "eval:doc.action == \"Update Settings\" && doc.validate_action"
},
{
- "depends_on": "eval:doc.action == \"Update Settings\"",
+ "depends_on": "eval:doc.action == \"Update Settings\" && doc.validate_action",
"description": "Use % for any non empty value.",
"fieldname": "value_to_validate",
"fieldtype": "Data",
- "label": "Value to Validate"
+ "label": "Value to Validate",
+ "mandatory_depends_on": "eval:doc.action == \"Update Settings\" && doc.validate_action"
},
{
"depends_on": "eval:doc.action == \"View Report\"",
@@ -136,10 +145,46 @@
"fieldname": "is_single",
"fieldtype": "Check",
"label": "Is Single"
+ },
+ {
+ "depends_on": "eval:doc.action == \"Go to Page\"",
+ "description": "Example: #Tree/Account",
+ "fieldname": "path",
+ "fieldtype": "Data",
+ "label": "Path",
+ "mandatory_depends_on": "eval:doc.action == \"Go to Page\""
+ },
+ {
+ "depends_on": "eval:doc.action == \"Go to Page\"",
+ "fieldname": "callback_title",
+ "fieldtype": "Data",
+ "label": "Callback Title"
+ },
+ {
+ "depends_on": "eval:doc.action == \"Go to Page\"",
+ "description": "This will be shown in a modal after routing",
+ "fieldname": "callback_message",
+ "fieldtype": "Small Text",
+ "label": "Callback Message"
+ },
+ {
+ "default": "1",
+ "depends_on": "eval:doc.action == \"Update Settings\"",
+ "fieldname": "validate_action",
+ "fieldtype": "Check",
+ "label": "Validate Field"
+ },
+ {
+ "default": "0",
+ "depends_on": "eval:doc.action == \"Create Entry\"",
+ "description": "Show full form instead of a quick entry modal",
+ "fieldname": "show_full_form",
+ "fieldtype": "Check",
+ "label": "Show Full Form?"
}
],
"links": [],
- "modified": "2020-05-11 13:24:05.457160",
+ "modified": "2020-05-14 15:10:05.627706",
"modified_by": "Administrator",
"module": "Desk",
"name": "Onboarding Step",
diff --git a/frappe/desk/doctype/onboarding_step/onboarding_step.py b/frappe/desk/doctype/onboarding_step/onboarding_step.py
index e1cc5dfba4..8086acbb2a 100644
--- a/frappe/desk/doctype/onboarding_step/onboarding_step.py
+++ b/frappe/desk/doctype/onboarding_step/onboarding_step.py
@@ -10,3 +10,7 @@ class OnboardingStep(Document):
def before_export(self, doc):
doc.is_complete = 0
doc.is_skipped = 0
+
+ def validate(self):
+ if self.action == "Go to Page":
+ self.is_mandatory = 0
diff --git a/frappe/desk/notifications.py b/frappe/desk/notifications.py
index 109dd25f4f..4a1302788b 100644
--- a/frappe/desk/notifications.py
+++ b/frappe/desk/notifications.py
@@ -212,7 +212,10 @@ def get_notification_config():
def get_filters_for(doctype):
'''get open filters for doctype'''
config = get_notification_config()
- return config.get("for_doctype").get(doctype, {})
+ doctype_config = config.get("for_doctype").get(doctype, {})
+ filters = doctype_config if not isinstance(doctype_config, string_types) else None
+
+ return filters
@frappe.whitelist()
@frappe.read_only()
diff --git a/frappe/desk/page/setup_wizard/install_fixtures.py b/frappe/desk/page/setup_wizard/install_fixtures.py
index 6917ef0426..60e1f3242a 100644
--- a/frappe/desk/page/setup_wizard/install_fixtures.py
+++ b/frappe/desk/page/setup_wizard/install_fixtures.py
@@ -14,6 +14,7 @@ def install():
update_global_search_doctypes()
setup_email_linking()
sync_dashboards()
+ add_unsubscribe()
@frappe.whitelist()
def update_genders():
@@ -37,3 +38,15 @@ def setup_email_linking():
"email_id": "email_linking@example.com",
})
doc.insert(ignore_permissions=True, ignore_if_duplicate=True)
+
+def add_unsubscribe():
+ email_unsubscribe = [
+ {"email": "admin@example.com", "global_unsubscribe": 1},
+ {"email": "guest@example.com", "global_unsubscribe": 1}
+ ]
+
+ for unsubscribe in email_unsubscribe:
+ if not frappe.get_all("Email Unsubscribe", filters=unsubscribe):
+ doc = frappe.new_doc("Email Unsubscribe")
+ doc.update(unsubscribe)
+ doc.insert(ignore_permissions=True)
diff --git a/frappe/desk/page/user_profile/user_profile.js b/frappe/desk/page/user_profile/user_profile.js
index ff1e906cff..c43ff27ba3 100644
--- a/frappe/desk/page/user_profile/user_profile.js
+++ b/frappe/desk/page/user_profile/user_profile.js
@@ -108,21 +108,6 @@ class UserProfile {
});
}
- get_years_since_creation() {
- //Get years since user account created
- this.user_creation = frappe.boot.user.creation;
- let creation_year = this.get_year(this.user_creation);
- let current_year = this.get_year(frappe.datetime.now_date());
- let years_list = [];
- for (var year = current_year; year >= creation_year; year--) {
- years_list.push(year);
- }
- return years_list;
- }
-
- get_year(date_str) {
- return date_str.substring(0, date_str.indexOf('-'));
- }
render_line_chart() {
this.line_chart_filters = [['Energy Point Log', 'user', '=', this.user_id, false]];
@@ -246,8 +231,8 @@ class UserProfile {
create_heatmap_chart_filters() {
let filters = [
{
- label: this.get_year(frappe.datetime.now_date()),
- options: this.get_years_since_creation(),
+ label: frappe.dashboard_utils.get_year(frappe.datetime.now_date()),
+ options: frappe.dashboard_utils.get_years_since_creation(frappe.boot.user.creation),
action: (selected_item) => {
this.update_heatmap_data(frappe.datetime.obj_to_str(selected_item));
}
diff --git a/frappe/integrations/frappe_providers/__init__.py b/frappe/integrations/frappe_providers/__init__.py
new file mode 100644
index 0000000000..0b689478d2
--- /dev/null
+++ b/frappe/integrations/frappe_providers/__init__.py
@@ -0,0 +1,14 @@
+# imports - standard imports
+import sys
+
+# imports - module imports
+from frappe.integrations.frappe_providers.frappecloud import frappecloud_migrator
+
+
+def migrate_to(local_site, frappe_provider):
+ if frappe_provider in ("frappe.cloud", "frappecloud.com"):
+ frappe_provider = "frappecloud.com"
+ return frappecloud_migrator(local_site, frappe_provider)
+ else:
+ print("{} is not supported yet".format(frappe_provider))
+ sys.exit(1)
diff --git a/frappe/integrations/frappe_providers/frappecloud.py b/frappe/integrations/frappe_providers/frappecloud.py
new file mode 100644
index 0000000000..4f33c990f9
--- /dev/null
+++ b/frappe/integrations/frappe_providers/frappecloud.py
@@ -0,0 +1,268 @@
+# imports - standard imports
+import getpass
+import json
+import re
+import sys
+
+# imports - third party imports
+import click
+from html2text import html2text
+import requests
+
+# imports - module imports
+import frappe
+import frappe.utils.backups
+from frappe.utils import get_installed_apps_info
+from frappe.utils.commands import render_table, add_line_after
+
+
+def get_new_site_options():
+ site_options_sc = session.post(options_url)
+
+ if site_options_sc.ok:
+ site_options = site_options_sc.json()["message"]
+ return site_options
+ else:
+ print("Couldn't retrive New site information: {}".format(site_options_sc.status_code))
+
+
+def is_valid_subdomain(subdomain):
+ if len(subdomain) < 5:
+ print("Subdomain too short. Use 5 or more characters")
+ return False
+ matched = re.match("^[a-z0-9][a-z0-9-]*[a-z0-9]$", subdomain)
+ if matched:
+ return True
+ print("Subdomain contains invalid characters. Use lowercase characters, numbers and hyphens")
+
+
+def is_subdomain_available(subdomain):
+ res = session.post(site_exists_url, {"subdomain": subdomain})
+ if res.ok:
+ available = not res.json()["message"]
+ if not available:
+ print("Subdomain already exists! Try another one")
+
+ return available
+
+
+def render_plan_table(plans_list):
+ plans_table = []
+
+ # title row
+ visible_headers = ["name", "cpu_time_per_day"]
+ plans_table.append(["Plan", "CPU Time"])
+
+ # all rows
+ for plan in plans_list:
+ plan, cpu_time = [plan[header] for header in visible_headers]
+ plans_table.append([plan, "{} hour{}/day".format(cpu_time, "" if cpu_time < 2 else "s")])
+
+ render_table(plans_table)
+
+
+@add_line_after
+def choose_plan(plans_list):
+ print("{} plans available".format(len(plans_list)))
+ available_plans = [plan["name"] for plan in plans_list]
+ render_plan_table(plans_list)
+
+ while True:
+ input_plan = click.prompt("Select Plan").strip()
+ if input_plan in available_plans:
+ print("{} Plan selected ✅".format(input_plan))
+ return input_plan
+ else:
+ print("Invalid Selection ❌")
+
+
+@add_line_after
+def check_app_compat(available_group):
+ is_compat = True
+ incompatible_apps, filtered_apps, branch_msgs = [], [], []
+ existing_group = [(app["app_name"], app["branch"]) for app in get_installed_apps_info()]
+ print("Checking availability of existing app group")
+
+ for (app, branch) in existing_group:
+ info = [ (a["name"], a["branch"]) for a in available_group["apps"] if a["scrubbed"] == app ]
+ if info:
+ app_title, available_branch = info[0]
+
+ if branch != available_branch:
+ print("⚠️ App {}:{} => {}".format(app, branch, available_branch))
+ branch_msgs.append([app, branch, available_branch])
+ filtered_apps.append(app_title)
+ is_compat = False
+
+ else:
+ print("✅ App {}:{}".format(app, branch))
+ filtered_apps.append(app_title)
+
+ else:
+ incompatible_apps.append(app)
+ print("❌ App {}:{}".format(app, branch))
+ is_compat = False
+
+ start_msg = "\nSelecting this group will "
+ incompatible_apps = ("\n\nDrop the following apps:\n" + "\n".join(incompatible_apps)) if incompatible_apps else ""
+ branch_change = ("\n\nUpgrade the following apps:\n" + "\n".join(["{}: {} => {}".format(*x) for x in branch_msgs])) if branch_msgs else ""
+ changes = (incompatible_apps + branch_change) or "be perfect for you :)"
+ warning_message = start_msg + changes
+ print(warning_message)
+
+ return is_compat, filtered_apps
+
+
+def render_group_table(app_groups):
+ # title row
+ app_groups_table = [["#", "App Group", "Apps"]]
+
+ # all rows
+ for idx, app_group in enumerate(app_groups):
+ apps_list = ", ".join(["{}:{}".format(app["scrubbed"], app["branch"]) for app in app_group["apps"]])
+ row = [idx + 1, app_group["name"], apps_list]
+ app_groups_table.append(row)
+
+ render_table(app_groups_table)
+
+
+@add_line_after
+def filter_apps(app_groups):
+ render_group_table(app_groups)
+
+ while True:
+ app_group_index = click.prompt("Select App Group Number", type=int) - 1
+ try:
+ if app_group_index == -1:
+ raise IndexError
+ selected_group = app_groups[app_group_index]
+ except IndexError:
+ print("Invalid Selection ❌")
+ continue
+
+ is_compat, filtered_apps = check_app_compat(selected_group)
+
+ if is_compat or click.confirm("Continue anyway?"):
+ print("App Group {} selected! ✅".format(selected_group["name"]))
+ break
+
+ return selected_group["name"], filtered_apps
+
+@add_line_after
+def create_session():
+ # take user input from STDIN
+ username = click.prompt("Username").strip()
+ password = getpass.unix_getpass()
+
+ auth_credentials = {"usr": username, "pwd": password}
+
+ session = requests.Session()
+ login_sc = session.post(login_url, auth_credentials)
+
+ if login_sc.ok:
+ print("Authorization Successful! ✅")
+ session.headers.update({"X-Press-Team": username})
+ return session
+ else:
+ print("Authorization Failed with Error Code {}".format(login_sc.status_code))
+
+
+@add_line_after
+def get_subdomain(domain):
+ while True:
+ subdomain = click.prompt("Enter subdomain").strip()
+ if is_valid_subdomain(subdomain) and is_subdomain_available(subdomain):
+ print("Site Domain: {}.{}".format(subdomain, domain))
+ return subdomain
+
+
+@add_line_after
+def upload_backup(local_site):
+ # take backup
+ files_session = {}
+ print("Taking backup for site {}".format(local_site))
+ odb = frappe.utils.backups.new_backup(ignore_files=False, force=True)
+
+ # upload files
+ for x, (file_type, file_path) in enumerate([
+ ("database", odb.backup_path_db),
+ ("public", odb.backup_path_files),
+ ("private", odb.backup_path_private_files)
+ ]):
+ file_upload_response = session.post(files_url, data={}, files={
+ "file": open(file_path, "rb"),
+ "is_private": 1,
+ "folder": "Home",
+ "method": "press.api.site.upload_backup",
+ "type": file_type
+ })
+ print("Uploading files ({}/3)".format(x+1), end="\r")
+ if file_upload_response.ok:
+ files_session[file_type] = file_upload_response.json()["message"]
+ else:
+ print("Upload failed for: {}".format(file_path))
+
+ files_uploaded = { k: v["file_url"] for k, v in files_session.items() }
+ print("Uploaded backup files! ✅")
+
+ return files_uploaded
+
+
+def frappecloud_migrator(local_site, remote_site):
+ global login_url, upload_url, files_url, options_url, site_exists_url, session
+
+ login_url = "https://{}/api/method/login".format(remote_site)
+ upload_url = "https://{}/api/method/press.api.site.new".format(remote_site)
+ files_url = "https://{}/api/method/upload_file".format(remote_site)
+ options_url = "https://{}/api/method/press.api.site.options_for_new".format(remote_site)
+ site_exists_url = "https://{}/api/method/press.api.site.exists".format(remote_site)
+
+ print("Frappe Cloud credentials @ {}".format(remote_site))
+
+ # get credentials + auth user + start session
+ session = create_session()
+
+ if session:
+ # connect to site db
+ frappe.init(site=local_site)
+ frappe.connect()
+
+ # get new site options
+ site_options = get_new_site_options()
+
+ # set preferences from site options
+ subdomain = get_subdomain(site_options["domain"])
+ plan = choose_plan(site_options["plans"])
+
+ app_groups = site_options["groups"]
+ selected_group, filtered_apps = filter_apps(app_groups)
+ files_uploaded = upload_backup(local_site)
+
+ # push to frappe_cloud
+ payload = json.dumps({
+ "site": {
+ "apps": filtered_apps,
+ "files": files_uploaded,
+ "group": selected_group,
+ "name": subdomain,
+ "plan": plan
+ }
+ })
+
+ session.headers.update({"Content-Type": "application/json; charset=utf-8"})
+ site_creation_request = session.post(upload_url, payload)
+ frappe.destroy()
+
+ if site_creation_request.ok:
+ site_url = site_creation_request.json()["message"]
+ print("Your site {} is being migrated ✨".format(local_site))
+ print("View your site dashboard at {}/dashboard/#/sites/{}".format(remote_site, site_url))
+ print("Your site URL: {}".format(site_url))
+ else:
+ print("Request failed with error code {}".format(site_creation_request.status_code))
+ reason = html2text(site_creation_request.text)
+ print(reason)
+ sys.exit(1)
+
+ else:
+ sys.exit(1)
diff --git a/frappe/migrate.py b/frappe/migrate.py
index 094abbe099..9ec23d8ae7 100644
--- a/frappe/migrate.py
+++ b/frappe/migrate.py
@@ -5,11 +5,13 @@ from __future__ import unicode_literals
import json
import os
+import sys
import frappe
import frappe.translate
import frappe.modules.patch_handler
import frappe.model.sync
from frappe.utils.fixtures import sync_fixtures
+from frappe.utils.connections import check_connection
from frappe.utils.dashboard import sync_dashboards
from frappe.cache_manager import clear_global_cache
from frappe.desk.notifications import clear_notifications
@@ -19,6 +21,7 @@ from frappe.modules.utils import sync_customizations
from frappe.core.doctype.scheduled_job_type.scheduled_job_type import sync_jobs
from frappe.utils import global_search
+
def migrate(verbose=True, rebuild_website=False, skip_failing=False):
'''Migrate all apps to the latest version, will:
- run before migrate hooks
@@ -32,6 +35,19 @@ def migrate(verbose=True, rebuild_website=False, skip_failing=False):
- run after migrate hooks
'''
+ service_status = check_connection(redis_services=["redis_cache"])
+ if False in service_status.values():
+ for service in service_status:
+ if not service_status.get(service, True):
+ print("{} service is not running.".format(service))
+ print("""Cannot run bench migrate without the services running.
+If you are running bench in development mode, make sure that bench is running:
+
+$ bench start
+
+Otherwise, check the server logs and ensure that all the required services are running.""")
+ sys.exit(1)
+
touched_tables_file = frappe.get_site_path('touched_tables.json')
if os.path.exists(touched_tables_file):
os.remove(touched_tables_file)
@@ -67,6 +83,9 @@ def migrate(verbose=True, rebuild_website=False, skip_failing=False):
# add static pages to global search
global_search.update_global_search_for_all_web_pages()
+ # updating installed applications data
+ frappe.get_single('Installed Applications').update_versions()
+
#run after_migrate hooks
for app in frappe.get_installed_apps():
for fn in frappe.get_hooks('after_migrate', app_name=app):
diff --git a/frappe/model/meta.py b/frappe/model/meta.py
index 7bf93d1968..c8fd1a2ac2 100644
--- a/frappe/model/meta.py
+++ b/frappe/model/meta.py
@@ -443,34 +443,41 @@ class Meta(Document):
def add_doctype_links(self, data):
'''add `links` child table in standard link dashboard format'''
+ dashboard_links = []
+
if hasattr(self, 'links') and self.links:
- if not data.transactions:
- # init groups
- data.transactions = []
- data.non_standard_fieldnames = {}
+ dashboard_links.extend(self.links)
- for link in self.links:
- link.added = False
- for group in data.transactions:
- group = frappe._dict(group)
- # group found
- if link.group and group.label == link.group:
- if link.link_doctype not in group.get('items'):
- group.get('items').append(link.link_doctype)
- link.added = True
+ if frappe.get_all("Custom Link", {"document_type": self.name}):
+ dashboard_links.extend(frappe.get_doc("Custom Link", self.name).links)
- if not link.added:
- # group not found, make a new group
- data.transactions.append(dict(
- label = link.group,
- items = [link.link_doctype]
- ))
+ if not data.transactions:
+ # init groups
+ data.transactions = []
+ data.non_standard_fieldnames = {}
- if link.link_fieldname != data.fieldname:
- if data.fieldname:
- data.non_standard_fieldnames[link.link_doctype] = link.link_fieldname
- else:
- data.fieldname = link.link_fieldname
+ for link in dashboard_links:
+ link.added = False
+ for group in data.transactions:
+ group = frappe._dict(group)
+ # group found
+ if link.group and group.label == link.group:
+ if link.link_doctype not in group.get('items'):
+ group.get('items').append(link.link_doctype)
+ link.added = True
+
+ if not link.added:
+ # group not found, make a new group
+ data.transactions.append(dict(
+ label = link.group,
+ items = [link.link_doctype]
+ ))
+
+ if link.link_fieldname != data.fieldname:
+ if data.fieldname:
+ data.non_standard_fieldnames[link.link_doctype] = link.link_fieldname
+ else:
+ data.fieldname = link.link_fieldname
def get_row_template(self):
diff --git a/frappe/patches.txt b/frappe/patches.txt
index 86491b32b4..8ab9418e6c 100644
--- a/frappe/patches.txt
+++ b/frappe/patches.txt
@@ -280,3 +280,4 @@ frappe.patches.v13_0.set_read_times
frappe.patches.v13_0.remove_web_view
frappe.patches.v13_0.remove_tailwind_from_page_builder
frappe.patches.v13_0.rename_onboarding
+frappe.patches.v13_0.email_unsubscribe
diff --git a/frappe/patches/v13_0/email_unsubscribe.py b/frappe/patches/v13_0/email_unsubscribe.py
new file mode 100644
index 0000000000..69ed1be948
--- /dev/null
+++ b/frappe/patches/v13_0/email_unsubscribe.py
@@ -0,0 +1,13 @@
+import frappe
+
+def execute():
+ email_unsubscribe = [
+ {"email": "admin@example.com", "global_unsubscribe": 1},
+ {"email": "guest@example.com", "global_unsubscribe": 1}
+ ]
+
+ for unsubscribe in email_unsubscribe:
+ if not frappe.get_all("Email Unsubscribe", filters=unsubscribe):
+ doc = frappe.new_doc("Email Unsubscribe")
+ doc.update(unsubscribe)
+ doc.insert(ignore_permissions=True)
\ No newline at end of file
diff --git a/frappe/public/js/frappe/chat.js b/frappe/public/js/frappe/chat.js
index f54b9e5cbe..6b723d508c 100644
--- a/frappe/public/js/frappe/chat.js
+++ b/frappe/public/js/frappe/chat.js
@@ -2259,14 +2259,19 @@ class extends Component {
) : null,
h("div","",
h("div", { class: "panel-title" },
- h("div", { class: "cursor-pointer", onclick: () => { frappe.set_route(item.route) }},
+ h("div", { class: "cursor-pointer", onclick: () => {
+ frappe.session.user !== "Guest" ?
+ frappe.set_route(item.route) : null;
+ }},
h(frappe.Chat.Widget.MediaProfile, { ...item })
)
)
),
- h("div", { class: popper ? "col-xs-1" : "col-xs-3" },
+ h("div", { class: popper ? "col-xs-2" : "col-xs-3" },
h("div", { class: "text-right" },
-
+ frappe._.is_mobile() && h(frappe.components.Button, { class: "frappe-chat-close", onclick: props.toggle },
+ h(frappe.components.Octicon, { type: "x" })
+ )
)
)
)
diff --git a/frappe/public/js/frappe/form/form.js b/frappe/public/js/frappe/form/form.js
index 01dfbf81f9..bad7c877fc 100644
--- a/frappe/public/js/frappe/form/form.js
+++ b/frappe/public/js/frappe/form/form.js
@@ -651,6 +651,12 @@ frappe.ui.form.Form = class FrappeForm {
callback && callback();
me.script_manager.trigger("on_submit")
.then(() => resolve(me));
+ if (frappe.route_hooks.after_submit) {
+ let route_callback = frappe.route_hooks.after_submit;
+ delete frappe.route_hooks.after_submit;
+
+ route_callback(me);
+ }
}
}, btn, () => me.handle_save_fail(btn, on_error), resolve);
});
diff --git a/frappe/public/js/frappe/form/layout.js b/frappe/public/js/frappe/form/layout.js
index 5aeb29b1ed..d6106255a0 100644
--- a/frappe/public/js/frappe/form/layout.js
+++ b/frappe/public/js/frappe/form/layout.js
@@ -599,12 +599,15 @@ frappe.ui.form.Section = Class.extend({
if(this.df.cssClass) {
this.wrapper.addClass(this.df.cssClass);
}
+ if (this.df.hide_border) {
+ this.wrapper.toggleClass("hide-border", true);
+ }
}
-
// for bc
this.body = $('
').appendTo(this.wrapper);
},
+
make_head: function() {
var me = this;
if(!this.df.collapsible) {
@@ -663,9 +666,11 @@ frappe.ui.form.Section = Class.extend({
}
});
},
+
is_collapsed() {
return this.body.hasClass('hide');
},
+
has_missing_mandatory: function() {
var missing_mandatory = false;
for (var j=0, l=this.fields_list.length; j < l; j++) {
diff --git a/frappe/public/js/frappe/form/quick_entry.js b/frappe/public/js/frappe/form/quick_entry.js
index 9996389a4e..68444c8a3b 100644
--- a/frappe/public/js/frappe/form/quick_entry.js
+++ b/frappe/public/js/frappe/form/quick_entry.js
@@ -107,7 +107,7 @@ frappe.ui.form.QuickEntryForm = Class.extend({
});
this.register_primary_action();
- this.render_edit_in_full_page_link();
+ !this.force && this.render_edit_in_full_page_link();
// ctrl+enter to save
this.dialog.wrapper.keydown(function(e) {
if((e.ctrlKey || e.metaKey) && e.which==13) {
@@ -213,8 +213,15 @@ frappe.ui.form.QuickEntryForm = Class.extend({
me.dialog.doc = r.message;
if (frappe._from_link) {
frappe.ui.form.update_calling_link(me.dialog.doc);
+ } else {
+ if (me.after_insert) {
+ me.after_insert(me.dialog.doc);
+ } else {
+ me.open_form_if_not_list();
+ }
}
- cur_frm.reload_doc();
+
+ cur_frm && cur_frm.reload_doc();
}
});
},
diff --git a/frappe/public/js/frappe/utils/dashboard_utils.js b/frappe/public/js/frappe/utils/dashboard_utils.js
index a1628be34a..d1621a3e15 100644
--- a/frappe/public/js/frappe/utils/dashboard_utils.js
+++ b/frappe/public/js/frappe/utils/dashboard_utils.js
@@ -82,5 +82,21 @@ frappe.dashboard_utils = {
).then(settings => {
return settings;
});
+ },
+
+ get_years_since_creation(creation) {
+ //Get years since user account created
+ let creation_year = this.get_year(creation);
+ let current_year = this.get_year(frappe.datetime.now_date());
+ let years_list = [];
+ for (var year = current_year; year >= creation_year; year--) {
+ years_list.push(year);
+ }
+ return years_list;
+ },
+
+ get_year(date_str) {
+ return date_str.substring(0, date_str.indexOf('-'));
}
+
};
\ No newline at end of file
diff --git a/frappe/public/js/frappe/utils/pretty_date.js b/frappe/public/js/frappe/utils/pretty_date.js
index ef235ed3b1..7618d58829 100644
--- a/frappe/public/js/frappe/utils/pretty_date.js
+++ b/frappe/public/js/frappe/utils/pretty_date.js
@@ -76,6 +76,7 @@ window.comment_when = function(datetime, mini) {
+ prettyDate(datetime, mini) + '';
};
frappe.datetime.comment_when = comment_when;
+frappe.datetime.prettyDate = prettyDate;
frappe.datetime.refresh_when = function() {
if (jQuery) {
diff --git a/frappe/public/js/frappe/utils/utils.js b/frappe/public/js/frappe/utils/utils.js
index 68628276a8..3cbbecea83 100644
--- a/frappe/public/js/frappe/utils/utils.js
+++ b/frappe/public/js/frappe/utils/utils.js
@@ -250,7 +250,8 @@ Object.assign(frappe.utils, {
regExp = /^\w+$/;
break;
case "email":
- regExp = /^((([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+(\.([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+)*)|((\x22)((((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(([\x01-\x08\x0b\x0c\x0e-\x1f\x7f]|\x21|[\x23-\x5b]|[\x5d-\x7e]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(\\([\x01-\x09\x0b\x0c\x0d-\x7f]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]))))*(((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(\x22)))@((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))$/i;
+ // from https://emailregex.com/
+ regExp = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
break;
case "url":
regExp = /^(https?|s?ftp):\/\/(((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:)*@)?(((\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5]))|((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.?)(:\d*)?)(\/((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)+(\/(([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)*)*)?)?(\?((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|[\uE000-\uF8FF]|\/|\?)*)?(#((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|\/|\?)*)?$/i;
diff --git a/frappe/public/js/frappe/views/desktop/desktop.js b/frappe/public/js/frappe/views/desktop/desktop.js
index 5956a6310d..51add61f07 100644
--- a/frappe/public/js/frappe/views/desktop/desktop.js
+++ b/frappe/public/js/frappe/views/desktop/desktop.js
@@ -294,7 +294,7 @@ class DesktopPage {
make_charts() {
return frappe.dashboard_utils.get_dashboard_settings().then(settings => {
- let chart_config = settings.chart_config? JSON.parse(settings.chart_config): {};
+ let chart_config = settings.chart_config ? JSON.parse(settings.chart_config): {};
if (this.data.charts.items) {
this.data.charts.items.map(chart => {
chart.chart_settings = chart_config[chart.chart_name] || {};
@@ -306,6 +306,7 @@ class DesktopPage {
container: this.page,
type: "chart",
columns: 1,
+ hidden: Boolean(this.onboarding_widget),
options: {
allow_sorting: this.allow_customization,
allow_create: this.allow_customization,
diff --git a/frappe/public/js/frappe/views/reports/query_report.js b/frappe/public/js/frappe/views/reports/query_report.js
index 5105494862..e79e43ae02 100644
--- a/frappe/public/js/frappe/views/reports/query_report.js
+++ b/frappe/public/js/frappe/views/reports/query_report.js
@@ -330,8 +330,8 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
evaluate_depends_on_value(expression, filter_label) {
let out = null;
- let filters = this.get_filter_values();
- if (filters) {
+ let doc = this.get_filter_values();
+ if (doc) {
if (typeof expression === 'boolean') {
out = expression;
} else if (expression.substr(0, 5) == 'eval:') {
@@ -341,7 +341,7 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
frappe.throw(__(`Invalid "depends_on" expression set in filter ${filter_label}`));
}
} else {
- var value = filters[expression];
+ var value = doc[expression];
if ($.isArray(value)) {
out = !!value.length;
} else {
diff --git a/frappe/public/js/frappe/views/reports/report_utils.js b/frappe/public/js/frappe/views/reports/report_utils.js
index a8149b9134..7b1205482f 100644
--- a/frappe/public/js/frappe/views/reports/report_utils.js
+++ b/frappe/public/js/frappe/views/reports/report_utils.js
@@ -20,7 +20,7 @@ frappe.report_utils = {
return {
data: {
- labels: labels,
+ labels: labels.length? labels: null,
datasets: datasets
},
truncateLegends: 1,
diff --git a/frappe/public/js/frappe/views/reports/report_view.js b/frappe/public/js/frappe/views/reports/report_view.js
index 856061f1f0..f7513611d1 100644
--- a/frappe/public/js/frappe/views/reports/report_view.js
+++ b/frappe/public/js/frappe/views/reports/report_view.js
@@ -1020,7 +1020,7 @@ frappe.views.ReportView = class ReportView extends frappe.views.ListView {
name: __('Totals Row'),
content: totals[col.id],
format: value => {
- return frappe.format(value, col.docfield, { always_show_decimals: true });
+ return frappe.format(value, col.docfield, { always_show_decimals: true }, data[0]);
}
}
})
diff --git a/frappe/public/js/frappe/widgets/chart_widget.js b/frappe/public/js/frappe/widgets/chart_widget.js
index a50acfcd9d..e5378cf2ab 100644
--- a/frappe/public/js/frappe/widgets/chart_widget.js
+++ b/frappe/public/js/frappe/widgets/chart_widget.js
@@ -40,6 +40,10 @@ export default class ChartWidget extends Widget {
setup_container() {
this.body.empty();
+ if (this.chart_doc.type == 'Heatmap') {
+ this.setup_heatmap_container();
+ }
+
this.loading = $(
`
${__(
"Loading..."
@@ -57,9 +61,16 @@ export default class ChartWidget extends Widget {
this.chart_wrapper = $(`
`);
this.chart_wrapper.appendTo(this.body);
+ this.$heatmap_legend = null;
this.set_chart_title();
}
+ setup_heatmap_container() {
+ this.widget.addClass('heatmap-chart');
+ this.widget.removeClass('full-width').addClass('full-width');
+ this.width = 'Full';
+ }
+
set_summary() {
if (!this.$summary) {
this.$summary = $(`
`).hide();
@@ -104,54 +115,7 @@ export default class ChartWidget extends Widget {
}
render_time_series_filters() {
- let filters = [
- {
- label: this.chart_settings.timespan || this.chart_doc.timespan,
- options: [
- "Select Date Range",
- "Last Year",
- "Last Quarter",
- "Last Month",
- "Last Week"
- ],
- action: selected_item => {
- this.selected_timespan = selected_item;
-
- if (this.selected_timespan === "Select Date Range") {
- this.render_date_range_fields();
- } else {
- this.selected_from_date = null;
- this.selected_to_date = null;
- if (this.date_field_wrapper) {
- this.date_field_wrapper.hide();
-
- // Title maybe hidden becuase of date range fields
- // in half width chart
- this.title_field.show();
- this.head.css('flex-direction', "row");
- }
-
- this.save_chart_config_for_user({
- 'timespan': this.selected_timespan,
- 'from_date': null,
- 'to_date': null
-
- });
- this.fetch_and_update_chart();
- }
- }
- },
- {
- label: this.chart_settings.time_interval || this.chart_doc.time_interval,
- options: ["Yearly", "Quarterly", "Monthly", "Weekly", "Daily"],
- action: selected_item => {
- this.selected_time_interval = selected_item;
- this.save_chart_config_for_user({'time_interval': this.selected_time_interval});
- this.fetch_and_update_chart();
- }
- }
- ];
-
+ let filters = this.get_time_series_filters();
frappe.dashboard_utils.render_chart_filters(
filters,
"chart-actions",
@@ -160,12 +124,77 @@ export default class ChartWidget extends Widget {
);
}
+ get_time_series_filters() {
+ let filters;
+ if (this.chart_doc.type == 'Heatmap') {
+ filters = [{
+ label: this.chart_settings.heatmap_year || this.chart_doc.heatmap_year,
+ options: frappe.dashboard_utils.get_years_since_creation(frappe.boot.user.creation),
+ action: selected_item => {
+ this.selected_heatmap_year = selected_item;
+ this.save_chart_config_for_user({'heatmap_year': this.selected_heatmap_year});
+ this.fetch_and_update_chart();
+ }
+ }];
+ } else {
+ filters = [
+ {
+ label: this.chart_settings.timespan || this.chart_doc.timespan,
+ options: [
+ "Select Date Range",
+ "Last Year",
+ "Last Quarter",
+ "Last Month",
+ "Last Week"
+ ],
+ action: selected_item => {
+ this.selected_timespan = selected_item;
+
+ if (this.selected_timespan === "Select Date Range") {
+ this.render_date_range_fields();
+ } else {
+ this.selected_from_date = null;
+ this.selected_to_date = null;
+ if (this.date_field_wrapper) {
+ this.date_field_wrapper.hide();
+
+ // Title maybe hidden becuase of date range fields
+ // in half width chart
+ this.title_field.show();
+ this.head.css('flex-direction', "row");
+ }
+
+ this.save_chart_config_for_user({
+ 'timespan': this.selected_timespan,
+ 'from_date': null,
+ 'to_date': null
+
+ });
+ this.fetch_and_update_chart();
+ }
+ }
+ },
+ {
+ label: this.chart_settings.time_interval || this.chart_doc.time_interval,
+ options: ["Yearly", "Quarterly", "Monthly", "Weekly", "Daily"],
+ action: selected_item => {
+ this.selected_time_interval = selected_item;
+ this.save_chart_config_for_user({'time_interval': this.selected_time_interval});
+ this.fetch_and_update_chart();
+ }
+ }
+ ];
+ }
+ return filters;
+ }
+
fetch_and_update_chart() {
this.args = {
timespan: this.selected_timespan || this.chart_settings.timespan,
time_interval: this.selected_time_interval || this.chart_settings.time_interval,
from_date: this.selected_from_date || this.chart_settings.from_date,
- to_date: this.selected_to_date || this.chart_settings.to_date
+ to_date: this.selected_to_date || this.chart_settings.to_date,
+ heatmap_year: this.selected_heatmap_year || this.chart_settings.heatmap_year,
};
this.fetch(this.filters, true, this.args).then(data => {
@@ -274,7 +303,7 @@ export default class ChartWidget extends Widget {
},
{
label: __("Reset Chart"),
- action: "action-list",
+ action: "action-reset",
handler: () => {
this.reset_chart();
delete this.dashboard_chart;
@@ -332,15 +361,12 @@ export default class ChartWidget extends Widget {
}
];
} else {
- fields = filters.filter(f => {
- if (f.on_change && !f.reqd) {
- return false;
- }
- if (f.get_query || f.get_data) {
- f.read_only = 1;
- }
- return f.fieldname;
- });
+ fields = filters
+ .filter(df => df.fieldname)
+ .map(df => {
+ Object.assign(df, df.dashboard_config || {});
+ return df;
+ });
}
} else {
fields = [
@@ -384,6 +410,8 @@ export default class ChartWidget extends Widget {
}
dialog.show();
+ //Set query report object so that it can be used while fetching filter values in the report
+ frappe.query_report = new frappe.views.QueryReport({'filters': dialog.fields_list});
dialog.set_values(this.filters);
}
@@ -391,6 +419,9 @@ export default class ChartWidget extends Widget {
this.save_chart_config_for_user(null, 1);
this.chart_settings = {};
this.filters = null;
+ this.selected_time_interval = null;
+ this.selected_timespan = null;
+ this.selected_heatmap_year = null;
}
save_chart_config_for_user(config, reset=0) {
@@ -458,58 +489,25 @@ export default class ChartWidget extends Widget {
time_interval: args && args.time_interval ? args.time_interval : null,
timespan: args && args.timespan ? args.timespan : null,
from_date: args && args.from_date ? args.from_date : null,
- to_date: args && args.to_date ? args.to_date : null
+ to_date: args && args.to_date ? args.to_date : null,
+ heatmap_year: args && args.heatmap_year ? args.heatmap_year : null,
};
}
return frappe.xcall(method, args);
}
render() {
- const chart_type_map = {
- Line: "line",
- Bar: "bar",
- Percentage: "percentage",
- Pie: "pie",
- Donut: "donut"
- };
-
- let colors = [];
-
- if (this.chart_doc.y_axis.length) {
- this.chart_doc.y_axis.map(field => {
- colors.push(field.color);
- });
- } else if (["Line", "Bar"].includes(this.chart_doc.type)) {
- colors = [this.chart_doc.color || []];
- }
-
- if (!this.data || !this.data.labels.length || !Object.keys(this.data).length) {
+ if (!this.data || !this.data.labels || !Object.keys(this.data).length) {
this.chart_wrapper.hide();
this.loading.hide();
- this.$summary.hide();
+ this.$summary && this.$summary.hide();
this.empty.show();
} else {
this.loading.hide();
this.empty.hide();
this.chart_wrapper.show();
- let chart_args = {
- data: this.data,
- type: chart_type_map[this.chart_doc.type],
- colors: colors,
- height: this.height,
- axisOptions: {
- xIsSeries: this.chart_doc.timeseries,
- shortenYAxisNumbers: 1
- }
- };
-
- if (this.chart_doc.custom_options) {
- let custom_options = JSON.parse(this.chart_doc.custom_options);
- for (let key in custom_options) {
- chart_args[key] = custom_options[key];
- }
- }
+ const chart_args = this.get_chart_args();
if (!this.dashboard_chart) {
this.dashboard_chart = new frappe.Chart(
@@ -519,7 +517,93 @@ export default class ChartWidget extends Widget {
} else {
this.dashboard_chart.update(this.data);
}
+
this.width == "Full" && this.summary && this.set_summary();
+ this.chart_doc.type == 'Heatmap' && this.render_heatmap_legend();
+ }
+ }
+
+ get_chart_args() {
+ let colors = this.get_chart_colors();
+
+ const chart_type_map = {
+ Line: "line",
+ Bar: "bar",
+ Percentage: "percentage",
+ Pie: "pie",
+ Donut: "donut",
+ Heatmap: "heatmap"
+ };
+
+ let chart_args = {
+ data: this.data,
+ type: chart_type_map[this.chart_doc.type],
+ colors: colors,
+ height: this.height,
+ axisOptions: {
+ xIsSeries: this.chart_doc.timeseries,
+ shortenYAxisNumbers: 1
+ }
+ };
+
+ if (this.chart_doc.type == "Heatmap") {
+ const heatmap_year = parseInt(this.selected_heatmap_year || this.chart_settings.heatmap_year || this.chart_doc.heatmap_year);
+ chart_args.data.start = new Date(`${heatmap_year}-01-01`);
+ chart_args.data.end = new Date(`${heatmap_year+1}-01-01`);
+ }
+
+ let set_options = (options) => {
+ let custom_options = JSON.parse(options);
+ for (let key in custom_options) {
+ chart_args[key] = custom_options[key];
+ }
+ };
+
+ if (this.custom_options) {
+ set_options(this.custom_options);
+ }
+
+ if (this.chart_doc.custom_options) {
+ set_options(this.chart_doc.custom_options);
+ }
+
+ return chart_args;
+ }
+
+ get_chart_colors() {
+ let colors = [];
+ if (this.chart_doc.y_axis.length) {
+ this.chart_doc.y_axis.map(field => {
+ colors.push(field.color);
+ });
+ } else if (["Line", "Bar"].includes(this.chart_doc.type)) {
+ colors = [this.chart_doc.color || "light-blue"];
+ } else if (this.chart_doc.type == "Heatmap") {
+ colors = [];
+ }
+
+ return colors;
+ }
+
+ render_heatmap_legend() {
+ if (!this.$heatmap_legend && this.widget.width() > 991) {
+ this.$heatmap_legend =
+ $(`
+
+
+
+
${__("Less")}
+
${__("More")}
+
+
+ `);
+ this.body.append(this.$heatmap_legend);
}
}
@@ -542,6 +626,10 @@ export default class ChartWidget extends Widget {
let saved_filters = this.chart_settings.filters || null;
this.filters =
saved_filters || this.filters || JSON.parse(this.chart_doc.filters_json || "[]");
+
+ if (this.chart_doc.type == 'Heatmap' && !this.chart_doc.heatmap_year) {
+ this.chart_doc.heatmap_year = frappe.dashboard_utils.get_year(frappe.datetime.now_date());
+ }
}
get_settings() {
diff --git a/frappe/public/js/frappe/widgets/number_card_widget.js b/frappe/public/js/frappe/widgets/number_card_widget.js
index cda17e08bc..77cb8a59c2 100644
--- a/frappe/public/js/frappe/widgets/number_card_widget.js
+++ b/frappe/public/js/frappe/widgets/number_card_widget.js
@@ -119,7 +119,8 @@ export default class NumberCardWidget extends Widget {
get_formatted_number() {
const based_on_df =
frappe.meta.get_docfield(this.card_doc.document_type, this.card_doc.aggregate_function_based_on);
- const shortened_number = shorten_number(this.number);
+ const default_country = frappe.sys_defaults.country;
+ const shortened_number = shorten_number(this.number, default_country);
let number_parts = shortened_number.split(' ');
const symbol = number_parts[1] || '';
diff --git a/frappe/public/js/frappe/widgets/onboarding_widget.js b/frappe/public/js/frappe/widgets/onboarding_widget.js
index e347a70036..821824a2d2 100644
--- a/frappe/public/js/frappe/widgets/onboarding_widget.js
+++ b/frappe/public/js/frappe/widgets/onboarding_widget.js
@@ -25,7 +25,7 @@ export default class OnboardingWidget extends Widget {
if (step.is_skipped) {
status = "skipped";
- icon_class = "fa-times-circle-o";
+ icon_class = "fa-check-circle-o";
}
if (step.is_complete) {
@@ -56,10 +56,17 @@ export default class OnboardingWidget extends Widget {
// Setup actions
let actions = {
"Watch Video": () => this.show_video(step),
- "Create Entry": () => this.show_quick_entry(step),
+ "Create Entry": () => {
+ if (step.show_full_form) {
+ this.create_entry(step);
+ } else {
+ this.show_quick_entry(step);
+ }
+ },
"Show Form Tour": () => this.show_form_tour(step),
"Update Settings": () => this.update_settings(step),
"View Report": () => this.open_report(step),
+ "Go to Page": () => this.go_to_page(step),
};
$step.find("#title").on("click", actions[step.action]);
@@ -68,6 +75,24 @@ export default class OnboardingWidget extends Widget {
return $step;
}
+ go_to_page(step) {
+ frappe.set_route(step.path).then(() => {
+ if (step.callback_message) {
+ let msg_dialog = frappe.msgprint({
+ message: __(step.callback_message),
+ title: __(step.callback_title),
+ primary_action: {
+ action: () => {
+ msg_dialog.hide();
+ },
+ label: () => __("Continue"),
+ },
+ wide: true,
+ });
+ }
+ });
+ }
+
open_report(step) {
let route = generate_route({
name: step.reference_report,
@@ -75,10 +100,9 @@ export default class OnboardingWidget extends Widget {
is_query_report: ["Query Report", "Script Report"].includes(
step.report_type
),
- doctype: step.report_reference_doctype
+ doctype: step.report_reference_doctype,
});
-
let current_route = frappe.get_route();
frappe.set_route(route).then(() => {
@@ -133,7 +157,7 @@ export default class OnboardingWidget extends Widget {
msg_dialog.hide();
},
label: () => __("Continue"),
- }
+ },
});
});
};
@@ -147,6 +171,7 @@ export default class OnboardingWidget extends Widget {
frappe.route_hooks = {};
frappe.route_hooks.after_load = (frm) => {
frm.scroll_to_field(step.field);
+ frm.doc.__unsaved = true;
};
frappe.route_hooks.after_save = (frm) => {
@@ -204,6 +229,44 @@ export default class OnboardingWidget extends Widget {
frappe.set_route("Form", step.reference_document);
}
+ create_entry(step) {
+ let current_route = frappe.get_route();
+
+ frappe.route_hooks = {};
+ let callback = () => {
+ frappe.msgprint({
+ message: __("You're doing great, let's take you back to the onboarding page."),
+ title: __("Good Work 🎉"),
+ primary_action: {
+ action: () => {
+ frappe.set_route(current_route).then(() => {
+ this.mark_complete(step);
+ });
+ },
+ label: __("Continue"),
+ },
+ });
+
+ frappe.msg_dialog.custom_onhide = () => {
+ this.mark_complete(step);
+ };
+ };
+
+ if (step.is_submittable) {
+ frappe.route_hooks.after_save = () => {
+ frappe.msgprint({
+ message: __("Submit this document to complete this step."),
+ title: __("Great")
+ });
+ };
+ frappe.route_hooks.after_submit = callback;
+ } else {
+ frappe.route_hooks.after_save = callback;
+ }
+
+ frappe.set_route(`Form/${step.reference_document}/New ${step.reference_document} 1`);
+ }
+
show_quick_entry(step) {
let current_route = frappe.get_route_str();
frappe.ui.form.make_quick_entry(
@@ -221,7 +284,7 @@ export default class OnboardingWidget extends Widget {
});
},
label: __("Continue"),
- }
+ },
});
frappe.msg_dialog.custom_onhide = () => {
@@ -271,8 +334,10 @@ export default class OnboardingWidget extends Widget {
update_step_status(step, status, value, callback) {
let icon_class = {
is_complete: "fa-check-circle-o",
- is_skipped: "fa-times-circle-o",
+ is_skipped: "fa-check-circle-o",
};
+ // Clear any hooks
+ frappe.route_hooks = {};
frappe
.call("frappe.desk.desktop.update_onboarding_step", {
@@ -394,4 +459,4 @@ export default class OnboardingWidget extends Widget {
});
dismiss.appendTo(this.action_area);
}
-}
\ No newline at end of file
+}
diff --git a/frappe/public/js/frappe/widgets/utils.js b/frappe/public/js/frappe/widgets/utils.js
index 0d93bb3784..c92bdc1b5f 100644
--- a/frappe/public/js/frappe/widgets/utils.js
+++ b/frappe/public/js/frappe/widgets/utils.js
@@ -24,6 +24,8 @@ function generate_route(item) {
route = "List/" + item.doctype + "/Report/" + item.name;
} else if (type === "page") {
route = item.name;
+ } else if (type === "dashboard") {
+ route = "dashboard/" + item.name;
}
route = "#" + route;
@@ -125,19 +127,44 @@ function go_to_list_with_filters(doctype, filters) {
});
}
-function shorten_number(number) {
+function shorten_number(number, country) {
+ country = country || '';
+ const number_system = get_number_system(country);
let x = Math.abs(Math.round(number));
-
- switch (true) {
- case x >= 1.0e+12:
- return Math.round(number/1.0e+12) + " T";
- case x >= 1.0e+9:
- return Math.round(number/1.0e+9) + " B";
- case x >= 1.0e+6:
- return Math.round(number/1.0e+6) + " M";
- default:
- return number.toFixed();
+ for (const map of number_system) {
+ if (x >= map.divisor) {
+ return Math.round(number/map.divisor) + ' ' + map.symbol;
+ }
}
+ return number.toFixed();
+}
+
+function get_number_system(country) {
+ let number_system_map = {
+ 'India':
+ [{
+ divisor: 1.0e+7,
+ symbol: 'Cr'
+ },
+ {
+ divisor: 1.0e+5,
+ symbol: 'Lakh'
+ }],
+ '':
+ [{
+ divisor: 1.0e+12,
+ symbol: 'T'
+ },
+ {
+ divisor: 1.0e+9,
+ symbol: 'B'
+ },
+ {
+ divisor: 1.0e+6,
+ symbol: 'M'
+ }]
+ };
+ return number_system_map[country];
}
export { generate_route, generate_grid, build_summary_item, go_to_list_with_filters, shorten_number };
\ No newline at end of file
diff --git a/frappe/public/js/frappe/widgets/widget_dialog.js b/frappe/public/js/frappe/widgets/widget_dialog.js
index 31215a40c3..5c44533b37 100644
--- a/frappe/public/js/frappe/widgets/widget_dialog.js
+++ b/frappe/public/js/frappe/widgets/widget_dialog.js
@@ -145,7 +145,7 @@ class ShortcutDialog extends WidgetDialog {
fieldname: "type",
label: "Type",
reqd: 1,
- options: "DocType\nReport\nPage",
+ options: "DocType\nReport\nPage\nDashboard",
onchange: () => {
if (this.dialog.get_value("type") == "DocType") {
this.dialog.fields_dict.link_to.get_query = () => {
diff --git a/frappe/public/js/frappe/widgets/widget_group.js b/frappe/public/js/frappe/widgets/widget_group.js
index 8c8dd02968..e82cbc6edf 100644
--- a/frappe/public/js/frappe/widgets/widget_group.js
+++ b/frappe/public/js/frappe/widgets/widget_group.js
@@ -52,6 +52,7 @@ export default class WidgetGroup {
`);
this.widget_area = widget_area;
+ if (this.hidden) this.widget_area.hide();
this.title_area = widget_area.find(".widget-group-title");
this.control_area = widget_area.find(".widget-group-control");
this.body = widget_area.find(".widget-group-body");
@@ -96,7 +97,7 @@ export default class WidgetGroup {
}
customize() {
- this.widget_area.show();
+ if (!this.hidden) this.widget_area.show();
this.widgets_list.forEach((wid) => {
wid.customize(this.options);
});
diff --git a/frappe/public/less/desktop.less b/frappe/public/less/desktop.less
index 1e64533079..eef0b29875 100644
--- a/frappe/public/less/desktop.less
+++ b/frappe/public/less/desktop.less
@@ -293,6 +293,75 @@
}
}
+ &.dashboard-widget-box.heatmap-chart {
+ min-height: 0px;
+ height: 180px;
+
+ .widget-footer {
+ display: none;
+ }
+
+ .widget-control {
+ z-index: 1;
+ }
+
+ .frappe-chart .chart-legend {
+ display: none;
+ }
+
+ .chart-loading-state {
+ height: 160px !important;
+ }
+
+ .widget-body {
+ display: flex;
+ max-height: 100%;
+ margin: auto;
+ margin-top: -15px;
+
+ .chart-container {
+ height: 100%;
+ .frappe-chart {
+ height: 100%;
+ }
+ }
+
+ .heatmap-legend {
+ display: flex;
+ margin: 45px 20px 0 20px;
+
+ .legend-colors {
+ padding-left: 1;
+ padding-left: 15px;
+ list-style: none;
+ }
+
+ li {
+ width: 10px;
+ height: 10px;
+ margin: 5px;
+ }
+
+ .legend-label {
+ color: #555b51;
+ font-size: 11px;
+ margin-left: 15px;
+ line-height: 1.6em;
+ }
+
+ @media (max-width: 991px) {
+ display: none;
+ }
+ }
+ }
+ }
+
+ @media (max-width: 768px) {
+ &.dashboard-widget-box.heatmap-chart {
+ display: none;
+ }
+ }
+
&.onboarding-widget-box {
margin-bottom: 50px;
margin-top: 10px;
diff --git a/frappe/public/less/form.less b/frappe/public/less/form.less
index 8e43b05122..df0334c14f 100644
--- a/frappe/public/less/form.less
+++ b/frappe/public/less/form.less
@@ -314,11 +314,20 @@ h6.uppercase, .h6.uppercase {
}
}
-.form-section:not(:last-child),
+.hide-border {
+ border-top: none !important;
+ padding-top: 0px;
+}
+
+.form-section:not(:first-child) {
+ border-top: 1px solid @border-color;
+}
+
.form-inner-toolbar {
border-bottom: 1px solid @border-color;
}
+
.empty-section {
display: none !important;
}
diff --git a/frappe/utils/backups.py b/frappe/utils/backups.py
index a4c2c4bb70..0a04db2c3e 100644
--- a/frappe/utils/backups.py
+++ b/frappe/utils/backups.py
@@ -103,7 +103,8 @@ class BackupGenerator:
cmd_string = """tar -cf %s %s""" % (backup_path, files_path)
err, out = frappe.utils.execute_in_shell(cmd_string)
- print('Backed up files', os.path.abspath(backup_path))
+ if verbose:
+ print('Backed up files', os.path.abspath(backup_path))
def take_dump(self):
import frappe.utils
@@ -151,7 +152,6 @@ def get_backup():
This function is executed when the user clicks on
Toos > Download Backup
"""
- #if verbose: print frappe.db.cur_db_name + " " + conf.db_password
delete_temp_backups()
odb = BackupGenerator(frappe.conf.db_name, frappe.conf.db_name,\
frappe.conf.db_password, db_host = frappe.db.host)
diff --git a/frappe/utils/commands.py b/frappe/utils/commands.py
new file mode 100644
index 0000000000..99322b50ba
--- /dev/null
+++ b/frappe/utils/commands.py
@@ -0,0 +1,42 @@
+import functools
+import requests
+from terminaltables import AsciiTable
+
+
+@functools.lru_cache(maxsize=1024)
+def get_first_party_apps():
+ """Get list of all apps under orgs: frappe. erpnext from GitHub"""
+ apps = []
+ for org in ["frappe", "erpnext"]:
+ req = requests.get(f"https://api.github.com/users/{org}/repos", {"type": "sources", "per_page": 200})
+ if req.ok:
+ apps.extend([x["name"] for x in req.json()])
+ return apps
+
+
+def render_table(data):
+ print(AsciiTable(data).table)
+
+
+def add_line_after(function):
+ """Adds an extra line to STDOUT after the execution of a function this decorates"""
+ def empty_line(*args, **kwargs):
+ result = function(*args, **kwargs)
+ print()
+ return result
+ return empty_line
+
+
+def log(message, colour=''):
+ """Coloured log outputs to STDOUT"""
+ colours = {
+ "nc": '\033[0m',
+ "blue": '\033[94m',
+ "green": '\033[92m',
+ "yellow": '\033[93m',
+ "red": '\033[91m',
+ "silver": '\033[90m'
+ }
+ colour = colours.get(colour, "")
+ end_line = '\033[0m'
+ print(colour + message + end_line)
diff --git a/frappe/utils/connections.py b/frappe/utils/connections.py
new file mode 100644
index 0000000000..6bd24d57ec
--- /dev/null
+++ b/frappe/utils/connections.py
@@ -0,0 +1,44 @@
+import socket
+
+from six.moves.urllib.parse import urlparse
+from frappe import get_conf
+
+config = get_conf()
+REDIS_KEYS = ('redis_cache', 'redis_queue', 'redis_socketio')
+
+
+def is_open(ip, port, timeout=10):
+ s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ s.settimeout(timeout)
+ try:
+ s.connect((ip, int(port)))
+ s.shutdown(socket.SHUT_RDWR)
+ return True
+ except socket.error:
+ return False
+ finally:
+ s.close()
+
+
+def check_database():
+ db_type = config.get("db_type", "mariadb")
+ db_host = config.get("db_host", "localhost")
+ db_port = config.get("db_port", 3306 if db_type == "mariadb" else 5342)
+ return {db_type: is_open(db_host, db_port)}
+
+
+def check_redis(redis_services=None):
+ services = redis_services or REDIS_KEYS
+ status = {}
+ for conn in services:
+ redis_url = urlparse(config.get(conn)).netloc
+ redis_host, redis_port = redis_url.split(":")
+ status[conn] = is_open(redis_host, redis_port)
+ return status
+
+
+def check_connection(redis_services=None):
+ service_status = {}
+ service_status.update(check_database())
+ service_status.update(check_redis(redis_services))
+ return service_status
diff --git a/frappe/utils/dashboard.py b/frappe/utils/dashboard.py
index bb835d6561..f06f9272b8 100644
--- a/frappe/utils/dashboard.py
+++ b/frappe/utils/dashboard.py
@@ -41,6 +41,7 @@ def generate_and_cache_results(args, function, cache_key, chart):
to_date = args.to_date or None,
time_interval = args.time_interval or None,
timespan = args.timespan or None,
+ heatmap_year = args.heatmap_year or None
)
except TypeError as e:
if str(e) == "'NoneType' object is not iterable":
diff --git a/frappe/utils/data.py b/frappe/utils/data.py
index 58c74a905d..9796aa3c4a 100644
--- a/frappe/utils/data.py
+++ b/frappe/utils/data.py
@@ -213,6 +213,19 @@ def get_datetime_str(datetime_obj):
datetime_obj = get_datetime(datetime_obj)
return datetime_obj.strftime(DATETIME_FORMAT)
+def get_date_str(date_obj):
+ if isinstance(date_obj, string_types):
+ date_obj = get_datetime(date_obj)
+ return date_obj.strftime(DATE_FORMAT)
+
+def get_time_str(timedelta_obj):
+ if isinstance(timedelta_obj, string_types):
+ timedelta_obj = to_timedelta(timedelta_obj)
+
+ hours, remainder = divmod(timedelta_obj.seconds, 3600)
+ minutes, seconds = divmod(remainder, 60)
+ return "{0}:{1}:{2}".format(hours, minutes, seconds)
+
def get_user_date_format():
"""Get the current user date format. The result will be cached."""
if getattr(frappe.local, "user_date_format", None) is None:
diff --git a/frappe/website/render.py b/frappe/website/render.py
index 2e448e7d5b..c1bca3f5c5 100644
--- a/frappe/website/render.py
+++ b/frappe/website/render.py
@@ -7,8 +7,10 @@ from frappe import _
import frappe.sessions
from frappe.utils import cstr
import os, mimetypes, json
+import re
import six
+from bs4 import BeautifulSoup
from six import iteritems
from werkzeug.wrappers import Response
from werkzeug.routing import Map, Rule, NotFound
@@ -128,12 +130,35 @@ def build_response(path, data, http_status_code, headers=None):
response.headers["X-Page-Name"] = path.encode("ascii", errors="xmlcharrefreplace")
response.headers["X-From-Cache"] = frappe.local.response.from_cache or False
+ add_preload_headers(response)
if headers:
for key, val in iteritems(headers):
response.headers[key] = val.encode("ascii", errors="xmlcharrefreplace")
return response
+
+def add_preload_headers(response):
+ try:
+ preload = []
+ soup = BeautifulSoup(response.data, "lxml")
+ for elem in soup.find_all('script', src=re.compile(".*")):
+ preload.append(("script", elem.get("src")))
+
+ for elem in soup.find_all('link', rel="stylesheet"):
+ preload.append(("style", elem.get("href")))
+
+ links = []
+ for type, link in preload:
+ links.append("{}>; rel=preload; as={}".format(link.lstrip("/"), type))
+
+ if links:
+ response.headers["Link"] = ",".join(links)
+ except Exception:
+ import traceback
+ traceback.print_exc()
+
+
def render_page_by_language(path):
translated_languages = frappe.get_hooks("translated_languages_for_website")
user_lang = guess_language(translated_languages)
diff --git a/frappe/website/website_theme/standard/standard.json b/frappe/website/website_theme/standard/standard.json
index 6135d51a8e..9365d5be27 100644
--- a/frappe/website/website_theme/standard/standard.json
+++ b/frappe/website/website_theme/standard/standard.json
@@ -18,4 +18,4 @@
"theme": "Standard",
"theme_scss": "$enable-shadows: false;\n$enable-gradients: false;\n$enable-rounded: true;\n\n// Bootstrap Variable Overrides\n\n\n@import \"frappe/public/scss/website\";\n\n\n\n// Custom Theme\n",
"theme_url": "/assets/css/standard_style.css"
-}
\ No newline at end of file
+}
diff --git a/requirements.txt b/requirements.txt
index f05e0f3870..431f216afa 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -59,6 +59,7 @@ semantic-version==2.8.4
six==1.14.0
sqlparse==0.2.4
stripe==2.40.0
+terminaltables==3.1.0
unittest-xml-reporting==2.5.2
urllib3==1.25.8
watchdog==0.8.0