Merge branch 'develop' into project-template-and-tasks
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index 043fafd..7b2428e 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -742,5 +742,6 @@
 erpnext.patches.v13_0.create_leave_policy_assignment_based_on_employee_current_leave_policy
 erpnext.patches.v13_0.add_po_to_global_search
 erpnext.patches.v13_0.update_returned_qty_in_pr_dn
+erpnext.patches.v13_0.update_project_template_tasks
 erpnext.patches.v13_0.set_company_in_leave_ledger_entry
 erpnext.patches.v13_0.convert_qi_parameter_to_link_field
diff --git a/erpnext/patches/v13_0/update_project_template_tasks.py b/erpnext/patches/v13_0/update_project_template_tasks.py
new file mode 100644
index 0000000..5fa0623
--- /dev/null
+++ b/erpnext/patches/v13_0/update_project_template_tasks.py
@@ -0,0 +1,44 @@
+# Copyright (c) 2019, Frappe and Contributors
+# License: GNU General Public License v3. See license.txt
+
+from __future__ import unicode_literals
+import frappe
+
+def execute():
+    frappe.reload_doc("projects", "doctype", "project_template")
+    frappe.reload_doc("projects", "doctype", "project_template_task")
+    frappe.reload_doc("projects", "doctype", "project_template")
+    frappe.reload_doc("projects", "doctype", "task")
+
+    for template_name in frappe.db.sql(""" 
+        select 
+            name 
+        from 
+            `tabProject Template` """, 
+        as_dict=1):
+       
+        template = frappe.get_doc("Project Template", template_name.name)
+        replace_tasks = False
+        new_tasks = []
+        for task in template.tasks:
+            if task.subject:
+                replace_tasks = True
+                new_task = frappe.get_doc(dict(
+                    doctype = "Task",
+                    subject = task.subject,
+                    start = task.start,
+                    duration = task.duration,
+                    task_weight = task.task_weight,
+                    description = task.description,
+                    is_template = 1
+                )).insert()
+                new_tasks.append(new_task)
+
+        if replace_tasks:
+            template.tasks = []
+            for tsk in new_tasks:
+                template.append("tasks", {
+                    "task": tsk.name,
+                    "subject": tsk.subject
+                })  
+            template.save()
\ No newline at end of file
diff --git a/erpnext/projects/doctype/project/project.py b/erpnext/projects/doctype/project/project.py
index 5bbd29c..60f85b0 100644
--- a/erpnext/projects/doctype/project/project.py
+++ b/erpnext/projects/doctype/project/project.py
@@ -13,6 +13,7 @@
 from erpnext.hr.doctype.daily_work_summary.daily_work_summary import get_users_email
 from erpnext.hr.doctype.holiday_list.holiday_list import is_holiday
 from frappe.model.document import Document
+from erpnext.education.doctype.student_attendance.student_attendance import get_holiday_list
 
 class Project(Document):
 	def get_feed(self):
@@ -54,17 +55,64 @@
 				self.project_type = template.project_type
 
 			# create tasks from template
+			project_tasks = []
+			tmp_task_details = []
 			for task in template.tasks:
-				frappe.get_doc(dict(
-					doctype = 'Task',
-					subject = task.subject,
-					project = self.name,
-					status = 'Open',
-					exp_start_date = add_days(self.expected_start_date, task.start),
-					exp_end_date = add_days(self.expected_start_date, task.start + task.duration),
-					description = task.description,
-					task_weight = task.task_weight
-				)).insert()
+				template_task_details = frappe.get_doc("Task", task.task)
+				tmp_task_details.append(template_task_details)
+				task = self.create_task_from_template(template_task_details)
+				project_tasks.append(task)
+			self.dependency_mapping(tmp_task_details, project_tasks)
+
+	def create_task_from_template(self, task_details):
+		return frappe.get_doc(dict(
+				doctype = 'Task',
+				subject = task_details.subject,
+				project = self.name,
+				status = 'Open',
+				exp_start_date = self.calculate_start_date(task_details),
+				exp_end_date = self.calculate_end_date(task_details),
+				description = task_details.description,
+				task_weight = task_details.task_weight,
+				type = task_details.type,
+				issue = task_details.issue,
+				is_group = task_details.is_group
+			)).insert()
+
+	def calculate_start_date(self, task_details):
+		self.start_date = add_days(self.expected_start_date, task_details.start)
+		self.start_date = update_if_holiday(self.holiday_list, self.start_date)
+		return self.start_date
+
+	def calculate_end_date(self, task_details):
+		self.end_date = add_days(self.start_date, task_details.duration)
+		return update_if_holiday(self.holiday_list, self.end_date)
+
+	def dependency_mapping(self, template_tasks, project_tasks):
+		for template_task in template_tasks:
+			project_task = list(filter(lambda x: x.subject == template_task.subject, project_tasks))[0]
+			project_task = frappe.get_doc("Task", project_task.name)
+			self.check_depends_on_value(template_task, project_task, project_tasks)
+			self.check_for_parent_tasks(template_task, project_task, project_tasks)
+
+	def check_depends_on_value(self, template_task, project_task, project_tasks):
+		if template_task.get("depends_on") and not project_task.get("depends_on"):
+			for child_task in template_task.get("depends_on"):
+				child_task_subject = frappe.db.get_value("Task", child_task.task, "subject")
+				corresponding_project_task = list(filter(lambda x: x.subject == child_task_subject, project_tasks))
+				if len(corresponding_project_task):
+					project_task.append("depends_on",{
+						"task": corresponding_project_task[0].name
+					})
+					project_task.save()
+
+	def check_for_parent_tasks(self, template_task, project_task, project_tasks):
+		if template_task.get("parent_task") and not project_task.get("parent_task"):
+			parent_task_subject = frappe.db.get_value("Task", template_task.get("parent_task"), "subject")
+			corresponding_project_task = list(filter(lambda x: x.subject == parent_task_subject, project_tasks))
+			if len(corresponding_project_task):
+				project_task.parent_task = corresponding_project_task[0].name
+				project_task.save()
 
 	def is_row_updated(self, row, existing_task_data, fields):
 		if self.get("__islocal") or not existing_task_data: return True
@@ -493,3 +541,9 @@
 
 	project.status = status
 	project.save()
+
+def update_if_holiday(holiday_list, date):
+	holiday_list = holiday_list or get_holiday_list()
+	while is_holiday(holiday_list, date):
+		date = add_days(date, 1)
+	return date
diff --git a/erpnext/projects/doctype/project/test_project.py b/erpnext/projects/doctype/project/test_project.py
index 0c4f6f1..97b67b3 100644
--- a/erpnext/projects/doctype/project/test_project.py
+++ b/erpnext/projects/doctype/project/test_project.py
@@ -7,60 +7,129 @@
 test_records = frappe.get_test_records('Project')
 test_ignore = ["Sales Order"]
 
-from erpnext.projects.doctype.project_template.test_project_template import get_project_template, make_project_template
-from erpnext.projects.doctype.project.project import set_project_status
-
-from frappe.utils import getdate
+from erpnext.projects.doctype.project_template.test_project_template import make_project_template
+from erpnext.projects.doctype.project.project import update_if_holiday
+from erpnext.projects.doctype.task.test_task import create_task
+from frappe.utils import getdate, nowdate, add_days
 
 class TestProject(unittest.TestCase):
-	def test_project_with_template(self):
-		frappe.db.sql('delete from tabTask where project = "Test Project with Template"')
-		frappe.delete_doc('Project', 'Test Project with Template')
+	def test_project_with_template_having_no_parent_and_depend_tasks(self):
+		project_name = "Test Project with Template - No Parent and Dependend Tasks"
+		frappe.db.sql(""" delete from tabTask where project = %s """, project_name)
+		frappe.delete_doc('Project', project_name)
 
-		project = get_project('Test Project with Template')
+		task1 = task_exists("Test Template Task with No Parent and Dependency")
+		if not task1:
+			task1 = create_task(subject="Test Template Task with No Parent and Dependency", is_template=1, begin=5, duration=3)
 
-		tasks = frappe.get_all('Task', '*', dict(project=project.name), order_by='creation asc')
+		template = make_project_template("Test Project Template - No Parent and Dependend Tasks", [task1])
+		project = get_project(project_name, template)
+		tasks = frappe.get_all('Task', ['subject','exp_end_date','depends_on_tasks'], dict(project=project.name), order_by='creation asc')
 
-		task1 = tasks[0]
-		self.assertEqual(task1.subject, 'Task 1')
-		self.assertEqual(task1.description, 'Task 1 description')
-		self.assertEqual(getdate(task1.exp_start_date), getdate('2019-01-01'))
-		self.assertEqual(getdate(task1.exp_end_date), getdate('2019-01-04'))
+		self.assertEqual(tasks[0].subject, 'Test Template Task with No Parent and Dependency')
+		self.assertEqual(getdate(tasks[0].exp_end_date), calculate_end_date(project, 5, 3))
+		self.assertEqual(len(tasks), 1)
 
-		self.assertEqual(len(tasks), 4)
-		task4 = tasks[3]
-		self.assertEqual(task4.subject, 'Task 4')
-		self.assertEqual(getdate(task4.exp_end_date), getdate('2019-01-06'))
+	def test_project_template_having_parent_child_tasks(self):
+		project_name = "Test Project with Template - Tasks with Parent-Child Relation"
+		frappe.db.sql(""" delete from tabTask where project = %s """, project_name)
+		frappe.delete_doc('Project', project_name)
 
-def get_project(name):
-	template = get_project_template()
+		task1 = task_exists("Test Template Task Parent")
+		if not task1:
+			task1 = create_task(subject="Test Template Task Parent", is_group=1, is_template=1, begin=1, duration=1)
+
+		task2 = task_exists("Test Template Task Child 1")
+		if not task2:
+			task2 = create_task(subject="Test Template Task Child 1", parent_task=task1.name, is_template=1, begin=1, duration=3)
+		
+		task3 = task_exists("Test Template Task Child 2")
+		if not task3:
+			task3 = create_task(subject="Test Template Task Child 2", parent_task=task1.name, is_template=1, begin=2, duration=3)
+
+		template = make_project_template("Test Project Template  - Tasks with Parent-Child Relation", [task1, task2, task3])
+		project = get_project(project_name, template)
+		tasks = frappe.get_all('Task', ['subject','exp_end_date','depends_on_tasks', 'name', 'parent_task'], dict(project=project.name), order_by='creation asc')
+
+		self.assertEqual(tasks[0].subject, 'Test Template Task Parent')
+		self.assertEqual(getdate(tasks[0].exp_end_date), calculate_end_date(project, 1, 1))
+
+		self.assertEqual(tasks[1].subject, 'Test Template Task Child 1')
+		self.assertEqual(getdate(tasks[1].exp_end_date), calculate_end_date(project, 1, 3))
+		self.assertEqual(tasks[1].parent_task, tasks[0].name)
+
+		self.assertEqual(tasks[2].subject, 'Test Template Task Child 2')
+		self.assertEqual(getdate(tasks[2].exp_end_date), calculate_end_date(project, 2, 3))
+		self.assertEqual(tasks[2].parent_task, tasks[0].name)
+
+		self.assertEqual(len(tasks), 3)
+
+	def test_project_template_having_dependent_tasks(self):
+		project_name = "Test Project with Template - Dependent Tasks"
+		frappe.db.sql(""" delete from tabTask where project = %s  """, project_name)
+		frappe.delete_doc('Project', project_name)
+
+		task1 = task_exists("Test Template Task for Dependency")
+		if not task1:
+			task1 = create_task(subject="Test Template Task for Dependency", is_template=1, begin=3, duration=1)
+
+		task2 = task_exists("Test Template Task with Dependency")
+		if not task2:
+			task2 = create_task(subject="Test Template Task with Dependency", depends_on=task1.name, is_template=1, begin=2, duration=2)
+		
+		template = make_project_template("Test Project with Template - Dependent Tasks", [task1, task2])
+		project = get_project(project_name, template)
+		tasks = frappe.get_all('Task', ['subject','exp_end_date','depends_on_tasks', 'name'], dict(project=project.name), order_by='creation asc')
+
+		self.assertEqual(tasks[1].subject, 'Test Template Task with Dependency')
+		self.assertEqual(getdate(tasks[1].exp_end_date), calculate_end_date(project, 2, 2))
+		self.assertTrue(tasks[1].depends_on_tasks.find(tasks[0].name) >= 0 )
+
+		self.assertEqual(tasks[0].subject, 'Test Template Task for Dependency')
+		self.assertEqual(getdate(tasks[0].exp_end_date), calculate_end_date(project, 3, 1) )
+
+		self.assertEqual(len(tasks), 2)
+
+def get_project(name, template):
 
 	project = frappe.get_doc(dict(
 		doctype = 'Project',
 		project_name = name,
 		status = 'Open',
 		project_template = template.name,
-		expected_start_date = '2019-01-01'
+		expected_start_date = nowdate()
 	)).insert()
 
 	return project
 
 def make_project(args):
 	args = frappe._dict(args)
-	if args.project_template_name:
-		template = make_project_template(args.project_template_name)
-	else:
-		template = get_project_template()
 
 	project = frappe.get_doc(dict(
 		doctype = 'Project',
 		project_name = args.project_name,
 		status = 'Open',
-		project_template = template.name,
 		expected_start_date = args.start_date
 	))
 
+	if args.project_template_name:
+		template = make_project_template(args.project_template_name)
+		project.project_template = template.name
+
 	if not frappe.db.exists("Project", args.project_name):
 		project.insert()
 
-	return project
\ No newline at end of file
+	return project
+
+def task_exists(subject):
+	result = frappe.db.get_list("Task", filters={"subject": subject},fields=["name"])
+	if not len(result):
+		return False
+	return frappe.get_doc("Task", result[0].name)
+
+def calculate_end_date(project, start, duration):
+	start = add_days(project.expected_start_date, start)
+	start = update_if_holiday(project.holiday_list, start)
+	end = add_days(start, duration)
+	end = update_if_holiday(project.holiday_list, end)
+	return getdate(end)
\ No newline at end of file
diff --git a/erpnext/projects/doctype/project_template/project_template.js b/erpnext/projects/doctype/project_template/project_template.js
index d7a876d..3d3c15c 100644
--- a/erpnext/projects/doctype/project_template/project_template.js
+++ b/erpnext/projects/doctype/project_template/project_template.js
@@ -5,4 +5,23 @@
 	// refresh: function(frm) {
 
 	// }
+	setup: function (frm) {
+		frm.set_query("task", "tasks", function () {
+			return {
+				filters: {
+					"is_template": 1
+				}
+			};
+		});
+	}
+});
+
+frappe.ui.form.on('Project Template Task', {
+	task: function (frm, cdt, cdn) {
+		var row = locals[cdt][cdn];
+		frappe.db.get_value("Task", row.task, "subject", (value) => {
+			row.subject = value.subject;
+			refresh_field("tasks");
+		});
+	}
 });
diff --git a/erpnext/projects/doctype/project_template/project_template.py b/erpnext/projects/doctype/project_template/project_template.py
index ac78135..aace402 100644
--- a/erpnext/projects/doctype/project_template/project_template.py
+++ b/erpnext/projects/doctype/project_template/project_template.py
@@ -3,8 +3,28 @@
 # For license information, please see license.txt
 
 from __future__ import unicode_literals
-# import frappe
+import frappe
 from frappe.model.document import Document
+from frappe import _
+from frappe.utils import get_link_to_form
 
 class ProjectTemplate(Document):
-	pass
+
+	def validate(self):
+		self.validate_dependencies()
+
+	def validate_dependencies(self):
+		for task in self.tasks:
+			task_details = frappe.get_doc("Task", task.task)
+			if task_details.depends_on:
+				for dependency_task in task_details.depends_on:
+					if not self.check_dependent_task_presence(dependency_task.task):
+						task_details_format = get_link_to_form("Task",task_details.name)
+						dependency_task_format = get_link_to_form("Task", dependency_task.task)
+						frappe.throw(_("Task {0} depends on Task {1}. Please add Task {1} to the Tasks list.").format(frappe.bold(task_details_format), frappe.bold(dependency_task_format)))
+	
+	def check_dependent_task_presence(self, task):
+		for task_details in self.tasks:
+			if task_details.task == task:
+				return True
+		return False
diff --git a/erpnext/projects/doctype/project_template/test_project_template.py b/erpnext/projects/doctype/project_template/test_project_template.py
index 2c5831a..95663cd 100644
--- a/erpnext/projects/doctype/project_template/test_project_template.py
+++ b/erpnext/projects/doctype/project_template/test_project_template.py
@@ -5,44 +5,25 @@
 
 import frappe
 import unittest
+from erpnext.projects.doctype.task.test_task import create_task
 
 class TestProjectTemplate(unittest.TestCase):
 	pass
 
-def get_project_template():
-	if not frappe.db.exists('Project Template', 'Test Project Template'):
-		frappe.get_doc(dict(
-			doctype = 'Project Template',
-			name = 'Test Project Template',
-			tasks = [
-				dict(subject='Task 1', description='Task 1 description',
-					start=0, duration=3),
-				dict(subject='Task 2', description='Task 2 description',
-					start=0, duration=2),
-				dict(subject='Task 3', description='Task 3 description',
-					start=2, duration=4),
-				dict(subject='Task 4', description='Task 4 description',
-					start=3, duration=2),
-			]
-		)).insert()
-
-	return frappe.get_doc('Project Template', 'Test Project Template')
-
 def make_project_template(project_template_name, project_tasks=[]):
 	if not frappe.db.exists('Project Template', project_template_name):
-		frappe.get_doc(dict(
-			doctype = 'Project Template',
-			name = project_template_name,
-			tasks = project_tasks or [
-				dict(subject='Task 1', description='Task 1 description',
-					start=0, duration=3),
-				dict(subject='Task 2', description='Task 2 description',
-					start=0, duration=2),
-				dict(subject='Task 3', description='Task 3 description',
-					start=2, duration=4),
-				dict(subject='Task 4', description='Task 4 description',
-					start=3, duration=2),
+		project_tasks = project_tasks or [
+				create_task(subject="_Test Template Task 1", is_template=1, begin=0, duration=3),
+				create_task(subject="_Test Template Task 2", is_template=1, begin=0, duration=2),
 			]
-		)).insert()
+		doc = frappe.get_doc(dict(
+			doctype = 'Project Template',
+			name = project_template_name
+		))
+		for task in project_tasks:
+			doc.append("tasks",{
+				"task": task.name
+			})
+		doc.insert()
 
 	return frappe.get_doc('Project Template', project_template_name)
\ No newline at end of file
diff --git a/erpnext/projects/doctype/project_template_task/project_template_task.json b/erpnext/projects/doctype/project_template_task/project_template_task.json
index 8644d89..69530b1 100644
--- a/erpnext/projects/doctype/project_template_task/project_template_task.json
+++ b/erpnext/projects/doctype/project_template_task/project_template_task.json
@@ -1,203 +1,41 @@
 {
- "allow_copy": 0,
- "allow_events_in_timeline": 0,
- "allow_guest_to_view": 0,
- "allow_import": 0,
- "allow_rename": 0,
- "beta": 0,
+ "actions": [],
  "creation": "2019-02-18 17:24:41.830096",
- "custom": 0,
- "docstatus": 0,
  "doctype": "DocType",
- "document_type": "",
  "editable_grid": 1,
  "engine": "InnoDB",
+ "field_order": [
+  "task",
+  "subject"
+ ],
  "fields": [
   {
-   "allow_bulk_edit": 0,
-   "allow_in_quick_entry": 0,
-   "allow_on_submit": 0,
-   "bold": 0,
-   "collapsible": 0,
-   "columns": 0,
+   "columns": 2,
+   "fieldname": "task",
+   "fieldtype": "Link",
+   "in_list_view": 1,
+   "label": "Task",
+   "options": "Task",
+   "reqd": 1
+  },
+  {
+   "columns": 6,
    "fieldname": "subject",
-   "fieldtype": "Data",
-   "hidden": 0,
-   "ignore_user_permissions": 0,
-   "ignore_xss_filter": 0,
-   "in_filter": 0,
-   "in_global_search": 0,
+   "fieldtype": "Read Only",
    "in_list_view": 1,
-   "in_standard_filter": 0,
-   "label": "Subject",
-   "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,
-   "fieldname": "start",
-   "fieldtype": "Int",
-   "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": "Begin On (Days)",
-   "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,
-   "fieldname": "duration",
-   "fieldtype": "Int",
-   "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": "Duration (Days)",
-   "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,
-   "fieldname": "task_weight",
-   "fieldtype": "Float",
-   "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": "Task Weight",
-   "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
-  },
-  {
-   "allow_bulk_edit": 0,
-   "allow_in_quick_entry": 0,
-   "allow_on_submit": 0,
-   "bold": 0,
-   "collapsible": 0,
-   "columns": 0,
-   "fieldname": "description",
-   "fieldtype": "Text Editor",
-   "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": "Description",
-   "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
+   "label": "Subject"
   }
  ],
- "has_web_view": 0,
- "hide_heading": 0,
- "hide_toolbar": 0,
- "idx": 0,
- "image_view": 0,
- "in_create": 0,
- "is_submittable": 0,
- "issingle": 0,
  "istable": 1,
- "max_attachments": 0,
- "modified": "2019-02-18 18:30:22.688966",
+ "links": [],
+ "modified": "2021-01-07 15:13:40.995071",
  "modified_by": "Administrator",
  "module": "Projects",
  "name": "Project Template Task",
- "name_case": "",
  "owner": "Administrator",
  "permissions": [],
  "quick_entry": 1,
- "read_only": 0,
- "read_only_onload": 0,
- "show_name_in_global_search": 0,
  "sort_field": "modified",
  "sort_order": "DESC",
- "track_changes": 1,
- "track_seen": 0,
- "track_views": 0
+ "track_changes": 1
 }
\ No newline at end of file
diff --git a/erpnext/projects/doctype/task/task.json b/erpnext/projects/doctype/task/task.json
index 27f1a71..160cc58 100644
--- a/erpnext/projects/doctype/task/task.json
+++ b/erpnext/projects/doctype/task/task.json
@@ -12,6 +12,7 @@
   "issue",
   "type",
   "is_group",
+  "is_template",
   "column_break0",
   "status",
   "priority",
@@ -22,9 +23,11 @@
   "sb_timeline",
   "exp_start_date",
   "expected_time",
+  "start",
   "column_break_11",
   "exp_end_date",
   "progress",
+  "duration",
   "is_milestone",
   "sb_details",
   "description",
@@ -112,7 +115,7 @@
    "no_copy": 1,
    "oldfieldname": "status",
    "oldfieldtype": "Select",
-   "options": "Open\nWorking\nPending Review\nOverdue\nCompleted\nCancelled"
+   "options": "Open\nWorking\nPending Review\nOverdue\nTemplate\nCompleted\nCancelled"
   },
   {
    "fieldname": "priority",
@@ -360,6 +363,24 @@
    "label": "Completed By",
    "no_copy": 1,
    "options": "User"
+  },
+  {
+   "default": "0",
+   "fieldname": "is_template",
+   "fieldtype": "Check",
+   "label": "Is Template"
+  },
+  {
+   "depends_on": "is_template",
+   "fieldname": "start",
+   "fieldtype": "Int",
+   "label": "Begin On (Days)"
+  },
+  {
+   "depends_on": "is_template",
+   "fieldname": "duration",
+   "fieldtype": "Int",
+   "label": "Duration (Days)"
   }
  ],
  "icon": "fa fa-check",
@@ -367,7 +388,7 @@
  "is_tree": 1,
  "links": [],
  "max_attachments": 5,
- "modified": "2020-07-03 12:36:04.960457",
+ "modified": "2020-12-28 11:32:58.714991",
  "modified_by": "Administrator",
  "module": "Projects",
  "name": "Task",
diff --git a/erpnext/projects/doctype/task/task.py b/erpnext/projects/doctype/task/task.py
index fb84094..a2095c9 100755
--- a/erpnext/projects/doctype/task/task.py
+++ b/erpnext/projects/doctype/task/task.py
@@ -17,291 +17,312 @@
 class EndDateCannotBeGreaterThanProjectEndDateError(frappe.ValidationError): pass
 
 class Task(NestedSet):
-	nsm_parent_field = 'parent_task'
+    nsm_parent_field = 'parent_task'
 
-	def get_feed(self):
-		return '{0}: {1}'.format(_(self.status), self.subject)
+    def get_feed(self):
+        return '{0}: {1}'.format(_(self.status), self.subject)
 
-	def get_customer_details(self):
-		cust = frappe.db.sql("select customer_name from `tabCustomer` where name=%s", self.customer)
-		if cust:
-			ret = {'customer_name': cust and cust[0][0] or ''}
-			return ret
+    def get_customer_details(self):
+        cust = frappe.db.sql("select customer_name from `tabCustomer` where name=%s", self.customer)
+        if cust:
+            ret = {'customer_name': cust and cust[0][0] or ''}
+            return ret
 
-	def validate(self):
-		self.validate_dates()
-		self.validate_parent_project_dates()
-		self.validate_progress()
-		self.validate_status()
-		self.update_depends_on()
+    def validate(self):
+        self.validate_dates()
+        self.validate_parent_project_dates()
+        self.validate_progress()
+        self.validate_status()
+        self.update_depends_on()
+        self.validate_dependencies_for_template_task()
 
-	def validate_dates(self):
-		if self.exp_start_date and self.exp_end_date and getdate(self.exp_start_date) > getdate(self.exp_end_date):
-			frappe.throw(_("{0} can not be greater than {1}").format(frappe.bold("Expected Start Date"), \
-				frappe.bold("Expected End Date")))
+    def validate_dates(self):
+        if self.exp_start_date and self.exp_end_date and getdate(self.exp_start_date) > getdate(self.exp_end_date):
+            frappe.throw(_("{0} can not be greater than {1}").format(frappe.bold("Expected Start Date"), \
+                frappe.bold("Expected End Date")))
 
-		if self.act_start_date and self.act_end_date and getdate(self.act_start_date) > getdate(self.act_end_date):
-			frappe.throw(_("{0} can not be greater than {1}").format(frappe.bold("Actual Start Date"), \
-				frappe.bold("Actual End Date")))
+        if self.act_start_date and self.act_end_date and getdate(self.act_start_date) > getdate(self.act_end_date):
+            frappe.throw(_("{0} can not be greater than {1}").format(frappe.bold("Actual Start Date"), \
+                frappe.bold("Actual End Date")))
 
-	def validate_parent_project_dates(self):
-		if not self.project or frappe.flags.in_test:
-			return
+    def validate_parent_project_dates(self):
+        if not self.project or frappe.flags.in_test:
+            return
 
-		expected_end_date = frappe.db.get_value("Project", self.project, "expected_end_date")
+        expected_end_date = frappe.db.get_value("Project", self.project, "expected_end_date")
 
-		if expected_end_date:
-			validate_project_dates(getdate(expected_end_date), self, "exp_start_date", "exp_end_date", "Expected")
-			validate_project_dates(getdate(expected_end_date), self, "act_start_date", "act_end_date", "Actual")
+        if expected_end_date:
+            validate_project_dates(getdate(expected_end_date), self, "exp_start_date", "exp_end_date", "Expected")
+            validate_project_dates(getdate(expected_end_date), self, "act_start_date", "act_end_date", "Actual")
 
-	def validate_status(self):
-		if self.status!=self.get_db_value("status") and self.status == "Completed":
-			for d in self.depends_on:
-				if frappe.db.get_value("Task", d.task, "status") not in ("Completed", "Cancelled"):
-					frappe.throw(_("Cannot complete task {0} as its dependant task {1} are not ccompleted / cancelled.").format(frappe.bold(self.name), frappe.bold(d.task)))
+    def validate_status(self):
+        if self.is_template and self.status != "Template":
+            self.status = "Template"
+        if self.status!=self.get_db_value("status") and self.status == "Completed":
+            for d in self.depends_on:
+                if frappe.db.get_value("Task", d.task, "status") not in ("Completed", "Cancelled"):
+                    frappe.throw(_("Cannot complete task {0} as its dependant task {1} are not ccompleted / cancelled.").format(frappe.bold(self.name), frappe.bold(d.task)))
 
-			close_all_assignments(self.doctype, self.name)
+            close_all_assignments(self.doctype, self.name)
 
-	def validate_progress(self):
-		if flt(self.progress or 0) > 100:
-			frappe.throw(_("Progress % for a task cannot be more than 100."))
+    def validate_progress(self):
+        if flt(self.progress or 0) > 100:
+            frappe.throw(_("Progress % for a task cannot be more than 100."))
 
-		if flt(self.progress) == 100:
-			self.status = 'Completed'
+        if flt(self.progress) == 100:
+            self.status = 'Completed'
 
-		if self.status == 'Completed':
-			self.progress = 100
+        if self.status == 'Completed':
+            self.progress = 100
 
-	def update_depends_on(self):
-		depends_on_tasks = self.depends_on_tasks or ""
-		for d in self.depends_on:
-			if d.task and not d.task in depends_on_tasks:
-				depends_on_tasks += d.task + ","
-		self.depends_on_tasks = depends_on_tasks
+    def validate_dependencies_for_template_task(self):
+        if self.is_template:
+            self.validate_parent_template_task()
+            self.validate_depends_on_tasks()
+        
+    def validate_parent_template_task(self):
+        if self.parent_task:
+            if not frappe.db.get_value("Task", self.parent_task, "is_template"):
+                parent_task_format = """<a href="#Form/Task/{0}">{0}</a>""".format(self.parent_task)
+                frappe.throw(_("Parent Task {0} is not a Template Task").format(parent_task_format))
+                
+    def validate_depends_on_tasks(self):
+        if self.depends_on:
+            for task in self.depends_on:
+                if not frappe.db.get_value("Task", task.task, "is_template"):
+                    dependent_task_format = """<a href="#Form/Task/{0}">{0}</a>""".format(task.task)
+                    frappe.throw(_("Dependent Task {0} is not a Template Task").format(dependent_task_format))
 
-	def update_nsm_model(self):
-		frappe.utils.nestedset.update_nsm(self)
+    def update_depends_on(self):
+        depends_on_tasks = self.depends_on_tasks or ""
+        for d in self.depends_on:
+            if d.task and d.task not in depends_on_tasks:
+                depends_on_tasks += d.task + ","
+        self.depends_on_tasks = depends_on_tasks
 
-	def on_update(self):
-		self.update_nsm_model()
-		self.check_recursion()
-		self.reschedule_dependent_tasks()
-		self.update_project()
-		self.unassign_todo()
-		self.populate_depends_on()
+    def update_nsm_model(self):
+        frappe.utils.nestedset.update_nsm(self)
 
-	def unassign_todo(self):
-		if self.status == "Completed":
-			close_all_assignments(self.doctype, self.name)
-		if self.status == "Cancelled":
-			clear(self.doctype, self.name)
+    def on_update(self):
+        self.update_nsm_model()
+        self.check_recursion()
+        self.reschedule_dependent_tasks()
+        self.update_project()
+        self.unassign_todo()
+        self.populate_depends_on()
 
-	def update_total_expense_claim(self):
-		self.total_expense_claim = frappe.db.sql("""select sum(total_sanctioned_amount) from `tabExpense Claim`
-			where project = %s and task = %s and docstatus=1""",(self.project, self.name))[0][0]
+    def unassign_todo(self):
+        if self.status == "Completed":
+            close_all_assignments(self.doctype, self.name)
+        if self.status == "Cancelled":
+            clear(self.doctype, self.name)
 
-	def update_time_and_costing(self):
-		tl = frappe.db.sql("""select min(from_time) as start_date, max(to_time) as end_date,
-			sum(billing_amount) as total_billing_amount, sum(costing_amount) as total_costing_amount,
-			sum(hours) as time from `tabTimesheet Detail` where task = %s and docstatus=1"""
-			,self.name, as_dict=1)[0]
-		if self.status == "Open":
-			self.status = "Working"
-		self.total_costing_amount= tl.total_costing_amount
-		self.total_billing_amount= tl.total_billing_amount
-		self.actual_time= tl.time
-		self.act_start_date= tl.start_date
-		self.act_end_date= tl.end_date
+    def update_total_expense_claim(self):
+        self.total_expense_claim = frappe.db.sql("""select sum(total_sanctioned_amount) from `tabExpense Claim`
+            where project = %s and task = %s and docstatus=1""",(self.project, self.name))[0][0]
 
-	def update_project(self):
-		if self.project and not self.flags.from_project:
-			frappe.get_cached_doc("Project", self.project).update_project()
+    def update_time_and_costing(self):
+        tl = frappe.db.sql("""select min(from_time) as start_date, max(to_time) as end_date,
+            sum(billing_amount) as total_billing_amount, sum(costing_amount) as total_costing_amount,
+            sum(hours) as time from `tabTimesheet Detail` where task = %s and docstatus=1"""
+            ,self.name, as_dict=1)[0]
+        if self.status == "Open":
+            self.status = "Working"
+        self.total_costing_amount= tl.total_costing_amount
+        self.total_billing_amount= tl.total_billing_amount
+        self.actual_time= tl.time
+        self.act_start_date= tl.start_date
+        self.act_end_date= tl.end_date
 
-	def check_recursion(self):
-		if self.flags.ignore_recursion_check: return
-		check_list = [['task', 'parent'], ['parent', 'task']]
-		for d in check_list:
-			task_list, count = [self.name], 0
-			while (len(task_list) > count ):
-				tasks = frappe.db.sql(" select %s from `tabTask Depends On` where %s = %s " %
-					(d[0], d[1], '%s'), cstr(task_list[count]))
-				count = count + 1
-				for b in tasks:
-					if b[0] == self.name:
-						frappe.throw(_("Circular Reference Error"), CircularReferenceError)
-					if b[0]:
-						task_list.append(b[0])
+    def update_project(self):
+        if self.project and not self.flags.from_project:
+            frappe.get_cached_doc("Project", self.project).update_project()
 
-				if count == 15:
-					break
+    def check_recursion(self):
+        if self.flags.ignore_recursion_check: return
+        check_list = [['task', 'parent'], ['parent', 'task']]
+        for d in check_list:
+            task_list, count = [self.name], 0
+            while (len(task_list) > count ):
+                tasks = frappe.db.sql(" select %s from `tabTask Depends On` where %s = %s " %
+                    (d[0], d[1], '%s'), cstr(task_list[count]))
+                count = count + 1
+                for b in tasks:
+                    if b[0] == self.name:
+                        frappe.throw(_("Circular Reference Error"), CircularReferenceError)
+                    if b[0]:
+                        task_list.append(b[0])
 
-	def reschedule_dependent_tasks(self):
-		end_date = self.exp_end_date or self.act_end_date
-		if end_date:
-			for task_name in frappe.db.sql("""
-				select name from `tabTask` as parent
-				where parent.project = %(project)s
-					and parent.name in (
-						select parent from `tabTask Depends On` as child
-						where child.task = %(task)s and child.project = %(project)s)
-			""", {'project': self.project, 'task':self.name }, as_dict=1):
-				task = frappe.get_doc("Task", task_name.name)
-				if task.exp_start_date and task.exp_end_date and task.exp_start_date < getdate(end_date) and task.status == "Open":
-					task_duration = date_diff(task.exp_end_date, task.exp_start_date)
-					task.exp_start_date = add_days(end_date, 1)
-					task.exp_end_date = add_days(task.exp_start_date, task_duration)
-					task.flags.ignore_recursion_check = True
-					task.save()
+                if count == 15:
+                    break
 
-	def has_webform_permission(self):
-		project_user = frappe.db.get_value("Project User", {"parent": self.project, "user":frappe.session.user} , "user")
-		if project_user:
-			return True
+    def reschedule_dependent_tasks(self):
+        end_date = self.exp_end_date or self.act_end_date
+        if end_date:
+            for task_name in frappe.db.sql("""
+                select name from `tabTask` as parent
+                where parent.project = %(project)s
+                    and parent.name in (
+                        select parent from `tabTask Depends On` as child
+                        where child.task = %(task)s and child.project = %(project)s)
+            """, {'project': self.project, 'task':self.name }, as_dict=1):
+                task = frappe.get_doc("Task", task_name.name)
+                if task.exp_start_date and task.exp_end_date and task.exp_start_date < getdate(end_date) and task.status == "Open":
+                    task_duration = date_diff(task.exp_end_date, task.exp_start_date)
+                    task.exp_start_date = add_days(end_date, 1)
+                    task.exp_end_date = add_days(task.exp_start_date, task_duration)
+                    task.flags.ignore_recursion_check = True
+                    task.save()
 
-	def populate_depends_on(self):
-		if self.parent_task:
-			parent = frappe.get_doc('Task', self.parent_task)
-			if not self.name in [row.task for row in parent.depends_on]:
-				parent.append("depends_on", {
-					"doctype": "Task Depends On",
-					"task": self.name,
-					"subject": self.subject
-				})
-				parent.save()
+    def has_webform_permission(self):
+        project_user = frappe.db.get_value("Project User", {"parent": self.project, "user":frappe.session.user} , "user")
+        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."))
+    def populate_depends_on(self):
+        if self.parent_task:
+            parent = frappe.get_doc('Task', self.parent_task)
+            if self.name not in [row.task for row in parent.depends_on]:
+                parent.append("depends_on", {
+                    "doctype": "Task Depends On",
+                    "task": self.name,
+                    "subject": self.subject
+                })
+                parent.save()
 
-		self.update_nsm_model()
+    def on_trash(self):
+        if check_if_child_exists(self.name):
+            throw(_("Child Task exists for this Task. You can not delete this Task."))
 
-	def after_delete(self):
-		self.update_project()
+        self.update_nsm_model()
 
-	def update_status(self):
-		if self.status not in ('Cancelled', 'Completed') and self.exp_end_date:
-			from datetime import datetime
-			if self.exp_end_date < datetime.now().date():
-				self.db_set('status', 'Overdue', update_modified=False)
-				self.update_project()
+    def after_delete(self):
+        self.update_project()
+
+    def update_status(self):
+        if self.status not in ('Cancelled', 'Completed') and self.exp_end_date:
+            from datetime import datetime
+            if self.exp_end_date < datetime.now().date():
+                self.db_set('status', 'Overdue', update_modified=False)
+                self.update_project()
 
 @frappe.whitelist()
 def check_if_child_exists(name):
-	child_tasks = frappe.get_all("Task", filters={"parent_task": name})
-	child_tasks = [get_link_to_form("Task", task.name) for task in child_tasks]
-	return child_tasks
+    child_tasks = frappe.get_all("Task", filters={"parent_task": name})
+    child_tasks = [get_link_to_form("Task", task.name) for task in child_tasks]
+    return child_tasks
 
 
 @frappe.whitelist()
 @frappe.validate_and_sanitize_search_inputs
 def get_project(doctype, txt, searchfield, start, page_len, filters):
-	from erpnext.controllers.queries import get_match_cond
-	return frappe.db.sql(""" select name from `tabProject`
-			where %(key)s like %(txt)s
-				%(mcond)s
-			order by name
-			limit %(start)s, %(page_len)s""" % {
-				'key': searchfield,
-				'txt': frappe.db.escape('%' + txt + '%'),
-				'mcond':get_match_cond(doctype),
-				'start': start,
-				'page_len': page_len
-			})
+    from erpnext.controllers.queries import get_match_cond
+    return frappe.db.sql(""" select name from `tabProject`
+            where %(key)s like %(txt)s
+                %(mcond)s
+            order by name
+            limit %(start)s, %(page_len)s""" % {
+                'key': searchfield,
+                'txt': frappe.db.escape('%' + txt + '%'),
+                'mcond':get_match_cond(doctype),
+                'start': start,
+                'page_len': page_len
+            })
 
 
 @frappe.whitelist()
 def set_multiple_status(names, status):
-	names = json.loads(names)
-	for name in names:
-		task = frappe.get_doc("Task", name)
-		task.status = status
-		task.save()
+    names = json.loads(names)
+    for name in names:
+        task = frappe.get_doc("Task", name)
+        task.status = status
+        task.save()
 
 def set_tasks_as_overdue():
-	tasks = frappe.get_all("Task", filters={"status": ["not in", ["Cancelled", "Completed"]]}, fields=["name", "status", "review_date"])
-	for task in tasks:
-		if task.status == "Pending Review":
-			if getdate(task.review_date) > getdate(today()):
-				continue
-		frappe.get_doc("Task", task.name).update_status()
+    tasks = frappe.get_all("Task", filters={"status": ["not in", ["Cancelled", "Completed"]]}, fields=["name", "status", "review_date"])
+    for task in tasks:
+        if task.status == "Pending Review":
+            if getdate(task.review_date) > getdate(today()):
+                continue
+        frappe.get_doc("Task", task.name).update_status()
 
 
 @frappe.whitelist()
 def make_timesheet(source_name, target_doc=None, ignore_permissions=False):
-	def set_missing_values(source, target):
-		target.append("time_logs", {
-			"hours": source.actual_time,
-			"completed": source.status == "Completed",
-			"project": source.project,
-			"task": source.name
-		})
+    def set_missing_values(source, target):
+        target.append("time_logs", {
+            "hours": source.actual_time,
+            "completed": source.status == "Completed",
+            "project": source.project,
+            "task": source.name
+        })
 
-	doclist = get_mapped_doc("Task", source_name, {
-			"Task": {
-				"doctype": "Timesheet"
-			}
-		}, target_doc, postprocess=set_missing_values, ignore_permissions=ignore_permissions)
+    doclist = get_mapped_doc("Task", source_name, {
+            "Task": {
+                "doctype": "Timesheet"
+            }
+        }, target_doc, postprocess=set_missing_values, ignore_permissions=ignore_permissions)
 
-	return doclist
+    return doclist
 
 
 @frappe.whitelist()
 def get_children(doctype, parent, task=None, project=None, is_root=False):
 
-	filters = [['docstatus', '<', '2']]
+    filters = [['docstatus', '<', '2']]
 
-	if task:
-		filters.append(['parent_task', '=', task])
-	elif parent and not is_root:
-		# via expand child
-		filters.append(['parent_task', '=', parent])
-	else:
-		filters.append(['ifnull(`parent_task`, "")', '=', ''])
+    if task:
+        filters.append(['parent_task', '=', task])
+    elif parent and not is_root:
+        # via expand child
+        filters.append(['parent_task', '=', parent])
+    else:
+        filters.append(['ifnull(`parent_task`, "")', '=', ''])
 
-	if project:
-		filters.append(['project', '=', project])
+    if project:
+        filters.append(['project', '=', project])
 
-	tasks = frappe.get_list(doctype, fields=[
-		'name as value',
-		'subject as title',
-		'is_group as expandable'
-	], filters=filters, order_by='name')
+    tasks = frappe.get_list(doctype, fields=[
+        'name as value',
+        'subject as title',
+        'is_group as expandable'
+    ], filters=filters, order_by='name')
 
-	# return tasks
-	return tasks
+    # 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)
+    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 == 'All Tasks' or args.parent_task == args.project:
-		args.parent_task = None
+    if args.parent_task == 'All Tasks' or args.parent_task == args.project:
+        args.parent_task = None
 
-	frappe.get_doc(args).insert()
+    frappe.get_doc(args).insert()
 
 @frappe.whitelist()
 def add_multiple_tasks(data, parent):
-	data = json.loads(data)
-	new_doc = {'doctype': 'Task', 'parent_task': parent if parent!="All Tasks" else ""}
-	new_doc['project'] = frappe.db.get_value('Task', {"name": parent}, 'project') or ""
+    data = json.loads(data)
+    new_doc = {'doctype': 'Task', 'parent_task': parent if parent!="All Tasks" else ""}
+    new_doc['project'] = frappe.db.get_value('Task', {"name": parent}, 'project') or ""
 
-	for d in data:
-		if not d.get("subject"): continue
-		new_doc['subject'] = d.get("subject")
-		new_task = frappe.get_doc(new_doc)
-		new_task.insert()
+    for d in data:
+        if not d.get("subject"): continue
+        new_doc['subject'] = d.get("subject")
+        new_task = frappe.get_doc(new_doc)
+        new_task.insert()
 
 def on_doctype_update():
-	frappe.db.add_index("Task", ["lft", "rgt"])
+    frappe.db.add_index("Task", ["lft", "rgt"])
 
 def validate_project_dates(project_end_date, task, task_start, task_end, actual_or_expected_date):
-	if task.get(task_start) and date_diff(project_end_date, getdate(task.get(task_start))) < 0:
-		frappe.throw(_("Task's {0} Start Date cannot be after Project's End Date.").format(actual_or_expected_date))
+    if task.get(task_start) and date_diff(project_end_date, getdate(task.get(task_start))) < 0:
+        frappe.throw(_("Task's {0} Start Date cannot be after Project's End Date.").format(actual_or_expected_date))
 
-	if task.get(task_end) and date_diff(project_end_date, getdate(task.get(task_end))) < 0:
-		frappe.throw(_("Task's {0} End Date cannot be after Project's End Date.").format(actual_or_expected_date))
+    if task.get(task_end) and date_diff(project_end_date, getdate(task.get(task_end))) < 0:
+        frappe.throw(_("Task's {0} End Date cannot be after Project's End Date.").format(actual_or_expected_date))
diff --git a/erpnext/projects/doctype/task/task_list.js b/erpnext/projects/doctype/task/task_list.js
index 941fe97..39734ee 100644
--- a/erpnext/projects/doctype/task/task_list.js
+++ b/erpnext/projects/doctype/task/task_list.js
@@ -20,7 +20,8 @@
 			"Pending Review": "orange",
 			"Working": "orange",
 			"Completed": "green",
-			"Cancelled": "dark grey"
+			"Cancelled": "dark grey",
+			"Template": "blue"
 		}
 		return [__(doc.status), colors[doc.status], "status,=," + doc.status];
 	},
diff --git a/erpnext/projects/doctype/task/test_task.py b/erpnext/projects/doctype/task/test_task.py
index 47a28fd..25714f8 100644
--- a/erpnext/projects/doctype/task/test_task.py
+++ b/erpnext/projects/doctype/task/test_task.py
@@ -97,14 +97,19 @@
 
 		self.assertEqual(frappe.db.get_value("Task", task.name, "status"), "Overdue")
 
-def create_task(subject, start=None, end=None, depends_on=None, project=None, save=True):
+def create_task(subject, start=None, end=None, depends_on=None, project=None, parent_task=None, is_group=0, is_template=0, begin=0, duration=0, save=True):
 	if not frappe.db.exists("Task", subject):
 		task = frappe.new_doc('Task')
 		task.status = "Open"
 		task.subject = subject
 		task.exp_start_date = start or nowdate()
 		task.exp_end_date = end or nowdate()
-		task.project = project or "_Test Project"
+		task.project = project or None if is_template else "_Test Project"
+		task.is_template = is_template
+		task.start = begin
+		task.duration = duration
+		task.is_group = is_group
+		task.parent_task = parent_task
 		if save:
 			task.save()
 	else:
@@ -116,5 +121,4 @@
 		})
 		if save:
 			task.save()
-
 	return task