diff --git a/addons/hr_timesheet/security/hr_timesheet_security.xml b/addons/hr_timesheet/security/hr_timesheet_security.xml index 2b20fc60087..63d1b679d9d 100644 --- a/addons/hr_timesheet/security/hr_timesheet_security.xml +++ b/addons/hr_timesheet/security/hr_timesheet_security.xml @@ -39,7 +39,7 @@ ('project_id.message_partner_ids', 'child_of', [user.partner_id.commercial_partner_id.id]), ('task_id.message_partner_ids', 'child_of', [user.partner_id.commercial_partner_id.id]), ('project_id.privacy_visibility', '=', 'portal'), - ('project_id.project_sharing_access_ids.user_id', 'in', [user.id]), + ('project_id.collaborator_ids.partner_id', 'in', [user.partner_id.id]), ] diff --git a/addons/portal/wizard/portal_share.py b/addons/portal/wizard/portal_share.py index c8c36418a01..48f19e8ff22 100644 --- a/addons/portal/wizard/portal_share.py +++ b/addons/portal/wizard/portal_share.py @@ -17,13 +17,26 @@ class PortalShare(models.TransientModel): result['share_link'] = record.get_base_url() + record._get_share_url(redirect=True) return result + @api.model + def _selection_target_model(self): + return [(model.model, model.name) for model in self.env['ir.model'].sudo().search([])] + res_model = fields.Char('Related Document Model', required=True) res_id = fields.Integer('Related Document ID', required=True) + resource_ref = fields.Reference('_selection_target_model', 'Related Document', compute='_compute_resource_ref') partner_ids = fields.Many2many('res.partner', string="Recipients", required=True) note = fields.Text(help="Add extra content to display in the email") share_link = fields.Char(string="Link", compute='_compute_share_link') access_warning = fields.Text("Access warning", compute="_compute_access_warning") + @api.depends('res_model', 'res_id') + def _compute_resource_ref(self): + for wizard in self: + if wizard.res_model and wizard.res_model in self.env: + wizard.resource_ref = '%s,%s' % (wizard.res_model, wizard.res_id or 0) + else: + wizard.resource_ref = None + @api.depends('res_model', 'res_id') def _compute_share_link(self): for rec in self: @@ -44,48 +57,61 @@ class PortalShare(models.TransientModel): record = res_model.browse(rec.res_id) rec.access_warning = record.access_warning + @api.model + def _get_note(self): + return self.env.ref('mail.mt_note') + + def _send_public_link(self, note, partners=None): + if not partners: + partners = self.partner_ids + for partner in partners: + share_link = self.resource_ref.get_base_url() + self.resource_ref._get_share_url(redirect=True, pid=partner.id) + saved_lang = self.env.lang + self = self.with_context(lang=partner.lang) + template = self.env.ref('portal.portal_share_template', False) + self.resource_ref.message_post_with_view(template, + values={'partner': partner, 'note': self.note, 'record': self.resource_ref, + 'share_link': share_link}, + subject=_("You are invited to access %s", self.resource_ref.display_name), + subtype_id=note.id, + email_layout_xmlid='mail.mail_notification_light', + partner_ids=[(6, 0, partner.ids)]) + self = self.with_context(lang=saved_lang) + + def _send_signup_link(self, note, partners=None): + if not partners: + partners = self.partner_ids.filtered(lambda partner: not partner.user_ids) + for partner in partners: + # prepare partner for signup and send singup url with redirect url + partner.signup_get_auth_param() + share_link = partner._get_signup_url_for_action(action='/mail/view', res_id=self.res_id, model=self.res_model)[partner.id] + saved_lang = self.env.lang + self = self.with_context(lang=partner.lang) + template = self.env.ref('portal.portal_share_template', False) + self.resource_ref.message_post_with_view(template, + values={'partner': partner, 'note': self.note, 'record': self.resource_ref, + 'share_link': share_link}, + subject=_("You are invited to access %s", self.resource_ref.display_name), + subtype_id=note.id, + email_layout_xmlid='mail.mail_notification_light', + partner_ids=[(6, 0, partner.ids)]) + self = self.with_context(lang=saved_lang) + def action_send_mail(self): - active_record = self.env[self.res_model].browse(self.res_id) - note = self.env.ref('mail.mt_note') + note = self._get_note() signup_enabled = self.env['ir.config_parameter'].sudo().get_param('auth_signup.invitation_scope') == 'b2c' - if hasattr(active_record, 'access_token') and active_record.access_token or not signup_enabled: + if getattr(self.resource_ref, 'access_token', False) or not signup_enabled: partner_ids = self.partner_ids else: partner_ids = self.partner_ids.filtered(lambda x: x.user_ids) # if partner already user or record has access token send common link in batch to all user - for partner in self.partner_ids: - share_link = active_record.get_base_url() + active_record._get_share_url(redirect=True, pid=partner.id) - saved_lang = self.env.lang - self = self.with_context(lang=partner.lang) - template = self.env.ref('portal.portal_share_template', False) - active_record.message_post_with_view(template, - values={'partner': partner, 'note': self.note, 'record': active_record, - 'share_link': share_link}, - subject=_("You are invited to access %s", active_record.display_name), - subtype_id=note.id, - email_layout_xmlid='mail.mail_notification_light', - partner_ids=[(6, 0, partner.ids)]) - self = self.with_context(lang=saved_lang) + self._send_public_link(note, partner_ids) # when partner not user send individual mail with signup token - for partner in self.partner_ids - partner_ids: - # prepare partner for signup and send singup url with redirect url - partner.signup_get_auth_param() - share_link = partner._get_signup_url_for_action(action='/mail/view', res_id=self.res_id, model=self.model)[partner.id] - saved_lang = self.env.lang - self = self.with_context(lang=partner.lang) - template = self.env.ref('portal.portal_share_template', False) - active_record.message_post_with_view(template, - values={'partner': partner, 'note': self.note, 'record': active_record, - 'share_link': share_link}, - subject=_("You are invited to access %s", active_record.display_name), - subtype_id=note.id, - email_layout_xmlid='mail.mail_notification_light', - partner_ids=[(6, 0, partner.ids)]) - self = self.with_context(lang=saved_lang) + self._send_signup_link(note, self.partner_ids - partner_ids) # subscribe all recipients so that they receive future communication (better than # using autofollow as more precise) - active_record.message_subscribe(partner_ids=self.partner_ids.ids) + self.resource_ref.message_subscribe(partner_ids=self.partner_ids.ids) return {'type': 'ir.actions.act_window_close'} diff --git a/addons/project/__manifest__.py b/addons/project/__manifest__.py index bde65068b5d..0c28fc67a61 100644 --- a/addons/project/__manifest__.py +++ b/addons/project/__manifest__.py @@ -33,6 +33,7 @@ 'views/project_update_templates.xml', 'views/project_project_stage_views.xml', 'wizard/project_share_wizard_views.xml', + 'views/project_collaborator_views.xml', 'views/project_views.xml', 'views/res_partner_views.xml', 'views/res_config_settings_views.xml', diff --git a/addons/project/controllers/portal.py b/addons/project/controllers/portal.py index cd04920d309..152e531f5f7 100644 --- a/addons/project/controllers/portal.py +++ b/addons/project/controllers/portal.py @@ -113,7 +113,6 @@ class ProjectCustomerPortal(CustomerPortal): cache_hashes=cache_hashes, action_name='project.project_sharing_project_task_action', project_id=project.id, - can_edit=project._check_project_sharing_access('edit'), user_companies={ 'current_company': project_company.id, 'allowed_companies': { diff --git a/addons/project/controllers/project_sharing_chatter.py b/addons/project/controllers/project_sharing_chatter.py index c29123bb216..c3e3bc2b9c2 100644 --- a/addons/project/controllers/project_sharing_chatter.py +++ b/addons/project/controllers/project_sharing_chatter.py @@ -23,7 +23,7 @@ class ProjectSharingChatter(PortalChatter): If it is the case, then we give the access token of the task. """ project_sudo = ProjectCustomerPortal._document_check_access(self, 'project.project', project_id, token) - can_access = project_sudo and res_model == 'project.task' and project_sudo.with_user(request.env.user)._check_project_sharing_access('comment') + can_access = project_sudo and res_model == 'project.task' and project_sudo.with_user(request.env.user)._check_project_sharing_access() if token: can_access &= consteq(project_sudo.access_token, token) else: diff --git a/addons/project/models/__init__.py b/addons/project/models/__init__.py index dae83cffa9e..2f66f8d42bc 100644 --- a/addons/project/models/__init__.py +++ b/addons/project/models/__init__.py @@ -6,7 +6,7 @@ from . import project_milestone from . import project_project_stage from . import project_task_recurrence from . import project -from . import project_sharing_access +from . import project_collaborator from . import project_update from . import res_config_settings from . import res_partner diff --git a/addons/project/models/project.py b/addons/project/models/project.py index a75561cf9cb..8f4a3b7c2d6 100644 --- a/addons/project/models/project.py +++ b/addons/project/models/project.py @@ -7,13 +7,12 @@ from collections import defaultdict from datetime import timedelta, datetime from random import randint -from odoo import api, fields, models, tools, SUPERUSER_ID, _ +from odoo import api, Command, fields, models, tools, SUPERUSER_ID, _ from odoo.exceptions import UserError, ValidationError, AccessError -from odoo.osv import expression +from odoo.osv.expression import OR from .project_task_recurrence import DAYS, WEEKS from .project_update import STATUS_COLOR -from .project_sharing_access import PROJECT_SHARING_ACCESS_MODE PROJECT_TASK_READABLE_FIELDS = { @@ -38,7 +37,6 @@ PROJECT_TASK_READABLE_FIELDS = { 'displayed_image_id', 'display_name', 'priority', - 'project_sharing_access_mode', } PROJECT_TASK_WRITABLE_FIELDS = { @@ -283,7 +281,8 @@ class Project(models.Model): tag_ids = fields.Many2many('project.tags', relation='project_project_project_tags_rel', string='Tags') # Project Sharing fields - project_sharing_access_ids = fields.One2many('project.sharing.access', 'project_id', 'Project Sharing Access', copy=False) + collaborator_ids = fields.One2many('project.collaborator', 'project_id', string='Collaborators', copy=False) + collaborator_count = fields.Integer('# Collaborators', compute='_compute_collaborator_count') # rating fields rating_request_deadline = fields.Datetime(compute='_compute_rating_request_deadline', store=True) @@ -397,6 +396,18 @@ class Project(models.Model): else: project.alias_value = "%s@%s" % (project.alias_name, project.alias_domain) + @api.depends('collaborator_ids', 'privacy_visibility') + def _compute_collaborator_count(self): + project_sharings = self.filtered(lambda project: project.privacy_visibility == 'portal') + collaborator_read_group = self.env['project.collaborator'].read_group( + [('project_id', 'in', project_sharings.ids)], + ['project_id'], + ['project_id'], + ) + collaborator_count_by_project = {res['project_id'][0]: res['project_id_count'] for res in collaborator_read_group} + for project in self: + project.collaborator_count = collaborator_count_by_project.get(project.id, 0) + @api.model def _map_tasks_default_valeus(self, task, project): """ get the default value for the copied task on project duplication """ @@ -700,25 +711,6 @@ class Project(models.Model): }) project.write({'analytic_account_id': analytic_account.id}) - def _check_project_sharing_access(self, access_mode='read'): - self.ensure_one() - if access_mode not in map(lambda access: access[0], PROJECT_SHARING_ACCESS_MODE): - raise ValidationError('The %s does not exists in project sharing accesses.' % access_mode) - if self.privacy_visibility != 'portal' or self.env.user._is_public(): - return False - if self.env.user.has_group('base.group_portal'): - # check in project sharing access model of the project - project_sharing_access = self.project_sharing_access_ids.filtered(lambda access: access.user_id == self.env.user) - if not project_sharing_access: - return False - if access_mode == 'edit': - return project_sharing_access.access_mode == 'edit' - elif access_mode == 'comment': - return project_sharing_access.access_mode in ('comment', 'edit') - else: - return True - return self.env.user.has_group('base.group_user') - # --------------------------------------------------- # Rating business # --------------------------------------------------- @@ -750,6 +742,35 @@ class Project(models.Model): project.message_unsubscribe(partner_ids=portal_users.partner_id.ids) project.mapped('tasks')._change_project_privacy_visibility() + # --------------------------------------------------- + # Project sharing + # --------------------------------------------------- + def _check_project_sharing_access(self): + self.ensure_one() + if self.privacy_visibility != 'portal': + return False + if self.env.user.has_group('base.group_portal'): + return self.env.user.partner_id in self.collaborator_ids.partner_id + return self.env.user.has_group('base.group_user') + + def _add_collaborators(self, partners): + self.ensure_one() + user_group_id = self.env['ir.model.data']._xmlid_to_res_id('base.group_user') + all_collaborators = self.collaborator_ids.partner_id + new_collaborators = partners.filtered( + lambda partner: + partner not in all_collaborators + and (not partner.user_ids or user_group_id not in partner.user_ids[0].groups_id.ids) + ) + if not new_collaborators: + # Then we have nothing to do + return + self.write({'collaborator_ids': [ + Command.create({ + 'partner_id': collaborator.id, + }) for collaborator in new_collaborators], + }) + class Task(models.Model): _name = "project.task" _description = "Task" @@ -880,9 +901,6 @@ class Task(models.Model): allow_task_dependencies = fields.Boolean(related='project_id.allow_task_dependencies') depend_on_ids = fields.Many2many('project.task', relation="task_dependencies_rel", column1="task_id", column2="depends_on_id", string="Blocked By", domain="[('allow_task_dependencies', '=', True), ('id', '!=', id)]") - # Project Sharing fields - project_sharing_access_mode = fields.Selection(PROJECT_SHARING_ACCESS_MODE, compute='_compute_project_sharing_access_mode') - # recurrence fields allow_recurring_tasks = fields.Boolean(related='project_id.allow_recurring_tasks') recurring_task = fields.Boolean(string="Recurrent") @@ -1189,26 +1207,6 @@ class Task(models.Model): else: task.stage_id = False - @api.depends_context('uid') - @api.depends('project_id.project_sharing_access_ids', 'project_id.privacy_visibility') - def _compute_project_sharing_access_mode(self): - if self.env.user._is_public(): - self.project_sharing_access_mode = False - return - portal_user = self.user_has_groups('base.group_portal') - project_sharings = self.project_id.filtered(lambda project: project.privacy_visibility == 'portal') - if portal_user: - project_sharing_access_read = self.env['project.sharing.access'].search_read( - [('user_id', '=', self.env.user.id), ('project_id', 'in', project_sharings.ids)], - ['project_id', 'access_mode'] - ) - project_sharing_access_dict = {res['project_id'][0]: res['access_mode'] for res in project_sharing_access_read} - for task in self: - if not portal_user: - task.project_sharing_access_mode = task.project_id in project_sharings and 'edit' - continue - task.project_sharing_access_mode = project_sharing_access_dict.get(task.project_id.id, False) - @api.constrains('recurring_task') def _check_recurring_task(self): if self.filtered(lambda task: task.parent_id and task.recurring_task): @@ -1497,7 +1495,7 @@ class Task(models.Model): recurrence_domain = [] if recurrence_update == 'subsequent': for task in self: - recurrence_domain = expression.OR([recurrence_domain, ['&', ('recurrence_id', '=', task.recurrence_id.id), ('create_date', '>=', task.create_date)]]) + recurrence_domain = OR([recurrence_domain, ['&', ('recurrence_id', '=', task.recurrence_id.id), ('create_date', '>=', task.create_date)]]) else: recurrence_domain = [('recurrence_id', 'in', self.recurrence_id.ids)] tasks |= self.env['project.task'].search(recurrence_domain) diff --git a/addons/project/models/project_collaborator.py b/addons/project/models/project_collaborator.py new file mode 100644 index 00000000000..4268a62f705 --- /dev/null +++ b/addons/project/models/project_collaborator.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import api, fields, models + + +class ProjectCollaborator(models.Model): + _name = 'project.collaborator' + _description = 'Collaborators in project shared' + + project_id = fields.Many2one('project.project', 'Project Shared', domain=[('privacy_visibility', '=', 'portal')], required=True) + partner_id = fields.Many2one('res.partner', 'Collaborator', required=True) + + _sql_constraints = [ + ('unique_collaborator', 'UNIQUE(project_id, partner_id)', 'A collaborator cannot be selected more than once in the project sharing access. Please remove duplicate(s) and try again.'), + ] + + @api.model_create_multi + def create(self, vals_list): + project_collaborators = super().create(vals_list) + non_authenticated_collaborator = project_collaborators.partner_id.filtered(lambda partner: not partner.user_ids) + non_authenticated_collaborator._create_portal_users() + return project_collaborators diff --git a/addons/project/models/project_sharing_access.py b/addons/project/models/project_sharing_access.py deleted file mode 100644 index c164f89681e..00000000000 --- a/addons/project/models/project_sharing_access.py +++ /dev/null @@ -1,24 +0,0 @@ -# -*- coding: utf-8 -*- -# Part of Odoo. See LICENSE file for full copyright and licensing details. - -from odoo import fields, models - - -PROJECT_SHARING_ACCESS_MODE = [ - ('read', 'Read'), - ('comment', 'Comment'), - ('edit', 'Edit'), -] - - -class ProjectSharingAccess(models.Model): - _name = 'project.sharing.access' - _description = 'Project Sharing Access' - - project_id = fields.Many2one('project.project', 'Project Shared', required=True) - user_id = fields.Many2one('res.users', string='Portal Users', domain=lambda self: [('groups_id', 'in', self.env.ref('base.group_portal').id)], required=True) - access_mode = fields.Selection(PROJECT_SHARING_ACCESS_MODE, string='Access Mode', required=True) - - _sql_constraints = [ - ('unique_user', 'UNIQUE(project_id, user_id)', 'A user cannot be selected more than once in the project sharing access. Please remove duplicate(s) and try again.'), - ] diff --git a/addons/project/models/res_partner.py b/addons/project/models/res_partner.py index d7646c8d411..290e92d8474 100644 --- a/addons/project/models/res_partner.py +++ b/addons/project/models/res_partner.py @@ -2,6 +2,7 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. from odoo import fields, models +from odoo.tools import email_normalize class ResPartner(models.Model): @@ -28,3 +29,19 @@ class ResPartner(models.Model): if partner in self: partner.task_count += group['partner_id_count'] partner = partner.parent_id + + def _create_portal_users(self): + partners_without_user = self.filtered(lambda partner: not partner.user_ids) + if not partners_without_user: + return self.env['res.users'] + created_users = self.env['res.users'] + for partner in partners_without_user: + created_users += self.env['res.users'].with_context(no_reset_password=True)._create_user_from_template({ + 'email': email_normalize(partner.email), + 'login': email_normalize(partner.email), + 'partner_id': partner.id, + 'company_id': self.env.company.id, + 'company_ids': [(6, 0, self.env.company.ids)], + 'active': True, + }) + return created_users diff --git a/addons/project/security/ir.model.access.csv b/addons/project/security/ir.model.access.csv index 0f06777ee58..f9637300734 100644 --- a/addons/project/security/ir.model.access.csv +++ b/addons/project/security/ir.model.access.csv @@ -36,7 +36,6 @@ access_project_milestone_user,project.milestone.user,model_project_milestone,bas access_project_milestone_portal,project.milestone.portal,model_project_milestone,base.group_portal,1,0,0,0 access_project_milestone_project_user,project.milestone.project.user,model_project_milestone,project.group_project_user,1,0,0,0 access_project_milestone_project_manager,project.milestone.project.manager,model_project_milestone,project.group_project_manager,1,1,1,1 -access_project_share_project_manager,project.share.wizard.project.manager,model_project_share_wizard,project.group_project_manager,1,1,1,0 -access_project_share_wizard_line_project_manager,project.share.wizard.line.project.manager,model_project_share_wizard_line,project.group_project_manager,1,1,1,0 -access_project_sharing_access_project_manager,project.sharing.access.project.manager,model_project_sharing_access,project.group_project_manager,1,1,1,1 -access_project_sharing_access_project_portal,project.sharing.access.project.portal,model_project_sharing_access,base.group_portal,1,0,0,0 +access_project_collaborator_manager,project.collaborator.manager,model_project_collaborator,project.group_project_manager,1,1,1,1 +access_project_collaborator_portal,project.collaborator.portal,model_project_collaborator,base.group_portal,1,0,0,0 +access_project_share_manager,project.share.wizard.manager,model_project_share_wizard,project.group_project_manager,1,1,1,0 diff --git a/addons/project/security/project_security.xml b/addons/project/security/project_security.xml index e66dde35ba5..ebe00e1ca0f 100644 --- a/addons/project/security/project_security.xml +++ b/addons/project/security/project_security.xml @@ -145,14 +145,13 @@ '|', ('project_id.message_partner_ids', 'child_of', [user.partner_id.commercial_partner_id.id]), ('message_partner_ids', 'child_of', [user.partner_id.commercial_partner_id.id]), - ('project_id.project_sharing_access_ids.user_id', 'in', [user.id]), - ('project_id.project_sharing_access_ids.access_mode', '=', 'edit'), + ('project_id.collaborator_ids.partner_id', 'in', [user.partner_id.id]), ] + - @@ -183,12 +182,5 @@ - - Project Sharing Access: Portal user can read its own access - - [('user_id', '=', user.id)] - - - diff --git a/addons/project/static/src/project_sharing/project_sharing.js b/addons/project/static/src/project_sharing/project_sharing.js index cc59095bdfe..cf1affa05eb 100644 --- a/addons/project/static/src/project_sharing/project_sharing.js +++ b/addons/project/static/src/project_sharing/project_sharing.js @@ -56,15 +56,13 @@ export class ProjectSharingWebClient extends Component { } async _showView() { - const { action_name, project_id, can_edit } = session; + const { action_name, project_id } = session; await this.actionService.doAction( action_name, { clearBreadcrumbs: true, additionalContext: { active_id: project_id, - create: can_edit, - edit: can_edit, } } ); diff --git a/addons/project/tests/test_project_sharing.py b/addons/project/tests/test_project_sharing.py index a39b81b913f..04af79e5333 100644 --- a/addons/project/tests/test_project_sharing.py +++ b/addons/project/tests/test_project_sharing.py @@ -3,6 +3,7 @@ from odoo import Command from odoo.exceptions import AccessError +from odoo.tests import tagged from odoo.tests.common import Form from .test_project_base import TestProjectCommon @@ -18,6 +19,13 @@ class TestProjectSharingCommon(TestProjectCommon): (0, 0, {'name': 'To Do', 'sequence': 1}), (0, 0, {'name': 'Done', 'sequence': 10}), ] + + cls.partner_portal = cls.env['res.partner'].create({ + 'name': 'Chell Gladys', + 'email': 'chell@gladys.portal', + 'company_id': False, + 'user_ids': [Command.link(cls.user_portal.id)]}) + cls.project_cows = cls.env['project.project'].with_context({'mail_create_nolog': True}).create({ 'name': 'Cows', 'privacy_visibility': 'portal', @@ -49,16 +57,7 @@ class TestProjectSharingCommon(TestProjectCommon): view="project.project_sharing_project_task_view_form" ) - def get_project_sharing_access_for_user(self, with_user, project): - project_with_user = project.with_user(with_user or self.env.user) - return { - 'read': project_with_user._check_project_sharing_access(), - 'comment': project_with_user._check_project_sharing_access('comment'), - 'edit': project_with_user._check_project_sharing_access('edit') - } - - - +@tagged('project_sharing') class TestProjectSharing(TestProjectSharingCommon): def test_project_share_wizard(self): @@ -73,95 +72,30 @@ class TestProjectSharing(TestProjectSharingCommon): project_share_wizard = self.env['project.share.wizard'].create({ 'res_model': 'project.project', 'res_id': self.project_portal.id, + 'access_mode': 'edit', }) - self.assertFalse(project_share_wizard.line_ids, 'No access rights should be given to a portal user in this project.') + self.assertFalse(project_share_wizard.partner_ids, 'No collaborator should be in the wizard.') self.assertFalse(self.project_portal.with_user(self.user_portal)._check_project_sharing_access(), 'The portal user should not have accessed in project sharing views.') - project_share_wizard.write({'line_ids': [ - Command.create({ - 'user_id': self.user_portal.id, - 'access_mode': 'read', - }), - ]}) - project_share_wizard.action_confirm_access() - self.assertEqual(len(self.project_portal.project_sharing_access_ids), 1, 'The access right added in project share wizard should be added in the project when the user confirm the access in the wizard.') + project_share_wizard.write({'partner_ids': [Command.link(self.user_portal.partner_id.id)]}) + project_share_wizard.action_send_mail() + self.assertEqual(len(self.project_portal.collaborator_ids), 1, 'The access right added in project share wizard should be added in the project when the user confirm the access in the wizard.') self.assertDictEqual({ - 'user_id': self.project_portal.project_sharing_access_ids.user_id, - 'access_mode': self.project_portal.project_sharing_access_ids.access_mode, - 'project_id': self.project_portal.project_sharing_access_ids.project_id, + 'partner_id': self.project_portal.collaborator_ids.partner_id, + 'project_id': self.project_portal.collaborator_ids.project_id, }, { - 'user_id': self.user_portal, - 'access_mode': 'read', + 'partner_id': self.user_portal.partner_id, 'project_id': self.project_portal, }, 'The access rights added should be the read access for the portal project for Chell Gladys.') - self.assertTrue(self.project_portal._check_project_sharing_access(), 'The portal user should have read access to the portal project with project sharing feature.') - self.assertTrue(self.user_portal.partner_id in self.project_portal.message_partner_ids) + self.assertTrue(self.project_portal.with_user(self.user_portal)._check_project_sharing_access(), 'The portal user should have read access to the portal project with project sharing feature.') def test_project_sharing_access(self): - """ Check the _check_project_sharing_access returns the expected value for all access mode defined in project sharing. - """ - self.assertDictEqual( - self.get_project_sharing_access_for_user(self.user_public, self.project_portal), - { - 'read': False, - 'comment': False, - 'edit': False, - }, - 'The public user should not have any access to project sharing feature of the portal project.' - ) - self.assertDictEqual( - self.get_project_sharing_access_for_user(self.user_projectuser, self.project_portal), - { - 'read': True, - 'comment': True, - 'edit': True, - }, - 'The internal user should have all accesses to project sharing feature of the portal project.' - ) - self.assertDictEqual( - self.get_project_sharing_access_for_user(self.user_portal, self.project_portal), - { - 'read': False, - 'comment': False, - 'edit': False, - }, - 'The portal user should not have any access to project sharing feature of the portal project.' - ) - self.project_portal.write({ - 'project_sharing_access_ids': [Command.create({ - 'user_id': self.user_portal.id, - 'access_mode': 'read', - })], - }) - project_sharing_access = self.project_portal.project_sharing_access_ids - self.assertDictEqual( - self.get_project_sharing_access_for_user(self.user_portal, self.project_portal), - { - 'read': True, - 'comment': False, - 'edit': False, - }, - 'The portal user should have only the read access to project sharing feature of the portal project.' - ) - project_sharing_access.write({'access_mode': 'comment'}) - self.assertDictEqual( - self.get_project_sharing_access_for_user(self.user_portal, self.project_portal), - { - 'read': True, - 'comment': True, - 'edit': False, - }, - 'The portal user should can read and comment the tasks of the portal project in project sharing views.' - ) - project_sharing_access.write({'access_mode': 'edit'}) - self.assertDictEqual( - self.get_project_sharing_access_for_user(self.user_portal, self.project_portal), - { - 'read': True, - 'comment': True, - 'edit': True, - }, - 'The portal user should can read, comment and edit the tasks of the portal project in project sharing views.' - ) + """ Check if the different user types can access to project sharing feature as expected. """ + with self.assertRaises(AccessError, msg='The public user should not have any access to project sharing feature of the portal project.'): + self.project_portal.with_user(self.user_public)._check_project_sharing_access() + self.assertTrue(self.project_portal.with_user(self.user_projectuser)._check_project_sharing_access(), 'The internal user should have all accesses to project sharing feature of the portal project.') + self.assertFalse(self.project_portal.with_user(self.user_portal)._check_project_sharing_access(), 'The portal user should not have any access to project sharing feature of the portal project.') + self.project_portal.write({'collaborator_ids': [Command.create({'partner_id': self.user_portal.partner_id.id})]}) + self.assertTrue(self.project_portal.with_user(self.user_portal)._check_project_sharing_access(), 'The portal user can access to project sharing feature of the portal project.') def test_create_task_in_project_sharing(self): """ Test when portal user creates a task in project sharing views. @@ -174,29 +108,16 @@ class TestProjectSharing(TestProjectSharingCommon): 3.1) Try to change the project of the new task with this user. """ # 1) Give the 'read' access mode to a portal user in a project and try to create task with this user. + with self.assertRaises(AccessError, msg="Should not accept the portal user create a task in the project when he has not the edit access right."): + with self.get_project_sharing_form_view(self.env['project.task'].with_context({'tracking_disable': True, 'default_project_id': self.project_portal.id}), self.user_portal) as form: + form.name = 'Test' + task = form.save() + self.project_portal.write({ - 'project_sharing_access_ids': [ - Command.create({ - 'user_id': self.user_portal.id, - 'access_mode': 'read', - }), + 'collaborator_ids': [ + Command.create({'partner_id': self.user_portal.partner_id.id}), ], }) - with self.assertRaises(AccessError, msg="Should not accept the portal user create a task in the project when he has not the edit access right."): - with self.get_project_sharing_form_view(self.env['project.task'].with_context({'tracking_disable': True, 'default_project_id': self.project_portal.id}), self.user_portal) as form: - form.name = 'Test' - task = form.save() - - # 2) Give the 'comment' access mode to a portal user in a project and try to create task with this user. - project_sharing_access = self.project_portal.project_sharing_access_ids - project_sharing_access.write({'access_mode': 'comment'}) - with self.assertRaises(AccessError, msg="Should not accept the portal user create a task in the project when he has not the edit access right."): - with self.get_project_sharing_form_view(self.env['project.task'].with_context({'tracking_disable': True, 'default_project_id': self.project_portal.id}), self.user_portal) as form: - form.name = 'Test' - task = form.save() - - # 3) Give the 'edit' access mode to a portal user in a project and try to create task with this user. - project_sharing_access.write({'access_mode': 'edit'}) with self.get_project_sharing_form_view(self.env['project.task'].with_context({'tracking_disable': True, 'default_project_id': self.project_portal.id}), self.user_portal) as form: form.name = 'Test' task = form.save() @@ -219,32 +140,21 @@ class TestProjectSharing(TestProjectSharingCommon): 3.1) Try to change the project of the new task with this user. """ # 1) Give the 'read' access mode to a portal user in a project and try to create task with this user. + with self.assertRaises(AccessError, msg="Should not accept the portal user create a task in the project when he has not the edit access right."): + with self.get_project_sharing_form_view(self.task_cow.with_context({'tracking_disable': True, 'default_project_id': self.project_cows.id}), self.user_portal) as form: + form.name = 'Test' + task = form.save() + project_share_wizard = self.env['project.share.wizard'].create({ + 'access_mode': 'edit', 'res_model': 'project.project', 'res_id': self.project_cows.id, - 'line_ids': [ - Command.create({ - 'user_id': self.user_portal.id, - 'access_mode': 'read', - }), + 'partner_ids': [ + Command.link(self.user_portal.partner_id.id), ], }) - project_share_wizard.action_confirm_access() - with self.assertRaises(AccessError, msg="Should not accept the portal user create a task in the project when he has not the edit access right."): - with self.get_project_sharing_form_view(self.task_cow.with_context({'tracking_disable': True, 'default_project_id': self.project_cows.id}), self.user_portal) as form: - form.name = 'Test' - task = form.save() + project_share_wizard.action_send_mail() - # 2) Give the 'comment' access mode to a portal user in a project and try to create task with this user. - project_sharing_access = self.project_cows.project_sharing_access_ids - project_sharing_access.write({'access_mode': 'comment'}) - with self.assertRaises(AccessError, msg="Should not accept the portal user create a task in the project when he has not the edit access right."): - with self.get_project_sharing_form_view(self.task_cow.with_context({'tracking_disable': True, 'default_project_id': self.project_cows.id}), self.user_portal) as form: - form.name = 'Test' - task = form.save() - - # 3) Give the 'edit' access mode to a portal user in a project and try to create task with this user. - project_sharing_access.write({'access_mode': 'edit'}) with self.get_project_sharing_form_view(self.task_cow.with_context({'tracking_disable': True, 'default_project_id': self.project_cows.id, 'uid': self.user_portal.id}), self.user_portal) as form: form.name = 'Test' task = form.save() diff --git a/addons/project/views/project_collaborator_views.xml b/addons/project/views/project_collaborator_views.xml new file mode 100644 index 00000000000..09ebf0173d4 --- /dev/null +++ b/addons/project/views/project_collaborator_views.xml @@ -0,0 +1,53 @@ + + + + + project.collaborator.view.form + project.collaborator + +
+ + + + + + +
+
+
+ + + project.collaborator.view.tree + project.collaborator + + + + + + + + + + project.collaborator.view.search + project.collaborator + + + + + + + + + + + + + + Project Collaborators + project.collaborator + tree,form + [('project_id', '=', active_id)] + + + +
diff --git a/addons/project/views/project_sharing_views.xml b/addons/project/views/project_sharing_views.xml index 6426e3c8a75..ea603592e99 100644 --- a/addons/project/views/project_sharing_views.xml +++ b/addons/project/views/project_sharing_views.xml @@ -133,8 +133,7 @@
- - +
@@ -145,7 +144,7 @@

- +

@@ -196,7 +195,7 @@
-
+
diff --git a/addons/project/views/project_views.xml b/addons/project/views/project_views.xml index 47661621d4e..cf46269bf37 100644 --- a/addons/project/views/project_views.xml +++ b/addons/project/views/project_views.xml @@ -264,8 +264,10 @@
-
@@ -312,6 +314,16 @@
+
@@ -640,7 +652,7 @@
diff --git a/addons/project/wizard/project_share_wizard.py b/addons/project/wizard/project_share_wizard.py index 635a87c45cb..113f0252023 100644 --- a/addons/project/wizard/project_share_wizard.py +++ b/addons/project/wizard/project_share_wizard.py @@ -1,95 +1,47 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from odoo import api, Command, fields, models, _ -from odoo.exceptions import ValidationError, UserError - -from ..models.project_sharing_access import PROJECT_SHARING_ACCESS_MODE +from odoo import api, fields, models class ProjectShareWizard(models.TransientModel): _name = 'project.share.wizard' _inherit = 'portal.share' - _description = 'Project Share' + _description = 'Project Sharing' @api.model - def default_get(self, fields_list): - result = super().default_get(fields_list) - if result['res_model'] and result['res_id']: - project = self.env[result['res_model']].browse(result['res_id']) + def default_get(self, fields): + result = super().default_get(fields) + if not result.get('access_mode'): result.update( - line_ids=[ - Command.create( - {'user_id': access.user_id, 'access_mode': access.access_mode} - ) for access in project.project_sharing_access_ids - ], + access_mode='read', + display_access_mode=True, ) return result - partner_ids = fields.Many2many(compute="_compute_partner_ids", readonly=False, store=True, required=False) - line_ids = fields.One2many('project.share.wizard.line', 'project_share_id') - has_access_changed = fields.Boolean(compute='_compute_has_access_changed') + @api.model + def _selection_target_model(self): + return [(model.model, model.name) for model in self.env['ir.model'].sudo().search([('model', '=', 'project.project')])] - @api.depends('line_ids.user_id') - def _compute_partner_ids(self): - for project_share_wizard in self: - project_share_wizard.partner_ids |= project_share_wizard.line_ids.user_id.partner_id + access_mode = fields.Selection([('read', 'Readonly'), ('edit', 'Edit')]) + display_access_mode = fields.Boolean() - @api.depends('line_ids.user_id', 'line_ids.access_mode') - def _compute_has_access_changed(self): - project_sharing_access_read = self.env['project.sharing.access'].search_read([('project_id', '=', self.res_id)], ['user_id', 'access_mode']) - project_sharing_access_dict = {res['user_id'][0]: res['access_mode'] for res in project_sharing_access_read} - for project_share in self: - if len(project_share.line_ids) != len(project_sharing_access_dict): - project_share.has_access_changed = True - continue - has_access_changed = False - for line in project_share.line_ids: - access_mode = project_sharing_access_dict.get(line.user_id.id) - if not access_mode or access_mode != line.access_mode: - has_access_changed = True - break - project_share.has_access_changed = has_access_changed + @api.depends('res_model', 'res_id') + def _compute_resource_ref(self): + for wizard in self: + if wizard.res_model and wizard.res_model == 'project.project': + wizard.resource_ref = '%s,%s' % (wizard.res_model, wizard.res_id or 0) + else: + wizard.resource_ref = None - def action_send_mail_and_confirm_access(self): - if not self.partner_ids: - raise UserError(_('Impossible to send email, no recipients are mentionned.')) - self.action_confirm_access() - return self.action_send_mail() - - def action_confirm_access(self): - project = self.env[self.res_model].browse(self.res_id) - if not project or not project.exists(): - raise ValidationError(_('Impossible to confirm the project sharing access, the project is not found or project sharing is disabled for this project.')) - project_sharing_access_dict = {access.user_id.id: access for access in project.project_sharing_access_ids} - project_sharing_access_vals_list = [ - Command.delete(access.id) - for user_id, access in project_sharing_access_dict.items() - if user_id not in self.line_ids.user_id.ids - ] - for share_access in self.line_ids: - project_sharing_access = project_sharing_access_dict.get(share_access.user_id.id) - if not project_sharing_access: - project_sharing_access_vals_list.append( - Command.create( - {'user_id': share_access.user_id.id, 'access_mode': share_access.access_mode} - ) - ) - elif project_sharing_access.access_mode != share_access.access_mode: - project_sharing_access_vals_list.append(Command.update(project_sharing_access.id, {'access_mode': share_access.access_mode})) - project.write({'project_sharing_access_ids': project_sharing_access_vals_list}) - project.message_subscribe(self.line_ids.user_id.partner_id.ids) - return {'type': 'ir.actions.act_window_close'} - - -class ProjectShareAccessWizard(models.TransientModel): - _name = 'project.share.wizard.line' - _description = 'Project Share Access' - - project_share_id = fields.Many2one('project.share.wizard', required=True, ondelete='cascade') - user_id = fields.Many2one('res.users', string='Portal Users', domain=lambda self: [('groups_id', 'in', self.env.ref('base.group_portal').id)], required=True) - access_mode = fields.Selection(PROJECT_SHARING_ACCESS_MODE, string='Access Mode', required=True) - - _sql_constraints = [ - ('uniqueness_user', 'UNIQUE(project_share_id, user_id)', 'An user cannot be selected more than once in the project sharing access. Please remove duplicate(s) and try again.'), - ] + def action_send_mail(self): + self.ensure_one() + if self.access_mode == 'edit': + portal_partners = self.partner_ids.filtered('user_ids') + note = self._get_note() + self.resource_ref._add_collaborators(self.partner_ids) + self._send_public_link(note, portal_partners) + self._send_signup_link(note, partners=self.partner_ids - portal_partners) + self.resource_ref.message_subscribe(partner_ids=self.partner_ids.ids) + return {'type': 'ir.actions.act_window_close'} + return super().action_send_mail() diff --git a/addons/project/wizard/project_share_wizard_views.xml b/addons/project/wizard/project_share_wizard_views.xml index ee8bdb230c6..786a7383059 100644 --- a/addons/project/wizard/project_share_wizard_views.xml +++ b/addons/project/wizard/project_share_wizard_views.xml @@ -1,46 +1,43 @@ - - project.share.wizard + + project.share.wizard.view.form project.share.wizard - + + + + - - - + + + + - +
+
+
- - - - - - - -
- -
- + Share Project project.share.wizard - + form new