diff --git a/frappe/model/delete_doc.py b/frappe/model/delete_doc.py
index 33d7f8e0af..f80df04ebd 100644
--- a/frappe/model/delete_doc.py
+++ b/frappe/model/delete_doc.py
@@ -18,7 +18,8 @@ from frappe.model.naming import revert_series_if_last
from frappe.utils.global_search import delete_for_document
from frappe.desk.doctype.tag.tag import delete_tags_for_document
from frappe.exceptions import FileNotFoundError
-
+from six import string_types, integer_types
+from frappe.model.document import make_update_log
doctypes_to_skip = ("Communication", "ToDo", "DocShare", "Email Unsubscribe", "Activity Log", "File", "Version", "Document Follow", "Comment" , "View Log", "Tag Link")
@@ -120,6 +121,10 @@ def delete_doc(doctype=None, name=None, force=0, ignore_doctypes=None, for_reloa
# delete tag link entry
delete_tags_for_document(doc)
+ # update log if doctype has followers
+ if not frappe.flags.in_install:
+ make_update_log(doc, update_type = 'Delete')
+
if doc and not for_reload:
add_to_deleted_document(doc)
if not frappe.flags.in_patch:
diff --git a/frappe/model/document.py b/frappe/model/document.py
index f93c366ffb..1401505f8b 100644
--- a/frappe/model/document.py
+++ b/frappe/model/document.py
@@ -255,6 +255,8 @@ class Document(BaseDocument):
if self.get("amended_from"):
self.copy_attachments_from_amended_from()
+ #flag to prevent creation of update log for create and update both, during document creation
+ self.flags.update_log_for_doc_creation = True
self.run_post_save_methods()
self.flags.in_insert = False
@@ -937,6 +939,14 @@ class Document(BaseDocument):
if (self.doctype, self.name) in frappe.flags.currently_saving:
frappe.flags.currently_saving.remove((self.doctype, self.name))
+ # make update log for doctypes having followers
+ if not frappe.flags.in_install:
+ if self.flags.update_log_for_doc_creation:
+ make_update_log(self, update_type = 'Create')
+ self.flags.create_type_update_log = False
+ else:
+ make_update_log(self, update_type = 'Update')
+
self.latest = None
def clear_cache(self):
@@ -1261,3 +1271,30 @@ def execute_action(doctype, name, action, **kwargs):
doc.add_comment('Comment', _('Action Failed') + '
' + msg)
doc.notify_update()
+
+def make_update_log(doc, update_type):
+ '''Save update info for doctypes that have followers'''
+ doctype_has_followers = check_doctype_has_followers(doc.doctype)
+ if doctype_has_followers:
+ if update_type != 'Delete':
+ data = frappe.as_json(doc)
+ else:
+ data = None
+ doc = frappe.get_doc({
+ 'doctype': 'Update Log',
+ 'update_type': update_type,
+ 'ref_doctype': doc.doctype,
+ 'docname': doc.name,
+ 'data': data
+ })
+ doc.insert(ignore_permissions = True)
+ frappe.db.commit()
+
+def check_doctype_has_followers(doctype):
+ node_configs = frappe.get_all(doctype = 'Node Configuration')
+ for node_config in node_configs:
+ config = frappe.get_doc('Node Configuration', node_config.name)
+ for entry in config.following_doctypes:
+ if doctype == entry.ref_doctype:
+ return True
+ return False
\ No newline at end of file
diff --git a/frappe/modules.txt b/frappe/modules.txt
index 6505174327..5a3e4fefe1 100644
--- a/frappe/modules.txt
+++ b/frappe/modules.txt
@@ -12,3 +12,4 @@ Data Migration
Chat
Social
Automation
+Offline
diff --git a/frappe/offline/__init__.py b/frappe/offline/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/frappe/offline/doctype/__init__.py b/frappe/offline/doctype/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/frappe/offline/doctype/change_request/__init__.py b/frappe/offline/doctype/change_request/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/frappe/offline/doctype/change_request/change_request.js b/frappe/offline/doctype/change_request/change_request.js
new file mode 100644
index 0000000000..4d4fd01060
--- /dev/null
+++ b/frappe/offline/doctype/change_request/change_request.js
@@ -0,0 +1,8 @@
+// Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('Change Request', {
+ // refresh: function(frm) {
+
+ // }
+});
diff --git a/frappe/offline/doctype/change_request/change_request.json b/frappe/offline/doctype/change_request/change_request.json
new file mode 100644
index 0000000000..a00385de3a
--- /dev/null
+++ b/frappe/offline/doctype/change_request/change_request.json
@@ -0,0 +1,65 @@
+{
+ "creation": "2019-07-30 16:10:05.784349",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "follower_node",
+ "ref_doctype",
+ "document_name",
+ "requested_change",
+ "request_status"
+ ],
+ "fields": [
+ {
+ "fieldname": "follower_node",
+ "fieldtype": "Link",
+ "label": "Follower Node",
+ "options": "Node"
+ },
+ {
+ "fieldname": "ref_doctype",
+ "fieldtype": "Data",
+ "label": "DocType"
+ },
+ {
+ "fieldname": "document_name",
+ "fieldtype": "Data",
+ "label": "Document Name"
+ },
+ {
+ "fieldname": "requested_change",
+ "fieldtype": "Code",
+ "label": "Requested Change"
+ },
+ {
+ "fieldname": "request_status",
+ "fieldtype": "Select",
+ "label": "Request Status",
+ "options": "\nApproved\nRejected\nResolve Conflict"
+ }
+ ],
+ "modified": "2019-08-02 09:48:41.263955",
+ "modified_by": "Administrator",
+ "module": "Offline",
+ "name": "Change Request",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 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/offline/doctype/change_request/change_request.py b/frappe/offline/doctype/change_request/change_request.py
new file mode 100644
index 0000000000..7daeb7d72a
--- /dev/null
+++ b/frappe/offline/doctype/change_request/change_request.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+# import frappe
+from frappe.model.document import Document
+
+class ChangeRequest(Document):
+ pass
diff --git a/frappe/offline/doctype/change_request/test_change_request.py b/frappe/offline/doctype/change_request/test_change_request.py
new file mode 100644
index 0000000000..83ae1b560f
--- /dev/null
+++ b/frappe/offline/doctype/change_request/test_change_request.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+from __future__ import unicode_literals
+
+# import frappe
+import unittest
+
+class TestChangeRequest(unittest.TestCase):
+ pass
diff --git a/frappe/offline/doctype/node/__init__.py b/frappe/offline/doctype/node/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/frappe/offline/doctype/node/node.js b/frappe/offline/doctype/node/node.js
new file mode 100644
index 0000000000..eb7622b28a
--- /dev/null
+++ b/frappe/offline/doctype/node/node.js
@@ -0,0 +1,8 @@
+// Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('Node', {
+ // refresh: function(frm) {
+
+ // }
+});
diff --git a/frappe/offline/doctype/node/node.json b/frappe/offline/doctype/node/node.json
new file mode 100644
index 0000000000..795333e146
--- /dev/null
+++ b/frappe/offline/doctype/node/node.json
@@ -0,0 +1,73 @@
+{
+ "_comments": "[]",
+ "_liked_by": "[]",
+ "autoname": "field:host_name",
+ "creation": "2019-07-30 15:10:47.993692",
+ "doctype": "DocType",
+ "engine": "InnoDB",
+ "field_order": [
+ "host_name",
+ "api_key",
+ "api_secret",
+ "allow_auto_changes",
+ "last_updated"
+ ],
+ "fields": [
+ {
+ "fieldname": "host_name",
+ "fieldtype": "Data",
+ "label": "Hostname (URL)",
+ "unique": 1
+ },
+ {
+ "default": "0",
+ "description": "Automatic changes from Follower nodes will be fetched and updated",
+ "fieldname": "allow_auto_changes",
+ "fieldtype": "Check",
+ "label": "Allow Automatic Changes"
+ },
+ {
+ "fieldname": "api_key",
+ "fieldtype": "Data",
+ "label": "API Key",
+ "read_only": 1,
+ "unique": 1
+ },
+ {
+ "fieldname": "api_secret",
+ "fieldtype": "Password",
+ "label": "API Secret",
+ "read_only": 1
+ },
+ {
+ "fieldname": "last_updated",
+ "fieldtype": "Link",
+ "label": "Last Updated",
+ "options": "Update Log",
+ "read_only": 1
+ }
+ ],
+ "modified": "2019-08-09 14:54:07.173962",
+ "modified_by": "Administrator",
+ "module": "Offline",
+ "name": "Node",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 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/offline/doctype/node/node.py b/frappe/offline/doctype/node/node.py
new file mode 100644
index 0000000000..1553a9c5a2
--- /dev/null
+++ b/frappe/offline/doctype/node/node.py
@@ -0,0 +1,13 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+import frappe
+from frappe.frappeclient import FrappeClient
+from frappe.model.document import Document
+
+class Node(Document):
+ def before_insert(self):
+ self.api_key = frappe.generate_hash(length = 15)
+ self.api_secret = frappe.generate_hash(length = 15)
diff --git a/frappe/offline/doctype/node/test_node.py b/frappe/offline/doctype/node/test_node.py
new file mode 100644
index 0000000000..d610f45b8c
--- /dev/null
+++ b/frappe/offline/doctype/node/test_node.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+from __future__ import unicode_literals
+
+# import frappe
+import unittest
+
+class TestNode(unittest.TestCase):
+ pass
diff --git a/frappe/offline/doctype/node_configuration/__init__.py b/frappe/offline/doctype/node_configuration/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/frappe/offline/doctype/node_configuration/node_configuration.js b/frappe/offline/doctype/node_configuration/node_configuration.js
new file mode 100644
index 0000000000..cf979bb791
--- /dev/null
+++ b/frappe/offline/doctype/node_configuration/node_configuration.js
@@ -0,0 +1,8 @@
+// Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('Node Configuration', {
+ // refresh: function(frm) {
+ //
+ // }
+});
diff --git a/frappe/offline/doctype/node_configuration/node_configuration.json b/frappe/offline/doctype/node_configuration/node_configuration.json
new file mode 100644
index 0000000000..fc03016e0e
--- /dev/null
+++ b/frappe/offline/doctype/node_configuration/node_configuration.json
@@ -0,0 +1,85 @@
+{
+ "autoname": "format:NODE-CONFIG-{#####}",
+ "creation": "2019-07-30 15:39:28.765991",
+ "doctype": "DocType",
+ "engine": "InnoDB",
+ "field_order": [
+ "master_node",
+ "following_doctypes",
+ "follower_node",
+ "rules_section",
+ "allow_creation",
+ "allow_update",
+ "allow_deletion"
+ ],
+ "fields": [
+ {
+ "fieldname": "follower_node",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Follower Node",
+ "options": "Node",
+ "reqd": 1
+ },
+ {
+ "fieldname": "master_node",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Master Node",
+ "options": "Node",
+ "reqd": 1
+ },
+ {
+ "fieldname": "rules_section",
+ "fieldtype": "Section Break",
+ "label": "Rules for Follower"
+ },
+ {
+ "default": "0",
+ "fieldname": "allow_creation",
+ "fieldtype": "Check",
+ "label": "Allow Creation"
+ },
+ {
+ "default": "0",
+ "fieldname": "allow_update",
+ "fieldtype": "Check",
+ "label": "Allow Update"
+ },
+ {
+ "default": "0",
+ "fieldname": "allow_deletion",
+ "fieldtype": "Check",
+ "label": "Allow Deletion"
+ },
+ {
+ "fieldname": "following_doctypes",
+ "fieldtype": "Table",
+ "label": "Following DocTypes",
+ "options": "Node Configuration DocType",
+ "reqd": 1
+ }
+ ],
+ "modified": "2019-08-11 22:35:18.561804",
+ "modified_by": "Administrator",
+ "module": "Offline",
+ "name": "Node Configuration",
+ "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/offline/doctype/node_configuration/node_configuration.py b/frappe/offline/doctype/node_configuration/node_configuration.py
new file mode 100644
index 0000000000..371c121f7d
--- /dev/null
+++ b/frappe/offline/doctype/node_configuration/node_configuration.py
@@ -0,0 +1,67 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+import frappe
+from frappe import _
+import json
+from frappe.model.document import Document
+from frappe.frappeclient import FrappeClient
+
+class NodeConfiguration(Document):
+ def before_insert(self):
+ config_exists = frappe.db.get_all(
+ doctype = 'Node Configuration',
+ filters = [
+ ['master_node', '=', self.master_node],
+ ['follower_node', '=', self.follower_node]
+ ]
+ )
+ if config_exists:
+ frappe.throw(_('Node Configuration already exists'))
+
+@frappe.whitelist()
+def sync_master_data():
+ '''Sync master data to all follower nodes, triggered when update log is created'''
+ current_node = frappe.utils.get_url()
+ port = frappe.conf.http_port or frappe.conf.webserver_port
+ current_node = current_node + ':' + str(port)
+
+ node_configurations = frappe.get_all(
+ doctype = 'Node Configuration',
+ filters = {'master_node': current_node},
+ group_by = 'follower_node'
+ )
+
+ for node_config in node_configurations:
+ config = frappe.get_doc('Node Configuration', node_config.name)
+
+ last_updated = frappe.db.get_value('Node', config.follower_node, 'last_updated')
+ last_update_synced = frappe.db.get_value('Update Log', last_updated, 'creation')
+
+ doctypes = []
+ for entry in config.following_doctypes:
+ doctypes.append(entry.ref_doctype)
+
+ updates_to_be_synced = frappe.get_all(
+ doctype = 'Update Log',
+ filters = [['creation', '>', last_update_synced], ['ref_doctype', 'in', doctypes]],
+ fields = ['update_type', 'ref_doctype', 'docname', 'data', 'name'],
+ order_by = 'creation'
+ )
+
+ if updates_to_be_synced != []:
+ client = FrappeClient(config.follower_node, 'Administrator', 'root')
+
+ for doc in updates_to_be_synced:
+ if doc.update_type == 'Create':
+ client.insert(json.loads(doc.data))
+ elif doc.update_type == 'Update':
+ client.update(json.loads(doc.data))
+ elif doc.update == 'Delete':
+ client.delete(doc.ref_doctype, doc.docname)
+
+ #set the last update for node
+ frappe.db.set_value('Node', config.follower_node, 'last_updated', updates_to_be_synced[-1].get('name'))
+
\ No newline at end of file
diff --git a/frappe/offline/doctype/node_configuration/test_node_configuration.py b/frappe/offline/doctype/node_configuration/test_node_configuration.py
new file mode 100644
index 0000000000..6943f98164
--- /dev/null
+++ b/frappe/offline/doctype/node_configuration/test_node_configuration.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+from __future__ import unicode_literals
+
+import frappe
+import unittest
+
+class TestNodeConfiguration(unittest.TestCase):
+ pass
diff --git a/frappe/offline/doctype/node_configuration_doctype/__init__.py b/frappe/offline/doctype/node_configuration_doctype/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/frappe/offline/doctype/node_configuration_doctype/node_configuration_doctype.js b/frappe/offline/doctype/node_configuration_doctype/node_configuration_doctype.js
new file mode 100644
index 0000000000..de57bc763d
--- /dev/null
+++ b/frappe/offline/doctype/node_configuration_doctype/node_configuration_doctype.js
@@ -0,0 +1,8 @@
+// Copyright (c) 2019, Frappe Technologies and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('Node Configuration DocType', {
+ // refresh: function(frm) {
+
+ // }
+});
diff --git a/frappe/offline/doctype/node_configuration_doctype/node_configuration_doctype.json b/frappe/offline/doctype/node_configuration_doctype/node_configuration_doctype.json
new file mode 100644
index 0000000000..0d89391d60
--- /dev/null
+++ b/frappe/offline/doctype/node_configuration_doctype/node_configuration_doctype.json
@@ -0,0 +1,30 @@
+{
+ "creation": "2019-08-09 16:19:58.344278",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "ref_doctype"
+ ],
+ "fields": [
+ {
+ "fieldname": "ref_doctype",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Document Type",
+ "options": "DocType",
+ "reqd": 1
+ }
+ ],
+ "istable": 1,
+ "modified": "2019-08-09 16:21:12.082995",
+ "modified_by": "Administrator",
+ "module": "Offline",
+ "name": "Node Configuration DocType",
+ "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/offline/doctype/node_configuration_doctype/node_configuration_doctype.py b/frappe/offline/doctype/node_configuration_doctype/node_configuration_doctype.py
new file mode 100644
index 0000000000..c039115a4e
--- /dev/null
+++ b/frappe/offline/doctype/node_configuration_doctype/node_configuration_doctype.py
@@ -0,0 +1,10 @@
+# -*- 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 NodeConfigurationDocType(Document):
+ pass
diff --git a/frappe/offline/doctype/node_configuration_doctype/test_node_configuration_doctype.py b/frappe/offline/doctype/node_configuration_doctype/test_node_configuration_doctype.py
new file mode 100644
index 0000000000..3a05db962f
--- /dev/null
+++ b/frappe/offline/doctype/node_configuration_doctype/test_node_configuration_doctype.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 TestNodeConfigurationDocType(unittest.TestCase):
+ pass
diff --git a/frappe/offline/doctype/update_log/__init__.py b/frappe/offline/doctype/update_log/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/frappe/offline/doctype/update_log/test_update_log.py b/frappe/offline/doctype/update_log/test_update_log.py
new file mode 100644
index 0000000000..f9388e939f
--- /dev/null
+++ b/frappe/offline/doctype/update_log/test_update_log.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+from __future__ import unicode_literals
+
+# import frappe
+import unittest
+
+class TestUpdateLog(unittest.TestCase):
+ pass
diff --git a/frappe/offline/doctype/update_log/update_log.js b/frappe/offline/doctype/update_log/update_log.js
new file mode 100644
index 0000000000..41bd9e2762
--- /dev/null
+++ b/frappe/offline/doctype/update_log/update_log.js
@@ -0,0 +1,8 @@
+// Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('Update Log', {
+ // refresh: function(frm) {
+
+ // }
+});
diff --git a/frappe/offline/doctype/update_log/update_log.json b/frappe/offline/doctype/update_log/update_log.json
new file mode 100644
index 0000000000..b532f54101
--- /dev/null
+++ b/frappe/offline/doctype/update_log/update_log.json
@@ -0,0 +1,65 @@
+{
+ "creation": "2019-07-30 15:31:26.352527",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "update_type",
+ "ref_doctype",
+ "docname",
+ "data"
+ ],
+ "fields": [
+ {
+ "fieldname": "update_type",
+ "fieldtype": "Select",
+ "label": "Update Type",
+ "options": "Create\nUpdate\nDelete",
+ "read_only": 1
+ },
+ {
+ "fieldname": "ref_doctype",
+ "fieldtype": "Link",
+ "label": "DocType",
+ "options": "DocType",
+ "read_only": 1
+ },
+ {
+ "fieldname": "docname",
+ "fieldtype": "Data",
+ "label": "Document Name",
+ "options": "ref_doctype",
+ "read_only": 1
+ },
+ {
+ "fieldname": "data",
+ "fieldtype": "Code",
+ "label": "Data",
+ "read_only": 1
+ }
+ ],
+ "in_create": 1,
+ "modified": "2019-08-04 23:28:11.548894",
+ "modified_by": "Administrator",
+ "module": "Offline",
+ "name": "Update Log",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 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/offline/doctype/update_log/update_log.py b/frappe/offline/doctype/update_log/update_log.py
new file mode 100644
index 0000000000..5bb2bd3061
--- /dev/null
+++ b/frappe/offline/doctype/update_log/update_log.py
@@ -0,0 +1,16 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+import frappe, json
+from frappe.model.document import Document
+from frappe.utils.background_jobs import get_jobs
+
+class UpdateLog(Document):
+ def validate(self):
+ '''Sync master data to followers whenever update log is generated'''
+ enqueued_method = 'frappe.offline.doctype.node_configuration.node_configuration.sync_master_data'
+ jobs = get_jobs()
+ if not jobs or enqueued_method not in jobs[frappe.local.site]:
+ frappe.enqueue(enqueued_method, queue = 'default')