diff --git a/frappe/automation/doctype/assignment_rule/assignment_rule.py b/frappe/automation/doctype/assignment_rule/assignment_rule.py index 6c6ad3b68a..d729e87437 100644 --- a/frappe/automation/doctype/assignment_rule/assignment_rule.py +++ b/frappe/automation/doctype/assignment_rule/assignment_rule.py @@ -7,13 +7,14 @@ from __future__ import unicode_literals import frappe from frappe.model.document import Document from frappe.desk.form import assign_to +import frappe.cache_manager class AssignmentRule(Document): def on_update(self): # pylint: disable=no-self-use - frappe.cache().delete_value('assignment_rule') + frappe.cache_manager.clear_doctype_map('Assignment Rule', self.name) def after_rename(self): # pylint: disable=no-self-use - frappe.cache().delete_value('assignment_rule') + frappe.cache_manager.clear_doctype_map('Assignment Rule', self.name) def apply_unassign(self, doc, assignments): if (self.unassign_condition and @@ -113,14 +114,14 @@ def apply(doc, method): if frappe.flags.in_patch or frappe.flags.in_install: return - assignment_rules = frappe.cache().get_value('assignment_rule', get_assignment_rules) + assignment_rules = frappe.cache_manager.get_doctype_map('Assignment Rule', doc.doctype, dict( + document_type = doc.doctype, disabled = 0), order_by = 'priority desc') + assignment_rule_docs = [] - # build rules - if doc.doctype in assignment_rules: - # multiple auto assigns - for d in frappe.db.get_all('Assignment Rule', dict(document_type=doc.doctype, disabled = 0), order_by = 'priority desc'): - assignment_rule_docs.append(frappe.get_doc('Assignment Rule', d.name)) + # multiple auto assigns + for d in assignment_rules: + assignment_rule_docs.append(frappe.get_doc('Assignment Rule', d.name)) if not assignment_rule_docs: return diff --git a/frappe/automation/doctype/milestone/__init__.py b/frappe/automation/doctype/milestone/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/automation/doctype/milestone/milestone.js b/frappe/automation/doctype/milestone/milestone.js new file mode 100644 index 0000000000..9a1cf577ff --- /dev/null +++ b/frappe/automation/doctype/milestone/milestone.js @@ -0,0 +1,8 @@ +// Copyright (c) 2019, Frappe Technologies and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Milestone', { + // refresh: function(frm) { + + // } +}); diff --git a/frappe/automation/doctype/milestone/milestone.json b/frappe/automation/doctype/milestone/milestone.json new file mode 100644 index 0000000000..8360ce7bf4 --- /dev/null +++ b/frappe/automation/doctype/milestone/milestone.json @@ -0,0 +1,230 @@ +{ + "allow_copy": 0, + "allow_events_in_timeline": 0, + "allow_guest_to_view": 0, + "allow_import": 0, + "allow_rename": 0, + "autoname": "", + "beta": 0, + "creation": "2019-04-17 09:39:15.647817", + "custom": 0, + "docstatus": 0, + "doctype": "DocType", + "document_type": "", + "editable_grid": 0, + "engine": "InnoDB", + "fields": [ + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fetch_if_empty": 0, + "fieldname": "reference_type", + "fieldtype": "Link", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 1, + "in_standard_filter": 0, + "label": "Document Type", + "length": 0, + "no_copy": 0, + "options": "DocType", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 1, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 1, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fetch_if_empty": 0, + "fieldname": "reference_name", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 1, + "in_standard_filter": 0, + "label": "Document", + "length": 0, + "no_copy": 0, + "options": "", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 1, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fetch_if_empty": 0, + "fieldname": "track_field", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Track Field", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 1, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fetch_if_empty": 0, + "fieldname": "value", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 1, + "in_standard_filter": 0, + "label": "Value", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 1, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fetch_if_empty": 0, + "fieldname": "milestone_tracker", + "fieldtype": "Link", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Milestone Tracker", + "length": 0, + "no_copy": 0, + "options": "Milestone Tracker", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + } + ], + "has_web_view": 0, + "hide_toolbar": 0, + "idx": 0, + "in_create": 1, + "is_submittable": 0, + "issingle": 0, + "istable": 0, + "max_attachments": 0, + "modified": "2019-04-17 16:01:21.430344", + "modified_by": "Administrator", + "module": "Automation", + "name": "Milestone", + "name_case": "", + "owner": "Administrator", + "permissions": [ + { + "amend": 0, + "cancel": 0, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "if_owner": 0, + "import": 0, + "permlevel": 0, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "set_user_permissions": 0, + "share": 1, + "submit": 0, + "write": 1 + } + ], + "quick_entry": 1, + "read_only": 0, + "show_name_in_global_search": 0, + "sort_field": "modified", + "sort_order": "ASC", + "title_field": "reference_type", + "track_changes": 1, + "track_seen": 0, + "track_views": 0 +} \ No newline at end of file diff --git a/frappe/automation/doctype/milestone/milestone.py b/frappe/automation/doctype/milestone/milestone.py new file mode 100644 index 0000000000..64c073a378 --- /dev/null +++ b/frappe/automation/doctype/milestone/milestone.py @@ -0,0 +1,14 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019, 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 Milestone(Document): + pass + +def on_doctype_update(): + frappe.db.add_index("Milestone", ["reference_type", "reference_name"]) diff --git a/frappe/automation/doctype/milestone/test_milestone.py b/frappe/automation/doctype/milestone/test_milestone.py new file mode 100644 index 0000000000..75602d48db --- /dev/null +++ b/frappe/automation/doctype/milestone/test_milestone.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019, Frappe Technologies and Contributors +# See license.txt +from __future__ import unicode_literals + +#import frappe +import unittest + +class TestMilestone(unittest.TestCase): + pass diff --git a/frappe/automation/doctype/milestone_tracker/__init__.py b/frappe/automation/doctype/milestone_tracker/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/automation/doctype/milestone_tracker/milestone_tracker.js b/frappe/automation/doctype/milestone_tracker/milestone_tracker.js new file mode 100644 index 0000000000..4f61e8355e --- /dev/null +++ b/frappe/automation/doctype/milestone_tracker/milestone_tracker.js @@ -0,0 +1,30 @@ +// Copyright (c) 2019, Frappe Technologies and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Milestone Tracker', { + refresh: function(frm) { + frm.trigger('update_options'); + }, + update_options: function(frm) { + // update select options for `track_field` + let doctype = frm.doc.document_type; + let track_fields = []; + + if (doctype) { + frappe.model.with_doctype(doctype, () => { + // get all date and datetime fields + frappe.get_meta(doctype).fields.map(df => { + if (['Link', 'Select'].includes(df.fieldtype)) { + track_fields.push({label: df.label, value: df.fieldname}); + } + }); + frm.set_df_property('track_field', 'options', track_fields); + }); + } else { + // update select options + frm.set_df_property('track_field', 'options', []); + } + + }, + +}); diff --git a/frappe/automation/doctype/milestone_tracker/milestone_tracker.json b/frappe/automation/doctype/milestone_tracker/milestone_tracker.json new file mode 100644 index 0000000000..30d108e2e3 --- /dev/null +++ b/frappe/automation/doctype/milestone_tracker/milestone_tracker.json @@ -0,0 +1,161 @@ +{ + "allow_copy": 0, + "allow_events_in_timeline": 0, + "allow_guest_to_view": 0, + "allow_import": 0, + "allow_rename": 0, + "autoname": "format:{document_type}-{track_field}", + "beta": 0, + "creation": "2019-04-17 09:36:41.774774", + "custom": 0, + "description": "Track milestones for any document", + "docstatus": 0, + "doctype": "DocType", + "document_type": "", + "editable_grid": 0, + "engine": "InnoDB", + "fields": [ + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fetch_if_empty": 0, + "fieldname": "document_type", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 1, + "in_standard_filter": 0, + "label": "Document Type to Track", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 1 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fetch_if_empty": 0, + "fieldname": "track_field", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 1, + "in_standard_filter": 0, + "label": "Field to Track", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fetch_if_empty": 0, + "fieldname": "disabled", + "fieldtype": "Check", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Disabled", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + } + ], + "has_web_view": 0, + "hide_toolbar": 0, + "idx": 0, + "in_create": 0, + "is_submittable": 0, + "issingle": 0, + "istable": 0, + "max_attachments": 0, + "modified": "2019-04-17 14:48:29.510679", + "modified_by": "Administrator", + "module": "Automation", + "name": "Milestone Tracker", + "name_case": "", + "owner": "Administrator", + "permissions": [ + { + "amend": 0, + "cancel": 0, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "if_owner": 0, + "import": 0, + "permlevel": 0, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "set_user_permissions": 0, + "share": 1, + "submit": 0, + "write": 1 + } + ], + "quick_entry": 1, + "read_only": 0, + "show_name_in_global_search": 0, + "sort_field": "modified", + "sort_order": "ASC", + "track_changes": 1, + "track_seen": 0, + "track_views": 0 +} \ No newline at end of file diff --git a/frappe/automation/doctype/milestone_tracker/milestone_tracker.py b/frappe/automation/doctype/milestone_tracker/milestone_tracker.py new file mode 100644 index 0000000000..eadf6180de --- /dev/null +++ b/frappe/automation/doctype/milestone_tracker/milestone_tracker.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019, Frappe Technologies and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals + +import frappe +from frappe.model.document import Document +import frappe.cache_manager + +class MilestoneTracker(Document): + def on_update(self): + frappe.cache_manager.clear_doctype_map('Milestone Tracker', self.name) + + def on_trash(self): + frappe.cache_manager.clear_doctype_map('Milestone Tracker', self.name) + + def apply(self, doc): + before_save = doc.get_doc_before_save() + from_value = before_save and before_save.get(self.track_field) or None + if from_value != doc.get(self.track_field): + frappe.get_doc(dict( + doctype = 'Milestone', + reference_type = doc.doctype, + reference_name = doc.name, + track_field = self.track_field, + from_value = from_value, + value = doc.get(self.track_field), + milestone_tracker = self.name, + )).insert() + +def evaluate_milestone(doc, event): + for d in frappe.cache_manager.get_doctype_map('Milestone Tracker', doc.doctype, + dict(document_type = doc.doctype, disabled=0)): + frappe.get_doc('Milestone Tracker', d.name).apply(doc) diff --git a/frappe/automation/doctype/milestone_tracker/test_milestone_tracker.py b/frappe/automation/doctype/milestone_tracker/test_milestone_tracker.py new file mode 100644 index 0000000000..c9bb6b7d5f --- /dev/null +++ b/frappe/automation/doctype/milestone_tracker/test_milestone_tracker.py @@ -0,0 +1,42 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019, Frappe Technologies and Contributors +# See license.txt +from __future__ import unicode_literals + +import frappe +import unittest + +class TestMilestoneTracker(unittest.TestCase): + def test_milestone(self): + frappe.db.sql('delete from `tabMilestone Tracker`') + frappe.get_doc(dict( + doctype = 'Milestone Tracker', + document_type = 'ToDo', + track_field = 'status' + )).insert() + + todo = frappe.get_doc(dict( + doctype = 'ToDo', + description = 'test milestone' + )).insert() + + milestones = frappe.get_all('Milestone', + fields = ['track_field', 'value', 'milestone_tracker'], + filters = dict(reference_type = todo.doctype, reference_name=todo.name)) + + self.assertEqual(len(milestones), 1) + self.assertEqual(milestones[0].track_field, 'status') + self.assertEqual(milestones[0].value, 'Open') + + todo.status = 'Closed' + todo.save() + + milestones = frappe.get_all('Milestone', + fields = ['track_field', 'value', 'milestone_tracker'], + filters = dict(reference_type = todo.doctype, reference_name=todo.name), + order_by = 'modified desc') + + self.assertEqual(len(milestones), 2) + self.assertEqual(milestones[0].track_field, 'status') + self.assertEqual(milestones[0].value, 'Closed') + diff --git a/frappe/cache_manager.py b/frappe/cache_manager.py index a913f5fba9..6c9a59d375 100644 --- a/frappe/cache_manager.py +++ b/frappe/cache_manager.py @@ -3,31 +3,40 @@ from __future__ import unicode_literals -import frappe +import frappe, json import frappe.defaults from frappe.desk.notifications import (delete_notification_count_for, clear_notifications) common_default_keys = ["__default", "__global"] -def clear_user_cache(user=None): - cache = frappe.cache() +global_cache_keys = ("app_hooks", "installed_apps", + "app_modules", "module_app", "notification_config", 'system_settings', + 'scheduler_events', 'time_zone', 'webhooks', 'active_domains', + 'active_modules', 'assignment_rule') - groups = ("bootinfo", "user_recent", "roles", "user_doc", "lang", +user_cache_keys = ("bootinfo", "user_recent", "roles", "user_doc", "lang", "defaults", "user_permissions", "home_page", "linked_with", "desktop_icons", 'portal_menu_items') +doctype_cache_keys = ("meta", "form_meta", "table_columns", "last_modified", + "linked_doctypes", 'notifications', 'workflow' ,'energy_point_rule_map') + + +def clear_user_cache(user=None): + cache = frappe.cache() + # this will automatically reload the global cache # so it is important to clear this first clear_notifications(user) if user: - for name in groups: + for name in user_cache_keys: cache.hdel(name, user) cache.delete_keys("user:" + user) clear_defaults_cache(user) else: - for name in groups: + for name in user_cache_keys: cache.delete_key(name) clear_defaults_cache() clear_global_cache() @@ -37,10 +46,7 @@ def clear_global_cache(): clear_doctype_cache() clear_website_cache() - frappe.cache().delete_value(["app_hooks", "installed_apps", - "app_modules", "module_app", "notification_config", 'system_settings', - 'scheduler_events', 'time_zone', 'webhooks', 'active_domains', - 'active_modules', 'assignment_rule']) + frappe.cache().delete_value(global_cache_keys) frappe.setup_module_map() def clear_defaults_cache(user=None): @@ -63,11 +69,8 @@ def clear_doctype_cache(doctype=None): for key in ('is_table', 'doctype_modules'): cache.delete_value(key) - groups = ["meta", "form_meta", "table_columns", "last_modified", - "linked_doctypes", 'notifications', 'workflow'] - def clear_single(dt): - for name in groups: + for name in doctype_cache_keys: cache.hdel(name, dt) if doctype: @@ -84,9 +87,31 @@ def clear_doctype_cache(doctype=None): else: # clear all - for name in groups: + for name in doctype_cache_keys: cache.delete_value(name) # Clear all document's cache. To clear documents of a specific DocType document_cache should be restructured clear_document_cache() +def get_doctype_map(doctype, name, filters, order_by=None): + cache = frappe.cache() + cache_key = frappe.scrub(doctype) + '_map' + doctype_map = cache.hget(cache_key, name) + + if doctype_map: + # cached, return + items = json.loads(doctype_map) + else: + # non cached, build cache + try: + items = frappe.get_all(doctype, filters=filters, order_by = order_by) + cache.hset(cache_key, doctype, json.dumps(items)) + except frappe.db.TableMissingError: + # executed from inside patch, ignore + items = [] + + return items + +def clear_doctype_map(doctype, name): + cache_key = frappe.scrub(doctype) + '_map' + frappe.cache().hdel(cache_key, name) \ No newline at end of file diff --git a/frappe/core/doctype/version/version.py b/frappe/core/doctype/version/version.py index 28f3ea5364..284e79cf62 100644 --- a/frappe/core/doctype/version/version.py +++ b/frappe/core/doctype/version/version.py @@ -40,6 +40,9 @@ def get_diff(old, new, for_child=False): ], }''' + if not new: + return None + out = frappe._dict(changed = [], added = [], removed = [], row_changed = []) for df in new.meta.fields: if df.fieldtype in no_value_fields and df.fieldtype not in table_fields: diff --git a/frappe/database/database.py b/frappe/database/database.py index d3b44913fc..1702e81185 100644 --- a/frappe/database/database.py +++ b/frappe/database/database.py @@ -833,7 +833,7 @@ class Database(object): """Returns list of column names from given doctype.""" columns = self.get_db_table_columns('tab' + doctype) if not columns: - raise self.ProgrammingError + raise self.TableMissingError return columns def has_column(self, doctype, column): diff --git a/frappe/database/mariadb/database.py b/frappe/database/mariadb/database.py index 69a9cf5e9f..b59a8fe0eb 100644 --- a/frappe/database/mariadb/database.py +++ b/frappe/database/mariadb/database.py @@ -17,6 +17,7 @@ from frappe.database.mariadb.schema import MariaDBTable class MariaDBDatabase(Database): ProgrammingError = pymysql.err.ProgrammingError + TableMissingError = pymysql.err.ProgrammingError OperationalError = pymysql.err.OperationalError InternalError = pymysql.err.InternalError SQLError = pymysql.err.ProgrammingError diff --git a/frappe/database/postgres/database.py b/frappe/database/postgres/database.py index 4b8a4ae500..ef4b220483 100644 --- a/frappe/database/postgres/database.py +++ b/frappe/database/postgres/database.py @@ -21,6 +21,7 @@ psycopg2.extensions.register_type(DEC2FLOAT) class PostgresDatabase(Database): ProgrammingError = psycopg2.ProgrammingError + TableMissingError = psycopg2.ProgrammingError OperationalError = psycopg2.OperationalError InternalError = psycopg2.InternalError SQLError = psycopg2.ProgrammingError diff --git a/frappe/desk/doctype/dashboard_chart/dashboard_chart.js b/frappe/desk/doctype/dashboard_chart/dashboard_chart.js index 68396f4c6a..1eb64a1718 100644 --- a/frappe/desk/doctype/dashboard_chart/dashboard_chart.js +++ b/frappe/desk/doctype/dashboard_chart/dashboard_chart.js @@ -76,12 +76,17 @@ frappe.ui.form.on('Dashboard Chart', { frm.trigger('render_filters_table'); } else { if (frm.doc.chart_type==='Custom') { - frappe.xcall('frappe.desk.doctype.dashboard_chart_source.dashboard_chart_source.get_config', {name: frm.doc.source}) - .then(config => { - frappe.dom.eval(config); - frm.filters = frappe.dashboards.chart_sources[frm.doc.source].filters; - frm.trigger('render_filters_table'); - }); + if (frm.doc.source) { + frappe.xcall('frappe.desk.doctype.dashboard_chart_source.dashboard_chart_source.get_config', {name: frm.doc.source}) + .then(config => { + frappe.dom.eval(config); + frm.filters = frappe.dashboards.chart_sources[frm.doc.source].filters; + frm.trigger('render_filters_table'); + }); + } else { + frm.filters = []; + frm.trigger('render_filters_table'); + } } else { // standard filters if (frm.doc.document_type) { diff --git a/frappe/desk/form/load.py b/frappe/desk/form/load.py index 3107c97895..7dec249be1 100644 --- a/frappe/desk/form/load.py +++ b/frappe/desk/form/load.py @@ -103,10 +103,14 @@ def get_docinfo(doc=None, doctype=None, name=None): "rating": get_feedback_rating(doc.doctype, doc.name), "views": get_view_logs(doc.doctype, doc.name), "energy_point_logs": get_point_logs(doc.doctype, doc.name), - "is_document_followed": is_document_followed(doc.doctype, doc.name, frappe.session.user), - "document_follow_enabled": frappe.db.get_value("User", frappe.session.user, "document_follow_notify") + "milestones": get_milestones(doc.doctype, doc.name), + "is_document_followed": is_document_followed(doc.doctype, doc.name, frappe.session.user) } +def get_milestones(doctype, name): + return frappe.db.get_all('Milestone', fields = ['creation', 'owner', 'track_field', 'value'], + filters=dict(reference_type=doctype, reference_name=name)) + def get_attachments(dt, dn): return frappe.get_all("File", fields=["name", "file_name", "file_url", "is_private"], filters = {"attached_to_name": dn, "attached_to_doctype": dt}) diff --git a/frappe/hooks.py b/frappe/hooks.py index 9865f6f4ac..0cd68d18f7 100644 --- a/frappe/hooks.py +++ b/frappe/hooks.py @@ -118,7 +118,8 @@ doc_events = { "frappe.core.doctype.activity_log.feed.update_feed", "frappe.workflow.doctype.workflow_action.workflow_action.process_workflow_actions", "frappe.automation.doctype.assignment_rule.assignment_rule.apply", - "frappe.social.doctype.energy_point_rule.energy_point_rule.process_energy_points" + "frappe.social.doctype.energy_point_rule.energy_point_rule.process_energy_points", + "frappe.automation.doctype.milestone_tracker.milestone_tracker.evaluate_milestone" ], "after_rename": "frappe.desk.notifications.clear_doctype_notifications", "on_cancel": [ diff --git a/frappe/model/document.py b/frappe/model/document.py index c71e8e1a16..5fe1ba3bc6 100644 --- a/frappe/model/document.py +++ b/frappe/model/document.py @@ -117,6 +117,12 @@ class Document(BaseDocument): # incorrect arguments. let's not proceed. raise ValueError('Illegal arguments') + @staticmethod + def whitelist(f): + """Decorator: Whitelist method to be called remotely via REST API.""" + f.whitelisted = True + return f + def reload(self): """Reload document from database""" self.load_from_db() @@ -370,13 +376,7 @@ class Document(BaseDocument): (self.name, self.doctype, fieldname)) def get_doc_before_save(self): - if not getattr(self, '_doc_before_save', None): - try: - self._doc_before_save = frappe.get_doc(self.doctype, self.name) - except frappe.DoesNotExistError: - self._doc_before_save = None - frappe.clear_last_message() - return self._doc_before_save + return getattr(self, '_doc_before_save', None) def set_new_name(self, force=False): """Calls `frappe.naming.se_new_name` for parent and child docs.""" @@ -834,11 +834,6 @@ class Document(BaseDocument): elif alert.event=='Method' and method == alert.method: _evaluate_alert(alert) - @staticmethod - def whitelist(f): - f.whitelisted = True - return f - @whitelist.__func__ def _submit(self): """Submit the document. Sets `docstatus` = 1, then saves.""" @@ -899,11 +894,12 @@ class Document(BaseDocument): def load_doc_before_save(self): '''Save load document from db before saving''' self._doc_before_save = None - if not (self.is_new() - and (getattr(self.meta, 'track_changes', False) - or self.meta.get_set_only_once_fields() - or self.meta.get_workflow())): - self.get_doc_before_save() + if not self.is_new(): + try: + self._doc_before_save = frappe.get_doc(self.doctype, self.name) + except frappe.DoesNotExistError: + self._doc_before_save = None + frappe.clear_last_message() def run_post_save_methods(self): """Run standard methods after `INSERT` or `UPDATE`. Standard Methods are: @@ -1018,12 +1014,6 @@ class Document(BaseDocument): if not frappe.flags.in_migrate: follow_document(self.doctype, self.name, frappe.session.user) - @staticmethod - def whitelist(f): - """Decorator: Whitelist method to be called remotely via REST API.""" - f.whitelisted = True - return f - @staticmethod def hook(f): """Decorator: Make method `hookable` (i.e. extensible by another app). diff --git a/frappe/public/js/frappe/form/document_follow.js b/frappe/public/js/frappe/form/document_follow.js index aaae2091d7..1743bc254a 100644 --- a/frappe/public/js/frappe/form/document_follow.js +++ b/frappe/public/js/frappe/form/document_follow.js @@ -20,7 +20,7 @@ frappe.ui.form.DocumentFollow = class DocumentFollow { render_sidebar() { const docinfo = this.frm.get_docinfo(); - const document_follow_enabled = docinfo && docinfo.document_follow_enabled; + const document_follow_enabled = frappe.boot.user.document_follow_notify; const document_can_be_followed = frappe.get_meta(this.frm.doctype).track_changes; if (frappe.session.user === 'Administrator' || !document_follow_enabled diff --git a/frappe/public/js/frappe/form/footer/timeline.js b/frappe/public/js/frappe/form/footer/timeline.js index 5f2acd3929..4d8585b045 100644 --- a/frappe/public/js/frappe/form/footer/timeline.js +++ b/frappe/public/js/frappe/form/footer/timeline.js @@ -163,6 +163,9 @@ frappe.ui.form.Timeline = class Timeline { // append energy point logs timeline = timeline.concat(this.get_energy_point_logs()); + // append milestones + timeline = timeline.concat(this.get_milestones()); + // sort timeline .filter(a => a.content) @@ -429,7 +432,6 @@ frappe.ui.form.Timeline = class Timeline { if(c.communication_type == "Feedback"){ c.icon = "octicon octicon-comment-discussion" c.rating_icons = frappe.render_template("rating_icons", {rating: c.rating, show_label: true}) - c.color = "#f39c12" } else { c.icon = { "Email": "octicon octicon-mail", @@ -439,12 +441,12 @@ frappe.ui.form.Timeline = class Timeline { "Event": "fa fa-calendar", "Meeting": "octicon octicon-briefcase", "ToDo": "fa fa-check", - "Created": "octicon octicon-plus", "Submitted": "octicon octicon-lock", "Cancelled": "octicon octicon-x", "Assigned": "octicon octicon-person", "Assignment Completed": "octicon octicon-check", "Comment": "octicon octicon-comment-discussion", + "Milestone": "octicon octicon-milestone", "Workflow": "octicon octicon-git-branch", "Label": "octicon octicon-tag", "Attachment": "octicon octicon-cloud-upload", @@ -457,25 +459,6 @@ frappe.ui.form.Timeline = class Timeline { "Reply": "octicon octicon-mail-reply" }[c.comment_type || c.communication_medium] - c.color = { - "Email": "#3498db", - "Chat": "#3498db", - "Phone": "#3498db", - "SMS": "#3498db", - "Created": "#1abc9c", - "Submitted": "#1abc9c", - "Cancelled": "#c0392b", - "Assigned": "#f39c12", - "Assignment Completed": "#16a085", - "Comment": "#f39c12", - "Workflow": "#2c3e50", - "Label": "#2c3e50", - "Attachment": "#7f8c8d", - "Attachment Removed": "#eee", - "Relinked": "#16a085", - "Reply": "#8d99a6" - }[c.comment_type || c.communication_medium]; - c.icon_fg = { "Attachment Removed": "#333", }[c.comment_type || c.communication_medium] @@ -528,6 +511,21 @@ frappe.ui.form.Timeline = class Timeline { return energy_point_logs; } + get_milestones() { + let milestones = this.frm.get_docinfo().milestones; + milestones.map(log => { + log.color = 'dark'; + log.sender = log.owner; + log.comment_type = 'Milestone'; + log.content = __('{0} changed {1} to {2}', [ + frappe.user.full_name(log.owner).bold(), + frappe.meta.get_label(this.frm.doctype, log.track_field), + log.value.bold()]); + return log; + }); + return milestones; + } + cast_comment_as_communication(c) { c.sender = c.comment_email; c.sender_full_name = c.comment_by; diff --git a/frappe/public/js/frappe/form/footer/timeline_item.html b/frappe/public/js/frappe/form/footer/timeline_item.html index 3a79bed370..c2965dd98c 100755 --- a/frappe/public/js/frappe/form/footer/timeline_item.html +++ b/frappe/public/js/frappe/form/footer/timeline_item.html @@ -1,4 +1,5 @@ -