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 @@
-
+
+
+
+
+
+ attrs="{'invisible': ['|', '|', ('is_portal', '=', True), ('is_internal', '=', True), ('email_state', '!=', 'ok')]}"/>
+ attrs="{'invisible': ['|', '|', ('is_portal', '=', False), ('is_internal', '=', True), ('email_state', '!=', 'ok')]}"/>