Converting Task to a Tree structure (#11117)

* added support for tree view

* nestedset added to handle tree based structure

* treeview ui added

* removed is_group dependency

* added validation while editing a group-task

* codacy fix

* BOM like filter added

* Added ui-test for treeview-task
This commit is contained in:
Zarrar 2017-10-31 13:10:55 +05:30 committed by Rushabh Mehta
parent 160e710ebf
commit 6d41a9a647
8 changed files with 457 additions and 37 deletions

View File

@ -15,6 +15,7 @@ def get_data():
{
"type": "doctype",
"name": "Task",
"route": "Tree/Task",
"description": _("Project activity / task."),
},
{

View File

@ -19,38 +19,47 @@ frappe.ui.form.on("Task", {
},
refresh: function(frm) {
var doc = frm.doc;
if(doc.__islocal) {
if(!frm.doc.exp_end_date) {
frm.set_value("exp_end_date", frappe.datetime.add_days(new Date(), 7));
frm.fields_dict['parent_task'].get_query = function() {
return {
filters: {
"is_group": 1,
}
}
}
if(!doc.__islocal) {
if(frappe.model.can_read("Timesheet")) {
frm.add_custom_button(__("Timesheet"), function() {
frappe.route_options = {"project": doc.project, "task": doc.name}
frappe.set_route("List", "Timesheet");
}, __("View"), true);
}
if(frappe.model.can_read("Expense Claim")) {
frm.add_custom_button(__("Expense Claims"), function() {
frappe.route_options = {"project": doc.project, "task": doc.name}
frappe.set_route("List", "Expense Claim");
}, __("View"), true);
if(!frm.is_group){
var doc = frm.doc;
if(doc.__islocal) {
if(!frm.doc.exp_end_date) {
frm.set_value("exp_end_date", frappe.datetime.add_days(new Date(), 7));
}
}
if(frm.perm[0].write) {
if(frm.doc.status!=="Closed" && frm.doc.status!=="Cancelled") {
frm.add_custom_button(__("Close"), function() {
frm.set_value("status", "Closed");
frm.save();
});
} else {
frm.add_custom_button(__("Reopen"), function() {
frm.set_value("status", "Open");
frm.save();
});
if(!doc.__islocal) {
if(frappe.model.can_read("Timesheet")) {
frm.add_custom_button(__("Timesheet"), function() {
frappe.route_options = {"project": doc.project, "task": doc.name}
frappe.set_route("List", "Timesheet");
}, __("View"), true);
}
if(frappe.model.can_read("Expense Claim")) {
frm.add_custom_button(__("Expense Claims"), function() {
frappe.route_options = {"project": doc.project, "task": doc.name}
frappe.set_route("List", "Expense Claim");
}, __("View"), true);
}
if(frm.perm[0].write) {
if(frm.doc.status!=="Closed" && frm.doc.status!=="Cancelled") {
frm.add_custom_button(__("Close"), function() {
frm.set_value("status", "Closed");
frm.save();
});
} else {
frm.add_custom_button(__("Reopen"), function() {
frm.set_value("status", "Open");
frm.save();
});
}
}
}
}
@ -71,6 +80,21 @@ frappe.ui.form.on("Task", {
}
},
is_group: function(frm) {
frappe.call({
method:"erpnext.projects.doctype.task.task.check_if_child_exists",
args: {
name: frm.doc.name
},
callback: function(r){
if(r.message){
frappe.msgprint(__('Cannot convert it to non-group. Child Tasks exist.'));
frm.reload_doc();
}
}
})
},
validate: function(frm) {
frm.doc.project && frappe.model.remove_from_locals("Project",
frm.doc.project);

View File

@ -3,7 +3,7 @@
"allow_guest_to_view": 0,
"allow_import": 1,
"allow_rename": 1,
"autoname": "TASK.#####",
"autoname": "field:subject",
"beta": 0,
"creation": "2013-01-29 19:25:50",
"custom": 0,
@ -30,9 +30,8 @@
"label": "Subject",
"length": 0,
"no_copy": 0,
"oldfieldname": "subject",
"oldfieldtype": "Data",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
@ -75,6 +74,37 @@
"set_only_once": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 1,
"collapsible": 0,
"columns": 0,
"default": "0",
"fieldname": "is_group",
"fieldtype": "Check",
"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": "Is Group",
"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,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
@ -173,9 +203,42 @@
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"bold": 1,
"collapsible": 0,
"columns": 0,
"fieldname": "parent_task",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 1,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Parent Task",
"length": 0,
"no_copy": 0,
"options": "Task",
"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": 1,
"set_only_once": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"collapsible_depends_on": "",
"columns": 0,
"depends_on": "",
"fieldname": "section_break_10",
"fieldtype": "Section Break",
"hidden": 0,
@ -205,6 +268,7 @@
"bold": 0,
"collapsible": 0,
"columns": 0,
"depends_on": "",
"fieldname": "exp_start_date",
"fieldtype": "Date",
"hidden": 0,
@ -237,6 +301,7 @@
"collapsible": 0,
"columns": 0,
"default": "0",
"depends_on": "",
"description": "",
"fieldname": "expected_time",
"fieldtype": "Float",
@ -269,6 +334,7 @@
"bold": 0,
"collapsible": 0,
"columns": 0,
"depends_on": "",
"fieldname": "task_weight",
"fieldtype": "Float",
"hidden": 0,
@ -328,6 +394,7 @@
"bold": 1,
"collapsible": 0,
"columns": 0,
"depends_on": "",
"fieldname": "exp_end_date",
"fieldtype": "Date",
"hidden": 0,
@ -359,6 +426,7 @@
"bold": 0,
"collapsible": 0,
"columns": 0,
"depends_on": "",
"fieldname": "progress",
"fieldtype": "Percent",
"hidden": 0,
@ -389,6 +457,7 @@
"bold": 0,
"collapsible": 0,
"columns": 0,
"depends_on": "",
"fieldname": "is_milestone",
"fieldtype": "Check",
"hidden": 0,
@ -418,7 +487,9 @@
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"collapsible_depends_on": "",
"columns": 0,
"depends_on": "",
"fieldname": "section_break0",
"fieldtype": "Section Break",
"hidden": 0,
@ -449,6 +520,7 @@
"bold": 0,
"collapsible": 0,
"columns": 0,
"depends_on": "",
"fieldname": "description",
"fieldtype": "Text Editor",
"hidden": 0,
@ -481,7 +553,9 @@
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"collapsible_depends_on": "",
"columns": 0,
"depends_on": "",
"fieldname": "section_break",
"fieldtype": "Section Break",
"hidden": 0,
@ -512,6 +586,7 @@
"bold": 0,
"collapsible": 0,
"columns": 0,
"depends_on": "",
"fieldname": "depends_on",
"fieldtype": "Table",
"hidden": 0,
@ -543,6 +618,7 @@
"bold": 0,
"collapsible": 0,
"columns": 0,
"depends_on": "",
"fieldname": "depends_on_tasks",
"fieldtype": "Data",
"hidden": 1,
@ -572,7 +648,9 @@
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"collapsible_depends_on": "",
"columns": 0,
"depends_on": "",
"description": "",
"fieldname": "actual",
"fieldtype": "Section Break",
@ -606,6 +684,7 @@
"bold": 0,
"collapsible": 0,
"columns": 0,
"depends_on": "",
"fieldname": "act_start_date",
"fieldtype": "Date",
"hidden": 0,
@ -638,6 +717,7 @@
"collapsible": 0,
"columns": 0,
"default": "",
"depends_on": "",
"description": "",
"fieldname": "actual_time",
"fieldtype": "Float",
@ -699,6 +779,7 @@
"bold": 0,
"collapsible": 0,
"columns": 0,
"depends_on": "",
"fieldname": "act_end_date",
"fieldtype": "Date",
"hidden": 0,
@ -730,6 +811,7 @@
"bold": 0,
"collapsible": 0,
"columns": 0,
"depends_on": "",
"fieldname": "section_break_17",
"fieldtype": "Section Break",
"hidden": 0,
@ -759,6 +841,7 @@
"bold": 0,
"collapsible": 0,
"columns": 0,
"depends_on": "",
"fieldname": "total_costing_amount",
"fieldtype": "Currency",
"hidden": 0,
@ -791,6 +874,7 @@
"bold": 0,
"collapsible": 0,
"columns": 0,
"depends_on": "",
"fieldname": "total_expense_claim",
"fieldtype": "Currency",
"hidden": 0,
@ -851,6 +935,7 @@
"bold": 0,
"collapsible": 0,
"columns": 0,
"depends_on": "",
"fieldname": "total_billing_amount",
"fieldtype": "Currency",
"hidden": 0,
@ -1025,6 +1110,96 @@
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "lft",
"fieldtype": "Int",
"hidden": 1,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "lft",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "rgt",
"fieldtype": "Int",
"hidden": 1,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "rgt",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "old_parent",
"fieldtype": "Data",
"hidden": 1,
"ignore_user_permissions": 1,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Old Parent",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
}
],
"has_web_view": 0,
@ -1039,7 +1214,7 @@
"istable": 0,
"max_attachments": 5,
"menu_index": 0,
"modified": "2017-05-23 11:28:28.161600",
"modified": "2017-10-06 03:57:37.901446",
"modified_by": "Administrator",
"module": "Projects",
"name": "Task",

View File

@ -5,13 +5,14 @@ from __future__ import unicode_literals
import frappe, json
from frappe.utils import getdate, date_diff, add_days, cstr
from frappe import _
from frappe.model.document import Document
from frappe import _, throw
from frappe.utils.nestedset import NestedSet, rebuild_tree
class CircularReferenceError(frappe.ValidationError): pass
class Task(Document):
class Task(NestedSet):
nsm_parent_field = 'parent_task'
def get_feed(self):
return '{0}: {1}'.format(_(self.status), self.subject)
@ -59,11 +60,16 @@ class Task(Document):
depends_on_tasks += d.task + ","
self.depends_on_tasks = depends_on_tasks
def update_nsm_model(self):
frappe.utils.nestedset.update_nsm(self)
def on_update(self):
self.update_nsm_model()
self.check_recursion()
self.reschedule_dependent_tasks()
self.update_project()
self.unassign_todo()
rebuild_tree("Task", "parent_task")
def unassign_todo(self):
if self.status == "Closed" or self.status == "Cancelled":
@ -128,6 +134,17 @@ class Task(Document):
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."))
self.update_nsm_model()
@frappe.whitelist()
def check_if_child_exists(name):
return frappe.db.sql("""select name from `tabTask`
where parent_task = %s""", name)
@frappe.whitelist()
def get_events(start, end, filters=None):
"""Returns events for Gantt / Calendar view rendering.
@ -177,4 +194,48 @@ def set_tasks_as_overdue():
and exp_end_date < CURDATE()
and `status` not in ('Closed', 'Cancelled')""")
@frappe.whitelist()
def get_children():
doctype = frappe.local.form_dict.get('doctype')
parent_field = 'parent_' + doctype.lower().replace(' ', '_')
parent = frappe.form_dict.get("parent") or ""
if parent == "task":
parent = ""
tasks = frappe.db.sql("""select name as value,
is_group as expandable
from `tab{doctype}`
where docstatus < 2
and ifnull(`{parent_field}`,'') = %s
order by name""".format(doctype=frappe.db.escape(doctype),
parent_field=frappe.db.escape(parent_field)), (parent), as_dict=1)
# 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)
if args.parent_task == 'task':
args.parent_task = None
frappe.get_doc(args).insert()
@frappe.whitelist()
def add_multiple_tasks(data, parent):
data = json.loads(data)['tasks']
tasks = data.split('\n')
new_doc = {'doctype': 'Task', 'parent_task': parent}
for d in tasks:
new_doc['subject'] = d
new_task = frappe.get_doc(new_doc)
new_task.insert()

View File

@ -0,0 +1,59 @@
frappe.provide("frappe.treeview_settings");
frappe.treeview_settings['Task'] = {
get_tree_nodes: "erpnext.projects.doctype.task.task.get_children",
add_tree_node: "erpnext.projects.doctype.task.task.add_node",
filters: [
{
fieldname: "task",
fieldtype:"Link",
options: "Task",
label: __("Task"),
get_query: function(){
return {
filters: [["Task", 'is_group', '=', 1]]
};
}
}
],
title: "Task",
breadcrumb: "Projects",
get_tree_root: false,
root_label: "task",
ignore_fields:["parent_task"],
get_label: function(node) {
return node.data.value;
},
onload: function(me){
me.make_tree();
me.set_root = true;
},
toolbar: [
{
label:__("Add Multiple"),
condition: function(node) {
return node.expandable;
},
click: function(node) {
var d = new frappe.ui.Dialog({
'fields': [
{'fieldname': 'tasks', 'label': 'Tasks', 'fieldtype': 'Text'},
],
primary_action: function(){
d.hide();
return frappe.call({
method: "erpnext.projects.doctype.task.task.add_multiple_tasks",
args: {
data: d.get_values(),
parent: node.data.value
},
callback: function() { }
});
}
});
d.show();
}
}
],
extend_toolbar: true
};

View File

@ -0,0 +1,99 @@
/* eslint-disable */
// rename this file from _test_[name] to test_[name] to activate
// and remove above this line
QUnit.test("test: Task Tree", function (assert) {
let done = assert.async();
// number of asserts
assert.expect(5);
frappe.run_serially([
// insert a new Task
() => frappe.set_route('Tree', 'Task'),
() => frappe.timeout(0.5),
// Checking adding child without selecting any Node
() => frappe.tests.click_button('New'),
() => frappe.timeout(0.5),
() => {assert.equal($(`.msgprint`).text(), "Select a group node first.", "Error message success");},
() => frappe.tests.click_button('Close'),
() => frappe.timeout(0.5),
// Creating child nodes
() => frappe.tests.click_link('task'),
() => frappe.map_group.make('Test-1'),
() => frappe.map_group.make('Test-2'),
() => frappe.map_group.make('Test-3', 1),
() => frappe.timeout(1),
() => frappe.tests.click_link('Test-3'),
() => frappe.map_group.make('Test-4', 0),
// Checking Edit button
() => frappe.timeout(0.5),
() => frappe.tests.click_link('Test-1'),
() => frappe.tests.click_button('Edit'),
() => frappe.timeout(0.5),
() => {assert.deepEqual(frappe.get_route(), ["Form", "Task", "Test-1"], "Edit route checks");},
// Deleting child Node
() => frappe.set_route('Tree', 'Task'),
() => frappe.timeout(0.5),
() => frappe.tests.click_link('Test-1'),
() => frappe.tests.click_button('Delete'),
() => frappe.timeout(0.5),
() => frappe.tests.click_button('Yes'),
// Deleting Group Node that has child nodes in it
() => frappe.timeout(0.5),
() => frappe.tests.click_link('Test-3'),
() => frappe.tests.click_button('Delete'),
() => frappe.timeout(0.5),
() => frappe.tests.click_button('Yes'),
() => frappe.timeout(1),
() => {assert.equal(cur_dialog.title, 'Message', 'Error thrown correctly');},
() => frappe.tests.click_button('Close'),
// Renaming Child node
() => frappe.timeout(0.5),
() => frappe.tests.click_link('Test-2'),
() => frappe.tests.click_button('Rename'),
() => frappe.timeout(1),
() => cur_dialog.set_value('new_name', 'Test-5'),
() => frappe.timeout(1.5),
() => cur_dialog.get_primary_btn().click(),
() => frappe.timeout(1),
() => {assert.equal($(`a:contains("Test-5"):visible`).length, 1, 'Rename successfull');},
// Add multiple child tasks
() => frappe.tests.click_link('Test-3'),
() => frappe.timeout(0.5),
() => frappe.click_button('Add Multiple'),
() => frappe.timeout(1),
() => cur_dialog.set_value('tasks', 'Test-6\nTest-7'),
() => frappe.timeout(0.5),
() => frappe.click_button('Submit'),
() => frappe.timeout(2),
() => frappe.click_button('Expand All'),
() => frappe.timeout(1),
() => {
let count = $(`a:contains("Test-6"):visible`).length + $(`a:contains("Test-7"):visible`).length;
assert.equal(count, 2, "Multiple Tasks added successfully");
},
() => done()
]);
});
frappe.map_group = {
make:function(subject, is_group = 0){
return frappe.run_serially([
() => frappe.click_button('Add Child'),
() => frappe.timeout(1),
() => cur_dialog.set_value('is_group', is_group),
() => cur_dialog.set_value('subject', subject),
() => frappe.click_button('Create New'),
() => frappe.timeout(1.5)
]);
}
};

View File

@ -133,3 +133,4 @@ erpnext/restaurant/doctype/restaurant/test_restaurant.js
erpnext/restaurant/doctype/restaurant_table/test_restaurant_table.js
erpnext/restaurant/doctype/restaurant_menu/test_restaurant_menu.js
erpnext/restaurant/doctype/restaurant_order_entry/restaurant_order_entry.js
erpnext/projects/doctype/task/tests/test_task_tree.js