diff --git a/addons/project/models/project.py b/addons/project/models/project.py
index 4baad1250ec..089d67ab8fc 100644
--- a/addons/project/models/project.py
+++ b/addons/project/models/project.py
@@ -38,6 +38,7 @@ PROJECT_TASK_READABLE_FIELDS = {
'displayed_image_id',
'display_name',
'priority',
+ 'portal_user_names',
}
PROJECT_TASK_WRITABLE_FIELDS = {
@@ -956,6 +957,8 @@ class Task(models.Model):
# Tracking of this field is done in the write function
user_ids = fields.Many2many('res.users', relation='project_task_user_rel', column1='task_id', column2='user_id',
string='Assignees', default=lambda self: self.env.user)
+ # User names displayed in project sharing views
+ portal_user_names = fields.Char(compute='_compute_portal_user_names', compute_sudo=True, search='_search_portal_user_names')
# Second Many2many containing the actual personal stage for the current user
# See project_task_stage_personal.py for the model defininition
personal_stage_type_ids = fields.Many2many('project.task.type', 'project_task_user_rel', column1='task_id', column2='stage_id',
@@ -1417,6 +1420,35 @@ class Task(models.Model):
else:
task.stage_id = False
+ @api.depends('user_ids')
+ def _compute_portal_user_names(self):
+ """ This compute method allows to see all the names of assigned users to each task contained in `self`.
+
+ When we are in the project sharing feature, the `user_ids` contains only the users if we are a portal user.
+ That is, only the users in the same company of the current user.
+ So this compute method is a related of `user_ids.name` but with more records that the portal user
+ can normally see.
+ (In other words, this compute is only used in project sharing views to see all assignees for each task)
+ """
+ if self.ids:
+ # fetch 'user_ids' in superuser mode (and override value in cache)
+ self._read(['user_ids'])
+ for task in self.with_context(prefetch_fields=False):
+ task.portal_user_names = ', '.join(task.user_ids.mapped('name'))
+
+ def _search_portal_user_names(self, operator, value):
+ if operator != 'ilike' and not isinstance(value, str):
+ raise ValidationError('Not Implemented.')
+
+ query = """
+ SELECT task_user.task_id
+ FROM project_task_user_rel task_user
+ INNER JOIN res_users users ON task_user.user_id = users.id
+ INNER JOIN res_partner partners ON partners.id = users.partner_id
+ WHERE partners.name ILIKE %s
+ """
+ return [('id', 'inselect', (query, [f'%{value}%']))]
+
@api.returns('self', lambda value: value.id)
def copy(self, default=None):
if default is None:
@@ -1989,6 +2021,9 @@ class Task(models.Model):
def action_assign_to_me(self):
self.write({'user_ids': [(4, self.env.user.id)]})
+ def action_unassign_me(self):
+ self.write({'user_ids': [Command.unlink(self.env.uid)]})
+
# If depth == 1, return only direct children
# If depth == 3, return children to third generation
# If depth <= 0, return all children without depth limit
diff --git a/addons/project/tests/test_project_sharing.py b/addons/project/tests/test_project_sharing.py
index ff3ff4f948b..443df6df388 100644
--- a/addons/project/tests/test_project_sharing.py
+++ b/addons/project/tests/test_project_sharing.py
@@ -123,7 +123,7 @@ class TestProjectSharing(TestProjectSharingCommon):
task = form.save()
self.assertEqual(task.name, 'Test')
self.assertEqual(task.project_id, self.project_portal)
- self.assertEqual(task.user_ids, self.user_portal)
+ self.assertEqual(task.portal_user_names, self.user_portal.name)
# 3.1) Try to change the project of the new task with this user.
with self.assertRaises(AssertionError, msg="Should not accept the portal user changes the project of the task."):
form.project_id = self.project_cows
@@ -160,16 +160,49 @@ class TestProjectSharing(TestProjectSharingCommon):
task = form.save()
self.assertEqual(task.name, 'Test')
self.assertEqual(task.project_id, self.project_cows)
- # 3.1) Try to change the project of the new task with this user.
- with self.assertRaises(AssertionError, msg="Should not accept the portal user changes the project of the task."):
+
+ # 3.1) Try to change the project of the new task with this user.
+ with self.assertRaises(AssertionError, msg="Should not accept the portal user changes the project of the task."):
+ with self.get_project_sharing_form_view(task, self.user_portal) as form:
form.project_id = self.project_portal
- task = form.save()
- # 3.2) Create a sub-task
+
+ # 3.2) Create a sub-task
+ with self.get_project_sharing_form_view(task, self.user_portal) as form:
with form.child_ids.new() as subtask_form:
subtask_form.name = 'Test Subtask'
with self.assertRaises(AssertionError, msg="Should not accept the portal user changes the project of the task."):
subtask_form.display_project_id = self.project_portal
- form.save()
- self.assertEqual(task.child_ids.name, 'Test Subtask')
- self.assertEqual(task.child_ids.project_id, self.project_cows)
- self.assertEqual(task.child_ids.user_ids, self.user_portal)
+ self.assertEqual(task.child_ids.name, 'Test Subtask')
+ self.assertEqual(task.child_ids.project_id, self.project_cows)
+ self.assertEqual(task.child_ids.portal_user_names, self.user_portal.name)
+ self.assertEqual(task.child_ids.user_ids, self.user_portal)
+
+ def test_portal_user_cannot_see_all_assignees(self):
+ """ Test when the portal sees a task he cannot see all the assignees.
+
+ Because of a ir.rule in res.partner filters the assignees, the portal
+ can only see the assignees in the same company than him.
+
+ Test Cases:
+ ==========
+ 1) add many assignees in a task
+ 2) check the portal user can read no assignee in this task. Should have an AccessError exception
+ """
+ self.task_cow.write({'user_ids': [Command.link(self.user_projectmanager.id)]})
+ with self.assertRaises(AccessError, msg="Should not accept the portal user to access to a task he does not follow it and its project."):
+ self.task_cow.with_user(self.user_portal).read(['portal_user_names'])
+ self.assertEqual(len(self.task_cow.user_ids), 2, '2 users should be assigned in this task.')
+
+ project_share_wizard = self.env['project.share.wizard'].create({
+ 'access_mode': 'edit',
+ 'res_model': 'project.project',
+ 'res_id': self.project_cows.id,
+ 'partner_ids': [
+ Command.link(self.user_portal.partner_id.id),
+ ],
+ })
+ project_share_wizard.action_send_mail()
+
+ self.assertFalse(self.task_cow.with_user(self.user_portal).user_ids, 'the portal user should see no assigness in the task.')
+ task_portal_read = self.task_cow.with_user(self.user_portal).read(['portal_user_names'])
+ self.assertEqual(self.task_cow.portal_user_names, task_portal_read[0]['portal_user_names'], 'the portal user should see assignees name in the task via the `portal_user_names` field.')
diff --git a/addons/project/views/project_sharing_views.xml b/addons/project/views/project_sharing_views.xml
index 60cd3f88a5f..01264b38385 100644
--- a/addons/project/views/project_sharing_views.xml
+++ b/addons/project/views/project_sharing_views.xml
@@ -31,7 +31,9 @@