Skip to content

Commit 4436b9a

Browse files
committed
[IMP] project: add project roles, role-user mapping models, tasks assignation
1 parent 1514277 commit 4436b9a

12 files changed

+274
-4
lines changed

addons/project/__manifest__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
'views/project_task_type_views.xml',
3636
'views/project_project_views.xml',
3737
'views/project_task_views.xml',
38+
'views/project_role_views.xml',
3839
'views/project_tags_views.xml',
3940
'views/project_milestone_views.xml',
4041
'views/res_partner_views.xml',

addons/project/models/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from . import project_task_stage_personal
1010
from . import project_milestone
1111
from . import project_project
12+
from . import project_role
1213
from . import project_task
1314
from . import project_task_type
1415
from . import project_tags

addons/project/models/project_project.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,6 @@ def _set_favorite_user_ids(self, is_favorite):
146146
task_properties_definition = fields.PropertiesDefinition('Task Properties')
147147
closed_task_count = fields.Integer(compute="_compute_closed_task_count", export_string_translation=False)
148148
task_completion_percentage = fields.Float(compute="_compute_task_completion_percentage", export_string_translation=False)
149-
role_user_ids = fields.One2many('project.role.user.map', 'project_id')
150149

151150
# Project Sharing fields
152151
collaborator_ids = fields.One2many('project.collaborator', 'project_id', string='Collaborators', copy=False, export_string_translation=False)

addons/project/models/project_role.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
from random import randint
2+
3+
from odoo import fields, models
4+
5+
6+
class ProjectRole(models.Model):
7+
_name = 'project.role'
8+
_description = 'Project Role'
9+
10+
def _get_default_color(self):
11+
return randint(1, 11)
12+
13+
active = fields.Boolean(default=True)
14+
name = fields.Char(required=True, translate=True)
15+
color = fields.Integer(default=_get_default_color)
16+
sequence = fields.Integer(export_string_translation=False)
17+
18+
def copy_data(self, default=None):
19+
vals_list = super().copy_data(default=default)
20+
return [dict(vals, name=self.env._('%s (copy)', role.name)) for role, vals in zip(self, vals_list)]

addons/project/models/project_task.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,7 @@ def _read_group_personal_stage_type_ids(self, stages, domain):
186186
allocated_hours = fields.Float("Allocated Time", tracking=True)
187187
subtask_allocated_hours = fields.Float("Sub-tasks Allocated Time", compute='_compute_subtask_allocated_hours', export_string_translation=False,
188188
help="Sum of the hours allocated for all the sub-tasks (and their own sub-tasks) linked to this task. Usually less than or equal to the allocated hours of this task.")
189+
role_ids = fields.Many2many('project.role', string='Project Roles')
189190
# Tracking of this field is done in the write function
190191
user_ids = fields.Many2many('res.users', relation='project_task_user_rel', column1='task_id', column2='user_id',
191192
string='Assignees', context={'active_test': False}, tracking=True, default=_default_user_ids, domain="[('share', '=', False), ('active', '=', True)]", falsy_value_label=_lt("👤 Unassigned"))
@@ -1934,6 +1935,7 @@ def action_convert_to_template(self):
19341935
def action_undo_convert_to_template(self):
19351936
self.ensure_one()
19361937
self.is_template = False
1938+
self.role_ids = False
19371939
self.message_post(body=_("Template converted back to regular task"))
19381940
return {
19391941
'type': 'ir.actions.client',

addons/project/security/ir.model.access.csv

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,3 +48,7 @@ access_mail_activity_plan_project_manager,mail.activity.plan.project.manager,mai
4848
access_mail_activity_plan_template_project_manager,mail.activity.plan.template.project.manager,mail.model_mail_activity_plan_template,project.group_project_manager,1,1,1,1
4949
access_project_template_create_wizard_user,project.template.create.wizard.user,project.model_project_template_create_wizard,project.group_project_user,1,1,0,0
5050
access_project_template_create_wizard_manager,project.template.create.wizard.manager,project.model_project_template_create_wizard,project.group_project_manager,1,1,1,1
51+
access_project_template_role_to_users_map_user,project.template.role.to.users.map.user,project.model_project_template_role_to_users_map,project.group_project_user,1,1,0,0
52+
access_project_template_role_to_users_map_manager,project.template.role.to.users.map.manager,project.model_project_template_role_to_users_map,project.group_project_manager,1,1,1,1
53+
access_project_role_user,project.role.user,model_project_role,project.group_project_user,1,0,0,0
54+
access_project_role_manager,project.role.manager,model_project_role,project.group_project_manager,1,1,1,1

addons/project/tests/test_project_template.py

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from odoo import Command
12
from odoo.addons.project.tests.test_project_base import TestProjectCommon
23

34

@@ -38,3 +39,111 @@ def test_revert_template(self):
3839
"""
3940
self.project_template.action_undo_convert_to_template()
4041
self.assertFalse(self.project_template.is_template, "The reverted template should become a normal template.")
42+
43+
def test_tasks_dispatching_from_template(self):
44+
"""
45+
The tasks of a project template should be dispatched to the new project according to the role-to-users mapping defined
46+
on the project template wizard.
47+
"""
48+
role1, role2, role3, role4, role5 = self.env['project.role'].create([
49+
{'name': 'Developer'},
50+
{'name': 'Designer'},
51+
{'name': 'Project Manager'},
52+
{'name': 'Tester'},
53+
{'name': 'Product Owner'},
54+
])
55+
project_template = self.env['project.project'].create({
56+
'name': 'Project template',
57+
'is_template': True,
58+
'task_ids': [
59+
Command.create({
60+
'name': 'Task 1',
61+
'role_ids': [role1.id, role3.id],
62+
}),
63+
Command.create({
64+
'name': 'Task 2',
65+
'role_ids': [role5.id, role4.id],
66+
}),
67+
Command.create({
68+
'name': 'Task 3',
69+
'role_ids': [role2.id, role5.id],
70+
}),
71+
Command.create({
72+
'name': 'Task 4',
73+
'role_ids': [role3.id],
74+
}),
75+
Command.create({
76+
'name': 'Task 5',
77+
'role_ids': [role5.id],
78+
}),
79+
Command.create({
80+
'name': 'Task 6',
81+
}),
82+
],
83+
})
84+
user1, user2 = self.env['res.users'].create([
85+
{
86+
'name': 'Test User 1',
87+
'login': 'test1',
88+
'password': 'testuser1',
89+
'email': '[email protected]',
90+
},
91+
{
92+
'name': 'Test User 2',
93+
'login': 'test2',
94+
'password': 'testuser2',
95+
'email': '[email protected]',
96+
}
97+
])
98+
wizard = self.env['project.template.create.wizard'].create({
99+
'template_id': project_template.id,
100+
'name': 'New Project from Template',
101+
'role_to_users_ids': [
102+
Command.create({
103+
'role_id': role1.id,
104+
'user_ids': [self.user_projectuser.id, self.user_projectmanager.id],
105+
}),
106+
Command.create({
107+
'role_id': role2.id,
108+
'user_ids': [user1.id],
109+
}),
110+
Command.create({
111+
'role_id': role3.id,
112+
'user_ids': [user2.id],
113+
}),
114+
Command.create({
115+
'role_id': role4.id,
116+
'user_ids': [self.user_projectuser.id],
117+
}),
118+
],
119+
})
120+
new_project = wizard._create_project_from_template()
121+
122+
self.assertEqual(
123+
new_project.task_ids.filtered(lambda t: t.name == 'Task 1').user_ids,
124+
self.user_projectuser + self.user_projectmanager,
125+
'Task 1 should be assigned to the users mapped to `role1`.',
126+
)
127+
self.assertEqual(
128+
new_project.task_ids.filtered(lambda t: t.name == 'Task 2').user_ids,
129+
self.user_projectuser,
130+
'Task 2 should be assigned to the users mapped to `role4`. As `role5` is not in the mapping.',
131+
)
132+
self.assertEqual(
133+
new_project.task_ids.filtered(lambda t: t.name == 'Task 3').user_ids,
134+
user1,
135+
'Task 3 should be assigned to the users mapped to `role2`.',
136+
)
137+
self.assertEqual(
138+
new_project.task_ids.filtered(lambda t: t.name == 'Task 4').user_ids,
139+
user2,
140+
'Task 4 should be assigned to the users mapped to `role3`.'
141+
)
142+
self.assertFalse(
143+
new_project.task_ids.filtered(lambda t: t.name == 'Task 5').user_ids,
144+
'Task 5 should not be assigned to any user as `role5` is not in the mapping.',
145+
)
146+
self.assertFalse(
147+
new_project.task_ids.filtered(lambda t: t.name == 'Task 6').user_ids,
148+
'Task 6 should not be assigned to any user as it has no role.',
149+
)

addons/project/views/project_menus.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,11 @@
113113
id="project_menu_config_task_templates"
114114
action="project_task_templates_action"
115115
/>
116+
<menuitem
117+
name="Project Roles"
118+
id="project_menu_config_project_roles"
119+
action="project_roles_action"
120+
/>
116121
<menuitem
117122
name="Activity Types"
118123
id="project_menu_config_activity_type"
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<odoo>
3+
<record id="project_role_view_list" model="ir.ui.view">
4+
<field name="name">project.role.list</field>
5+
<field name="model">project.role</field>
6+
<field name="arch" type="xml">
7+
<list editable="bottom" multi_edit="1">
8+
<field name="sequence" widget="handle"/>
9+
<field name="name" placeholder="e.g. Developer"/>
10+
<field name="color" widget="color_picker" optional="show"/>
11+
</list>
12+
</field>
13+
</record>
14+
15+
<record id="project_role_view_form" model="ir.ui.view">
16+
<field name="name">project.role.form</field>
17+
<field name="model">project.role</field>
18+
<field name="arch" type="xml">
19+
<form>
20+
<sheet>
21+
<widget name="web_ribbon" title="Archived" bg_color="text-bg-danger" invisible="active"/>
22+
<group>
23+
<field name="active" invisible="1"/>
24+
<field name="name"/>
25+
<field name="color" widget="color_picker"/>
26+
</group>
27+
</sheet>
28+
</form>
29+
</field>
30+
</record>
31+
32+
<record id="project_role_view_kanban" model="ir.ui.view">
33+
<field name="name">project.role.kanban</field>
34+
<field name="model">project.role</field>
35+
<field name="arch" type="xml">
36+
<kanban highlight_color="color">
37+
<templates>
38+
<t t-name="menu">
39+
<a t-if="widget.editable" role="menuitem" type="open" class="dropdown-item">Edit</a>
40+
<a t-if="widget.deletable" role="menuitem" type="delete" class="dropdown-item">Delete</a>
41+
<field name="color" widget="kanban_color_picker"/>
42+
</t>
43+
<t t-name="card">
44+
<field name="name" class="fw-bold fs-4 ms-1"/>
45+
</t>
46+
</templates>
47+
</kanban>
48+
</field>
49+
</record>
50+
51+
<record id="project_role_view_search" model="ir.ui.view">
52+
<field name="name">project.role.search</field>
53+
<field name="model">project.role</field>
54+
<field name="arch" type="xml">
55+
<search>
56+
<field name="name"/>
57+
<filter name="archived" string="Archived" domain="[('active', '=', False)]"/>
58+
</search>
59+
</field>
60+
</record>
61+
62+
<record id="project_roles_action" model="ir.actions.act_window">
63+
<field name="name">Project Roles</field>
64+
<field name="res_model">project.role</field>
65+
<field name="view_mode">list,kanban,form</field>
66+
<field name="search_view_id" ref="project_role_view_search"/>
67+
<field name="help" type="html">
68+
<p class="o_view_nocontent_smiling_face">
69+
No project role found. Let's create one!
70+
</p>
71+
</field>
72+
</record>
73+
74+
<record id="project_roles_action_list" model="ir.actions.act_window.view">
75+
<field name="act_window_id" ref="project_roles_action"/>
76+
<field name="view_mode">list</field>
77+
<field name="view_id" ref="project.project_role_view_list"/>
78+
</record>
79+
80+
<record id="project_roles_action_kanban" model="ir.actions.act_window.view">
81+
<field name="act_window_id" ref="project_roles_action"/>
82+
<field name="view_mode">kanban</field>
83+
<field name="view_id" ref="project.project_role_view_kanban"/>
84+
</record>
85+
</odoo>

addons/project/views/project_task_views.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -416,6 +416,7 @@
416416
class="o_task_user_field"
417417
options="{'no_open': True, 'no_quick_create': True}"
418418
widget="many2many_avatar_user"/>
419+
<field name="role_ids" invisible="not is_template" widget="many2many_tags" options="{'color_field': 'color', 'no_create_edit': True}" placeholder="Assign at project creation"/>
419420
<field name="priority" widget="priority_switch"/>
420421
<field name="tag_ids" widget="many2many_tags" options="{'color_field': 'color', 'no_create_edit': True}" context="{'project_id': project_id}"/>
421422
</group>

addons/project/wizard/project_template_create_wizard.py

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,18 +12,37 @@ class ProjectTemplateCreateWizard(models.TransientModel):
1212
alias_domain_id = fields.Many2one("mail.alias.domain", string="Alias Domain")
1313
partner_id = fields.Many2one("res.partner")
1414
template_id = fields.Many2one("project.project", default=lambda self: self._context.get('template_id'))
15+
task_role_ids = fields.Many2many('project.role', compute='_compute_task_role_ids', export_string_translation=False)
16+
role_to_users_ids = fields.One2many('project.template.role.to.users.map', 'wizard_id')
1517

16-
def create_project_from_template(self):
18+
@api.depends('template_id.task_ids.role_ids')
19+
def _compute_task_role_ids(self):
20+
for map in self:
21+
map.task_role_ids = map.template_id.task_ids.role_ids
22+
23+
def _get_project_template_wizard_fields(self):
24+
return ["name", "date_start", "date", "alias_name", "alias_domain_id", "partner_id"]
25+
26+
def _create_project_from_template(self):
1727
# Dictionary with all fields and their values
1828
field_values = self._convert_to_write(
1929
{
2030
fname: self[fname]
21-
for fname in self._fields.keys() - ["id", "template_id"]
31+
for fname in self._fields.keys() & self._get_project_template_wizard_fields()
2232
}
2333
)
2434
project = self.template_id.action_create_from_template(field_values)
35+
# Tasks dispatching using project roles
36+
for original_task, new_task in zip(self.template_id.task_ids, project.task_ids):
37+
for role in original_task.role_ids:
38+
if users := self.role_to_users_ids.filtered(lambda m: m.role_id == role).user_ids:
39+
new_task.user_ids |= users
40+
break
41+
return project
42+
43+
def create_project_from_template(self):
2544
# Opening project task views after creation of project from template
26-
return project.action_view_tasks()
45+
return self._create_project_from_template().action_view_tasks()
2746

2847
@api.model
2948
def action_open_template_view(self):
@@ -40,3 +59,20 @@ def action_open_template_view(self):
4059
'target': 'new',
4160
'context': self.env.context,
4261
}
62+
63+
64+
class ProjectTemplateRoleToUsersMap(models.TransientModel):
65+
_name = 'project.template.role.to.users.map'
66+
_description = 'Project role to users mapping'
67+
_order = 'sequence, id'
68+
69+
sequence = fields.Integer(default=10)
70+
wizard_id = fields.Many2one('project.template.create.wizard', export_string_translation=False)
71+
task_role_ids = fields.Many2many('project.role', related='wizard_id.task_role_ids', export_string_translation=False)
72+
role_id = fields.Many2one('project.role', string='Project Role', domain="[('id', 'in', task_role_ids)]", required=True)
73+
user_ids = fields.Many2many('res.users', string='Assignees', required=True)
74+
75+
_role_uniq = models.Constraint(
76+
'UNIQUE(wizard_id,role_id)',
77+
'A role cannot be selected more than once in the mapping. Please remove duplicate(s) and try again.',
78+
)

addons/project/wizard/project_template_create_wizard.xml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,13 @@
2424
options="{'no_create': True, 'no_open': True}"/>
2525
</span>
2626
</div>
27+
<field name="role_to_users_ids" mode="list" invisible="not task_role_ids">
28+
<list editable="bottom">
29+
<field name="sequence" widget="handle"/>
30+
<field name="role_id" options="{'no_create': True, 'no_open': True}"/>
31+
<field name="user_ids" widget="many2many_avatar_user"/>
32+
</list>
33+
</field>
2734
<footer>
2835
<button string="Create project" name="create_project_from_template" type="object" class="btn-primary o_open_tasks" data-hotkey="q"/>
2936
<button string="Discard" class="btn-secondary" special="cancel" data-hotkey="x"/>

0 commit comments

Comments
 (0)