diff --git a/frappe/desk/doctype/dashboard/dashboard.js b/frappe/desk/doctype/dashboard/dashboard.js
index 609e943995..cf06d91c4d 100644
--- a/frappe/desk/doctype/dashboard/dashboard.js
+++ b/frappe/desk/doctype/dashboard/dashboard.js
@@ -5,6 +5,20 @@ frappe.ui.form.on('Dashboard', {
refresh: function(frm) {
frm.add_custom_button(__("Show Dashboard"), () => frappe.set_route('dashboard', frm.doc.name));
+ if (!frappe.boot.developer_mode) {
+ frm.disable_form();
+ } else {
+ frm.add_custom_button(__("Export"), () => {
+ frappe.call({
+ method: 'frappe.desk.doctype.dashboard.dashboard.export_dashboard',
+ freeze: true,
+ args: {
+ doc: frm.doc
+ }
+ });
+ });
+ }
+
frm.set_query("chart", "charts", function() {
return {
filters: {
diff --git a/frappe/desk/doctype/dashboard/dashboard.json b/frappe/desk/doctype/dashboard/dashboard.json
index c0e2bddcf8..6093ac037a 100644
--- a/frappe/desk/doctype/dashboard/dashboard.json
+++ b/frappe/desk/doctype/dashboard/dashboard.json
@@ -8,6 +8,8 @@
"field_order": [
"dashboard_name",
"is_default",
+ "is_standard",
+ "module",
"charts",
"chart_options",
"cards"
@@ -35,21 +37,34 @@
"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"
+ "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",
"label": "Cards",
"options": "Number Card Link"
+ },
+ {
+ "default": "0",
+ "fieldname": "is_standard",
+ "fieldtype": "Check",
+ "label": "Is Standard",
+ "read_only": 1
+ },
+ {
+ "fieldname": "module",
+ "fieldtype": "Link",
+ "label": "Module",
+ "options": "Module Def"
}
],
"links": [],
- "modified": "2020-04-29 13:26:37.362482",
+ "modified": "2020-05-26 18:31:10.062311",
"modified_by": "Administrator",
"module": "Desk",
"name": "Dashboard",
diff --git a/frappe/desk/doctype/dashboard/dashboard.py b/frappe/desk/doctype/dashboard/dashboard.py
index af0c48d9c6..4fc0c89544 100644
--- a/frappe/desk/doctype/dashboard/dashboard.py
+++ b/frappe/desk/doctype/dashboard/dashboard.py
@@ -4,9 +4,11 @@
from __future__ import unicode_literals
from frappe.model.document import Document
+from frappe.modules.export_file import export_to_files
import frappe
from frappe import _
import json
+from frappe.utils.dashboard import create_filters_file_after_export
class Dashboard(Document):
def on_update(self):
@@ -16,6 +18,8 @@ class Dashboard(Document):
tabDashboard set is_default = 0 where name != %s''', self.name)
def validate(self):
+ if not frappe.conf.developer_mode and self.is_standard:
+ frappe.throw('Cannot edit Standard Dashboards')
self.validate_custom_options()
def validate_custom_options(self):
@@ -25,6 +29,30 @@ class Dashboard(Document):
except ValueError as error:
frappe.throw(_("Invalid json added in the custom options: {0}").format(error))
+@frappe.whitelist()
+def export_dashboard(doc):
+ doc = frappe._dict(frappe.parse_json(doc))
+ card_count = 0
+ chart_count = 0
+
+ if not doc.module:
+ frappe.msgprint(_('Please set Module'))
+
+ if frappe.conf.developer_mode and doc.module:
+ export_to_files(record_list=[['Dashboard', doc.name, doc.module + ' Dashboard']], record_module=doc.module,)
+ record_list = []
+ for chart in doc.charts:
+ record_list.append(['Dashboard Chart', chart.get('chart'), 'Dashboard Charts'])
+ chart_count+=1
+ for card in doc.cards:
+ record_list.append(['Number Card', card.get('card'), 'Number Cards'])
+ card_count+=1
+
+ export_to_files(record_list=record_list, record_module=doc.module)
+ frappe.msgprint(_('Successfully exported {chart_count} Charts and {card_count} Cards').format(chart_count=chart_count, card_count=card_count))
+
+ create_filters_file_after_export(module_name=doc.module.lower(), dashboard_name=doc.name)
+
@frappe.whitelist()
def get_permitted_charts(dashboard_name):
permitted_charts = []
diff --git a/frappe/desk/doctype/dashboard_chart/dashboard_chart.js b/frappe/desk/doctype/dashboard_chart/dashboard_chart.js
index a10d3d96f2..a24526957d 100644
--- a/frappe/desk/doctype/dashboard_chart/dashboard_chart.js
+++ b/frappe/desk/doctype/dashboard_chart/dashboard_chart.js
@@ -12,6 +12,11 @@ frappe.ui.form.on('Dashboard Chart', {
refresh: function(frm) {
frm.chart_filters = null;
+
+ if (!frappe.boot.developer_mode && frm.doc.is_standard) {
+ frm.disable_form();
+ }
+
frm.add_custom_button('Add Chart to Dashboard', () => {
const d = new frappe.ui.Dialog({
title: __('Add to Dashboard'),
@@ -240,11 +245,11 @@ frappe.ui.form.on('Dashboard Chart', {
show_filters: function(frm) {
frm.chart_filters = [];
frappe.dashboard_utils.get_filters_for_chart_type(frm.doc).then(filters => {
- if (filters) {
- frm.chart_filters = filters;
- }
+ if (filters) {
+ frm.chart_filters = filters;
+ }
- frm.trigger('render_filters_table');
+ frm.trigger('render_filters_table');
});
},
diff --git a/frappe/desk/doctype/dashboard_chart/dashboard_chart.json b/frappe/desk/doctype/dashboard_chart/dashboard_chart.json
index 4bab76337f..1e67b56574 100644
--- a/frappe/desk/doctype/dashboard_chart/dashboard_chart.json
+++ b/frappe/desk/doctype/dashboard_chart/dashboard_chart.json
@@ -22,6 +22,7 @@
"aggregate_function_based_on",
"number_of_groups",
"column_break_6",
+ "is_standard",
"is_public",
"heatmap_year",
"timespan",
@@ -33,9 +34,9 @@
"filters_section",
"filters_json",
"chart_options_section",
- "color",
- "column_break_2",
"custom_options",
+ "column_break_2",
+ "color",
"section_break_10",
"last_synced_on"
],
@@ -235,10 +236,18 @@
"fieldname": "heatmap_year",
"fieldtype": "Select",
"label": "Year"
+ },
+ {
+ "default": "0",
+ "fieldname": "is_standard",
+ "fieldtype": "Check",
+ "label": "Is Standard",
+ "show_days": 1,
+ "show_seconds": 1
}
],
"links": [],
- "modified": "2020-05-16 15:03:02.455395",
+ "modified": "2020-06-04 15:59:52.046492",
"modified_by": "Administrator",
"module": "Desk",
"name": "Dashboard Chart",
diff --git a/frappe/desk/doctype/dashboard_chart/dashboard_chart.py b/frappe/desk/doctype/dashboard_chart/dashboard_chart.py
index 4ad6943e0b..a50d51955b 100644
--- a/frappe/desk/doctype/dashboard_chart/dashboard_chart.py
+++ b/frappe/desk/doctype/dashboard_chart/dashboard_chart.py
@@ -80,7 +80,9 @@ def get(chart_name = None, chart = None, no_cache = None, filters = None, from_d
to_date = get_datetime(chart.to_date)
timegrain = time_interval or chart.time_interval
- filters = frappe.parse_json(filters) or frappe.parse_json(chart.filters_json) or []
+ filters = frappe.parse_json(filters) or frappe.parse_json(chart.filters_json)
+ if not filters:
+ filters = []
# don't include cancelled documents
filters.append([chart.document_type, 'docstatus', '<', 2, False])
@@ -349,6 +351,8 @@ class DashboardChart(Document):
frappe.cache().delete_key('chart-data:{}'.format(self.name))
def validate(self):
+ if not frappe.conf.developer_mode and self.is_standard:
+ frappe.throw('Cannot edit Standard charts')
if self.chart_type != 'Custom' and self.chart_type != 'Report':
self.check_required_field()
self.check_document_type()
diff --git a/frappe/desk/doctype/number_card/number_card.js b/frappe/desk/doctype/number_card/number_card.js
index 184fe5e6cb..56f1231760 100644
--- a/frappe/desk/doctype/number_card/number_card.js
+++ b/frappe/desk/doctype/number_card/number_card.js
@@ -3,6 +3,9 @@
frappe.ui.form.on('Number Card', {
refresh: function(frm) {
+ if (!frappe.boot.developer_mode && frm.doc.is_standard) {
+ frm.disable_form();
+ }
frm.set_df_property("filters_section", "hidden", 1);
frm.trigger('set_options');
frm.trigger('render_filters_table');
diff --git a/frappe/desk/doctype/number_card/number_card.json b/frappe/desk/doctype/number_card/number_card.json
index ec6a1e9190..f911737e8e 100644
--- a/frappe/desk/doctype/number_card/number_card.json
+++ b/frappe/desk/doctype/number_card/number_card.json
@@ -10,6 +10,7 @@
"aggregate_function_based_on",
"column_break_2",
"document_type",
+ "is_standard",
"is_public",
"stats_section",
"show_percentage_stats",
@@ -95,10 +96,17 @@
"fieldname": "stats_section",
"fieldtype": "Section Break",
"label": "Stats"
+ },
+ {
+ "default": "0",
+ "fieldname": "is_standard",
+ "fieldtype": "Check",
+ "label": "Is Standard",
+ "read_only": 1
}
],
"links": [],
- "modified": "2020-05-06 19:47:57.753574",
+ "modified": "2020-05-26 17:30:41.248436",
"modified_by": "Administrator",
"module": "Desk",
"name": "Number Card",
diff --git a/frappe/desk/doctype/number_card/number_card.py b/frappe/desk/doctype/number_card/number_card.py
index c4a427c4e0..b57913dfac 100644
--- a/frappe/desk/doctype/number_card/number_card.py
+++ b/frappe/desk/doctype/number_card/number_card.py
@@ -67,6 +67,9 @@ def get_result(doc, to_date=None):
filters = frappe.parse_json(doc.filters_json)
+ if not filters:
+ filters = []
+
if to_date:
filters.append([doc.document_type, 'creation', '<', to_date, False])
diff --git a/frappe/modules/export_file.py b/frappe/modules/export_file.py
index b904132530..4b22c82105 100644
--- a/frappe/modules/export_file.py
+++ b/frappe/modules/export_file.py
@@ -12,16 +12,17 @@ def export_doc(doc):
def export_to_files(record_list=None, record_module=None, verbose=0, create_init=None):
"""
- Export record_list to files. record_list is a list of lists ([doctype],[docname] ) ,
+ Export record_list to files. record_list is a list of lists ([doctype, docname, folder name],) ,
"""
if frappe.flags.in_import:
return
if record_list:
for record in record_list:
- write_document_file(frappe.get_doc(record[0], record[1]), record_module, create_init=create_init)
+ folder_name = record[2] if len(record) == 3 else None
+ write_document_file(frappe.get_doc(record[0], record[1]), record_module, create_init=create_init, folder_name=folder_name)
-def write_document_file(doc, record_module=None, create_init=True):
+def write_document_file(doc, record_module=None, create_init=True, folder_name=None):
newdoc = doc.as_dict(no_nulls=True)
doc.run_method("before_export", newdoc)
@@ -35,7 +36,10 @@ def write_document_file(doc, record_module=None, create_init=True):
module = record_module or get_module_name(doc)
# create folder
- folder = create_folder(module, doc.doctype, doc.name, create_init)
+ if folder_name:
+ folder = create_folder(module, folder_name, doc.name, create_init)
+ else:
+ folder = create_folder(module, doc.doctype, doc.name, create_init)
# write the data file
fname = scrub(doc.name)
diff --git a/frappe/public/js/frappe/form/form.js b/frappe/public/js/frappe/form/form.js
index ebe94b4cdb..2e41246cbd 100644
--- a/frappe/public/js/frappe/form/form.js
+++ b/frappe/public/js/frappe/form/form.js
@@ -842,6 +842,15 @@ frappe.ui.form.Form = class FrappeForm {
this.page.clear_primary_action();
}
+ disable_form() {
+ this.set_read_only();
+ this.fields
+ .forEach((field) => {
+ this.set_df_property(field.df.fieldname, "read_only", "1");
+ });
+ this.disable_save();
+ }
+
handle_save_fail(btn, on_error) {
$(btn).prop('disabled', false);
if (on_error) {
diff --git a/frappe/utils/dashboard.py b/frappe/utils/dashboard.py
index b7023427e2..55e9e89464 100644
--- a/frappe/utils/dashboard.py
+++ b/frappe/utils/dashboard.py
@@ -6,6 +6,9 @@ from frappe import _
from functools import wraps
from frappe.utils import add_to_date, cint, get_link_to_form
from frappe.modules.import_file import import_doc
+import os
+from os.path import isfile, join
+import ast
def cache_source(function):
@@ -74,6 +77,18 @@ def get_from_date_from_timespan(to_date, timespan):
return add_to_date(to_date, years=years, months=months, days=days,
as_datetime=True)
+def create_filters_file_after_export(module_name, dashboard_name):
+ dashboard_path = frappe.get_module_path(\
+ module_name,\
+ "{module_name}_dashboard".format(module_name=module_name),\
+ "{dashboard}".format(dashboard=dashboard_name)\
+ )
+ charts_path = frappe.get_module_path(module_name, "dashboard charts")
+ create_filters_file(charts_path, dashboard_path, 'dashboard_chart_filters')
+ cards_path = frappe.get_module_path(module_name, "number cards")
+ create_filters_file(cards_path, dashboard_path, 'number_card_filters')
+
+
def sync_dashboards(app=None):
"""Import, overwrite fixtures from `[app]/fixtures`"""
if not cint(frappe.db.get_single_value('System Settings', 'setup_complete')):
@@ -86,39 +101,88 @@ def sync_dashboards(app=None):
for app_name in apps:
print("Updating Dashboard for {app}".format(app=app_name))
for module_name in frappe.local.app_modules.get(app_name) or []:
- config = get_config(app_name, module_name)
- if config:
- frappe.flags.in_import = True
- try:
- make_records(config.charts, "Dashboard Chart")
- make_records(config.number_cards, "Number Card")
- make_records(config.dashboards, "Dashboard")
- except Exception as e:
- frappe.log_error(e, _("Dashboard Import Error"))
- finally:
- frappe.flags.in_import = False
+ frappe.flags.in_import = True
+ setup_dashboards_from_file(app_name, module_name)
+ frappe.flags.in_import = False
-def make_records(config, doctype):
- if not config:
- return
+def setup_dashboards_from_file(app, module):
+ dashboards_path = frappe.get_module_path(module, "{module}_dashboard".format(module=module))
+ chart_filters = {}
+ card_filters = {}
+ if os.path.isdir(dashboards_path):
+ for fname in os.listdir(dashboards_path):
+ dashboard_path = dashboards_path + '/{}'.format(fname)
+ if os.path.isdir(dashboard_path):
+ if fname == '__pycache__':
+ continue
+ # create records for all dashboards in the module
+ make_records(dashboards_path)
+ chart_filters.update(get_filters(app, module, fname, 'dashboard_chart_filters'))
+ card_filters.update(get_filters(app, module, fname, 'number_card_filters'))
+
+ charts_path = frappe.get_module_path(module, "dashboard charts")
+ cards_path = frappe.get_module_path(module, "number cards")
+ make_records(charts_path, chart_filters)
+ make_records(cards_path, card_filters)
+
+def get_filters(app, module, dashboard, filters_file):
+ module_name = '{app}.{module}.{module}_dashboard.{dashboard}.{filters_file}'.format(
+ app=app,
+ module=module,
+ dashboard=dashboard,
+ filters_file=filters_file
+ )
try:
- for item in config:
- item["doctype"] = doctype
- import_doc(item)
- frappe.db.commit()
- except frappe.DuplicateEntryError:
- pass
+ module = frappe.get_module(module_name)
+ filters = getattr(module, 'get_filters')()
+ return filters
+ except ModuleNotFoundError:
+ frappe.throw('No Dashboard filters file created')
-def get_config(app, module):
- try:
- module_dashboards = frappe.get_module('{app}.{module}.dashboard_fixtures'.format(app=app, module=module))
- if hasattr(module_dashboards, 'get_data'):
- return frappe._dict(module_dashboards.get_data())
- return None
- except ImportError:
- return None
- except Exception as e:
- print(_("Failed to import dashboard fixtures for module {module}").format(module=module))
- frappe.log_error(e, _("Dashboard Fixture Import Error"))
- return None
+def create_filters_file(doc_folder_path, dashboard_path, fname):
+ filters_dict = get_filters_dict(doc_folder_path)
+ file_path = '{dashboard_path}/{fname}.py'.format(dashboard_path=dashboard_path, fname=fname)
+
+ with open(file_path, "w") as f:
+ f.write('''import frappe\n
+def get_filters():\n\treturn\\
+''')
+ f.write(frappe.as_json(filters_dict, indent='\t'))
+
+
+def get_filters_dict(path):
+ filters_list = []
+ for fname in os.listdir(path):
+ try:
+ doc_dict = frappe.get_file_json("{path}/{fname}/{fname}.json".format(path=path, fname=fname))
+ doc_name = doc_dict['name']
+ filters = frappe.parse_json(doc_dict.get('filters_json'))
+ if isinstance(filters, list):
+ for f in filters:
+ if len(f) == 5:
+ f[4] = cint(f)
+ doc_filter = '''"{doc_name}": {filters}'''.format(doc_name=doc_name, filters=filters)
+ filters_list.append(doc_filter)
+ except FileNotFoundError:
+ frappe.log_error(message=frappe.get_traceback(), title="Dashboard Import Error")
+ pass
+
+ filters_dict = ast.literal_eval('{' + ', '.join(filters_list) + '}')
+ return filters_dict
+
+def make_records(path, filters=None):
+ for fname in os.listdir(path):
+ if os.path.isdir(join(path, fname)):
+ if fname == '__pycache__':
+ continue
+ try:
+ doc_dict = frappe.get_file_json("{path}/{fname}/{fname}.json".format(path=path, fname=fname))
+ doc_name = doc_dict['name']
+ doc_dict['is_standard'] = 1
+ if filters:
+ doc_dict['filters_json'] = frappe.as_json(filters[doc_name])
+ import_doc(doc_dict)
+ except FileNotFoundError as e:
+ frappe.log_error(message=frappe.get_traceback(), title="Dashboard Import Error")
+ pass