[IMP] project: allow modification of personal stages
This commit allows project users to create and modify their own personal stages. A lot of overrides have been done on the kanban view to handle our multiple cases. Project user will now only see the options to create columns/edit stages when grouping by personal stage. This is hardcoded and does not follow possible custom access rules however. Any stage created while grouping by personal stage will directly be assigned to the user and will be seen in the 'My Tasks' menu. A special method has been added to delete a personal stage. Upon deletion any task assigned to this personal stage will move to a lower sequence stage if possible otherwise the next in line. TaskId-2858445 closes odoo/odoo#92103 X-original-commit: 7b3acbc0b0c21ab106b1b34d9f5c8bc1ceb2fe58 Signed-off-by: William Braeckman (wbr) <wbr@odoo.com> Signed-off-by: Laurent Stukkens (ltu) <ltu@odoo.com>
This commit is contained in:
@@ -666,6 +666,7 @@ msgid "Add Milestone"
|
||||
msgstr ""
|
||||
|
||||
#. module: project
|
||||
#: model_terms:ir.ui.view,arch_db:project.personal_task_type_edit
|
||||
#: model_terms:ir.ui.view,arch_db:project.task_type_edit
|
||||
msgid "Add a description..."
|
||||
msgstr ""
|
||||
@@ -839,6 +840,7 @@ msgstr ""
|
||||
|
||||
#. module: project
|
||||
#: model_terms:ir.ui.view,arch_db:project.edit_project
|
||||
#: model_terms:ir.ui.view,arch_db:project.personal_task_type_edit
|
||||
#: model_terms:ir.ui.view,arch_db:project.project_project_stage_view_form
|
||||
#: model_terms:ir.ui.view,arch_db:project.project_project_stage_view_search
|
||||
#: model_terms:ir.ui.view,arch_db:project.project_sharing_project_task_view_form
|
||||
@@ -1774,6 +1776,13 @@ msgstr ""
|
||||
msgid "Edit"
|
||||
msgstr ""
|
||||
|
||||
#. module: project
|
||||
#. openerp-web
|
||||
#: code:addons/project/static/src/js/project_kanban.js:0
|
||||
#, python-format
|
||||
msgid "Edit Personal Stage"
|
||||
msgstr ""
|
||||
|
||||
#. module: project
|
||||
#: model_terms:ir.ui.view,arch_db:project.view_task_form2
|
||||
msgid "Edit recurring task"
|
||||
@@ -4441,6 +4450,7 @@ msgstr ""
|
||||
|
||||
#. module: project
|
||||
#: model:ir.model,name:project.model_project_task_type
|
||||
#: model_terms:ir.ui.view,arch_db:project.personal_task_type_edit
|
||||
#: model_terms:ir.ui.view,arch_db:project.task_type_edit
|
||||
#: model_terms:ir.ui.view,arch_db:project.task_type_tree
|
||||
msgid "Task Stage"
|
||||
@@ -5232,6 +5242,14 @@ msgstr ""
|
||||
msgid "You have not write access of %s field."
|
||||
msgstr ""
|
||||
|
||||
#. module: project
|
||||
#: code:addons/project/models/project.py:0
|
||||
#, python-format
|
||||
msgid ""
|
||||
"You should at least have one personal stage. Create a new stage to which the"
|
||||
" tasks can be transferred after this one is deleted."
|
||||
msgstr ""
|
||||
|
||||
#. module: project
|
||||
#: model_terms:ir.ui.view,arch_db:project.project_sharing_project_task_view_kanban
|
||||
msgid "assignee"
|
||||
|
||||
@@ -144,6 +144,37 @@ class ProjectTaskType(models.Model):
|
||||
else:
|
||||
stage.disabled_rating_warning = False
|
||||
|
||||
def remove_personal_stage(self):
|
||||
"""
|
||||
Remove a personal stage, tasks using that stage will move to the first
|
||||
stage with a lower priority if it exists higher if not.
|
||||
This method will not allow to delete the last personal stage.
|
||||
Having no personal_stage_type_id makes the task not appear when grouping by personal stage.
|
||||
"""
|
||||
self.ensure_one()
|
||||
assert self.user_id == self.env.user or self.env.su
|
||||
|
||||
users_personal_stages = self.env['project.task.type']\
|
||||
.search([('user_id', '=', self.user_id.id)], order='sequence DESC')
|
||||
if len(users_personal_stages) == 1:
|
||||
raise ValidationError(_("You should at least have one personal stage. Create a new stage to which the tasks can be transferred after this one is deleted."))
|
||||
|
||||
# Find the most suitable stage, they are already sorted by sequence
|
||||
new_stage = self.env['project.task.type']
|
||||
for stage in users_personal_stages:
|
||||
if stage == self:
|
||||
continue
|
||||
if stage.sequence > self.sequence:
|
||||
new_stage = stage
|
||||
elif stage.sequence <= self.sequence:
|
||||
new_stage = stage
|
||||
break
|
||||
|
||||
self.env['project.task.stage.personal'].search([('stage_id', '=', self.id)]).write({
|
||||
'stage_id': new_stage.id,
|
||||
})
|
||||
self.unlink()
|
||||
|
||||
class Project(models.Model):
|
||||
_name = "project.project"
|
||||
_description = "Project"
|
||||
|
||||
@@ -9,6 +9,11 @@ import KanbanModel from 'web.KanbanModel';
|
||||
import viewRegistry from 'web.view_registry';
|
||||
import { ProjectControlPanel } from '@project/js/project_control_panel';
|
||||
import viewUtils from 'web.viewUtils';
|
||||
import { Domain } from '@web/core/domain';
|
||||
import view_dialogs from 'web.view_dialogs';
|
||||
import core from 'web.core';
|
||||
|
||||
const _t = core._t;
|
||||
|
||||
// PROJECTS
|
||||
|
||||
@@ -66,9 +71,33 @@ const ProjectTaskKanbanColumn = KanbanColumn.extend({
|
||||
if (this.groupedBy === 'stage_id') {
|
||||
event.preventDefault();
|
||||
this.trigger_up('kanban_column_delete_wizard');
|
||||
} else {
|
||||
this._super(...arguments);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Open alternative view when editing personal stages.
|
||||
*
|
||||
* @private
|
||||
* @override
|
||||
*/
|
||||
_onEditColumn: function (event) {
|
||||
if (this.groupedBy !== 'personal_stage_type_ids') {
|
||||
this._super(...arguments);
|
||||
return;
|
||||
}
|
||||
this._super.apply(this, arguments);
|
||||
event.preventDefault();
|
||||
const context = Object.assign({}, this.getSession().user_context, {
|
||||
form_view_ref: 'project.personal_task_type_edit',
|
||||
});
|
||||
new view_dialogs.FormViewDialog(this, {
|
||||
res_model: this.relation,
|
||||
res_id: this.id,
|
||||
context: context,
|
||||
title: _t("Edit Personal Stage"),
|
||||
on_saved: this.trigger_up.bind(this, 'reload'),
|
||||
}).open();
|
||||
},
|
||||
});
|
||||
|
||||
@@ -77,6 +106,23 @@ const ProjectTaskKanbanRenderer = KanbanRenderer.extend({
|
||||
KanbanColumn: ProjectTaskKanbanColumn,
|
||||
}),
|
||||
|
||||
init: function () {
|
||||
this._super.apply(this, arguments);
|
||||
this.isProjectManager = false;
|
||||
},
|
||||
|
||||
willStart: function () {
|
||||
const superPromise = this._super.apply(this, arguments);
|
||||
|
||||
const isProjectManager = this.getSession().user_has_group('project.group_project_manager').then((hasGroup) => {
|
||||
this.isProjectManager = hasGroup;
|
||||
this._setState();
|
||||
return Promise.resolve();
|
||||
});
|
||||
|
||||
return Promise.all([superPromise, isProjectManager]);
|
||||
},
|
||||
|
||||
/**
|
||||
* Allows record drag when grouping by `personal_stage_type_ids`
|
||||
*
|
||||
@@ -92,12 +138,25 @@ const ProjectTaskKanbanRenderer = KanbanRenderer.extend({
|
||||
const grouped_by_date = ["date", "datetime"].includes(field.type);
|
||||
const grouped_by_m2m = field.type === "many2many";
|
||||
const readonly = !!field.readonly || !!fieldInfo.readonly;
|
||||
const groupedByPersonalStage = (groupByFieldName === 'personal_stage_type_ids');
|
||||
|
||||
const draggable = !readonly && (!grouped_by_m2m || groupByFieldName == 'personal_stage_type_ids') &&
|
||||
const draggable = !readonly && (!grouped_by_m2m || groupedByPersonalStage) &&
|
||||
(!grouped_by_date || fieldInfo.allowGroupRangeValue);
|
||||
|
||||
// When grouping by personal stage we allow any project user to create
|
||||
let editable = this.columnOptions.editable;
|
||||
let deletable = this.columnOptions.deletable;
|
||||
if (['stage_id', 'personal_stage_type_ids'].includes(groupByFieldName)) {
|
||||
this.groupedByM2O = groupedByPersonalStage || this.groupedByM2O;
|
||||
const allow_crud = this.isProjectManager || groupedByPersonalStage;
|
||||
this.createColumnEnabled = editable = deletable = allow_crud;
|
||||
}
|
||||
|
||||
Object.assign(this.columnOptions, {
|
||||
draggable,
|
||||
grouped_by_m2o: this.groupedByM2O,
|
||||
editable: editable,
|
||||
deletable: deletable,
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -121,6 +180,24 @@ export const ProjectKanbanController = KanbanController.extend({
|
||||
self.do_action(res);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
_onDeleteColumn: function (ev) {
|
||||
const state = this.model.get(this.handle, {raw: true});
|
||||
const groupedByFieldname = state.groupedBy[0];
|
||||
if (groupedByFieldname !== 'personal_stage_type_ids') {
|
||||
this._super(...arguments);
|
||||
return;
|
||||
}
|
||||
const column = ev.target;
|
||||
this._rpc({
|
||||
model: 'project.task.type',
|
||||
method: 'remove_personal_stage',
|
||||
args: [[column.id]],
|
||||
}).then(this.update.bind(this, {}, {}));
|
||||
},
|
||||
});
|
||||
|
||||
const ProjectTaskKanbanModel = KanbanModel.extend({
|
||||
@@ -132,12 +209,12 @@ const ProjectTaskKanbanModel = KanbanModel.extend({
|
||||
* @private
|
||||
*/
|
||||
moveRecord: function (recordID, groupID, parentID) {
|
||||
var self = this;
|
||||
var parent = this.localData[parentID];
|
||||
var new_group = this.localData[groupID];
|
||||
var changes = {};
|
||||
var groupedFieldName = viewUtils.getGroupByField(parent.groupedBy[0]);
|
||||
var groupedField = parent.fields[groupedFieldName];
|
||||
const self = this;
|
||||
const parent = this.localData[parentID];
|
||||
const new_group = this.localData[groupID];
|
||||
const changes = {};
|
||||
const groupedFieldName = viewUtils.getGroupByField(parent.groupedBy[0]);
|
||||
const groupedField = parent.fields[groupedFieldName];
|
||||
// for a date/datetime field, we take the last moment of the group as the group value
|
||||
if (['date', 'datetime'].includes(groupedField.type)) {
|
||||
changes[groupedFieldName] = viewUtils.getGroupValue(new_group, groupedFieldName);
|
||||
@@ -147,7 +224,7 @@ const ProjectTaskKanbanModel = KanbanModel.extend({
|
||||
display_name: new_group.value,
|
||||
};
|
||||
} else if (groupedField.type === 'selection') {
|
||||
var value = _.findWhere(groupedField.selection, {1: new_group.value});
|
||||
const value = _.findWhere(groupedField.selection, {1: new_group.value});
|
||||
changes[groupedFieldName] = value && value[0] || false;
|
||||
} else if (groupedField.type == 'many2many' && groupedFieldName == 'personal_stage_type_ids') {
|
||||
changes['personal_stage_type_id'] = {
|
||||
@@ -161,13 +238,13 @@ const ProjectTaskKanbanModel = KanbanModel.extend({
|
||||
// Manually updates groups data. Note: this is done before the actual
|
||||
// save as it might need to perform a read group in some cases so those
|
||||
// updated data might be overridden again.
|
||||
var record = self.localData[recordID];
|
||||
var resID = record.res_id;
|
||||
const record = self.localData[recordID];
|
||||
const resID = record.res_id;
|
||||
// Remove record from its current group
|
||||
var old_group;
|
||||
for (var i = 0; i < parent.data.length; i++) {
|
||||
let old_group;
|
||||
for (let i = 0; i < parent.data.length; i++) {
|
||||
old_group = self.localData[parent.data[i]];
|
||||
var index = _.indexOf(old_group.data, recordID);
|
||||
const index = _.indexOf(old_group.data, recordID);
|
||||
if (index >= 0) {
|
||||
old_group.data.splice(index, 1);
|
||||
old_group.count--;
|
||||
@@ -193,6 +270,83 @@ const ProjectTaskKanbanModel = KanbanModel.extend({
|
||||
return [old_group.id, new_group.id];
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* When grouped by personal stage create a new personal stage instead of
|
||||
* a regular stage.
|
||||
* Meaning setting `user_id` on the stage.
|
||||
*
|
||||
* @override
|
||||
*/
|
||||
createGroup: function (name, parentID) {
|
||||
const parent = this.localData[parentID];
|
||||
const groupedFieldName = viewUtils.getGroupByField(parent.groupedBy[0]);
|
||||
if (groupedFieldName !== 'personal_stage_type_ids') {
|
||||
return this._super(...arguments);
|
||||
}
|
||||
const groupBy = parent.groupedBy[0];
|
||||
const context = Object.assign({}, parent.context, {
|
||||
default_user_id: this.getSession().user_id[0],
|
||||
});
|
||||
// In case it's a personal stage we don't want to assign it to the project.
|
||||
delete context.default_project_id;
|
||||
return this._rpc({
|
||||
model: 'project.task.type',
|
||||
method: 'name_create',
|
||||
args: [name],
|
||||
context: context,
|
||||
})
|
||||
.then((result) => {
|
||||
const createGroupDataPoint = (model, parent) => {
|
||||
const newGroup = model._makeDataPoint({
|
||||
modelName: parent.model,
|
||||
context: parent.context,
|
||||
domain: parent.domain.concat([[groupBy, "=", result[0]]]),
|
||||
fields: parent.fields,
|
||||
fieldsInfo: parent.fieldsInfo,
|
||||
isOpen: true,
|
||||
limit: parent.limit,
|
||||
parentID: parent.id,
|
||||
openGroupByDefault: true,
|
||||
orderedBy: parent.orderedBy,
|
||||
value: result,
|
||||
viewType: parent.viewType,
|
||||
});
|
||||
if (parent.progressBar) {
|
||||
newGroup.progressBarValues = _.extend({
|
||||
counts: {},
|
||||
}, parent.progressBar);
|
||||
}
|
||||
return newGroup;
|
||||
};
|
||||
const newGroup = createGroupDataPoint(this, parent);
|
||||
parent.data.push(newGroup.id);
|
||||
if (this.isInSampleMode()) {
|
||||
// in sample mode, create the new group in both models (main + sample)
|
||||
const sampleParent = this.sampleModel.localData[parentID];
|
||||
const newSampleGroup = createGroupDataPoint(this.sampleModel, sampleParent);
|
||||
sampleParent.data.push(newSampleGroup.id);
|
||||
}
|
||||
return newGroup.id;
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Force tasks assigned to the user when grouping by personal stage.
|
||||
*
|
||||
* @override
|
||||
* @private
|
||||
*/
|
||||
_readGroup: function (list) {
|
||||
const groupedBy = list.groupedBy[0];
|
||||
if (groupedBy === 'personal_stage_type_ids') {
|
||||
list.domain = Domain.and([
|
||||
[['user_ids', 'in', this.getSession().user_id[0]]],
|
||||
list.domain
|
||||
]).toList();
|
||||
}
|
||||
return this._super(...arguments);
|
||||
},
|
||||
})
|
||||
|
||||
const ProjectKanbanView = KanbanView.extend({
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
/** @odoo-module */
|
||||
|
||||
import tour from 'web_tour.tour';
|
||||
|
||||
tour.register('personal_stage_tour', {
|
||||
test: true,
|
||||
url: '/web',
|
||||
},
|
||||
[tour.stepUtils.showAppsMenuItem(), {
|
||||
trigger: '.o_app[data-menu-xmlid="project.menu_main_pm"]',
|
||||
}, {
|
||||
content: "Open Pig Project",
|
||||
trigger: '.o_kanban_record:contains("Pig")',
|
||||
}, {
|
||||
// Default is grouped by stage, user should not be able to create/edit a column
|
||||
content: "Check that there is no create column",
|
||||
trigger: "body:not(.o_column_quick_create)",
|
||||
run: function () {},
|
||||
}, {
|
||||
content: "Check that there is no create column",
|
||||
trigger: "body:not(.o_column_edit)",
|
||||
run: function () {},
|
||||
}, {
|
||||
content: "Check that there is no create column",
|
||||
trigger: "body:not(.o_column_delete)",
|
||||
run: function () {},
|
||||
}, {
|
||||
content: "Go to my tasks", // My tasks is grouped by personal stage by default
|
||||
trigger: 'a[data-menu-xmlid="project.menu_project_management"]',
|
||||
}, {
|
||||
content: "Check that we can create a new stage",
|
||||
trigger: '.o_column_quick_create .o_quick_create_folded'
|
||||
}, {
|
||||
content: "Create a new personal stage",
|
||||
trigger: 'input[placeholder="Column title"]',
|
||||
run: 'text Never',
|
||||
}, {
|
||||
content: "Confirm create",
|
||||
trigger: '.o_kanban_add',
|
||||
}, {
|
||||
content: "Check that column exists",
|
||||
trigger: '.o_kanban_header:contains("Never")',
|
||||
run: function () {},
|
||||
}, {
|
||||
content: 'Open column edit dropdown',
|
||||
trigger: '.o_kanban_header:eq(0)',
|
||||
run: function () {
|
||||
$('.o_kanban_config.dropdown .dropdown-toggle').eq(0).click();
|
||||
},
|
||||
}, {
|
||||
content: "Try editing inbox",
|
||||
trigger: ".dropdown-item.o_column_edit",
|
||||
}, {
|
||||
content: "Change title",
|
||||
trigger: 'input.o_field_char[name="name"]',
|
||||
run: 'text (Todo)',
|
||||
}, {
|
||||
content: "Save changes",
|
||||
trigger: '.btn-primary:contains("Save")',
|
||||
}, {
|
||||
content: "Check that column was updated",
|
||||
trigger: '.o_kanban_header:contains("Todo")',
|
||||
}]);
|
||||
@@ -1,6 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from odoo.tests import tagged
|
||||
from odoo.tests import tagged, HttpCase
|
||||
|
||||
from .test_project_base import TestProjectCommon
|
||||
|
||||
@@ -89,3 +89,10 @@ class TestPersonalStages(TestProjectCommon):
|
||||
'read_group should not have returned more tasks than the user is assigned to.')
|
||||
self.assertEqual(1, total_stage_0)
|
||||
self.assertEqual(1, total_stage_1)
|
||||
|
||||
@tagged('-at_install', 'post_install')
|
||||
class TestPersonalStageTour(HttpCase, TestProjectCommon):
|
||||
|
||||
def test_personal_stage_tour(self):
|
||||
# Test customizing personal stages as a project user
|
||||
self.start_tour('/web', 'personal_stage_tour', login="armandel")
|
||||
|
||||
@@ -41,7 +41,8 @@ class TestProjectCommon(TransactionCase):
|
||||
'groups_id': [(6, 0, [cls.env.ref('base.group_portal').id])]})
|
||||
cls.user_projectuser = Users.create({
|
||||
'name': 'Armande ProjectUser',
|
||||
'login': 'Armande',
|
||||
'login': 'armandel',
|
||||
'password': 'armandel',
|
||||
'email': 'armande.projectuser@example.com',
|
||||
'groups_id': [(6, 0, [user_group_employee.id, user_group_project_user.id])]
|
||||
})
|
||||
|
||||
@@ -274,6 +274,33 @@
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="personal_task_type_edit" model="ir.ui.view">
|
||||
<field name="name">project.task.type.form</field>
|
||||
<field name="model">project.task.type</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Task Stage" delete="0">
|
||||
<field name="active" invisible="1" />
|
||||
<sheet>
|
||||
<widget name="web_ribbon" title="Archived" bg_color="bg-danger" attrs="{'invisible': [('active', '=', True)]}" />
|
||||
<group>
|
||||
<group>
|
||||
<field name="name"/>
|
||||
<field name="sequence" groups="base.group_no_one"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="fold"/>
|
||||
</group>
|
||||
</group>
|
||||
<group>
|
||||
<group>
|
||||
<field name="description" placeholder="Add a description..." nolabel="1" colspan="2"/>
|
||||
</group>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="task_type_tree" model="ir.ui.view">
|
||||
<field name="name">project.task.type.tree</field>
|
||||
<field name="model">project.task.type</field>
|
||||
|
||||
Reference in New Issue
Block a user