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",