diff --git a/addons/portal/__manifest__.py b/addons/portal/__manifest__.py index f545e16ee60..3d11a22f5fe 100644 --- a/addons/portal/__manifest__.py +++ b/addons/portal/__manifest__.py @@ -32,6 +32,9 @@ a dependency towards website editing and customization capabilities.""", 'web._assets_frontend_helpers': [ ('prepend', 'portal/static/src/scss/bootstrap_overridden.scss'), ], + 'web.assets_backend': [ + 'portal/static/src/views/**/*', + ], 'web.assets_frontend': [ 'portal/static/src/scss/bootstrap.extend.scss', 'portal/static/src/scss/portal.scss', diff --git a/addons/portal/static/src/views/fields/portal_wizard_user_one2many.js b/addons/portal/static/src/views/fields/portal_wizard_user_one2many.js new file mode 100644 index 00000000000..840e52f86df --- /dev/null +++ b/addons/portal/static/src/views/fields/portal_wizard_user_one2many.js @@ -0,0 +1,14 @@ +/** @odoo-module **/ + +import { PortalWizardUserListRenderer } from "../list/portal_wizard_user_list_renderer"; +import { X2ManyField } from "@web/views/fields/x2many/x2many_field"; +import { registry } from "@web/core/registry"; + +export class PortalUserX2ManyField extends X2ManyField { + setup() { + super.setup(); + this.Renderer = PortalWizardUserListRenderer; + } +} + +registry.category("fields").add("portal_wizard_user_one2many", PortalUserX2ManyField); diff --git a/addons/portal/static/src/views/fields/portal_wizard_user_one2many.scss b/addons/portal/static/src/views/fields/portal_wizard_user_one2many.scss new file mode 100644 index 00000000000..bb5713fa64b --- /dev/null +++ b/addons/portal/static/src/views/fields/portal_wizard_user_one2many.scss @@ -0,0 +1,3 @@ +.o_portal_wizard_user_one2many td { + width: 1%; +} diff --git a/addons/portal/static/src/views/list/portal_wizard_user_list_renderer.js b/addons/portal/static/src/views/list/portal_wizard_user_list_renderer.js new file mode 100644 index 00000000000..4d21b441b1b --- /dev/null +++ b/addons/portal/static/src/views/list/portal_wizard_user_list_renderer.js @@ -0,0 +1,23 @@ +/** @odoo-module **/ + +import { ListRenderer } from "@web/views/list/list_renderer"; + +const { useSubEnv } = owl; + +export class PortalWizardUserListRenderer extends ListRenderer { + setup() { + super.setup(); + this.isPortalActionOngoing = false; + const defaultOnClickViewButton = this.env.onClickViewButton; + useSubEnv({ + onClickViewButton: async (params) => { + if (params.clickParams.name === 'action_refresh_modal' || this.isPortalActionOngoing) { + return false; + } + this.isPortalActionOngoing = true; + await defaultOnClickViewButton(params); + this.isPortalActionOngoing = false; + }, + }); + } +} diff --git a/addons/portal/tests/test_portal_wizard.py b/addons/portal/tests/test_portal_wizard.py index 6ba1ddbc502..f759bc5c841 100644 --- a/addons/portal/tests/test_portal_wizard.py +++ b/addons/portal/tests/test_portal_wizard.py @@ -141,16 +141,19 @@ class TestPortalWizard(MailCommon): portal_user = portal_wizard.user_ids self.internal_user.login = 'test_error@example.com' - portal_user.email = 'test_error@example.com' + portal_user.email = 'test_error@example.com' with self.assertRaises(UserError, msg='Must detect the already used email.'): - portal_user.action_revoke_access() + portal_user._assert_user_email_uniqueness() + self.assertEqual(portal_user.email_state, 'exist', msg='Must detect the already used email.') portal_user.email = 'wrong email format' with self.assertRaises(UserError, msg='Must detect wrong email format.'): - portal_user.action_revoke_access() + portal_user._assert_user_email_uniqueness() + self.assertEqual(portal_user.email_state, 'ko', msg='Must detect wrong email format.') portal_wizard = self.env['portal.wizard'].with_context(active_ids=[self.internal_user.partner_id.id]).create({}) + portal_user = portal_wizard.user_ids with self.assertRaises(UserError, msg='Must not be able to change internal user group.'): portal_user.action_revoke_access() diff --git a/addons/portal/wizard/portal_wizard.py b/addons/portal/wizard/portal_wizard.py index f0b14e46608..e4d37de2fcc 100644 --- a/addons/portal/wizard/portal_wizard.py +++ b/addons/portal/wizard/portal_wizard.py @@ -88,6 +88,25 @@ class PortalWizardUser(models.TransientModel): login_date = fields.Datetime(related='user_id.login_date', string='Latest Authentication') is_portal = fields.Boolean('Is Portal', compute='_compute_group_details') is_internal = fields.Boolean('Is Internal', compute='_compute_group_details') + email_state = fields.Selection([ + ('ok', 'Valid'), + ('ko', 'Invalid'), + ('exist', 'Already Registered')], + string='Status', compute='_compute_email_state', default='ok') + + @api.depends('email') + def _compute_email_state(self): + portal_users_with_email = self.filtered(lambda user: email_normalize(user.email)) + (self - portal_users_with_email).email_state = 'ko' + + normalized_emails = [email_normalize(portal_user.email) for portal_user in portal_users_with_email] + existing_users = self.env['res.users'].with_context(active_test=False).sudo().search_read([('login', 'in', normalized_emails)], ['id', 'login']) + + for portal_user in portal_users_with_email: + if next((user for user in existing_users if user['login'] == email_normalize(portal_user.email) and user['id'] != portal_user.user_id.id), None): + portal_user.email_state = 'exist' + else: + portal_user.email_state = 'ok' @api.depends('partner_id') def _compute_user_id(self): @@ -127,10 +146,7 @@ class PortalWizardUser(models.TransientModel): group_portal = self.env.ref('base.group_portal') group_public = self.env.ref('base.group_public') - # update partner email, if a new one was introduced - if self.partner_id.email != self.email: - self.partner_id.write({'email': self.email}) - + self._update_partner_email() user_sudo = self.user_id.sudo() if not user_sudo: @@ -145,7 +161,7 @@ class PortalWizardUser(models.TransientModel): self.with_context(active_test=True)._send_email() - return self.wizard_id._action_open_modal() + return self.action_refresh_modal() def action_revoke_access(self): """Remove the user of the partner from the portal group. @@ -153,17 +169,13 @@ class PortalWizardUser(models.TransientModel): If the user was only in the portal group, we archive it. """ self.ensure_one() - self._assert_user_email_uniqueness() - if not self.is_portal: - raise UserError(_('The partner "%s" has no portal access.', self.partner_id.name)) + raise UserError(_('The partner "%s" has no portal access or is internal.', self.partner_id.name)) group_portal = self.env.ref('base.group_portal') group_public = self.env.ref('base.group_public') - # update partner email, if a new one was introduced - if self.partner_id.email != self.email: - self.partner_id.write({'email': self.email}) + self._update_partner_email() # Remove the sign up token, so it can not be used self.partner_id.sudo().signup_token = False @@ -178,21 +190,24 @@ class PortalWizardUser(models.TransientModel): else: user_sudo.write({'groups_id': [(3, group_portal.id), (4, group_public.id)]}) - return self.wizard_id._action_open_modal() + return self.action_refresh_modal() def action_invite_again(self): """Re-send the invitation email to the partner.""" self.ensure_one() + self._assert_user_email_uniqueness() if not self.is_portal: raise UserError(_('You should first grant the portal access to the partner "%s".', self.partner_id.name)) - # update partner email, if a new one was introduced - if self.partner_id.email != self.email: - self.partner_id.write({'email': self.email}) - + self._update_partner_email() self.with_context(active_test=True)._send_email() + return self.action_refresh_modal() + + def action_refresh_modal(self): + """Refresh the portal wizard modal and keep it open. Used as fallback action of email state icon buttons, + required as they must be non-disabled buttons to fire mouse events to show tooltips on email state.""" return self.wizard_id._action_open_modal() def _create_user(self): @@ -229,16 +244,13 @@ class PortalWizardUser(models.TransientModel): def _assert_user_email_uniqueness(self): """Check that the email can be used to create a new user.""" self.ensure_one() - - email = email_normalize(self.email) - - if not email: + if self.email_state == 'ko': raise UserError(_('The contact "%s" does not have a valid email.', self.partner_id.name)) + if self.email_state == 'exist': + raise UserError(_('The contact "%s" has the same email as an existing user', self.partner_id.name)) - user = self.env['res.users'].sudo().with_context(active_test=False).search([ - ('id', '!=', self.user_id.id), - ('login', '=ilike', email), - ]) - - if user: - raise UserError(_('The contact "%s" has the same email has an existing user (%s).', self.partner_id.name, user.name)) + def _update_partner_email(self): + """Update partner email on portal action, if a new one was introduced and is valid.""" + email_normalized = email_normalize(self.email) + if self.email_state == 'ok' and email_normalize(self.partner_id.email) != email_normalized: + self.partner_id.write({'email': email_normalized}) diff --git a/addons/portal/wizard/portal_wizard_views.xml b/addons/portal/wizard/portal_wizard_views.xml index 8f0f1d15997..a83805294e4 100644 --- a/addons/portal/wizard/portal_wizard_views.xml +++ b/addons/portal/wizard/portal_wizard_views.xml @@ -31,19 +31,26 @@ - + + +