# -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. import contextlib import logging from ast import literal_eval from collections import defaultdict from dateutil.relativedelta import relativedelta from odoo import api, fields, models, _ from odoo.exceptions import UserError from odoo.osv import expression from odoo.tools.misc import ustr from odoo.http import request from odoo.addons.base.models.ir_mail_server import MailDeliveryException from odoo.addons.auth_signup.models.res_partner import SignupError, now _logger = logging.getLogger(__name__) class ResUsers(models.Model): _inherit = 'res.users' state = fields.Selection(compute='_compute_state', search='_search_state', string='Status', selection=[('new', 'Never Connected'), ('active', 'Confirmed')]) def _search_state(self, operator, value): negative = operator in expression.NEGATIVE_TERM_OPERATORS # In case we have no value if not value: return expression.TRUE_DOMAIN if negative else expression.FALSE_DOMAIN if operator in ['in', 'not in']: if len(value) > 1: return expression.FALSE_DOMAIN if negative else expression.TRUE_DOMAIN if value[0] == 'new': comp = '!=' if negative else '=' if value[0] == 'active': comp = '=' if negative else '!=' return [('log_ids', comp, False)] if operator in ['=', '!=']: # In case we search against anything else than new, we have to invert the operator if value != 'new': operator = expression.TERM_OPERATORS_NEGATION[operator] return [('log_ids', operator, False)] return expression.TRUE_DOMAIN def _compute_state(self): for user in self: user.state = 'active' if user.login_date else 'new' @api.model def signup(self, values, token=None): """ signup a user, to either: - create a new user (no token), or - create a user for a partner (with token, but no user for partner), or - change the password of a user (with token, and existing user). :param values: a dictionary with field values that are written on user :param token: signup token (optional) :return: (dbname, login, password) for the signed up user """ if token: # signup with a token: find the corresponding partner id partner = self.env['res.partner']._signup_retrieve_partner(token, check_validity=True, raise_exception=True) # invalidate signup token partner.write({'signup_token': False, 'signup_type': False, 'signup_expiration': False}) partner_user = partner.user_ids and partner.user_ids[0] or False # avoid overwriting existing (presumably correct) values with geolocation data if partner.country_id or partner.zip or partner.city: values.pop('city', None) values.pop('country_id', None) if partner.lang: values.pop('lang', None) if partner_user: # user exists, modify it according to values values.pop('login', None) values.pop('name', None) partner_user.write(values) if not partner_user.login_date: partner_user._notify_inviter() return (partner_user.login, values.get('password')) else: # user does not exist: sign up invited user values.update({ 'name': partner.name, 'partner_id': partner.id, 'email': values.get('email') or values.get('login'), }) if partner.company_id: values['company_id'] = partner.company_id.id values['company_ids'] = [(6, 0, [partner.company_id.id])] partner_user = self._signup_create_user(values) partner_user._notify_inviter() else: # no token, sign up an external user values['email'] = values.get('email') or values.get('login') self._signup_create_user(values) return (values.get('login'), values.get('password')) @api.model def _get_signup_invitation_scope(self): return self.env['ir.config_parameter'].sudo().get_param('auth_signup.invitation_scope', 'b2b') @api.model def _signup_create_user(self, values): """ signup a new user using the template user """ # check that uninvited users may sign up if 'partner_id' not in values: if self._get_signup_invitation_scope() != 'b2c': raise SignupError(_('Signup is not allowed for uninvited users')) return self._create_user_from_template(values) @classmethod def authenticate(cls, db, login, password, user_agent_env): uid = super().authenticate(db, login, password, user_agent_env) try: with cls.pool.cursor() as cr: env = api.Environment(cr, uid, {}) if env.user._should_alert_new_device(): env.user._alert_new_device() except MailDeliveryException: pass return uid def _notify_inviter(self): for user in self: invite_partner = user.create_uid.partner_id if invite_partner: # notify invite user that new user is connected self.env['bus.bus']._sendone(invite_partner, 'res.users/connection', { 'username': user.name, 'partnerId': user.partner_id.id, }) def _create_user_from_template(self, values): template_user_id = literal_eval(self.env['ir.config_parameter'].sudo().get_param('base.template_portal_user_id', 'False')) template_user = self.browse(template_user_id) if not template_user.exists(): raise ValueError(_('Signup: invalid template user')) if not values.get('login'): raise ValueError(_('Signup: no login given for new user')) if not values.get('partner_id') and not values.get('name'): raise ValueError(_('Signup: no name or partner given for new user')) # create a copy of the template user (attached to a specific partner_id if given) values['active'] = True try: with self.env.cr.savepoint(): return template_user.with_context(no_reset_password=True).copy(values) except Exception as e: # copy may failed if asked login is not available. raise SignupError(ustr(e)) def reset_password(self, login): """ retrieve the user corresponding to login (login or email), and reset their password """ users = self.search(self._get_login_domain(login)) if not users: users = self.search(self._get_email_domain(login)) if not users: raise Exception(_('No account found for this login')) if len(users) > 1: raise Exception(_('Multiple accounts found for this login')) return users.action_reset_password() def action_reset_password(self): try: return self._action_reset_password() except MailDeliveryException as mde: if len(mde.args) == 2 and isinstance(mde.args[1], ConnectionRefusedError): raise UserError(_("Could not contact the mail server, please check your outgoing email server configuration")) from mde else: raise UserError(_("There was an error when trying to deliver your Email, please check your configuration")) from mde def _action_reset_password(self): """ create signup token for each user, and send their signup url by email """ if self.env.context.get('install_mode') or self.env.context.get('import_file'): return if self.filtered(lambda user: not user.active): raise UserError(_("You cannot perform this action on an archived user.")) # prepare reset password signup create_mode = bool(self.env.context.get('create_user')) # no time limit for initial invitation, only for reset password expiration = False if create_mode else now(days=+1) self.mapped('partner_id').signup_prepare(signup_type="reset", expiration=expiration) # send email to users with their signup url account_created_template = None if create_mode: account_created_template = self.env.ref('auth_signup.set_password_email', raise_if_not_found=False) if account_created_template and account_created_template._name != 'mail.template': _logger.error("Wrong set password template %r", account_created_template) return email_values = { 'email_cc': False, 'auto_delete': True, 'message_type': 'user_notification', 'recipient_ids': [], 'partner_ids': [], 'scheduled_date': False, } for user in self: if not user.email: raise UserError(_("Cannot send email: user %s has no email address.", user.name)) email_values['email_to'] = user.email with contextlib.closing(self.env.cr.savepoint()): if account_created_template: account_created_template.send_mail( user.id, force_send=True, raise_exception=True, email_values=email_values) else: body = self.env['mail.render.mixin']._render_template( self.env.ref('auth_signup.reset_password_email'), model='res.users', res_ids=user.ids, engine='qweb_view', options={'post_process': True})[user.id] mail = self.env['mail.mail'].sudo().create({ 'subject': _('Password reset'), 'email_from': user.company_id.email_formatted or user.email_formatted, 'body_html': body, **email_values, }) mail.send() _logger.info("Password reset email sent for user <%s> to <%s>", user.login, user.email) def send_unregistered_user_reminder(self, after_days=5): email_template = self.env.ref('auth_signup.mail_template_data_unregistered_users', raise_if_not_found=False) if not email_template: _logger.warning("Template 'auth_signup.mail_template_data_unregistered_users' was not found. Cannot send reminder notifications.") return datetime_min = fields.Datetime.today() - relativedelta(days=after_days) datetime_max = datetime_min + relativedelta(hours=23, minutes=59, seconds=59) res_users_with_details = self.env['res.users'].search_read([ ('share', '=', False), ('create_uid.email', '!=', False), ('create_date', '>=', datetime_min), ('create_date', '<=', datetime_max), ('log_ids', '=', False)], ['create_uid', 'name', 'login']) # group by invited by invited_users = defaultdict(list) for user in res_users_with_details: invited_users[user.get('create_uid')[0]].append("%s (%s)" % (user.get('name'), user.get('login'))) # For sending mail to all the invitors about their invited users for user in invited_users: template = email_template.with_context(dbname=self._cr.dbname, invited_users=invited_users[user]) template.send_mail(user, email_layout_xmlid='mail.mail_notification_light', force_send=False) def _alert_new_device(self): self.ensure_one() if self.email: email_values = { 'email_cc': False, 'auto_delete': True, 'message_type': 'user_notification', 'recipient_ids': [], 'partner_ids': [], 'scheduled_date': False, 'email_to': self.email } body = self.env['mail.render.mixin']._render_template( 'auth_signup.alert_login_new_device', model='res.users', res_ids=self.ids, engine='qweb_view', options={'post_process': True}, add_context=self._prepare_new_device_notice_values())[self.id] mail = self.env['mail.mail'].sudo().create({ 'subject': _('New Connection to your Account'), 'email_from': self.company_id.email_formatted or self.email_formatted, 'body_html': body, **email_values, }) mail.send() _logger.info("New device alert email sent for user <%s> to <%s>", self.login, self.email) def _prepare_new_device_notice_values(self): values = { 'login_date': fields.Datetime.now(), 'location_address': False, 'ip_address': False, 'browser': False, 'useros': False, } if not request: return values city = request.geoip.get('city') or False region = request.geoip.get('region_name') or False country = request.geoip.get('country') or False if country: if region and city: values['location_address'] = _("Near %(city)s, %(region)s, %(country)s", city=city, region=region, country=country) elif region: values['location_address'] = _("Near %(region)s, %(country)s", region=region, country=country) else: values['location_address'] = _("In %(country)s", country=country) else: values['location_address'] = False values['ip_address'] = request.httprequest.environ['REMOTE_ADDR'] if request.httprequest.user_agent: if request.httprequest.user_agent.browser: values['browser'] = request.httprequest.user_agent.browser.capitalize() if request.httprequest.user_agent.platform: values['useros'] = request.httprequest.user_agent.platform.capitalize() return values @api.model def web_create_users(self, emails): inactive_users = self.search([('state', '=', 'new'), '|', ('login', 'in', emails), ('email', 'in', emails)]) new_emails = set(emails) - set(inactive_users.mapped('email')) res = super(ResUsers, self).web_create_users(list(new_emails)) if inactive_users: inactive_users.with_context(create_user=True).action_reset_password() return res @api.model_create_multi def create(self, vals_list): # overridden to automatically invite user to sign up users = super(ResUsers, self).create(vals_list) if not self.env.context.get('no_reset_password'): users_with_email = users.filtered('email') if users_with_email: try: users_with_email.with_context(create_user=True)._action_reset_password() except MailDeliveryException: users_with_email.partner_id.with_context(create_user=True).signup_cancel() return users @api.returns('self', lambda value: value.id) def copy(self, default=None): self.ensure_one() sup = super(ResUsers, self) if not default or not default.get('email'): # avoid sending email to the user we are duplicating sup = super(ResUsers, self.with_context(no_reset_password=True)) return sup.copy(default=default)