diff --git a/erpnext/projects/doctype/dependent_task/__init__.py b/erpnext/projects/doctype/dependent_task/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/projects/doctype/dependent_task/dependent_task.json b/erpnext/projects/doctype/dependent_task/dependent_task.json new file mode 100644 index 0000000000..c649b53b17 --- /dev/null +++ b/erpnext/projects/doctype/dependent_task/dependent_task.json @@ -0,0 +1,50 @@ +{ + "allow_copy": 0, + "allow_import": 0, + "allow_rename": 0, + "creation": "2015-04-29 04:52:48.868079", + "custom": 0, + "docstatus": 0, + "doctype": "DocType", + "document_type": "", + "fields": [ + { + "allow_on_submit": 0, + "fieldname": "task", + "fieldtype": "Link", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 1, + "label": "Task", + "no_copy": 0, + "options": "Task", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0 + } + ], + "hide_heading": 0, + "hide_toolbar": 0, + "in_create": 0, + "in_dialog": 0, + "is_submittable": 0, + "issingle": 0, + "istable": 1, + "modified": "2015-04-29 04:54:36.024844", + "modified_by": "Administrator", + "module": "Projects", + "name": "Dependent Task", + "name_case": "", + "owner": "Administrator", + "permissions": [], + "read_only": 0, + "read_only_onload": 0, + "sort_field": "modified", + "sort_order": "DESC" +} \ No newline at end of file diff --git a/erpnext/projects/doctype/dependent_task/dependent_task.py b/erpnext/projects/doctype/dependent_task/dependent_task.py new file mode 100644 index 0000000000..90a96ac1b7 --- /dev/null +++ b/erpnext/projects/doctype/dependent_task/dependent_task.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +from frappe.model.document import Document + +class DependentTask(Document): + pass diff --git a/erpnext/projects/doctype/task/task.js b/erpnext/projects/doctype/task/task.js index 975633186b..d9a611eed6 100644 --- a/erpnext/projects/doctype/task/task.js +++ b/erpnext/projects/doctype/task/task.js @@ -40,6 +40,7 @@ erpnext.projects.Task = frappe.ui.form.Controller.extend({ } }); +cur_frm.add_fetch('task', 'subject', 'subject'); cur_frm.cscript = new erpnext.projects.Task({frm: cur_frm}); diff --git a/erpnext/projects/doctype/task/task.json b/erpnext/projects/doctype/task/task.json index fc604c264c..ddcc48b2d3 100644 --- a/erpnext/projects/doctype/task/task.json +++ b/erpnext/projects/doctype/task/task.json @@ -78,12 +78,26 @@ "width": "300px" }, { - "fieldname": "time_and_budget", + "fieldname": "section_break", "fieldtype": "Section Break", - "label": "", + "label": "Depends On", "oldfieldtype": "Section Break", "permlevel": 0 }, + { + "fieldname": "depends_on", + "fieldtype": "Table", + "label": "depends_on", + "options": "Task Depends On", + "permlevel": 0, + "precision": "" + }, + { + "fieldname": "section_break_10", + "fieldtype": "Section Break", + "permlevel": 0, + "precision": "" + }, { "fieldname": "exp_start_date", "fieldtype": "Date", @@ -249,7 +263,7 @@ "idx": 1, "istable": 0, "max_attachments": 5, - "modified": "2015-04-14 07:56:24.481667", + "modified": "2015-04-30 05:48:55.176993", "modified_by": "Administrator", "module": "Projects", "name": "Task", diff --git a/erpnext/projects/doctype/task/task.py b/erpnext/projects/doctype/task/task.py index a03340f7de..456c40a97e 100644 --- a/erpnext/projects/doctype/task/task.py +++ b/erpnext/projects/doctype/task/task.py @@ -4,12 +4,13 @@ from __future__ import unicode_literals import frappe, json -from frappe.utils import getdate +from frappe.utils import getdate, date_diff, add_days, cstr from frappe import _ - from frappe.model.document import Document +class CircularReferenceError(frappe.ValidationError): pass + class Task(Document): def get_feed(self): return '{0}: {1}'.format(_(self.status), self.subject) @@ -36,6 +37,8 @@ class Task(Document): frappe.throw(_("'Actual Start Date' can not be greater than 'Actual End Date'")) def on_update(self): + self.check_recursion() + self.reschedule_dependent_tasks() self.update_percentage() self.update_project() @@ -52,8 +55,8 @@ class Task(Document): 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 `tabTime Log` where project = %s and task = %s and docstatus=1""", - (self.project, self.name),as_dict=1)[0] + sum(hours) as time from `tabTime Log` 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 @@ -68,6 +71,36 @@ class Task(Document): project.flags.dont_sync_tasks = True project.update_costing() project.save() + + 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]) + if count == 15: + break + + 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 %s in \ + (select task from `tabTask Depends On` as child where parent.name = child.parent )", 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() @frappe.whitelist() def get_events(start, end, filters=None): diff --git a/erpnext/projects/doctype/task/test_task.py b/erpnext/projects/doctype/task/test_task.py index 62e560fbe7..8880763fa2 100644 --- a/erpnext/projects/doctype/task/test_task.py +++ b/erpnext/projects/doctype/task/test_task.py @@ -1,7 +1,148 @@ # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # License: GNU General Public License v3. See license.txt from __future__ import unicode_literals - - import frappe -test_records = frappe.get_test_records('Task') +import unittest +from frappe.utils import getdate + +# test_records = frappe.get_test_records('Task') + +from erpnext.projects.doctype.task.task import CircularReferenceError + +class TestTask(unittest.TestCase): + def test_circular_reference(self): + + task1 = frappe.new_doc('Task') + task1.update({ + "status": "Open", + "subject": "_Test Task 1", + "exp_start_date": "2015-1-1", + "exp_end_date": "2015-1-10" + }) + task1.save() + + task2 = frappe.new_doc('Task') + task2.update({ + "status": "Open", + "subject": "_Test Task 2", + "exp_start_date": "2015-1-11", + "exp_end_date": "2015-1-15", + "depends_on":[ + { + "task": task1.name + } + ] + }) + task2.save() + + task3 = frappe.new_doc('Task') + task3.update({ + "status": "Open", + "subject": "_Test Task 2", + "exp_start_date": "2015-1-11", + "exp_end_date": "2015-1-15", + "depends_on":[ + { + "task": task2.name + } + ] + }) + task3.save() + + task1.append("depends_on", { + "task": task3.name + }) + self.assertRaises(CircularReferenceError, task1.save) + + task1.set("depends_on", []) + task1.save() + + task4 = frappe.new_doc('Task') + task4.update({ + "status": "Open", + "subject": "_Test Task 1", + "exp_start_date": "2015-1-1", + "exp_end_date": "2015-1-15", + "depends_on":[ + { + "task": task1.name + } + ] + }) + task4.save() + + task3.append("depends_on", { + "task": task4.name + }) + + def test_reschedule_dependent_task(self): + task1 = frappe.new_doc('Task') + task1.update({ + "status": "Open", + "subject": "_Test Task 1", + "exp_start_date": "2015-1-1", + "exp_end_date": "2015-1-10" + }) + task1.save() + + task2 = frappe.new_doc('Task') + task2.update({ + "status": "Open", + "subject": "_Test Task 2", + "exp_start_date": "2015-1-11", + "exp_end_date": "2015-1-15", + "depends_on":[ + { + "task": task1.name + } + ] + }) + task2.save() + + task3 = frappe.new_doc('Task') + task3.update({ + "status": "Open", + "subject": "_Test Task 3", + "exp_start_date": "2015-1-16", + "exp_end_date": "2015-1-18", + "depends_on":[ + { + "task": task2.name + } + ] + }) + task3.save() + + task1.update({ + "exp_end_date": "2015-1-20" + }) + task1.save() + + self.assertEqual(frappe.db.get_value("Task", task2.name, "exp_start_date"), getdate('2015-1-21')) + self.assertEqual(frappe.db.get_value("Task", task2.name, "exp_end_date"), getdate('2015-1-25')) + + self.assertEqual(frappe.db.get_value("Task", task3.name, "exp_start_date"), getdate('2015-1-26')) + self.assertEqual(frappe.db.get_value("Task", task3.name, "exp_end_date"), getdate('2015-1-28')) + + time_log = frappe.new_doc('Time Log') + time_log.update({ + "from_time": "2015-1-1", + "to_time": "2015-1-20", + "task": task1.name + }) + time_log.submit() + + self.assertEqual(frappe.db.get_value("Task", task2.name, "exp_start_date"), getdate('2015-1-21')) + self.assertEqual(frappe.db.get_value("Task", task2.name, "exp_end_date"), getdate('2015-1-25')) + + self.assertEqual(frappe.db.get_value("Task", task3.name, "exp_start_date"), getdate('2015-1-26')) + self.assertEqual(frappe.db.get_value("Task", task3.name, "exp_end_date"), getdate('2015-1-28')) + + time_log.cancel() + + self.assertEqual(frappe.db.get_value("Task", task2.name, "exp_start_date"), getdate('2015-1-21')) + self.assertEqual(frappe.db.get_value("Task", task2.name, "exp_end_date"), getdate('2015-1-25')) + + self.assertEqual(frappe.db.get_value("Task", task3.name, "exp_start_date"), getdate('2015-1-26')) + self.assertEqual(frappe.db.get_value("Task", task3.name, "exp_end_date"), getdate('2015-1-28')) + \ No newline at end of file diff --git a/erpnext/projects/doctype/task_depends_on/__init__.py b/erpnext/projects/doctype/task_depends_on/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/projects/doctype/task_depends_on/task_depends_on.json b/erpnext/projects/doctype/task_depends_on/task_depends_on.json new file mode 100644 index 0000000000..7a960c1c41 --- /dev/null +++ b/erpnext/projects/doctype/task_depends_on/task_depends_on.json @@ -0,0 +1,66 @@ +{ + "allow_copy": 0, + "allow_import": 0, + "allow_rename": 0, + "creation": "2015-04-29 04:52:48.868079", + "custom": 0, + "docstatus": 0, + "doctype": "DocType", + "document_type": "", + "fields": [ + { + "allow_on_submit": 0, + "fieldname": "task", + "fieldtype": "Link", + "hidden": 0, + "ignore_user_permissions": 0, + "in_filter": 0, + "in_list_view": 1, + "label": "Task", + "no_copy": 0, + "options": "Task", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0 + }, + { + "fieldname": "column_break_2", + "fieldtype": "Column Break", + "permlevel": 0, + "precision": "" + }, + { + "fieldname": "subject", + "fieldtype": "Text", + "in_list_view": 1, + "label": "Subject", + "options": "", + "permlevel": 0, + "precision": "", + "read_only": 1 + } + ], + "hide_heading": 0, + "hide_toolbar": 0, + "in_create": 0, + "in_dialog": 0, + "is_submittable": 0, + "issingle": 0, + "istable": 1, + "modified": "2015-04-30 05:52:16.250948", + "modified_by": "Administrator", + "module": "Projects", + "name": "Task Depends On", + "name_case": "", + "owner": "Administrator", + "permissions": [], + "read_only": 0, + "read_only_onload": 0, + "sort_field": "modified", + "sort_order": "DESC" +} \ No newline at end of file diff --git a/erpnext/projects/doctype/task_depends_on/task_depends_on.py b/erpnext/projects/doctype/task_depends_on/task_depends_on.py new file mode 100644 index 0000000000..723a0fc339 --- /dev/null +++ b/erpnext/projects/doctype/task_depends_on/task_depends_on.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +from frappe.model.document import Document + +class TaskDependsOn(Document): + pass diff --git a/erpnext/projects/doctype/time_log/test_time_log.py b/erpnext/projects/doctype/time_log/test_time_log.py index 3d9e0be5f9..9b43b0dffe 100644 --- a/erpnext/projects/doctype/time_log/test_time_log.py +++ b/erpnext/projects/doctype/time_log/test_time_log.py @@ -1,6 +1,5 @@ # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # License: GNU General Public License v3. See license.txt -from __future__ import unicode_literals from __future__ import unicode_literals import frappe