[IMP] project: change the way we work with subtasks
This commit changes some of the way that users will work with subtasks in the project app. Initially, it changes subtasks from a many2many to a one2many field. We also add the ability for subtasks to be viewed straight from the kanban view by drawing a list of them inside of the kanban box of the parent task. task-3085016 closes odoo/odoo#112279 Signed-off-by: Xavier Bol (xbo) <xbo@odoo.com>
This commit is contained in:
@@ -0,0 +1,73 @@
|
||||
/** @odoo-module */
|
||||
|
||||
import { useService } from '@web/core/utils/hooks';
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
import { Component } from '@odoo/owl';
|
||||
import { Record } from '@web/views/record';
|
||||
import { KanbanMany2ManyTagsAvatarUserField } from "@mail/views/fields/many2many_avatar_user_field/many2many_avatar_user_field";
|
||||
import { Field } from "@web/views/fields/field";
|
||||
import { standardWidgetProps } from "@web/views/widgets/standard_widget_props";
|
||||
|
||||
export class SubtaskKanbanList extends Component {
|
||||
|
||||
setup() {
|
||||
this.actionService = useService("action");
|
||||
}
|
||||
|
||||
async goToSubtask(subtask_id) {
|
||||
return this.actionService.doAction({
|
||||
type: "ir.actions.act_window",
|
||||
res_model: this.props.record.resModel,
|
||||
res_id: subtask_id,
|
||||
views: [[false, "form"]],
|
||||
target: "current",
|
||||
context: {
|
||||
active_id: subtask_id,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
get fieldsInfo() {
|
||||
return {
|
||||
child_ids: {
|
||||
type: "one2many",
|
||||
relation: "project.task",
|
||||
fieldsToFetch: {
|
||||
display_name: { type: "char" },
|
||||
kanban_state: { selection: [['normal', 'In Progress'], ['done', 'Ready'], ['blocked', 'Blocked']],
|
||||
string: "Status",
|
||||
type: "selection",
|
||||
field: this.props.record.activeFields.kanban_state.field,
|
||||
attrs: this.props.record.activeFields.kanban_state.attrs,
|
||||
options: this.props.record.activeFields.kanban_state.options },
|
||||
legend_blocked: { type: "char" },
|
||||
legend_done: { type: "char" },
|
||||
legend_normal: { type: "char" },
|
||||
user_ids: { type: "many2many",
|
||||
relation: "res.users",
|
||||
field: this.props.record.activeFields.user_ids.field,
|
||||
fieldsToFetch: this.props.record.activeFields.user_ids.fieldsToFetch,
|
||||
attrs: this.props.record.activeFields.user_ids.attrs,
|
||||
options: this.props.record.activeFields.user_ids.options },
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
SubtaskKanbanList.components = {
|
||||
Record,
|
||||
Field,
|
||||
KanbanMany2ManyTagsAvatarUserField,
|
||||
};
|
||||
SubtaskKanbanList.props = {
|
||||
...standardWidgetProps,
|
||||
};
|
||||
SubtaskKanbanList.template = 'project.SubtaskKanbanList';
|
||||
const subtaskKanbanList = {
|
||||
component: SubtaskKanbanList,
|
||||
fieldDependencies: [{ name: "child_ids", type: "one2many" }],
|
||||
};
|
||||
|
||||
registry.category("view_widgets").add("subtask_kanban_list", subtaskKanbanList);
|
||||
@@ -0,0 +1,21 @@
|
||||
.subtask_list {
|
||||
margin-top: 6px;
|
||||
|
||||
.subtask_list_row {
|
||||
display: grid;
|
||||
grid-template-columns: 20px auto auto;
|
||||
column-gap: 4px;
|
||||
padding: 4px 0px;
|
||||
|
||||
.subtask_name_col {
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
align-self: center;
|
||||
}
|
||||
}
|
||||
|
||||
.subtask_list_row:hover {
|
||||
background-color: mix($table-color, $table-bg, percentage($table-hover-bg-factor + 0.1));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<templates>
|
||||
|
||||
<t t-name="project.SubtaskKanbanList" owl="1">
|
||||
<Record resModel="'project.task'" resId="props.record.resId" fields="fieldsInfo" activeFields="fieldsInfo" t-slot-scope="data">
|
||||
<div class="subtask_list">
|
||||
<t t-foreach="data.record.data.child_ids.records" t-as="subTask" t-key="subTask.resId">
|
||||
<div class="subtask_list_row">
|
||||
<Field name="'kanban_state'"
|
||||
class="`subtask_state_widget_col d-flex justify-content-center align-items-center`"
|
||||
record="subTask"
|
||||
fieldInfo="subTask.fields.kanban_state"
|
||||
type="'state_selection'"/>
|
||||
<a class="subtask_name_col"
|
||||
t-att-title="subTask.data.display_name"
|
||||
style="color: inherit;"
|
||||
t-esc="subTask.data.display_name"
|
||||
t-on-click.prevent="() => this.goToSubtask(subTask.resId)"/>
|
||||
<Field value="subTask.data.user_ids"
|
||||
class="`subtask_user_widget_col justify-content-end align-items-center`"
|
||||
name="'user_ids'"
|
||||
record="subTask"
|
||||
fieldInfo="subTask.fields.user_ids"
|
||||
readonly="true"
|
||||
type="'many2many_avatar_user'"/>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
</Record>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
@@ -0,0 +1,20 @@
|
||||
/** @odoo-module */
|
||||
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
import { ConfirmationDialog } from "@web/core/confirmation_dialog/confirmation_dialog";
|
||||
import { ListRenderer } from '@web/views/list/list_renderer';
|
||||
|
||||
export class SubtaskListRenderer extends ListRenderer {
|
||||
setup() {
|
||||
super.setup();
|
||||
this.dialog = useService("dialog");
|
||||
}
|
||||
|
||||
async onDeleteRecord(record) {
|
||||
this.dialog.add(ConfirmationDialog, {
|
||||
body: this.env._t("Are you sure you want to delete this record?"),
|
||||
confirm: () => super.onDeleteRecord(record),
|
||||
cancel: () => {},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
/** @odoo-module */
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
import { X2ManyField, x2ManyField } from '@web/views/fields/x2many/x2many_field';
|
||||
|
||||
import { SubtaskListRenderer } from './subtask_list_renderer';
|
||||
|
||||
export class SubtaskOne2ManyField extends X2ManyField {}
|
||||
|
||||
SubtaskOne2ManyField.components = {
|
||||
...X2ManyField.components,
|
||||
ListRenderer: SubtaskListRenderer,
|
||||
}
|
||||
|
||||
export const subtaskOne2ManyField = {
|
||||
...x2ManyField,
|
||||
component: SubtaskOne2ManyField,
|
||||
}
|
||||
|
||||
registry.category("fields").add("subtasks_one2many", subtaskOne2ManyField);
|
||||
@@ -0,0 +1,68 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { KanbanCompiler } from "@web/views/kanban/kanban_compiler";
|
||||
import { append, createElement } from "@web/core/utils/xml";
|
||||
|
||||
export class ProjectTaskKanbanCompiler extends KanbanCompiler {
|
||||
setup() {
|
||||
super.setup();
|
||||
this.subtaskListComponentCompiled = {
|
||||
button: false,
|
||||
component: false,
|
||||
};
|
||||
this.compilers.push(
|
||||
{ selector: ".subtask_list_button", fn: this.compileSubtaskListButton },
|
||||
{ selector: "div.kanban_bottom_subtasks_section", fn: this.compileSubtaskListComponent },
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Element} el
|
||||
* @returns {Element}
|
||||
*/
|
||||
compileSubtaskListButton(el) {
|
||||
this.subtaskListComponentCompiled.button = true;
|
||||
el.setAttribute("t-on-click", `() => __comp__.state.folded = !__comp__.state.folded`);
|
||||
el.removeAttribute("class");
|
||||
el.setAttribute("t-attf-class", "subtask_list_button btn fa {{ state.folded ? 'fa-solid fa-angle-right' : 'fa-solid fa-angle-down' }}");
|
||||
const compiled = createElement(el.nodeName);
|
||||
for (const { name, value } of el.attributes) {
|
||||
compiled.setAttribute(name, value);
|
||||
}
|
||||
|
||||
return compiled;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Element} el
|
||||
* @returns {Element}
|
||||
*/
|
||||
compileSubtaskListComponent(el) {
|
||||
this.subtaskListComponentCompiled.component = true;
|
||||
el.setAttribute("t-if", `!__comp__.state.folded and !selection_mode`);
|
||||
const compiled = createElement(el.nodeName);
|
||||
for (const { name, value } of el.attributes) {
|
||||
compiled.setAttribute(name, value);
|
||||
}
|
||||
const listContainer = createElement('widget');
|
||||
const listElemenent = createElement('SubtaskKanbanList');
|
||||
listElemenent.setAttribute("record", '__comp__.props.record');
|
||||
|
||||
append(listContainer, listElemenent);
|
||||
append(compiled, listContainer);
|
||||
|
||||
return compiled;
|
||||
}
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
compile(key, params = {}) {
|
||||
const newRoot = super.compile(key, params);
|
||||
if (this.subtaskListComponentCompiled.component !== this.subtaskListComponentCompiled.button) {
|
||||
// Error since one of them is not compiled
|
||||
throw new Error("The subtask list component cannot be rendered if the button and the component are not in the view definition.");
|
||||
}
|
||||
return newRoot;
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,28 @@
|
||||
import { KanbanModel } from "@web/views/kanban/kanban_model";
|
||||
|
||||
import { ProjectTaskKanbanDynamicGroupList } from "./project_task_kanban_dynamic_group_list";
|
||||
import { ProjectTaskRecord } from './project_task_kanban_record';
|
||||
import { Record } from '@web/views/relational_model';
|
||||
|
||||
export class ProjectTaskRecord extends Record {
|
||||
async _applyChanges(changes) {
|
||||
const value = changes.personal_stage_type_ids;
|
||||
if (Array.isArray(value)) {
|
||||
delete changes.personal_stage_type_ids;
|
||||
changes.personal_stage_type_id = value;
|
||||
}
|
||||
await super._applyChanges(changes);
|
||||
}
|
||||
|
||||
get context() {
|
||||
const context = super.context;
|
||||
const value = context.default_personal_stage_type_ids;
|
||||
if (Array.isArray(value)) {
|
||||
context.default_personal_stage_type_id = value[0];
|
||||
delete context.default_personal_stage_type_ids;
|
||||
}
|
||||
return context;
|
||||
}
|
||||
}
|
||||
|
||||
export class ProjectTaskKanbanGroup extends KanbanModel.Group {
|
||||
get isPersonalStageGroup() {
|
||||
|
||||
@@ -1,24 +1,28 @@
|
||||
/* @odoo-module */
|
||||
|
||||
import { Record } from '@web/views/relational_model';
|
||||
import { KanbanRecord } from "@web/views/kanban/kanban_record";
|
||||
import { useState } from "@odoo/owl";
|
||||
import { ProjectTaskKanbanCompiler } from "./project_task_kanban_compiler";
|
||||
import { SubtaskKanbanList } from "@project/components/subtask_kanban_list/subtask_kanban_list"
|
||||
|
||||
export class ProjectTaskRecord extends Record {
|
||||
async _applyChanges(changes) {
|
||||
const value = changes.personal_stage_type_ids;
|
||||
if (value && Array.isArray(value)) {
|
||||
delete changes.personal_stage_type_ids;
|
||||
changes.personal_stage_type_id = value;
|
||||
}
|
||||
await super._applyChanges(changes);
|
||||
export class ProjectTaskKanbanRecord extends KanbanRecord {
|
||||
setup() {
|
||||
super.setup();
|
||||
this.state = useState({folded: true});
|
||||
}
|
||||
|
||||
get context() {
|
||||
const context = super.context;
|
||||
const value = context.default_personal_stage_type_ids;
|
||||
if (value && Array.isArray(value)) {
|
||||
context.default_personal_stage_type_id = value[0];
|
||||
delete context.default_personal_stage_type_ids;
|
||||
}
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
get renderingContext() {
|
||||
const context = super.renderingContext;
|
||||
context["state"] = this.state;
|
||||
return context;
|
||||
}
|
||||
}
|
||||
|
||||
ProjectTaskKanbanRecord.Compiler = ProjectTaskKanbanCompiler;
|
||||
ProjectTaskKanbanRecord.components = {
|
||||
...KanbanRecord.components,
|
||||
SubtaskKanbanList,
|
||||
};
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { useService } from '@web/core/utils/hooks';
|
||||
import { KanbanRenderer } from '@web/views/kanban/kanban_renderer';
|
||||
import { FormViewDialog } from "@web/views/view_dialogs/form_view_dialog";
|
||||
import { ProjectTaskKanbanRecord } from './project_task_kanban_record';
|
||||
|
||||
const { onWillStart } = owl;
|
||||
|
||||
@@ -89,3 +90,8 @@ export class ProjectTaskKanbanRenderer extends KanbanRenderer {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
ProjectTaskKanbanRenderer.components = {
|
||||
...KanbanRenderer.components,
|
||||
KanbanRecord: ProjectTaskKanbanRecord,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
/** @odoo-module */
|
||||
import {
|
||||
click,
|
||||
getFixture,
|
||||
} from '@web/../tests/helpers/utils';
|
||||
import { setupViewRegistries } from "@web/../tests/views/helpers";
|
||||
import { start, startServer } from '@mail/../tests/helpers/test_utils';
|
||||
import { addModelNamesToFetch } from '@bus/../tests/helpers/model_definitions_helpers';
|
||||
|
||||
addModelNamesToFetch([
|
||||
'project.project',
|
||||
'project.task',
|
||||
]);
|
||||
|
||||
let target;
|
||||
|
||||
QUnit.module('Subtask Kanban List tests', {
|
||||
beforeEach: async function () {
|
||||
const pyEnv = await startServer();
|
||||
const projectId = pyEnv['project.project'].create([
|
||||
{ name: "Project One", allow_subtasks: true },
|
||||
]);
|
||||
const userId = pyEnv['res.users'].create([
|
||||
{ name: "User One", login: 'one', password: 'one' },
|
||||
]);
|
||||
pyEnv['project.task'].create([
|
||||
{ name: 'task one', project_id: projectId, subtask_count: 2, child_ids: [2, 3], kanban_state: 'normal', user_ids: [userId] },
|
||||
{ name: 'task two', subtask_count: 0, child_ids: [], kanban_state: 'done' },
|
||||
{ name: 'task three', subtask_count: 0, child_ids: [], kanban_state: 'blocked' },
|
||||
{ name: 'task four', project_id: projectId, subtask_count: 0, child_ids: [], kanban_state: 'normal' },
|
||||
]);
|
||||
this.views = {
|
||||
"project.task,false,kanban":
|
||||
`<kanban js_class="project_task_kanban">
|
||||
<field name="subtask_count"/>
|
||||
<templates>
|
||||
<t t-name="kanban-box">
|
||||
<div>
|
||||
<field name="name" widget="name_with_subtask_count"/>
|
||||
<field name="user_ids" invisible="1"/>
|
||||
<field name="kanban_state" invisible="1"/>
|
||||
<a t-if="record.subtask_count.raw_value" class="subtask_list_button" title="See Subtasks" style="width: 29px;"/>
|
||||
</div>
|
||||
<div class="kanban_bottom_subtasks_section"/>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>`,
|
||||
};
|
||||
target = getFixture();
|
||||
setupViewRegistries();
|
||||
}
|
||||
}, function () {
|
||||
QUnit.test("Check whether subtask list functionality works as intended", async function (assert) {
|
||||
assert.expect(8);
|
||||
|
||||
const views = this.views;
|
||||
const { openView } = await start({ serverData: { views } });
|
||||
await openView({
|
||||
res_model: "project.task",
|
||||
views: [[false, "kanban"]],
|
||||
});
|
||||
|
||||
assert.containsOnce(target, '.subtask_list_button', "Only kanban boxes of parent tasks should have the drawdown button, in this case this is 1");
|
||||
assert.containsNone(target, '.subtask_list', "If the drawdown button is not clicked, the subtasks list should be hidden");
|
||||
|
||||
await click(target, '.subtask_list_button');
|
||||
|
||||
assert.containsOnce(target, '.subtask_list', "Clicking on the button should make the subtask list render, in this case we are expectig 1 list");
|
||||
assert.containsN(target, '.subtask_list_row', 2, "The list rendered should show all the subtasks of the task, in this case 2");
|
||||
assert.containsN(target, '.subtask_state_widget_col', 2, "Each of the list's rows should have 1 state widget, thus we are looking for 2 in total");
|
||||
assert.containsN(target, '.subtask_user_widget_col', 2, "Each of the list's rows should have 1 user widgets, thus we are looking for 2 in total");
|
||||
assert.containsN(target, '.subtask_name_col', 2, "Each of the list's rows should display the subtask's name, thus we are looking for 2 in total");
|
||||
|
||||
await click(target, '.subtask_list_button');
|
||||
|
||||
assert.containsNone(target, '.subtask_list', "If the drawdown button is clicked again, the subtasks list should be hidden again");
|
||||
});
|
||||
});
|
||||
@@ -108,25 +108,25 @@ class TestProjectSubtasks(TestProjectCommon):
|
||||
- The display project id should follow the project id
|
||||
"""
|
||||
# 1)
|
||||
test_subtask_1 = self.env['project.task'].create({
|
||||
'name': 'Test Subtask 1',
|
||||
})
|
||||
with Form(self.task_1.with_context({'tracking_disable': True})) as task_form:
|
||||
task_form.child_ids.add(test_subtask_1)
|
||||
with task_form.child_ids.new() as child_task_form:
|
||||
child_task_form.name = 'Test Subtask 1'
|
||||
|
||||
self.assertEqual(self.task_1.child_ids.project_id, self.project_pigs, "The project should be assigned from the default project.")
|
||||
self.assertFalse(self.task_1.child_ids.display_project_id, "The display project of a sub task should be false to project_id.")
|
||||
|
||||
# 2)
|
||||
with Form(self.task_1.with_context({'tracking_disable': True})) as task_form:
|
||||
task_form.child_ids[0].display_project_id = self.project_goats
|
||||
with task_form.child_ids.edit(0) as child_task_form:
|
||||
child_task_form.display_project_id = self.project_goats
|
||||
self.assertEqual(self.task_1.project_id, self.project_pigs, "Changing the project of a subtask should not change parent project")
|
||||
self.assertEqual(self.task_1.child_ids.display_project_id, self.project_goats, "Display Project of the task should be well assigned")
|
||||
self.assertEqual(self.task_1.child_ids.project_id, self.project_goats, "Changing display project id on a subtask should change project id")
|
||||
|
||||
# 3)
|
||||
with Form(self.task_1.with_context({'tracking_disable': True})) as task_form:
|
||||
task_form.child_ids[0].display_project_id = self.env['project.project']
|
||||
with task_form.child_ids.edit(0) as child_task_form:
|
||||
child_task_form.display_project_id = self.env['project.project']
|
||||
|
||||
self.assertFalse(self.task_1.child_ids.display_project_id, "Display Project of the task should be well assigned, to False")
|
||||
self.assertEqual(self.task_1.child_ids.project_id, self.project_pigs, "Resetting display project to False on a subtask should change project id to parent project id")
|
||||
@@ -141,7 +141,8 @@ class TestProjectSubtasks(TestProjectCommon):
|
||||
|
||||
# 5)
|
||||
with Form(self.task_1.with_context({'tracking_disable': True})) as task_form:
|
||||
task_form.child_ids[0].display_project_id = self.project_goats
|
||||
with task_form.child_ids.edit(0) as child_task_form:
|
||||
child_task_form.display_project_id = self.project_goats
|
||||
task_form.project_id = self.project_pigs
|
||||
|
||||
self.assertEqual(self.task_1.project_id, self.project_pigs, "Parent project should change back.")
|
||||
@@ -160,12 +161,10 @@ class TestProjectSubtasks(TestProjectCommon):
|
||||
self.assertFalse(orphan_subtask.parent_id, "Parent should be false")
|
||||
|
||||
# 7)
|
||||
test_subtask_1 = self.env['project.task'].create({
|
||||
'name': 'Test Subtask 1',
|
||||
})
|
||||
with Form(self.task_1.with_context({'tracking_disable': True})) as task_form:
|
||||
task_form.child_ids.add(test_subtask_1)
|
||||
task_form.child_ids[0].display_project_id = self.project_goats
|
||||
with task_form.child_ids.new() as child_task_form:
|
||||
child_task_form.name = 'Test Subtask 1'
|
||||
child_task_form.display_project_id = self.project_goats
|
||||
with Form(self.task_1.child_ids.with_context({'tracking_disable': True})) as subtask_form:
|
||||
subtask_form.display_project_id = self.env['project.project']
|
||||
subtask_form.parent_id = self.env['project.task']
|
||||
@@ -183,11 +182,9 @@ class TestProjectSubtasks(TestProjectCommon):
|
||||
self.project_pigs.type_ids |= stage_a
|
||||
self.project_pigs.type_ids |= stage_b
|
||||
|
||||
test_subtask_1 = self.env['project.task'].create({
|
||||
'name': 'Test Subtask 1',
|
||||
})
|
||||
with Form(self.task_1.with_context({'tracking_disable': True})) as task_form:
|
||||
task_form.child_ids.add(test_subtask_1)
|
||||
with task_form.child_ids.new() as child_task_form:
|
||||
child_task_form.name = 'Test Subtask 1'
|
||||
|
||||
self.assertEqual(self.task_1.child_ids.stage_id, stage_a, "The stage of the child task should be the default one of the project.")
|
||||
|
||||
@@ -196,17 +193,16 @@ class TestProjectSubtasks(TestProjectCommon):
|
||||
|
||||
self.assertEqual(self.task_1.child_ids.stage_id, stage_a, "The stage of the child task should remain the same while changing parent task stage.")
|
||||
|
||||
test_subtask_2 = self.env['project.task'].create({
|
||||
'name': 'Test Subtask 2',
|
||||
})
|
||||
with Form(self.task_1.with_context({'tracking_disable': True})) as task_form:
|
||||
task_form.child_ids.remove(test_subtask_1.id)
|
||||
task_form.child_ids.add(test_subtask_2)
|
||||
task_form.child_ids.remove(index=0)
|
||||
with task_form.child_ids.new() as child_task_form:
|
||||
child_task_form.name = 'Test Subtask 2'
|
||||
|
||||
self.assertEqual(self.task_1.child_ids.stage_id, stage_a, "The stage of the child task should be the default one of the project even if parent stage id is different.")
|
||||
|
||||
with Form(self.task_1.with_context({'tracking_disable': True})) as task_form:
|
||||
task_form.child_ids[0].display_project_id = self.project_goats
|
||||
with task_form.child_ids.edit(0) as child_task_form:
|
||||
child_task_form.display_project_id = self.project_goats
|
||||
|
||||
self.assertEqual(self.task_1.child_ids.stage_id.name, "New", "The stage of the child task should be the default one of the display project id, once set.")
|
||||
|
||||
@@ -245,23 +241,19 @@ class TestProjectSubtasks(TestProjectCommon):
|
||||
4) check the correct nb of sub-tasks is displayed in the 'sub-tasks' stat button and on the parent task kanban card
|
||||
5) sub-tasks should be copied when the parent task is duplicated
|
||||
"""
|
||||
test_subtask_1 = self.env['project.task'].create({
|
||||
'name': 'Test Subtask 1',
|
||||
})
|
||||
|
||||
task_form = Form(self.task_1.with_context({'tracking_disable': True}))
|
||||
task_form.child_ids.add(test_subtask_1)
|
||||
task_form.child_ids[0].display_project_id = self.env['project.project']
|
||||
with task_form.child_ids.new() as child_task_form:
|
||||
child_task_form.name = 'Test Subtask 1'
|
||||
child_task_form.display_project_id = self.env['project.project']
|
||||
task = task_form.save()
|
||||
|
||||
child_subtask = self.task_1.child_ids[0]
|
||||
test_subtask_2 = self.env['project.task'].create({
|
||||
'name': 'Test Subtask 2',
|
||||
})
|
||||
|
||||
with Form(child_subtask.with_context(tracking_disable=True)) as subtask_form:
|
||||
subtask_form.child_ids.add(test_subtask_2)
|
||||
subtask_form.child_ids[0].display_project_id = self.env['project.project']
|
||||
with subtask_form.child_ids.new() as child_subtask_form:
|
||||
child_subtask_form.name = 'Test Subtask 2'
|
||||
child_subtask_form.display_project_id = self.env['project.project']
|
||||
|
||||
self.assertEqual(task.subtask_count, 2, "Parent task should have 2 children")
|
||||
task_2 = task.copy()
|
||||
|
||||
@@ -1191,10 +1191,9 @@
|
||||
</page>
|
||||
<page name="sub_tasks_page" string="Sub-tasks" attrs="{'invisible': [('allow_subtasks', '=', False)]}">
|
||||
<field name="child_ids"
|
||||
context="{'search_view_ref' : 'project.view_task_search_form', 'default_project_id': project_id if not parent_id or not display_project_id else display_project_id, 'default_user_ids': user_ids, 'default_parent_id': id,
|
||||
'default_partner_id': partner_id, 'default_milestone_id': allow_milestones and milestone_id, 'search_default_display_project_id': project_id, 'tree_view_ref': 'project.open_view_all_tasks_list_view'}"
|
||||
widget="many2many"
|
||||
domain="['!', ('id', 'parent_of', id), ('recurring_task', '=', False)]">
|
||||
context="{'default_project_id': project_id if not parent_id or not display_project_id else display_project_id, 'default_user_ids': user_ids, 'default_parent_id': id,
|
||||
'default_partner_id': partner_id, 'default_milestone_id': allow_milestones and milestone_id}"
|
||||
widget="subtasks_one2many">
|
||||
<tree editable="bottom" decoration-muted="is_closed == True">
|
||||
<field name="legend_normal" invisible="1"/>
|
||||
<field name="legend_done" invisible="1"/>
|
||||
@@ -1417,6 +1416,7 @@
|
||||
<field name="has_late_and_unreached_milestone" />
|
||||
<field name="allow_milestones" />
|
||||
<field name="recurrence_id" />
|
||||
<field name="subtask_count"/>
|
||||
<progressbar field="kanban_state" colors='{"done": "success", "blocked": "danger", "normal": "200"}'/>
|
||||
<templates>
|
||||
<t t-name="kanban-menu" t-if="!selection_mode" groups="base.group_user">
|
||||
@@ -1464,12 +1464,14 @@
|
||||
<span style="font-weight:bold;" class="fa fa-fw mt4 fa-meh-o text-warning" t-elif="record.rating_avg.raw_value >= 2.33" title="Average Rating: Okay" role="img" aria-label="Neutral face"/>
|
||||
<span style="font-weight:bold;" class="fa fa-fw mt4 fa-frown-o text-danger" t-else="" title="Average Rating: Dissatisfied" role="img" aria-label="Sad face"/>
|
||||
</b>
|
||||
<a t-if="record.subtask_count.raw_value and record.allow_subtasks.raw_value" class="subtask_list_button" title="See Subtasks" style="padding: 0px 5px 0px 0px; box-sizing: border-box; font-size: 20px;"/>
|
||||
</div>
|
||||
<div class="oe_kanban_bottom_right" t-if="!selection_mode">
|
||||
<field name="kanban_state" widget="state_selection" groups="base.group_user"/>
|
||||
<t t-if="record.user_ids.raw_value"><field name="user_ids" widget="many2many_avatar_user"/></t>
|
||||
</div>
|
||||
</div>
|
||||
<div class="kanban_bottom_subtasks_section"/>
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user