Converting Task to a Tree structure (#11117)

* added support for tree view

* nestedset added to handle tree based structure

* treeview ui added

* removed is_group dependency

* added validation while editing a group-task

* codacy fix

* BOM like filter added

* Added ui-test for treeview-task
diff --git a/erpnext/config/projects.py b/erpnext/config/projects.py
index a8514b2..b97e097 100644
--- a/erpnext/config/projects.py
+++ b/erpnext/config/projects.py
@@ -15,6 +15,7 @@
 				{
 					"type": "doctype",
 					"name": "Task",
+					"route": "Tree/Task",
 					"description": _("Project activity / task."),
 				},
 				{
diff --git a/erpnext/projects/doctype/task/task.js b/erpnext/projects/doctype/task/task.js
index df38cfe..b8f324a 100644
--- a/erpnext/projects/doctype/task/task.js
+++ b/erpnext/projects/doctype/task/task.js
@@ -19,38 +19,47 @@
 	},
 
 	refresh: function(frm) {
-		var doc = frm.doc;
-		if(doc.__islocal) {
-			if(!frm.doc.exp_end_date) {
-				frm.set_value("exp_end_date", frappe.datetime.add_days(new Date(), 7));
+		frm.fields_dict['parent_task'].get_query = function() {
+			return {
+				filters: {
+					"is_group": 1,
+				}
 			}
 		}
-
-		if(!doc.__islocal) {
-			if(frappe.model.can_read("Timesheet")) {
-				frm.add_custom_button(__("Timesheet"), function() {
-					frappe.route_options = {"project": doc.project, "task": doc.name}
-					frappe.set_route("List", "Timesheet");
-				}, __("View"), true);
-			}
-			if(frappe.model.can_read("Expense Claim")) {
-				frm.add_custom_button(__("Expense Claims"), function() {
-					frappe.route_options = {"project": doc.project, "task": doc.name}
-					frappe.set_route("List", "Expense Claim");
-				}, __("View"), true);
+		if(!frm.is_group){
+			var doc = frm.doc;
+			if(doc.__islocal) {
+				if(!frm.doc.exp_end_date) {
+					frm.set_value("exp_end_date", frappe.datetime.add_days(new Date(), 7));
+				}
 			}
 
-			if(frm.perm[0].write) {
-				if(frm.doc.status!=="Closed" && frm.doc.status!=="Cancelled") {
-					frm.add_custom_button(__("Close"), function() {
-						frm.set_value("status", "Closed");
-						frm.save();
-					});
-				} else {
-					frm.add_custom_button(__("Reopen"), function() {
-						frm.set_value("status", "Open");
-						frm.save();
-					});
+			if(!doc.__islocal) {
+				if(frappe.model.can_read("Timesheet")) {
+					frm.add_custom_button(__("Timesheet"), function() {
+						frappe.route_options = {"project": doc.project, "task": doc.name}
+						frappe.set_route("List", "Timesheet");
+					}, __("View"), true);
+				}
+				if(frappe.model.can_read("Expense Claim")) {
+					frm.add_custom_button(__("Expense Claims"), function() {
+						frappe.route_options = {"project": doc.project, "task": doc.name}
+						frappe.set_route("List", "Expense Claim");
+					}, __("View"), true);
+				}
+
+				if(frm.perm[0].write) {
+					if(frm.doc.status!=="Closed" && frm.doc.status!=="Cancelled") {
+						frm.add_custom_button(__("Close"), function() {
+							frm.set_value("status", "Closed");
+							frm.save();
+						});
+					} else {
+						frm.add_custom_button(__("Reopen"), function() {
+							frm.set_value("status", "Open");
+							frm.save();
+						});
+					}
 				}
 			}
 		}
@@ -71,6 +80,21 @@
 		}
 	},
 
+	is_group: function(frm) {
+		frappe.call({
+			method:"erpnext.projects.doctype.task.task.check_if_child_exists",
+			args: {
+				name: frm.doc.name
+			},
+			callback: function(r){
+				if(r.message){
+					frappe.msgprint(__('Cannot convert it to non-group. Child Tasks exist.'));
+					frm.reload_doc();
+				}
+			}
+		})
+	},
+
 	validate: function(frm) {
 		frm.doc.project && frappe.model.remove_from_locals("Project",
 			frm.doc.project);
diff --git a/erpnext/projects/doctype/task/task.json b/erpnext/projects/doctype/task/task.json
index e4ab5a7..41950a3 100644
--- a/erpnext/projects/doctype/task/task.json
+++ b/erpnext/projects/doctype/task/task.json
@@ -3,7 +3,7 @@
  "allow_guest_to_view": 0, 
  "allow_import": 1, 
  "allow_rename": 1, 
- "autoname": "TASK.#####", 
+ "autoname": "field:subject", 
  "beta": 0, 
  "creation": "2013-01-29 19:25:50", 
  "custom": 0, 
@@ -30,9 +30,8 @@
    "label": "Subject", 
    "length": 0, 
    "no_copy": 0, 
-   "oldfieldname": "subject", 
-   "oldfieldtype": "Data", 
    "permlevel": 0, 
+   "precision": "", 
    "print_hide": 0, 
    "print_hide_if_no_value": 0, 
    "read_only": 0, 
@@ -78,6 +77,37 @@
   {
    "allow_bulk_edit": 0, 
    "allow_on_submit": 0, 
+   "bold": 1, 
+   "collapsible": 0, 
+   "columns": 0, 
+   "default": "0", 
+   "fieldname": "is_group", 
+   "fieldtype": "Check", 
+   "hidden": 0, 
+   "ignore_user_permissions": 0, 
+   "ignore_xss_filter": 0, 
+   "in_filter": 0, 
+   "in_global_search": 0, 
+   "in_list_view": 1, 
+   "in_standard_filter": 0, 
+   "label": "Is Group", 
+   "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, 
+   "unique": 0
+  }, 
+  {
+   "allow_bulk_edit": 0, 
+   "allow_on_submit": 0, 
    "bold": 0, 
    "collapsible": 0, 
    "columns": 0, 
@@ -173,9 +203,42 @@
   {
    "allow_bulk_edit": 0, 
    "allow_on_submit": 0, 
-   "bold": 0, 
+   "bold": 1, 
    "collapsible": 0, 
    "columns": 0, 
+   "fieldname": "parent_task", 
+   "fieldtype": "Link", 
+   "hidden": 0, 
+   "ignore_user_permissions": 1, 
+   "ignore_xss_filter": 0, 
+   "in_filter": 0, 
+   "in_global_search": 0, 
+   "in_list_view": 0, 
+   "in_standard_filter": 0, 
+   "label": "Parent Task", 
+   "length": 0, 
+   "no_copy": 0, 
+   "options": "Task", 
+   "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": 1, 
+   "set_only_once": 0, 
+   "unique": 0
+  }, 
+  {
+   "allow_bulk_edit": 0, 
+   "allow_on_submit": 0, 
+   "bold": 0, 
+   "collapsible": 0, 
+   "collapsible_depends_on": "", 
+   "columns": 0, 
+   "depends_on": "", 
    "fieldname": "section_break_10", 
    "fieldtype": "Section Break", 
    "hidden": 0, 
@@ -205,6 +268,7 @@
    "bold": 0, 
    "collapsible": 0, 
    "columns": 0, 
+   "depends_on": "", 
    "fieldname": "exp_start_date", 
    "fieldtype": "Date", 
    "hidden": 0, 
@@ -237,6 +301,7 @@
    "collapsible": 0, 
    "columns": 0, 
    "default": "0", 
+   "depends_on": "", 
    "description": "", 
    "fieldname": "expected_time", 
    "fieldtype": "Float", 
@@ -269,6 +334,7 @@
    "bold": 0, 
    "collapsible": 0, 
    "columns": 0, 
+   "depends_on": "", 
    "fieldname": "task_weight", 
    "fieldtype": "Float", 
    "hidden": 0, 
@@ -328,6 +394,7 @@
    "bold": 1, 
    "collapsible": 0, 
    "columns": 0, 
+   "depends_on": "", 
    "fieldname": "exp_end_date", 
    "fieldtype": "Date", 
    "hidden": 0, 
@@ -359,6 +426,7 @@
    "bold": 0, 
    "collapsible": 0, 
    "columns": 0, 
+   "depends_on": "", 
    "fieldname": "progress", 
    "fieldtype": "Percent", 
    "hidden": 0, 
@@ -389,6 +457,7 @@
    "bold": 0, 
    "collapsible": 0, 
    "columns": 0, 
+   "depends_on": "", 
    "fieldname": "is_milestone", 
    "fieldtype": "Check", 
    "hidden": 0, 
@@ -418,7 +487,9 @@
    "allow_on_submit": 0, 
    "bold": 0, 
    "collapsible": 0, 
+   "collapsible_depends_on": "", 
    "columns": 0, 
+   "depends_on": "", 
    "fieldname": "section_break0", 
    "fieldtype": "Section Break", 
    "hidden": 0, 
@@ -449,6 +520,7 @@
    "bold": 0, 
    "collapsible": 0, 
    "columns": 0, 
+   "depends_on": "", 
    "fieldname": "description", 
    "fieldtype": "Text Editor", 
    "hidden": 0, 
@@ -481,7 +553,9 @@
    "allow_on_submit": 0, 
    "bold": 0, 
    "collapsible": 0, 
+   "collapsible_depends_on": "", 
    "columns": 0, 
+   "depends_on": "", 
    "fieldname": "section_break", 
    "fieldtype": "Section Break", 
    "hidden": 0, 
@@ -512,6 +586,7 @@
    "bold": 0, 
    "collapsible": 0, 
    "columns": 0, 
+   "depends_on": "", 
    "fieldname": "depends_on", 
    "fieldtype": "Table", 
    "hidden": 0, 
@@ -543,6 +618,7 @@
    "bold": 0, 
    "collapsible": 0, 
    "columns": 0, 
+   "depends_on": "", 
    "fieldname": "depends_on_tasks", 
    "fieldtype": "Data", 
    "hidden": 1, 
@@ -572,7 +648,9 @@
    "allow_on_submit": 0, 
    "bold": 0, 
    "collapsible": 0, 
+   "collapsible_depends_on": "", 
    "columns": 0, 
+   "depends_on": "", 
    "description": "", 
    "fieldname": "actual", 
    "fieldtype": "Section Break", 
@@ -606,6 +684,7 @@
    "bold": 0, 
    "collapsible": 0, 
    "columns": 0, 
+   "depends_on": "", 
    "fieldname": "act_start_date", 
    "fieldtype": "Date", 
    "hidden": 0, 
@@ -638,6 +717,7 @@
    "collapsible": 0, 
    "columns": 0, 
    "default": "", 
+   "depends_on": "", 
    "description": "", 
    "fieldname": "actual_time", 
    "fieldtype": "Float", 
@@ -699,6 +779,7 @@
    "bold": 0, 
    "collapsible": 0, 
    "columns": 0, 
+   "depends_on": "", 
    "fieldname": "act_end_date", 
    "fieldtype": "Date", 
    "hidden": 0, 
@@ -730,6 +811,7 @@
    "bold": 0, 
    "collapsible": 0, 
    "columns": 0, 
+   "depends_on": "", 
    "fieldname": "section_break_17", 
    "fieldtype": "Section Break", 
    "hidden": 0, 
@@ -759,6 +841,7 @@
    "bold": 0, 
    "collapsible": 0, 
    "columns": 0, 
+   "depends_on": "", 
    "fieldname": "total_costing_amount", 
    "fieldtype": "Currency", 
    "hidden": 0, 
@@ -791,6 +874,7 @@
    "bold": 0, 
    "collapsible": 0, 
    "columns": 0, 
+   "depends_on": "", 
    "fieldname": "total_expense_claim", 
    "fieldtype": "Currency", 
    "hidden": 0, 
@@ -851,6 +935,7 @@
    "bold": 0, 
    "collapsible": 0, 
    "columns": 0, 
+   "depends_on": "", 
    "fieldname": "total_billing_amount", 
    "fieldtype": "Currency", 
    "hidden": 0, 
@@ -1025,6 +1110,96 @@
    "search_index": 0, 
    "set_only_once": 0, 
    "unique": 0
+  }, 
+  {
+   "allow_bulk_edit": 0, 
+   "allow_on_submit": 0, 
+   "bold": 0, 
+   "collapsible": 0, 
+   "columns": 0, 
+   "fieldname": "lft", 
+   "fieldtype": "Int", 
+   "hidden": 1, 
+   "ignore_user_permissions": 0, 
+   "ignore_xss_filter": 0, 
+   "in_filter": 0, 
+   "in_global_search": 0, 
+   "in_list_view": 0, 
+   "in_standard_filter": 0, 
+   "label": "lft", 
+   "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": 0, 
+   "search_index": 0, 
+   "set_only_once": 0, 
+   "unique": 0
+  }, 
+  {
+   "allow_bulk_edit": 0, 
+   "allow_on_submit": 0, 
+   "bold": 0, 
+   "collapsible": 0, 
+   "columns": 0, 
+   "fieldname": "rgt", 
+   "fieldtype": "Int", 
+   "hidden": 1, 
+   "ignore_user_permissions": 0, 
+   "ignore_xss_filter": 0, 
+   "in_filter": 0, 
+   "in_global_search": 0, 
+   "in_list_view": 0, 
+   "in_standard_filter": 0, 
+   "label": "rgt", 
+   "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": 0, 
+   "search_index": 0, 
+   "set_only_once": 0, 
+   "unique": 0
+  }, 
+  {
+   "allow_bulk_edit": 0, 
+   "allow_on_submit": 0, 
+   "bold": 0, 
+   "collapsible": 0, 
+   "columns": 0, 
+   "fieldname": "old_parent", 
+   "fieldtype": "Data", 
+   "hidden": 1, 
+   "ignore_user_permissions": 1, 
+   "ignore_xss_filter": 0, 
+   "in_filter": 0, 
+   "in_global_search": 0, 
+   "in_list_view": 0, 
+   "in_standard_filter": 0, 
+   "label": "Old Parent", 
+   "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": 0, 
+   "search_index": 0, 
+   "set_only_once": 0, 
+   "unique": 0
   }
  ], 
  "has_web_view": 0, 
@@ -1039,7 +1214,7 @@
  "istable": 0, 
  "max_attachments": 5, 
  "menu_index": 0, 
- "modified": "2017-05-23 11:28:28.161600", 
+ "modified": "2017-10-06 03:57:37.901446", 
  "modified_by": "Administrator", 
  "module": "Projects", 
  "name": "Task", 
diff --git a/erpnext/projects/doctype/task/task.py b/erpnext/projects/doctype/task/task.py
index 52ae132..5937f97 100644
--- a/erpnext/projects/doctype/task/task.py
+++ b/erpnext/projects/doctype/task/task.py
@@ -5,13 +5,14 @@
 import frappe, json
 
 from frappe.utils import getdate, date_diff, add_days, cstr
-from frappe import _
-
-from frappe.model.document import Document
+from frappe import _, throw
+from frappe.utils.nestedset import NestedSet, rebuild_tree
 
 class CircularReferenceError(frappe.ValidationError): pass
 
-class Task(Document):
+class Task(NestedSet):
+	nsm_parent_field = 'parent_task'
+
 	def get_feed(self):
 		return '{0}: {1}'.format(_(self.status), self.subject)
 
@@ -59,11 +60,16 @@
 				depends_on_tasks += d.task + ","
 		self.depends_on_tasks = depends_on_tasks
 
+	def update_nsm_model(self):
+		frappe.utils.nestedset.update_nsm(self)
+
 	def on_update(self):
+		self.update_nsm_model()
 		self.check_recursion()
 		self.reschedule_dependent_tasks()
 		self.update_project()
 		self.unassign_todo()
+		rebuild_tree("Task", "parent_task")
 
 	def unassign_todo(self):
 		if self.status == "Closed" or self.status == "Cancelled":
@@ -128,6 +134,17 @@
 		if project_user:
 			return True
 
+	def on_trash(self):
+		if check_if_child_exists(self.name):
+			throw(_("Child Task exists for this Task. You can not delete this Task."))
+
+		self.update_nsm_model()
+
+@frappe.whitelist()
+def check_if_child_exists(name):
+	return frappe.db.sql("""select name from `tabTask`
+		where parent_task = %s""", name)
+
 @frappe.whitelist()
 def get_events(start, end, filters=None):
 	"""Returns events for Gantt / Calendar view rendering.
@@ -177,4 +194,48 @@
 		and exp_end_date < CURDATE()
 		and `status` not in ('Closed', 'Cancelled')""")
 
+@frappe.whitelist()
+def get_children():
+	doctype = frappe.local.form_dict.get('doctype')
 
+	parent_field = 'parent_' + doctype.lower().replace(' ', '_')
+	parent = frappe.form_dict.get("parent") or ""
+
+	if parent == "task":
+		parent = ""
+
+	tasks = frappe.db.sql("""select name as value,
+		is_group as expandable
+		from `tab{doctype}`
+		where docstatus < 2
+		and ifnull(`{parent_field}`,'') = %s
+		order by name""".format(doctype=frappe.db.escape(doctype),
+		parent_field=frappe.db.escape(parent_field)), (parent), as_dict=1)
+
+	# return tasks
+	return tasks
+
+@frappe.whitelist()
+def add_node():
+    from frappe.desk.treeview import make_tree_args
+    args = frappe.form_dict
+    args.update({
+    	"name_field": "subject"
+    })
+    args = make_tree_args(**args)
+
+    if args.parent_task == 'task':
+        args.parent_task = None
+
+    frappe.get_doc(args).insert()
+
+@frappe.whitelist()
+def add_multiple_tasks(data, parent):
+    data = json.loads(data)['tasks']
+    tasks = data.split('\n')
+    new_doc = {'doctype': 'Task', 'parent_task': parent}
+
+    for d in tasks:
+        new_doc['subject'] = d
+        new_task = frappe.get_doc(new_doc)
+        new_task.insert()
diff --git a/erpnext/projects/doctype/task/task_tree.js b/erpnext/projects/doctype/task/task_tree.js
new file mode 100644
index 0000000..f11c34f
--- /dev/null
+++ b/erpnext/projects/doctype/task/task_tree.js
@@ -0,0 +1,59 @@
+frappe.provide("frappe.treeview_settings");
+
+frappe.treeview_settings['Task'] = {
+	get_tree_nodes: "erpnext.projects.doctype.task.task.get_children",
+	add_tree_node: "erpnext.projects.doctype.task.task.add_node",
+	filters: [
+		{
+			fieldname: "task",
+			fieldtype:"Link",
+			options: "Task",
+			label: __("Task"),
+			get_query: function(){
+				return {
+					filters: [["Task", 'is_group', '=', 1]]
+				};
+			}
+		}
+	],
+	title: "Task",
+	breadcrumb: "Projects",
+	get_tree_root: false,
+	root_label: "task",
+	ignore_fields:["parent_task"],
+	get_label: function(node) {
+		return node.data.value;
+	},
+	onload: function(me){
+		me.make_tree();
+		me.set_root = true;
+	},
+	toolbar: [
+		{
+			label:__("Add Multiple"),
+			condition: function(node) {
+				return node.expandable;
+			},
+			click: function(node) {
+				var d = new frappe.ui.Dialog({
+					'fields': [
+						{'fieldname': 'tasks', 'label': 'Tasks', 'fieldtype': 'Text'},
+					],
+					primary_action: function(){
+						d.hide();
+						return frappe.call({
+							method: "erpnext.projects.doctype.task.task.add_multiple_tasks",
+							args: {
+								data: d.get_values(),
+								parent: node.data.value
+							},
+							callback: function() { }
+						});
+					}
+				});
+				d.show();
+			}
+		}
+	],
+	extend_toolbar: true
+};
\ No newline at end of file
diff --git a/erpnext/projects/doctype/task/test_task.js b/erpnext/projects/doctype/task/tests/test_task.js
similarity index 100%
rename from erpnext/projects/doctype/task/test_task.js
rename to erpnext/projects/doctype/task/tests/test_task.js
diff --git a/erpnext/projects/doctype/task/tests/test_task_tree.js b/erpnext/projects/doctype/task/tests/test_task_tree.js
new file mode 100644
index 0000000..9cbcf85
--- /dev/null
+++ b/erpnext/projects/doctype/task/tests/test_task_tree.js
@@ -0,0 +1,99 @@
+/* eslint-disable */
+// rename this file from _test_[name] to test_[name] to activate
+// and remove above this line
+
+QUnit.test("test: Task Tree", function (assert) {
+	let done = assert.async();
+
+	// number of asserts
+	assert.expect(5);
+
+	frappe.run_serially([
+		// insert a new Task
+		() => frappe.set_route('Tree', 'Task'),
+		() => frappe.timeout(0.5),
+
+		// Checking adding child without selecting any Node
+		() => frappe.tests.click_button('New'),
+		() => frappe.timeout(0.5),
+		() => {assert.equal($(`.msgprint`).text(), "Select a group node first.", "Error message success");},
+		() => frappe.tests.click_button('Close'),
+		() => frappe.timeout(0.5),
+
+		// Creating child nodes
+		() => frappe.tests.click_link('task'),
+		() => frappe.map_group.make('Test-1'),
+		() => frappe.map_group.make('Test-2'),
+		() => frappe.map_group.make('Test-3', 1),
+		() => frappe.timeout(1),
+		() => frappe.tests.click_link('Test-3'),
+		() => frappe.map_group.make('Test-4', 0),
+
+		// Checking Edit button
+		() => frappe.timeout(0.5),
+		() => frappe.tests.click_link('Test-1'),
+		() => frappe.tests.click_button('Edit'),
+		() => frappe.timeout(0.5),
+		() => {assert.deepEqual(frappe.get_route(), ["Form", "Task", "Test-1"], "Edit route checks");},
+
+		// Deleting child Node
+		() => frappe.set_route('Tree', 'Task'),
+		() => frappe.timeout(0.5),
+		() => frappe.tests.click_link('Test-1'),
+		() => frappe.tests.click_button('Delete'),
+		() => frappe.timeout(0.5),
+		() => frappe.tests.click_button('Yes'),
+
+		// Deleting Group Node that has child nodes in it
+		() => frappe.timeout(0.5),
+		() => frappe.tests.click_link('Test-3'),
+		() => frappe.tests.click_button('Delete'),
+		() => frappe.timeout(0.5),
+		() => frappe.tests.click_button('Yes'),
+		() => frappe.timeout(1),
+		() => {assert.equal(cur_dialog.title, 'Message', 'Error thrown correctly');},
+		() => frappe.tests.click_button('Close'),
+
+		// Renaming Child node
+		() => frappe.timeout(0.5),
+		() => frappe.tests.click_link('Test-2'),
+		() => frappe.tests.click_button('Rename'),
+		() => frappe.timeout(1),
+		() => cur_dialog.set_value('new_name', 'Test-5'),
+		() => frappe.timeout(1.5),
+		() => cur_dialog.get_primary_btn().click(),
+		() => frappe.timeout(1),
+		() => {assert.equal($(`a:contains("Test-5"):visible`).length, 1, 'Rename successfull');},
+
+		// Add multiple child tasks
+		() => frappe.tests.click_link('Test-3'),
+		() => frappe.timeout(0.5),
+		() => frappe.click_button('Add Multiple'),
+		() => frappe.timeout(1),
+		() => cur_dialog.set_value('tasks', 'Test-6\nTest-7'),
+		() => frappe.timeout(0.5),
+		() => frappe.click_button('Submit'),
+		() => frappe.timeout(2),
+		() => frappe.click_button('Expand All'),
+		() => frappe.timeout(1),
+		() => {
+			let count = $(`a:contains("Test-6"):visible`).length + $(`a:contains("Test-7"):visible`).length;
+			assert.equal(count, 2, "Multiple Tasks added successfully");
+		},
+
+		() => done()
+	]);
+});
+
+frappe.map_group = {
+	make:function(subject, is_group = 0){
+		return frappe.run_serially([
+			() => frappe.click_button('Add Child'),
+			() => frappe.timeout(1),
+			() => cur_dialog.set_value('is_group', is_group),
+			() => cur_dialog.set_value('subject', subject),
+			() => frappe.click_button('Create New'),
+			() => frappe.timeout(1.5)
+		]);
+	}
+};
diff --git a/erpnext/tests/ui/tests.txt b/erpnext/tests/ui/tests.txt
index e7de604..24858f3 100644
--- a/erpnext/tests/ui/tests.txt
+++ b/erpnext/tests/ui/tests.txt
@@ -133,3 +133,4 @@
 erpnext/restaurant/doctype/restaurant_table/test_restaurant_table.js
 erpnext/restaurant/doctype/restaurant_menu/test_restaurant_menu.js
 erpnext/restaurant/doctype/restaurant_order_entry/restaurant_order_entry.js
+erpnext/projects/doctype/task/tests/test_task_tree.js