diff --git a/erpnext/projects/doctype/task/task.json b/erpnext/projects/doctype/task/task.json index 160cc5812f..ef4740d9ee 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 855ff5f83e..d1583f1473 100755 --- a/erpnext/projects/doctype/task/task.py +++ b/erpnext/projects/doctype/task/task.py @@ -36,6 +36,7 @@ class Task(NestedSet): 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 @@ class Task(NestedSet): dependent_task_format = """{0}""".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 0000000000..e69de29bb2 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 0000000000..5aa44c0a8c --- /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 = `
${value}
`; + } else { + value = `${value}
`; + } + } + 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 0000000000..100c422433 --- /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 0000000000..cdabe6487e --- /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 0000000000..dbeedb4be9 --- /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 dbbd7e1458..0ec17029a2 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",