feat: Delayed Tasks Summary (#25024)

* feat: delayed deliverables summary

* fix: sider

* fix: renamed to delayed tasks

* fix: renamed test

* fix: test

* fix: sider

* fix: dates, validations and chart

* fix: space and column width

* feat: Sort tasks by descending order of delay

Co-authored-by: Rucha Mahabal <ruchamahabal2@gmail.com>
diff --git a/erpnext/projects/doctype/task/task.json b/erpnext/projects/doctype/task/task.json
index 160cc58..ef4740d 100644
--- a/erpnext/projects/doctype/task/task.json
+++ b/erpnext/projects/doctype/task/task.json
@@ -11,15 +11,16 @@
   "project",
   "issue",
   "type",
+  "color",
   "is_group",
   "is_template",
   "column_break0",
   "status",
   "priority",
   "task_weight",
-  "completed_by",
-  "color",
   "parent_task",
+  "completed_by",
+  "completed_on",
   "sb_timeline",
   "exp_start_date",
   "expected_time",
@@ -358,6 +359,7 @@
    "read_only": 1
   },
   {
+   "depends_on": "eval: doc.status == \"Completed\"",
    "fieldname": "completed_by",
    "fieldtype": "Link",
    "label": "Completed By",
@@ -381,6 +383,13 @@
    "fieldname": "duration",
    "fieldtype": "Int",
    "label": "Duration (Days)"
+  },
+  {
+   "depends_on": "eval: doc.status == \"Completed\"",
+   "fieldname": "completed_on",
+   "fieldtype": "Date",
+   "label": "Completed On",
+   "mandatory_depends_on": "eval: doc.status == \"Completed\""
   }
  ],
  "icon": "fa fa-check",
@@ -388,7 +397,7 @@
  "is_tree": 1,
  "links": [],
  "max_attachments": 5,
- "modified": "2020-12-28 11:32:58.714991",
+ "modified": "2021-04-16 12:46:51.556741",
  "modified_by": "Administrator",
  "module": "Projects",
  "name": "Task",
diff --git a/erpnext/projects/doctype/task/task.py b/erpnext/projects/doctype/task/task.py
index 855ff5f..d1583f1 100755
--- a/erpnext/projects/doctype/task/task.py
+++ b/erpnext/projects/doctype/task/task.py
@@ -36,6 +36,7 @@
 		self.validate_status()
 		self.update_depends_on()
 		self.validate_dependencies_for_template_task()
+		self.validate_completed_on()
 
 	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):
@@ -100,6 +101,10 @@
 					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 validate_completed_on(self):
+		if self.completed_on and getdate(self.completed_on) > getdate():
+			frappe.throw(_("Completed On cannot be greater than Today"))
+
 	def update_depends_on(self):
 		depends_on_tasks = self.depends_on_tasks or ""
 		for d in self.depends_on:
diff --git a/erpnext/projects/report/delayed_tasks_summary/__init__.py b/erpnext/projects/report/delayed_tasks_summary/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/projects/report/delayed_tasks_summary/__init__.py
diff --git a/erpnext/projects/report/delayed_tasks_summary/delayed_tasks_summary.js b/erpnext/projects/report/delayed_tasks_summary/delayed_tasks_summary.js
new file mode 100644
index 0000000..5aa44c0
--- /dev/null
+++ b/erpnext/projects/report/delayed_tasks_summary/delayed_tasks_summary.js
@@ -0,0 +1,41 @@
+// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+/* eslint-disable */
+
+frappe.query_reports["Delayed Tasks Summary"] = {
+	"filters": [
+		{
+			"fieldname": "from_date",
+			"label": __("From Date"),
+			"fieldtype": "Date"
+		},
+		{
+			"fieldname": "to_date",
+			"label": __("To Date"),
+			"fieldtype": "Date"
+		},
+		{
+			"fieldname": "priority",
+			"label": __("Priority"),
+			"fieldtype": "Select",
+			"options": ["", "Low", "Medium", "High", "Urgent"]
+		},
+		{
+			"fieldname": "status",
+			"label": __("Status"),
+			"fieldtype": "Select",
+			"options": ["", "Open", "Working","Pending Review","Overdue","Completed"]
+		},
+	],
+	"formatter": function(value, row, column, data, default_formatter) {
+		value = default_formatter(value, row, column, data);
+		if (column.id == "delay") {
+			if (data["delay"] > 0) {
+				value = `<p style="color: red; font-weight: bold">${value}</p>`;
+			} else {
+				value = `<p style="color: green; font-weight: bold">${value}</p>`;
+			}
+		}
+		return value
+	}
+};
diff --git a/erpnext/projects/report/delayed_tasks_summary/delayed_tasks_summary.json b/erpnext/projects/report/delayed_tasks_summary/delayed_tasks_summary.json
new file mode 100644
index 0000000..100c422
--- /dev/null
+++ b/erpnext/projects/report/delayed_tasks_summary/delayed_tasks_summary.json
@@ -0,0 +1,29 @@
+{
+ "add_total_row": 0,
+ "columns": [],
+ "creation": "2021-03-25 15:03:19.857418",
+ "disable_prepared_report": 0,
+ "disabled": 0,
+ "docstatus": 0,
+ "doctype": "Report",
+ "filters": [],
+ "idx": 0,
+ "is_standard": "Yes",
+ "modified": "2021-04-15 15:49:35.432486",
+ "modified_by": "Administrator",
+ "module": "Projects",
+ "name": "Delayed Tasks Summary",
+ "owner": "Administrator",
+ "prepared_report": 0,
+ "ref_doctype": "Task",
+ "report_name": "Delayed Tasks Summary",
+ "report_type": "Script Report",
+ "roles": [
+  {
+   "role": "Projects User"
+  },
+  {
+   "role": "Projects Manager"
+  }
+ ]
+}
\ No newline at end of file
diff --git a/erpnext/projects/report/delayed_tasks_summary/delayed_tasks_summary.py b/erpnext/projects/report/delayed_tasks_summary/delayed_tasks_summary.py
new file mode 100644
index 0000000..cdabe64
--- /dev/null
+++ b/erpnext/projects/report/delayed_tasks_summary/delayed_tasks_summary.py
@@ -0,0 +1,133 @@
+# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+import frappe
+from frappe.utils import date_diff, nowdate
+
+def execute(filters=None):
+	columns, data = [], []
+	data = get_data(filters)
+	columns = get_columns()
+	charts = get_chart_data(data)
+	return columns, data, None, charts
+
+def get_data(filters):
+	conditions = get_conditions(filters)
+	tasks = frappe.get_all("Task",
+			filters = conditions,
+			fields = ["name", "subject", "exp_start_date", "exp_end_date",
+					"status", "priority", "completed_on", "progress"],
+			order_by="creation"
+		)
+	for task in tasks:
+		if task.exp_end_date:
+			if task.completed_on:
+				task.delay = date_diff(task.completed_on, task.exp_end_date)
+			elif task.status == "Completed":
+				# task is completed but completed on is not set (for older tasks)
+				task.delay = 0
+			else:
+				# task not completed
+				task.delay = date_diff(nowdate(), task.exp_end_date)
+		else:
+			# task has no end date, hence no delay
+			task.delay = 0
+
+	# Sort by descending order of delay
+	tasks.sort(key=lambda x: x["delay"], reverse=True)
+	return tasks
+
+def get_conditions(filters):
+	conditions = frappe._dict()
+	keys = ["priority", "status"]
+	for key in keys:
+		if filters.get(key):
+			conditions[key] = filters.get(key)
+	if filters.get("from_date"):
+		conditions.exp_end_date = [">=", filters.get("from_date")]
+	if filters.get("to_date"):
+		conditions.exp_start_date = ["<=", filters.get("to_date")]
+	return conditions
+
+def get_chart_data(data):
+	delay, on_track = 0, 0
+	for entry in data:
+		if entry.get("delay") > 0:
+			delay = delay + 1
+		else:
+			on_track = on_track + 1
+	charts = {
+		"data": {
+			"labels": ["On Track", "Delayed"],
+			"datasets": [
+				{
+					"name": "Delayed",
+					"values": [on_track, delay]
+				}
+			]
+		},
+		"type": "percentage",
+		"colors": ["#84D5BA", "#CB4B5F"]
+	}
+	return charts
+
+def get_columns():
+	columns = [
+		{
+			"fieldname": "name",
+			"fieldtype": "Link",
+			"label": "Task",
+			"options": "Task",
+			"width": 150
+		},
+		{
+			"fieldname": "subject",
+			"fieldtype": "Data",
+			"label": "Subject",
+			"width": 200
+		},
+		{
+			"fieldname": "status",
+			"fieldtype": "Data",
+			"label": "Status",
+			"width": 100
+		},
+		{
+			"fieldname": "priority",
+			"fieldtype": "Data",
+			"label": "Priority",
+			"width": 80
+		},
+		{
+			"fieldname": "progress",
+			"fieldtype": "Data",
+			"label": "Progress (%)",
+			"width": 120
+		},
+		{
+			"fieldname": "exp_start_date",
+			"fieldtype": "Date",
+			"label": "Expected Start Date",
+			"width": 150
+		},
+		{
+			"fieldname": "exp_end_date",
+			"fieldtype": "Date",
+			"label": "Expected End Date",
+			"width": 150
+		},
+		{
+			"fieldname": "completed_on",
+			"fieldtype": "Date",
+			"label": "Actual End Date",
+			"width": 130
+		},
+		{
+			"fieldname": "delay",
+			"fieldtype": "Data",
+			"label": "Delay (In Days)",
+			"width": 120
+		}
+	]
+	return columns
diff --git a/erpnext/projects/report/delayed_tasks_summary/test_delayed_tasks_summary.py b/erpnext/projects/report/delayed_tasks_summary/test_delayed_tasks_summary.py
new file mode 100644
index 0000000..dbeedb4
--- /dev/null
+++ b/erpnext/projects/report/delayed_tasks_summary/test_delayed_tasks_summary.py
@@ -0,0 +1,54 @@
+from __future__ import unicode_literals
+import unittest
+import frappe
+from frappe.utils import nowdate, add_days, add_months
+from erpnext.projects.doctype.task.test_task import create_task
+from erpnext.projects.report.delayed_tasks_summary.delayed_tasks_summary import execute
+
+class TestDelayedTasksSummary(unittest.TestCase):
+	@classmethod
+	def setUp(self):
+		task1 = create_task("_Test Task 98", add_days(nowdate(), -10), nowdate())
+		create_task("_Test Task 99", add_days(nowdate(), -10), add_days(nowdate(), -1))
+		
+		task1.status = "Completed"
+		task1.completed_on = add_days(nowdate(), -1)
+		task1.save()
+
+	def test_delayed_tasks_summary(self):
+		filters = frappe._dict({
+			"from_date": add_months(nowdate(), -1),
+			"to_date": nowdate(),
+			"priority": "Low",
+			"status": "Open"
+		})
+		expected_data = [
+			{
+				"subject": "_Test Task 99",
+				"status": "Open",
+				"priority": "Low",
+				"delay": 1
+			},
+			{
+				"subject": "_Test Task 98",
+				"status": "Completed",
+				"priority": "Low",
+				"delay": -1
+			}
+		]
+		report = execute(filters)
+		data = list(filter(lambda x: x.subject == "_Test Task 99", report[1]))[0]
+		
+		for key in ["subject", "status", "priority", "delay"]:
+			self.assertEqual(expected_data[0].get(key), data.get(key))
+
+		filters.status = "Completed"
+		report = execute(filters)
+		data = list(filter(lambda x: x.subject == "_Test Task 98", report[1]))[0]
+
+		for key in ["subject", "status", "priority", "delay"]:
+			self.assertEqual(expected_data[1].get(key), data.get(key))
+
+	def tearDown(self):
+		for task in ["_Test Task 98", "_Test Task 99"]:
+			frappe.get_doc("Task", {"subject": task}).delete()
\ No newline at end of file
diff --git a/erpnext/projects/workspace/projects/projects.json b/erpnext/projects/workspace/projects/projects.json
index dbbd7e1..0ec1702 100644
--- a/erpnext/projects/workspace/projects/projects.json
+++ b/erpnext/projects/workspace/projects/projects.json
@@ -15,6 +15,7 @@
  "hide_custom": 0,
  "icon": "project",
  "idx": 0,
+ "is_default": 0,
  "is_standard": 1,
  "label": "Projects",
  "links": [
@@ -148,9 +149,19 @@
    "link_type": "Report",
    "onboard": 0,
    "type": "Link"
+  },
+  {
+   "dependencies": "Task",
+   "hidden": 0,
+   "is_query_report": 1,
+   "label": "Delayed Tasks Summary",
+   "link_to": "Delayed Tasks Summary",
+   "link_type": "Report",
+   "onboard": 0,
+   "type": "Link"
   }
  ],
- "modified": "2020-12-01 13:38:37.856224",
+ "modified": "2021-03-26 16:32:00.628561",
  "modified_by": "Administrator",
  "module": "Projects",
  "name": "Projects",