# -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. import logging from odoo.tools.translate import _ from odoo.tools import email_normalize from odoo.exceptions import UserError from odoo import api, fields, models, Command _logger = logging.getLogger(__name__) class PortalWizard(models.TransientModel): """ A wizard to manage the creation/removal of portal users. """ _name = 'portal.wizard' _description = 'Grant Portal Access' def _default_partner_ids(self): partner_ids = self.env.context.get('default_partner_ids', []) or self.env.context.get('active_ids', []) contact_ids = set() for partner in self.env['res.partner'].sudo().browse(partner_ids): contact_partners = partner.child_ids.filtered(lambda p: p.type in ('contact', 'other')) | partner contact_ids |= set(contact_partners.ids) return [Command.link(contact_id) for contact_id in contact_ids] partner_ids = fields.Many2many('res.partner', string='Partners', default=_default_partner_ids) user_ids = fields.One2many('portal.wizard.user', 'wizard_id', string='Users', compute='_compute_user_ids', store=True, readonly=False) welcome_message = fields.Text('Invitation Message', help="This text is included in the email sent to new users of the portal.") @api.depends('partner_ids') def _compute_user_ids(self): for portal_wizard in self: portal_wizard.user_ids = [ Command.create({ 'partner_id': partner.id, 'email': partner.email, }) for partner in portal_wizard.partner_ids ] @api.model def action_open_wizard(self): """Create a "portal.wizard" and open the form view. We need a server action for that because the one2many "user_ids" records need to exist to be able to execute an a button action on it. If they have no ID, the buttons will be disabled and we won't be able to click on them. That's why we need a server action, to create the records and then open the form view on them. """ portal_wizard = self.create({}) return portal_wizard._action_open_modal() def _action_open_modal(self): """Allow to keep the wizard modal open after executing the action.""" return { 'name': _('Portal Access Management'), 'type': 'ir.actions.act_window', 'res_model': 'portal.wizard', 'view_mode': 'form', 'res_id': self.id, 'target': 'new', } class PortalWizardUser(models.TransientModel): """ A model to configure users in the portal wizard. """ _name = 'portal.wizard.user' _description = 'Portal User Config' wizard_id = fields.Many2one('portal.wizard', string='Wizard', required=True, ondelete='cascade') partner_id = fields.Many2one('res.partner', string='Contact', required=True, readonly=True, ondelete='cascade') email = fields.Char('Email') user_id = fields.Many2one('res.users', string='User', compute='_compute_user_id', compute_sudo=True) 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): for portal_wizard_user in self: user = portal_wizard_user.partner_id.with_context(active_test=False).user_ids portal_wizard_user.user_id = user[0] if user else False @api.depends('user_id', 'user_id.groups_id') def _compute_group_details(self): for portal_wizard_user in self: user = portal_wizard_user.user_id if user and user._is_internal(): portal_wizard_user.is_internal = True portal_wizard_user.is_portal = False elif user and user.has_group('base.group_portal'): portal_wizard_user.is_internal = False portal_wizard_user.is_portal = True else: portal_wizard_user.is_internal = False portal_wizard_user.is_portal = False def action_grant_access(self): """Grant the portal access to the partner. If the partner has no linked user, we will create a new one in the same company as the partner (or in the current company if not set). An invitation email will be sent to the partner. """ self.ensure_one() self._assert_user_email_uniqueness() if self.is_portal or self.is_internal: raise UserError(_('The partner "%s" already has the portal access.', self.partner_id.name)) group_portal = self.env.ref('base.group_portal') group_public = self.env.ref('base.group_public') self._update_partner_email() user_sudo = self.user_id.sudo() if not user_sudo: # create a user if necessary and make sure it is in the portal group company = self.partner_id.company_id or self.env.company user_sudo = self.sudo().with_company(company.id)._create_user() if not user_sudo.active or not self.is_portal: user_sudo.write({'active': True, 'groups_id': [(4, group_portal.id), (3, group_public.id)]}) # prepare for the signup process user_sudo.partner_id.signup_prepare() self.with_context(active_test=True)._send_email() return self.action_refresh_modal() def action_revoke_access(self): """Remove the user of the partner from the portal group. If the user was only in the portal group, we archive it. """ self.ensure_one() if not self.is_portal: 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') self._update_partner_email() # Remove the sign up token, so it can not be used self.partner_id.sudo().signup_token = False user_sudo = self.user_id.sudo() # remove the user from the portal group if user_sudo and user_sudo.has_group('base.group_portal'): user_sudo.write({'groups_id': [(3, group_portal.id), (4, group_public.id)], 'active': False}) 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)) 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): """ create a new user for wizard_user.partner_id :returns record of res.users """ return self.env['res.users'].with_context(no_reset_password=True)._create_user_from_template({ 'email': email_normalize(self.email), 'login': email_normalize(self.email), 'partner_id': self.partner_id.id, 'company_id': self.env.company.id, 'company_ids': [(6, 0, self.env.company.ids)], }) def _send_email(self): """ send notification email to a new portal user """ self.ensure_one() # determine subject and body in the portal user's language template = self.env.ref('portal.mail_template_data_portal_welcome') if not template: raise UserError(_('The template "Portal: new user" not found for sending email to the portal user.')) lang = self.user_id.sudo().lang partner = self.user_id.sudo().partner_id portal_url = partner.with_context(signup_force_type_in_url='', lang=lang)._get_signup_url_for_action()[partner.id] partner.signup_prepare() template.with_context(dbname=self._cr.dbname, portal_url=portal_url, lang=lang).send_mail(self.id, force_send=True) return True def _assert_user_email_uniqueness(self): """Check that the email can be used to create a new user.""" self.ensure_one() 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)) 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})