示例#1
0
class MailGroup(models.Model):
    """This model represents a mailing list.

    Users send emails to an alias to create new group messages or reply to existing
    group messages. Moderation can be activated on groups. In that case email have to
    be validated or rejected.
    """
    _name = 'mail.group'
    _description = 'Mail Group'
    # TDE CHECK: use blaclist mixin
    _inherit = ['mail.alias.mixin']
    _order = 'create_date DESC, id DESC'

    @api.model
    def default_get(self, fields):
        res = super(MailGroup, self).default_get(fields)
        if not res.get('alias_contact') and (not fields
                                             or 'alias_contact' in fields):
            res['alias_contact'] = 'everyone' if res.get(
                'access_mode') == 'public' else 'followers'
        return res

    active = fields.Boolean('Active', default=True)
    name = fields.Char('Name', required=True, translate=True)
    alias_name = fields.Char('Alias Name',
                             copy=False,
                             related='alias_id.alias_name',
                             readonly=False)
    alias_fullname = fields.Char('Alias Full Name',
                                 compute='_compute_alias_fullname')
    description = fields.Text('Description')
    image_128 = fields.Image('Image', max_width=128, max_height=128)
    # Messages
    mail_group_message_ids = fields.One2many('mail.group.message',
                                             'mail_group_id',
                                             string='Pending Messages')
    mail_group_message_last_month_count = fields.Integer(
        'Messages Per Month',
        compute='_compute_mail_group_message_last_month_count')
    mail_group_message_count = fields.Integer(
        'Messages Count',
        help='Number of message in this group',
        compute='_compute_mail_group_message_count')
    mail_group_message_moderation_count = fields.Integer(
        'Pending Messages Count',
        help='Messages that need an action',
        compute='_compute_mail_group_message_moderation_count')
    # Members
    is_member = fields.Boolean('Is Member', compute='_compute_is_member')
    member_ids = fields.One2many('mail.group.member',
                                 'mail_group_id',
                                 string='Members')
    member_partner_ids = fields.Many2many(
        'res.partner',
        string='Partners Member',
        compute='_compute_member_partner_ids',
        search='_search_member_partner_ids')
    member_count = fields.Integer('Members Count',
                                  compute='_compute_member_count')
    # Moderation
    is_moderator = fields.Boolean(
        string='Moderator',
        help='Current user is a moderator of the group',
        compute='_compute_is_moderator')
    moderation = fields.Boolean(string='Moderate this group')
    moderation_rule_count = fields.Integer(
        string='Moderated emails count',
        compute='_compute_moderation_rule_count')
    moderation_rule_ids = fields.One2many('mail.group.moderation',
                                          'mail_group_id',
                                          string='Moderated Emails')
    moderator_ids = fields.Many2many(
        'res.users',
        'mail_group_moderator_rel',
        string='Moderators',
        domain=lambda self: [('groups_id', 'in', self.env.ref('base.group_user'
                                                              ).id)])
    moderation_notify = fields.Boolean(
        string='Automatic notification',
        help=
        'People receive an automatic notification about their message being waiting for moderation.'
    )
    moderation_notify_msg = fields.Html(string='Notification message')
    moderation_guidelines = fields.Boolean(
        string='Send guidelines to new subscribers',
        help=
        'Newcomers on this moderated group will automatically receive the guidelines.'
    )
    moderation_guidelines_msg = fields.Html(string='Guidelines')
    # ACLs
    access_mode = fields.Selection([
        ('public', 'Everyone'),
        ('members', 'Members only'),
        ('groups', 'Selected group of users'),
    ],
                                   string='Privacy',
                                   required=True,
                                   default='public')
    access_group_id = fields.Many2one(
        'res.groups',
        string='Authorized Group',
        default=lambda self: self.env.ref('base.group_user'))
    # UI
    can_manage_group = fields.Boolean('Can Manage',
                                      help='Can manage the members',
                                      compute='_compute_can_manage_group')

    @api.depends('alias_name', 'alias_domain')
    def _compute_alias_fullname(self):
        for group in self:
            if group.alias_name and group.alias_domain:
                group.alias_fullname = f'{group.alias_name}@{group.alias_domain}'
            else:
                group.alias_fullname = group.alias_name

    @api.depends('mail_group_message_ids.create_date',
                 'mail_group_message_ids.moderation_status')
    def _compute_mail_group_message_last_month_count(self):
        month_date = datetime.today() - relativedelta.relativedelta(months=1)
        messages_data = self.env['mail.group.message']._read_group([
            ('mail_group_id', 'in', self.ids),
            ('create_date', '>=', fields.Datetime.to_string(month_date)),
            ('moderation_status', '=', 'accepted'),
        ], ['mail_group_id'], ['mail_group_id'])

        # { mail_discusison_id: number_of_mail_group_message_last_month_count }
        messages_data = {
            message['mail_group_id'][0]: message['mail_group_id_count']
            for message in messages_data
        }

        for group in self:
            group.mail_group_message_last_month_count = messages_data.get(
                group.id, 0)

    @api.depends('mail_group_message_ids')
    def _compute_mail_group_message_count(self):
        if not self:
            self.mail_group_message_count = 0
            return

        results = self.env['mail.group.message']._read_group(
            [('mail_group_id', 'in', self.ids)],
            ['mail_group_id'],
            ['mail_group_id'],
        )
        result_per_group = {
            result['mail_group_id'][0]: result['mail_group_id_count']
            for result in results
        }
        for group in self:
            group.mail_group_message_count = result_per_group.get(group.id, 0)

    @api.depends('mail_group_message_ids.moderation_status')
    def _compute_mail_group_message_moderation_count(self):
        results = self.env['mail.group.message']._read_group(
            [('mail_group_id', 'in', self.ids),
             ('moderation_status', '=', 'pending_moderation')],
            ['mail_group_id'],
            ['mail_group_id'],
        )
        result_per_group = {
            result['mail_group_id'][0]: result['mail_group_id_count']
            for result in results
        }

        for group in self:
            group.mail_group_message_moderation_count = result_per_group.get(
                group.id, 0)

    @api.depends('member_ids')
    def _compute_member_count(self):
        for group in self:
            group.member_count = len(group.member_ids)

    @api.depends_context('uid')
    def _compute_is_member(self):
        if not self or self.env.user._is_public():
            self.is_member = False
            return

        # SUDO to bypass the ACL rules
        members = self.env['mail.group.member'].sudo().search([
            ('partner_id', '=', self.env.user.partner_id.id),
            ('mail_group_id', 'in', self.ids),
        ])
        is_member = {member.mail_group_id.id: True for member in members}

        for group in self:
            group.is_member = is_member.get(group.id, False)

    @api.depends('member_ids')
    def _compute_member_partner_ids(self):
        for group in self:
            group.member_partner_ids = group.member_ids.partner_id

    def _search_member_partner_ids(self, operator, operand):
        return [('member_ids', 'in',
                 self.env['mail.group.member'].sudo()._search([
                     ('partner_id', operator, operand)
                 ]))]

    @api.depends('moderator_ids')
    @api.depends_context('uid')
    def _compute_is_moderator(self):
        for group in self:
            group.is_moderator = self.env.user.id in group.moderator_ids.ids

    @api.depends('moderation_rule_ids')
    def _compute_moderation_rule_count(self):
        for group in self:
            group.moderation_rule_count = len(group.moderation_rule_ids)

    @api.depends('is_moderator')
    @api.depends_context('uid')
    def _compute_can_manage_group(self):
        is_admin = self.env.user.has_group(
            'mail_group.group_mail_group_manager') or self.env.su
        for group in self:
            group.can_manage_group = is_admin or group.is_moderator

    @api.onchange('access_mode')
    def _onchange_access_mode(self):
        if self.access_mode == 'public':
            self.alias_contact = 'everyone'
        else:
            self.alias_contact = 'followers'

    @api.onchange('moderation')
    def _onchange_moderation(self):
        if self.moderation and self.env.user not in self.moderator_ids:
            self.moderator_ids |= self.env.user

    # CONSTRAINTS

    @api.constrains('moderator_ids')
    def _check_moderator_email(self):
        if any(not moderator.email for group in self
               for moderator in group.moderator_ids):
            raise ValidationError(_('Moderators must have an email address.'))

    @api.constrains('moderation_notify', 'moderation_notify_msg')
    def _check_moderation_notify(self):
        if any(group.moderation_notify and not group.moderation_notify_msg
               for group in self):
            raise ValidationError(_('The notification message is missing.'))

    @api.constrains('moderation_guidelines', 'moderation_guidelines_msg')
    def _check_moderation_guidelines(self):
        if any(group.moderation_guidelines
               and not group.moderation_guidelines_msg for group in self):
            raise ValidationError(_('The guidelines description is missing.'))

    @api.constrains('moderator_ids', 'moderation')
    def _check_moderator_existence(self):
        if any(not group.moderator_ids for group in self if group.moderation):
            raise ValidationError(_('Moderated group must have moderators.'))

    @api.constrains('access_mode', 'access_group_id')
    def _check_access_mode(self):
        if any(group.access_mode == 'groups' and not group.access_group_id
               for group in self):
            raise ValidationError(_('The "Authorized Group" is missing.'))

    def _alias_get_creation_values(self):
        """Return the default values for the automatically created alias."""
        values = super(MailGroup, self)._alias_get_creation_values()
        values['alias_model_id'] = self.env['ir.model']._get('mail.group').id
        values['alias_force_thread_id'] = self.id
        values['alias_defaults'] = literal_eval(self.alias_defaults or '{}')
        return values

    # ------------------------------------------------------------
    # MAILING
    # ------------------------------------------------------------

    def _alias_get_error_message(self, message, message_dict, alias):
        self.ensure_one()

        if alias.alias_contact == 'followers':
            # Members only
            if not self._find_member(message_dict.get('email_from')):
                return _('Only members can send email to the mailing list.')
            # Skip the verification because the partner is in the member list
            return

        return super(MailGroup,
                     self)._alias_get_error_message(message, message_dict,
                                                    alias)

    @api.model
    def message_new(self, msg_dict, custom_values=None):
        """Add the method to make the mail gateway flow work with this model."""
        return

    @api.model
    def message_update(self, msg_dict, update_vals=None):
        """Add the method to make the mail gateway flow work with this model."""
        return

    @api.returns('mail.message', lambda value: value.id)
    def message_post(self,
                     body='',
                     subject=None,
                     email_from=None,
                     author_id=None,
                     **kwargs):
        """ Custom posting process. This model does not inherit from ``mail.thread``
        but uses the mail gateway so few methods should be defined.

        This custom posting process works as follow

          * create a ``mail.message`` based on incoming email;
          * create linked ``mail.group.message`` that encapsulates message in a
            format used in mail groups;
          * apply moderation rules;

        :return message: newly-created mail.message
        """
        self.ensure_one()
        # First create the <mail.message>
        Mailthread = self.env['mail.thread']
        values = dict((key, val) for key, val in kwargs.items()
                      if key in self.env['mail.message']._fields)
        author_id, email_from = Mailthread._message_compute_author(
            author_id, email_from, raise_exception=True)

        values.update({
            'author_id': author_id,
            'body': self._clean_email_body(body),
            'email_from': email_from,
            'model': self._name,
            'partner_ids': [],
            'res_id': self.id,
            'subject': subject,
        })

        # Force the "reply-to" to make the mail group flow work
        values['reply_to'] = self.env['mail.message']._get_reply_to(values)

        # ensure message ID so that replies go to the right thread
        if not values.get('message_id'):
            values['message_id'] = generate_tracking_message_id(
                '%s-mail.group' % self.id)

        attachments = kwargs.get('attachments') or []
        attachment_ids = kwargs.get('attachment_ids') or []
        attachement_values = Mailthread._message_post_process_attachments(
            attachments, attachment_ids, values)
        values.update(attachement_values)

        mail_message = Mailthread._message_create(values)

        # Find the <mail.group.message> parent
        group_message_parent_id = False
        if mail_message.parent_id:
            group_message_parent = self.env['mail.group.message'].search([
                ('mail_message_id', '=', mail_message.parent_id.id)
            ])
            group_message_parent_id = group_message_parent.id if group_message_parent else False

        moderation_status = 'pending_moderation' if self.moderation else 'accepted'

        # Create the group message associated
        group_message = self.env['mail.group.message'].create({
            'mail_group_id':
            self.id,
            'mail_message_id':
            mail_message.id,
            'moderation_status':
            moderation_status,
            'group_message_parent_id':
            group_message_parent_id,
        })

        # Check the moderation rule to determine if we should accept or reject the email
        email_normalized = email_normalize(email_from)
        moderation_rule = self.env['mail.group.moderation'].search([
            ('mail_group_id', '=', self.id),
            ('email', '=', email_normalized),
        ],
                                                                   limit=1)

        if not self.moderation:
            self._notify_members(group_message)

        elif moderation_rule and moderation_rule.status == 'allow':
            group_message.action_moderate_accept()

        elif moderation_rule and moderation_rule.status == 'ban':
            group_message.action_moderate_reject()

        elif self.moderation_notify:
            self.env['mail.mail'].sudo().create({
                'author_id':
                self.env.user.partner_id.id,
                'auto_delete':
                True,
                'body_html':
                group_message.mail_group_id.moderation_notify_msg,
                'email_from':
                self.env.user.company_id.catchall_formatted
                or self.env.user.company_id.email_formatted,
                'email_to':
                email_from,
                'subject':
                'Re: %s' % (subject or ''),
                'state':
                'outgoing'
            })

        return mail_message

    def action_send_guidelines(self, members=None):
        """ Send guidelines to given members. """
        self.ensure_one()

        if not self.env.is_admin() and not self.is_moderator:
            raise UserError(
                _('Only an administrator or a moderator can send guidelines to group members.'
                  ))

        if not self.moderation_guidelines_msg:
            raise UserError(_('The guidelines description is empty.'))

        template = self.env.ref('mail_group.mail_template_guidelines',
                                raise_if_not_found=False)
        if not template:
            raise UserError(
                _('Template "mail_group.mail_template_guidelines" was not found. No email has been sent. Please contact an administrator to fix this issue.'
                  ))

        banned_emails = self.env['mail.group.moderation'].sudo().search([
            ('status', '=', 'ban'),
            ('mail_group_id', '=', self.id),
        ]).mapped('email')

        if members is None:
            members = self.member_ids
        members = members.filtered(
            lambda member: member.email_normalized not in banned_emails)

        for member in members:
            company = member.partner_id.company_id or self.env.company
            template.send_mail(
                member.id,
                email_values={
                    'author_id':
                    self.env.user.partner_id.id,
                    'email_from':
                    company.email_formatted or company.catchall_formatted,
                    'reply_to':
                    company.email_formatted or company.catchall_formatted,
                },
            )

        _logger.info('Send guidelines to %i members', len(members))

    def _notify_members(self, message):
        """Send the given message to all members of the mail group (except the author)."""
        self.ensure_one()

        if message.mail_group_id != self:
            raise UserError(_('The group of the message do not match.'))

        if not message.mail_message_id.reply_to:
            _logger.error(
                'The alias or the catchall domain is missing, group might not work properly.'
            )

        base_url = self.get_base_url()
        body = self.env['mail.render.mixin']._replace_local_links(message.body)
        access_token = self._generate_group_access_token()
        mail_values = []

        # Email added in a dict to be sure to send only once the email to each address
        member_emails = {
            email_normalize(member.email): member.email
            for member in self.member_ids
        }

        for batch_email_member in tools.split_every(GROUP_SEND_BATCH_SIZE,
                                                    member_emails.items()):
            for email_member_normalized, email_member in batch_email_member:
                if email_member_normalized == message.email_from_normalized:
                    # Do not send the email to his author
                    continue

                # SMTP headers related to the subscription
                email_url_encoded = urls.url_quote(email_member)
                headers = {
                    **self._notify_by_email_get_headers(),
                    'List-Archive': f'<{base_url}/groups/{slug(self)}>',
                    'List-Subscribe':
                    f'<{base_url}/groups?email={email_url_encoded}>',
                    'List-Unsubscribe':
                    f'<{base_url}/groups?unsubscribe&email={email_url_encoded}>',
                    'Precedence': 'list',
                    'X-Auto-Response-Suppress':
                    'OOF',  # avoid out-of-office replies from MS Exchange
                }
                if self.alias_name and self.alias_domain:
                    headers.update({
                        'List-Id':
                        f'<{self.alias_name}.{self.alias_domain}>',
                        'List-Post':
                        f'<mailto:{self.alias_name}@{self.alias_domain}>',
                        'X-Forge-To':
                        f'"{self.name}" <{self.alias_name}@{self.alias_domain}>',
                    })

                if message.mail_message_id.parent_id:
                    headers[
                        'In-Reply-To'] = message.mail_message_id.parent_id.message_id

                # Add the footer (member specific) in the body
                template_values = {
                    'mailto':
                    f'{self.alias_name}@{self.alias_domain}',
                    'group_url':
                    f'{base_url}/groups/{slug(self)}',
                    'unsub_label':
                    f'{base_url}/groups?unsubscribe',
                    'unsub_url':
                    f'{base_url}/groups?unsubscribe&group_id={self.id}&token={access_token}&email={email_url_encoded}',
                }
                footer = self.env['ir.qweb']._render(
                    'mail_group.mail_group_footer',
                    template_values,
                    minimal_qcontext=True)
                member_body = tools.append_content_to_html(body,
                                                           footer,
                                                           plaintext=False)

                mail_values.append({
                    'auto_delete': True,
                    'attachment_ids': message.attachment_ids.ids,
                    'body_html': member_body,
                    'email_from': message.email_from,
                    'email_to': email_member,
                    'headers': json.dumps(headers),
                    'mail_message_id': message.mail_message_id.id,
                    'message_id': message.mail_message_id.message_id,
                    'model': 'mail.group',
                    'reply_to': message.mail_message_id.reply_to,
                    'res_id': self.id,
                    'subject': message.subject,
                })

            if mail_values:
                self.env['mail.mail'].sudo().create(mail_values)

    @api.model
    def _cron_notify_moderators(self):
        moderated_groups = self.env['mail.group'].search([('moderation', '=',
                                                           True)])
        return moderated_groups._notify_moderators()

    def _notify_moderators(self):
        """Push a notification (Inbox / Email) to the moderators whose an action is waiting."""
        template = self.env.ref('mail_group.mail_group_notify_moderation',
                                raise_if_not_found=False)
        if not template:
            _logger.warning(
                'Template "mail_group.mail_group_notify_moderation" was not found. Cannot send reminder notifications.'
            )
            return

        results = self.env['mail.group.message'].read_group(
            [('mail_group_id', 'in', self.ids),
             ('moderation_status', '=', 'pending_moderation')],
            ['mail_group_id'],
            ['mail_group_id'],
        )
        groups = self.browse(
            [result['mail_group_id'][0] for result in results])

        for group in groups:
            moderators_to_notify = group.moderator_ids
            MailThread = self.env['mail.thread'].with_context(
                mail_notify_author=True)
            for moderator in moderators_to_notify:
                body = self.env['ir.qweb']._render(
                    'mail_group.mail_group_notify_moderation', {
                        'moderator': moderator,
                        'group': group,
                    },
                    minimal_qcontext=True)
                email_from = moderator.company_id.catchall_formatted or moderator.company_id.email_formatted
                MailThread.message_notify(
                    partner_ids=moderator.partner_id.ids,
                    subject=_('Messages are pending moderation'),
                    body=body,
                    email_from=email_from,
                    model='mail.group',
                    res_id=group.id,
                )

    @api.model
    def _clean_email_body(self, body_html):
        """When we receive an email, we want to clean it before storing it in the database."""
        tree = lxml.html.fromstring(body_html or '')
        # Remove the mailing footer
        xpath_footer = ".//div[contains(@id, 'o_mg_message_footer')]"
        for parent_footer in tree.xpath(xpath_footer + "/.."):
            for footer in parent_footer.xpath(xpath_footer):
                parent_footer.remove(footer)

        return lxml.etree.tostring(tree, encoding='utf-8').decode()

    # ------------------------------------------------------------
    # MEMBERSHIP
    # ------------------------------------------------------------

    def action_join(self):
        self.check_access_rights('read')
        self.check_access_rule('read')
        partner = self.env.user.partner_id
        self.sudo()._join_group(partner.email, partner.id)

        _logger.info('"%s" (#%s) joined mail.group "%s" (#%s)', partner.name,
                     partner.id, self.name, self.id)

    def action_leave(self):
        self.check_access_rights('read')
        self.check_access_rule('read')
        partner = self.env.user.partner_id
        self.sudo()._leave_group(partner.email, partner.id)

        _logger.info('"%s" (#%s) leaved mail.group "%s" (#%s)', partner.name,
                     partner.id, self.name, self.id)

    def _join_group(self, email, partner_id=None):
        self.ensure_one()

        if partner_id:
            partner = self.env['res.partner'].browse(partner_id).exists()
            if not partner:
                raise ValidationError(_('The partner can not be found.'))
            email = partner.email

        existing_member = self._find_member(email, partner_id)
        if existing_member:
            # Update the information of the partner to force the synchronization
            # If one the the value is not up to date (e.g. if our email is subscribed
            # but our partner was not set)
            existing_member.write({
                'email': email,
                'partner_id': partner_id,
            })
            return

        member = self.env['mail.group.member'].create({
            'partner_id': partner_id,
            'email': email,
            'mail_group_id': self.id,
        })

        if self.moderation_guidelines:
            # Automatically send the guidelines to the new member
            self.action_send_guidelines(member)

    def _leave_group(self, email, partner_id=None, all_members=False):
        """Remove the given email / partner from the group.

        If the "all_members" parameter is set to True, remove all members with the given
        email address (multiple members might have the same email address).

        Otherwise, remove the most appropriate.
        """
        self.ensure_one()
        if all_members and not partner_id:
            self.env['mail.group.member'].search([
                ('mail_group_id', '=', self.id),
                ('email_normalized', '=', email_normalize(email)),
            ]).unlink()
        else:
            member = self._find_member(email, partner_id)
            if member:
                member.unlink()

    def _send_subscribe_confirmation_email(self, email):
        """Send an email to the given address to subscribe / unsubscribe to the mailing list."""
        self.ensure_one()
        confirm_action_url = self._generate_action_url(email, 'subscribe')

        template = self.env.ref('mail_group.mail_template_list_subscribe')
        template.with_context(token_url=confirm_action_url).send_mail(
            self.id,
            email_layout_xmlid='mail.mail_notification_light',
            email_values={
                'author_id': self.create_uid.partner_id.id,
                'auto_delete': True,
                'email_from': self.env.company.email_formatted,
                'email_to': email,
                'message_type': 'user_notification',
            },
            force_send=True,
        )
        _logger.info('Subscription email sent to %s.', email)

    def _send_unsubscribe_confirmation_email(self, email):
        """Send an email to the given address to subscribe / unsubscribe to the mailing list."""
        self.ensure_one()
        confirm_action_url = self._generate_action_url(email, 'unsubscribe')

        template = self.env.ref('mail_group.mail_template_list_unsubscribe')
        template.with_context(token_url=confirm_action_url).send_mail(
            self.id,
            email_layout_xmlid='mail.mail_notification_light',
            email_values={
                'author_id': self.create_uid.partner_id.id,
                'auto_delete': True,
                'email_from': self.env.company.email_formatted,
                'email_to': email,
                'message_type': 'user_notification',
            },
            force_send=True,
        )
        _logger.info('Unsubscription email sent to %s.', email)

    def _generate_action_url(self, email, action):
        """Generate the confirmation URL to subscribe / unsubscribe from the mailing list."""
        if action not in ['subscribe', 'unsubscribe']:
            raise ValueError(
                _('Invalid action for URL generation (%s)', action))
        self.ensure_one()

        confirm_action_url = '/group/%s-confirm?%s' % (
            action,
            urls.url_encode(
                {
                    'group_id': self.id,
                    'email': email,
                    'token': self._generate_action_token(email, action),
                }))
        base_url = self.get_base_url()
        confirm_action_url = urls.url_join(base_url, confirm_action_url)
        return confirm_action_url

    def _generate_action_token(self, email, action):
        """Generate an action token to be able to subscribe / unsubscribe from the mailing list."""
        if action not in ['subscribe', 'unsubscribe']:
            raise ValueError(
                _('Invalid action for URL generation (%s)', action))
        self.ensure_one()

        email_normalized = email_normalize(email)
        if not email_normalized:
            raise UserError(_('Email %s is invalid', email))

        data = (self.id, email_normalized, action)
        return hmac(self.env(su=True), 'mail_group-email-subscription', data)

    def _generate_group_access_token(self):
        """Generate an action token to be able to subscribe / unsubscribe from the mailing list."""
        self.ensure_one()
        return hmac(self.env(su=True), 'mail_group-access-token-portal',
                    self.id)

    def _find_member(self, email, partner_id=None):
        """Return the <mail.group.member> corresponding to the given email address."""
        self.ensure_one()

        result = self._find_members(email, partner_id)
        return result.get(self.id)

    def _find_members(self, email, partner_id):
        """Get all the members record corresponding to the email / partner_id.

        Can be called in batch and return a dictionary
            {'group_id': <mail.group.member>}

        Multiple members might have the same email address, but with different partner
        because there's no unique constraint on the email field of the <res.partner>
        model.

        When a partner is given for the search, return in priority
        - The member whose partner match the given partner
        - The member without partner but whose email match the given email

        When no partner is given for the search, return in priority
        - A member whose email match the given email and has no partner
        - A member whose email match the given email and has partner
        """
        order = 'partner_id ASC'
        if not email_normalize(email):
            # empty email should match nobody
            return {}

        domain = [('email_normalized', '=', email_normalize(email))]
        if partner_id:
            domain = expression.OR([
                expression.AND([
                    [('partner_id', '=', False)],
                    domain,
                ]),
                [('partner_id', '=', partner_id)],
            ])
            order = 'partner_id DESC'

        domain = expression.AND([domain, [('mail_group_id', 'in', self.ids)]])
        members_data = self.env['mail.group.member'].sudo().search(domain,
                                                                   order=order)
        return {member.mail_group_id.id: member for member in members_data}
示例#2
0
class FleetVehicle(models.Model):
    _inherit = ['mail.thread', 'mail.activity.mixin']
    _name = 'fleet.vehicle'
    _description = 'Vehicle'
    _order = 'license_plate asc, acquisition_date asc'

    def _get_default_state(self):
        state = self.env.ref('fleet.fleet_vehicle_state_registered',
                             raise_if_not_found=False)
        return state if state and state.id else False

    name = fields.Char(compute="_compute_vehicle_name", store=True)
    active = fields.Boolean('Active', default=True, tracking=True)
    company_id = fields.Many2one('res.company',
                                 'Company',
                                 default=lambda self: self.env.company)
    currency_id = fields.Many2one('res.currency',
                                  related='company_id.currency_id')
    license_plate = fields.Char(
        tracking=True,
        help='License plate number of the vehicle (i = plate number for a car)'
    )
    vin_sn = fields.Char(
        'Chassis Number',
        help='Unique number written on the vehicle motor (VIN/SN number)',
        copy=False)
    driver_id = fields.Many2one('res.partner',
                                'Driver',
                                tracking=True,
                                help='Driver of the vehicle',
                                copy=False)
    future_driver_id = fields.Many2one(
        'res.partner',
        'Future Driver',
        tracking=True,
        help='Next Driver of the vehicle',
        copy=False,
        domain=
        "['|', ('company_id', '=', False), ('company_id', '=', company_id)]")
    model_id = fields.Many2one('fleet.vehicle.model',
                               'Model',
                               tracking=True,
                               required=True,
                               help='Model of the vehicle')
    manager_id = fields.Many2one('res.users', related='model_id.manager_id')
    brand_id = fields.Many2one('fleet.vehicle.model.brand',
                               'Brand',
                               related="model_id.brand_id",
                               store=True,
                               readonly=False)
    log_drivers = fields.One2many('fleet.vehicle.assignation.log',
                                  'vehicle_id',
                                  string='Assignation Logs')
    log_fuel = fields.One2many('fleet.vehicle.log.fuel', 'vehicle_id',
                               'Fuel Logs')
    log_services = fields.One2many('fleet.vehicle.log.services', 'vehicle_id',
                                   'Services Logs')
    log_contracts = fields.One2many('fleet.vehicle.log.contract', 'vehicle_id',
                                    'Contracts')
    cost_count = fields.Integer(compute="_compute_count_all", string="Costs")
    contract_count = fields.Integer(compute="_compute_count_all",
                                    string='Contract Count')
    service_count = fields.Integer(compute="_compute_count_all",
                                   string='Services')
    fuel_logs_count = fields.Integer(compute="_compute_count_all",
                                     string='Fuel Log Count')
    odometer_count = fields.Integer(compute="_compute_count_all",
                                    string='Odometer')
    history_count = fields.Integer(compute="_compute_count_all",
                                   string="Drivers History Count")
    next_assignation_date = fields.Date(
        'Assignation Date',
        help=
        'This is the date at which the car will be available, if not set it means available instantly'
    )
    acquisition_date = fields.Date(
        'Immatriculation Date',
        required=False,
        default=fields.Date.today,
        help='Date when the vehicle has been immatriculated')
    first_contract_date = fields.Date(string="First Contract Date",
                                      default=fields.Date.today)
    color = fields.Char(help='Color of the vehicle')
    state_id = fields.Many2one('fleet.vehicle.state',
                               'State',
                               default=_get_default_state,
                               group_expand='_read_group_stage_ids',
                               tracking=True,
                               help='Current state of the vehicle',
                               ondelete="set null")
    location = fields.Char(help='Location of the vehicle (garage, ...)')
    seats = fields.Integer('Seats Number',
                           help='Number of seats of the vehicle')
    model_year = fields.Char('Model Year', help='Year of the model')
    doors = fields.Integer('Doors Number',
                           help='Number of doors of the vehicle',
                           default=5)
    tag_ids = fields.Many2many('fleet.vehicle.tag',
                               'fleet_vehicle_vehicle_tag_rel',
                               'vehicle_tag_id',
                               'tag_id',
                               'Tags',
                               copy=False)
    odometer = fields.Float(
        compute='_get_odometer',
        inverse='_set_odometer',
        string='Last Odometer',
        help='Odometer measure of the vehicle at the moment of this log')
    odometer_unit = fields.Selection([('kilometers', 'Kilometers'),
                                      ('miles', 'Miles')],
                                     'Odometer Unit',
                                     default='kilometers',
                                     help='Unit of the odometer ',
                                     required=True)
    transmission = fields.Selection([('manual', 'Manual'),
                                     ('automatic', 'Automatic')],
                                    'Transmission',
                                    help='Transmission Used by the vehicle')
    fuel_type = fields.Selection([('gasoline', 'Gasoline'),
                                  ('diesel', 'Diesel'), ('lpg', 'LPG'),
                                  ('electric', 'Electric'),
                                  ('hybrid', 'Hybrid')],
                                 'Fuel Type',
                                 help='Fuel Used by the vehicle')
    horsepower = fields.Integer()
    horsepower_tax = fields.Float('Horsepower Taxation')
    power = fields.Integer('Power', help='Power in kW of the vehicle')
    co2 = fields.Float('CO2 Emissions', help='CO2 emissions of the vehicle')
    image_128 = fields.Image(related='model_id.image_128', readonly=False)
    contract_renewal_due_soon = fields.Boolean(
        compute='_compute_contract_reminder',
        search='_search_contract_renewal_due_soon',
        string='Has Contracts to renew',
        multi='contract_info')
    contract_renewal_overdue = fields.Boolean(
        compute='_compute_contract_reminder',
        search='_search_get_overdue_contract_reminder',
        string='Has Contracts Overdue',
        multi='contract_info')
    contract_renewal_name = fields.Text(
        compute='_compute_contract_reminder',
        string='Name of contract to renew soon',
        multi='contract_info')
    contract_renewal_total = fields.Text(
        compute='_compute_contract_reminder',
        string='Total of contracts due or overdue minus one',
        multi='contract_info')
    car_value = fields.Float(string="Catalog Value (VAT Incl.)",
                             help='Value of the bought vehicle')
    net_car_value = fields.Float(string="Purchase Value",
                                 help="Purchase Value of the car")
    residual_value = fields.Float()
    plan_to_change_car = fields.Boolean(related='driver_id.plan_to_change_car',
                                        store=True,
                                        readonly=False)

    @api.depends('model_id.brand_id.name', 'model_id.name', 'license_plate')
    def _compute_vehicle_name(self):
        for record in self:
            record.name = record.model_id.brand_id.name + '/' + record.model_id.name + '/' + (
                record.license_plate or _('No Plate'))

    def _get_odometer(self):
        FleetVehicalOdometer = self.env['fleet.vehicle.odometer']
        for record in self:
            vehicle_odometer = FleetVehicalOdometer.search(
                [('vehicle_id', '=', record.id)], limit=1, order='value desc')
            if vehicle_odometer:
                record.odometer = vehicle_odometer.value
            else:
                record.odometer = 0

    def _set_odometer(self):
        for record in self:
            if record.odometer:
                date = fields.Date.context_today(record)
                data = {
                    'value': record.odometer,
                    'date': date,
                    'vehicle_id': record.id
                }
                self.env['fleet.vehicle.odometer'].create(data)

    def _compute_count_all(self):
        Odometer = self.env['fleet.vehicle.odometer']
        LogFuel = self.env['fleet.vehicle.log.fuel']
        LogService = self.env['fleet.vehicle.log.services']
        LogContract = self.env['fleet.vehicle.log.contract']
        Cost = self.env['fleet.vehicle.cost']
        for record in self:
            record.odometer_count = Odometer.search_count([('vehicle_id', '=',
                                                            record.id)])
            record.fuel_logs_count = LogFuel.search_count([('vehicle_id', '=',
                                                            record.id)])
            record.service_count = LogService.search_count([('vehicle_id', '=',
                                                             record.id)])
            record.contract_count = LogContract.search_count([
                ('vehicle_id', '=', record.id), ('state', '!=', 'closed')
            ])
            record.cost_count = Cost.search_count([('vehicle_id', '=',
                                                    record.id),
                                                   ('parent_id', '=', False)])
            record.history_count = self.env[
                'fleet.vehicle.assignation.log'].search_count([
                    ('vehicle_id', '=', record.id)
                ])

    @api.depends('log_contracts')
    def _compute_contract_reminder(self):
        params = self.env['ir.config_parameter'].sudo()
        delay_alert_contract = int(
            params.get_param('hr_fleet.delay_alert_contract', default=30))
        for record in self:
            overdue = False
            due_soon = False
            total = 0
            name = ''
            for element in record.log_contracts:
                if element.state in ('open', 'diesoon',
                                     'expired') and element.expiration_date:
                    current_date_str = fields.Date.context_today(record)
                    due_time_str = element.expiration_date
                    current_date = fields.Date.from_string(current_date_str)
                    due_time = fields.Date.from_string(due_time_str)
                    diff_time = (due_time - current_date).days
                    if diff_time < 0:
                        overdue = True
                        total += 1
                    if diff_time < delay_alert_contract:
                        due_soon = True
                        total += 1
                    if overdue or due_soon:
                        log_contract = self.env[
                            'fleet.vehicle.log.contract'].search(
                                [('vehicle_id', '=', record.id),
                                 ('state', 'in',
                                  ('open', 'diesoon', 'expired'))],
                                limit=1,
                                order='expiration_date asc')
                        if log_contract:
                            # we display only the name of the oldest overdue/due soon contract
                            name = log_contract.cost_subtype_id.name

            record.contract_renewal_overdue = overdue
            record.contract_renewal_due_soon = due_soon
            record.contract_renewal_total = total - 1  # we remove 1 from the real total for display purposes
            record.contract_renewal_name = name

    def _search_contract_renewal_due_soon(self, operator, value):
        params = self.env['ir.config_parameter'].sudo()
        delay_alert_contract = int(
            params.get_param('hr_fleet.delay_alert_contract', default=30))
        res = []
        assert operator in ('=', '!=', '<>') and value in (
            True, False), 'Operation not supported'
        if (operator == '=' and value is True) or (operator in ('<>', '!=')
                                                   and value is False):
            search_operator = 'in'
        else:
            search_operator = 'not in'
        today = fields.Date.context_today(self)
        datetime_today = fields.Datetime.from_string(today)
        limit_date = fields.Datetime.to_string(datetime_today + relativedelta(
            days=+delay_alert_contract))
        self.env.cr.execute(
            """SELECT cost.vehicle_id,
                        count(contract.id) AS contract_number
                        FROM fleet_vehicle_cost cost
                        LEFT JOIN fleet_vehicle_log_contract contract ON contract.cost_id = cost.id
                        WHERE contract.expiration_date IS NOT NULL
                          AND contract.expiration_date > %s
                          AND contract.expiration_date < %s
                          AND contract.state IN ('open', 'diesoon', 'expired')
                        GROUP BY cost.vehicle_id""", (today, limit_date))
        res_ids = [x[0] for x in self.env.cr.fetchall()]
        res.append(('id', search_operator, res_ids))
        return res

    def _search_get_overdue_contract_reminder(self, operator, value):
        res = []
        assert operator in ('=', '!=', '<>') and value in (
            True, False), 'Operation not supported'
        if (operator == '=' and value is True) or (operator in ('<>', '!=')
                                                   and value is False):
            search_operator = 'in'
        else:
            search_operator = 'not in'
        today = fields.Date.context_today(self)
        self.env.cr.execute(
            '''SELECT cost.vehicle_id,
                        count(contract.id) AS contract_number
                        FROM fleet_vehicle_cost cost
                        LEFT JOIN fleet_vehicle_log_contract contract ON contract.cost_id = cost.id
                        WHERE contract.expiration_date IS NOT NULL
                          AND contract.expiration_date < %s
                          AND contract.state IN ('open', 'diesoon', 'expired')
                        GROUP BY cost.vehicle_id ''', (today, ))
        res_ids = [x[0] for x in self.env.cr.fetchall()]
        res.append(('id', search_operator, res_ids))
        return res

    @api.model
    def create(self, vals):
        res = super(FleetVehicle, self).create(vals)
        if 'driver_id' in vals and vals['driver_id']:
            res.create_driver_history(vals['driver_id'])
        if 'future_driver_id' in vals and vals['future_driver_id']:
            future_driver = self.env['res.partner'].browse(
                vals['future_driver_id'])
            future_driver.write({'plan_to_change_car': True})
        return res

    def write(self, vals):
        if 'driver_id' in vals and vals['driver_id']:
            driver_id = vals['driver_id']
            self.filtered(lambda v: v.driver_id.id != driver_id
                          ).create_driver_history(driver_id)

        if 'future_driver_id' in vals and vals['future_driver_id']:
            future_driver = self.env['res.partner'].browse(
                vals['future_driver_id'])
            future_driver.write({'plan_to_change_car': True})

        res = super(FleetVehicle, self).write(vals)
        if 'active' in vals and not vals['active']:
            self.mapped('log_contracts').write({'active': False})
        return res

    def _close_driver_history(self):
        self.env['fleet.vehicle.assignation.log'].search([
            ('vehicle_id', 'in', self.ids),
            ('driver_id', 'in', self.mapped('driver_id').ids),
            ('date_end', '=', False)
        ]).write({'date_end': fields.Date.today()})

    def create_driver_history(self, driver_id):
        for vehicle in self:
            self.env['fleet.vehicle.assignation.log'].create({
                'vehicle_id':
                vehicle.id,
                'driver_id':
                driver_id,
                'date_start':
                fields.Date.today(),
            })

    def action_accept_driver_change(self):
        # Find all the vehicles for which the driver is the future_driver_id
        # remove their driver_id and close their history using current date
        vehicles = self.search([('driver_id', 'in',
                                 self.mapped('future_driver_id').ids)])
        vehicles.write({'driver_id': False})
        vehicles._close_driver_history()

        for vehicle in self:
            vehicle.future_driver_id.write({'plan_to_change_car': False})
            vehicle.driver_id = vehicle.future_driver_id
            vehicle.future_driver_id = False

    @api.model
    def _read_group_stage_ids(self, stages, domain, order):
        return self.env['fleet.vehicle.state'].search([], order=order)

    @api.model
    def _name_search(self,
                     name,
                     args=None,
                     operator='ilike',
                     limit=100,
                     name_get_uid=None):
        args = args or []
        if operator == 'ilike' and not (name or '').strip():
            domain = []
        else:
            domain = [
                '|', ('name', operator, name),
                ('driver_id.name', operator, name)
            ]
        rec = self._search(expression.AND([domain, args]),
                           limit=limit,
                           access_rights_uid=name_get_uid)
        return self.browse(rec).name_get()

    def return_action_to_open(self):
        """ This opens the xml view specified in xml_id for the current vehicle """
        self.ensure_one()
        xml_id = self.env.context.get('xml_id')
        if xml_id:
            res = self.env['ir.actions.act_window'].for_xml_id('fleet', xml_id)
            res.update(context=dict(self.env.context,
                                    default_vehicle_id=self.id,
                                    group_by=False),
                       domain=[('vehicle_id', '=', self.id)])
            return res
        return False

    def act_show_log_cost(self):
        """ This opens log view to view and add new log for this vehicle, groupby default to only show effective costs
            @return: the costs log view
        """
        self.ensure_one()
        copy_context = dict(self.env.context)
        copy_context.pop('group_by', None)
        res = self.env['ir.actions.act_window'].for_xml_id(
            'fleet', 'fleet_vehicle_costs_action')
        res.update(context=dict(copy_context,
                                default_vehicle_id=self.id,
                                search_default_parent_false=True),
                   domain=[('vehicle_id', '=', self.id)])
        return res

    def _track_subtype(self, init_values):
        self.ensure_one()
        if 'driver_id' in init_values:
            return self.env.ref('fleet.mt_fleet_driver_updated')
        return super(FleetVehicle, self)._track_subtype(init_values)

    def open_assignation_logs(self):
        self.ensure_one()
        return {
            'type': 'ir.actions.act_window',
            'name': 'Assignation Logs',
            'view_mode': 'tree',
            'res_model': 'fleet.vehicle.assignation.log',
            'domain': [('vehicle_id', '=', self.id)],
            'context': {
                'default_driver_id': self.driver_id.id,
                'default_vehicle_id': self.id
            }
        }
示例#3
0
class Sponsor(models.Model):
    _name = "event.sponsor"
    _description = 'Event Sponsor'
    _order = "sequence, sponsor_type_id"
    # _order = 'sponsor_type_id, sequence' TDE FIXME
    _rec_name = 'name'
    _inherit = [
        'mail.thread', 'mail.activity.mixin', 'website.published.mixin',
        'chat.room.mixin'
    ]

    def _default_sponsor_type_id(self):
        return self.env['event.sponsor.type'].search([],
                                                     order="sequence desc",
                                                     limit=1).id

    event_id = fields.Many2one('event.event', 'Event', required=True)
    sponsor_type_id = fields.Many2one(
        'event.sponsor.type',
        'Sponsoring Level',
        default=lambda self: self._default_sponsor_type_id(),
        required=True,
        auto_join=True)
    url = fields.Char('Sponsor Website',
                      compute='_compute_url',
                      readonly=False,
                      store=True)
    sequence = fields.Integer('Sequence')
    active = fields.Boolean(default=True)
    # description
    subtitle = fields.Char('Slogan',
                           help='Catchy marketing sentence for promote')
    exhibitor_type = fields.Selection([('sponsor', 'Sponsor'),
                                       ('exhibitor', 'Exhibitor'),
                                       ('online', 'Online Exhibitor')],
                                      string="Sponsor Type",
                                      default="sponsor")
    website_description = fields.Html('Description',
                                      compute='_compute_website_description',
                                      sanitize_attributes=False,
                                      sanitize_form=True,
                                      translate=html_translate,
                                      readonly=False,
                                      store=True)
    # contact information
    partner_id = fields.Many2one('res.partner',
                                 'Partner',
                                 required=True,
                                 auto_join=True)
    partner_name = fields.Char('Name', related='partner_id.name')
    partner_email = fields.Char('Email', related='partner_id.email')
    partner_phone = fields.Char('Phone', related='partner_id.phone')
    partner_mobile = fields.Char('Mobile', related='partner_id.mobile')
    name = fields.Char('Sponsor Name',
                       compute='_compute_name',
                       readonly=False,
                       store=True)
    email = fields.Char('Sponsor Email',
                        compute='_compute_email',
                        readonly=False,
                        store=True)
    phone = fields.Char('Sponsor Phone',
                        compute='_compute_phone',
                        readonly=False,
                        store=True)
    mobile = fields.Char('Sponsor Mobile',
                         compute='_compute_mobile',
                         readonly=False,
                         store=True)
    # image
    image_512 = fields.Image(string="Logo",
                             max_width=512,
                             max_height=512,
                             compute='_compute_image_512',
                             readonly=False,
                             store=True)
    image_256 = fields.Image("Image 256",
                             related="image_512",
                             max_width=256,
                             max_height=256,
                             store=False)
    image_128 = fields.Image("Image 128",
                             related="image_512",
                             max_width=128,
                             max_height=128,
                             store=False)
    website_image_url = fields.Char(string='Image URL',
                                    compute='_compute_website_image_url',
                                    compute_sudo=True,
                                    store=False)
    # live mode
    hour_from = fields.Float('Opening hour', default=8.0)
    hour_to = fields.Float('End hour', default=18.0)
    event_date_tz = fields.Selection(string='Timezone',
                                     related='event_id.date_tz',
                                     readonly=True)
    is_in_opening_hours = fields.Boolean(
        'Within opening hours', compute='_compute_is_in_opening_hours')
    # chat room
    chat_room_id = fields.Many2one(readonly=False)
    room_name = fields.Char(readonly=False)
    # country information (related to ease frontend templates)
    country_id = fields.Many2one('res.country',
                                 string='Country',
                                 related='partner_id.country_id',
                                 readonly=True)
    country_flag_url = fields.Char(string='Country Flag',
                                   compute='_compute_country_flag_url',
                                   compute_sudo=True)

    @api.depends('partner_id')
    def _compute_url(self):
        for sponsor in self:
            if sponsor.partner_id.website or not sponsor.url:
                sponsor.url = sponsor.partner_id.website

    @api.depends('partner_id')
    def _compute_name(self):
        self._synchronize_with_partner('name')

    @api.depends('partner_id')
    def _compute_email(self):
        self._synchronize_with_partner('email')

    @api.depends('partner_id')
    def _compute_phone(self):
        self._synchronize_with_partner('phone')

    @api.depends('partner_id')
    def _compute_mobile(self):
        self._synchronize_with_partner('mobile')

    @api.depends('partner_id')
    def _compute_image_512(self):
        self._synchronize_with_partner('image_512')

    @api.depends('image_256', 'partner_id.image_256')
    def _compute_website_image_url(self):
        for sponsor in self:
            if sponsor.image_256:
                sponsor.website_image_url = self.env['website'].image_url(
                    sponsor, 'image_256', size=256)
            elif sponsor.partner_id.image_256:
                sponsor.website_image_url = self.env['website'].image_url(
                    sponsor.partner_id, 'image_256', size=256)
            else:
                sponsor.website_image_url = get_resource_path(
                    'website_event_exhibitor', 'static/src/img',
                    'event_sponsor_default_%d.png' % (sponsor.id % 1))

    def _synchronize_with_partner(self, fname):
        """ Synchronize with partner if not set. Setting a value does not write
        on partner as this may be event-specific information. """
        for sponsor in self:
            if not sponsor[fname]:
                sponsor[fname] = sponsor.partner_id[fname]

    @api.onchange('exhibitor_type')
    def _onchange_exhibitor_type(self):
        """ Keep an explicit onchange to allow configuration of room names, even
        if this field is normally a related on chat_room_id.name. It is not a real
        computed field, an onchange used in form view is sufficient. """
        for sponsor in self:
            if sponsor.exhibitor_type == 'online' and not sponsor.room_name:
                if sponsor.name:
                    room_name = "odoo-exhibitor-%s" % sponsor.name
                else:
                    room_name = self.env['chat.room']._default_name(
                        objname='exhibitor')
                sponsor.room_name = self._jitsi_sanitize_name(room_name)
            if sponsor.exhibitor_type == 'online' and not sponsor.room_max_capacity:
                sponsor.room_max_capacity = '8'

    @api.depends('partner_id')
    def _compute_website_description(self):
        for sponsor in self:
            if is_html_empty(sponsor.website_description):
                sponsor.website_description = sponsor.partner_id.website_description

    @api.depends('event_id.is_ongoing', 'hour_from', 'hour_to',
                 'event_id.date_begin', 'event_id.date_end')
    def _compute_is_in_opening_hours(self):
        """ Opening hours: hour_from and hour_to are given within event TZ or UTC.
        Now() must therefore be computed based on that TZ. """
        for sponsor in self:
            if not sponsor.event_id.is_ongoing:
                sponsor.is_in_opening_hours = False
            elif not sponsor.hour_from or not sponsor.hour_to:
                sponsor.is_in_opening_hours = True
            else:
                event_tz = timezone(sponsor.event_id.date_tz)
                # localize now, begin and end datetimes in event tz
                dt_begin = sponsor.event_id.date_begin.astimezone(event_tz)
                dt_end = sponsor.event_id.date_end.astimezone(event_tz)
                now_utc = utc.localize(
                    fields.Datetime.now().replace(microsecond=0))
                now_tz = now_utc.astimezone(event_tz)

                # compute opening hours
                opening_from_tz = event_tz.localize(
                    datetime.combine(now_tz.date(),
                                     float_to_time(sponsor.hour_from)))
                opening_to_tz = event_tz.localize(
                    datetime.combine(now_tz.date(),
                                     float_to_time(sponsor.hour_to)))

                opening_from = max([dt_begin, opening_from_tz])
                opening_to = min([dt_end, opening_to_tz])

                sponsor.is_in_opening_hours = opening_from <= now_tz < opening_to

    @api.depends('partner_id.country_id.image_url')
    def _compute_country_flag_url(self):
        for sponsor in self:
            if sponsor.partner_id.country_id:
                sponsor.country_flag_url = sponsor.partner_id.country_id.image_url
            else:
                sponsor.country_flag_url = False

    # ------------------------------------------------------------
    # MIXINS
    # ---------------------------------------------------------

    @api.depends('name', 'event_id.name')
    def _compute_website_url(self):
        super(Sponsor, self)._compute_website_url()
        for sponsor in self:
            if sponsor.id:  # avoid to perform a slug on a not yet saved record in case of an onchange.
                base_url = sponsor.event_id.get_base_url()
                sponsor.website_url = '%s/event/%s/exhibitor/%s' % (
                    base_url, slug(sponsor.event_id), slug(sponsor))

    # ------------------------------------------------------------
    # CRUD
    # ------------------------------------------------------------

    @api.model_create_multi
    def create(self, values_list):
        for values in values_list:
            if values.get('is_exhibitor') and not values.get('room_name'):
                exhibitor_name = values['name'] if values.get(
                    'name') else self.env['res.partner'].browse(
                        values['partner_id']).name
                name = 'odoo-exhibitor-%s' % exhibitor_name or 'sponsor'
                values['room_name'] = name
        return super(Sponsor, self).create(values_list)

    def write(self, values):
        toupdate = self.env['event.sponsor']
        if values.get('is_exhibitor') and not values.get(
                'chat_room_id') and not values.get('room_name'):
            toupdate = self.filtered(
                lambda exhibitor: not exhibitor.chat_room_id)
            # go into sequential update in order to create a custom room name for each sponsor
            for exhibitor in toupdate:
                values['room_name'] = 'odoo-exhibitor-%s' % exhibitor.name
                super(Sponsor, exhibitor).write(values)
        return super(Sponsor, self - toupdate).write(values)

    # ------------------------------------------------------------
    # ACTIONS
    # ---------------------------------------------------------

    def get_backend_menu_id(self):
        return self.env.ref('event.event_main_menu').id

    # ------------------------------------------------------------
    # MESSAGING
    # ------------------------------------------------------------

    def _message_get_suggested_recipients(self):
        recipients = super(Sponsor, self)._message_get_suggested_recipients()
        for sponsor in self:
            if sponsor.partner_id:
                sponsor._message_add_suggested_recipient(
                    recipients,
                    partner=sponsor.partner_id,
                    reason=_('Sponsor'))
        return recipients
示例#4
0
class stuff(models.Model):
    _name = 'game.stuff'
    name = fields.Char(readonly=True)
    hash = fields.Char(readonly=True)
    image = fields.Image(width=100, readonly=True)
    character = fields.Many2one('game.character')
    player = fields.Many2one('res.partner')
    type = fields.Selection([('0', 'Fire Weapons'), ('1', 'Melee Weapons'),
                             ('2', 'Armor'), ('3', 'Chemist'),
                             ('4', 'Nutrition'), ('5', 'Medicine'),
                             ('6', 'Energy')],
                            required=True)
    # Propietats:
    melee = fields.Integer()
    shoot = fields.Integer()
    armor = fields.Integer()

    science = fields.Integer()
    cook = fields.Integer()
    medicine = fields.Integer()
    energy = fields.Integer()

    minutes_left = fields.Integer()
    duration = fields.Integer()

    def generate_name(self):
        for s in self:
            words = {
                '1': [
                    "Sword", "Knive", "Dagger", "Axe", "Sickle", "Kama",
                    "Halberd", "Spear", "Guandao", "scythe", "Mace", "Stick",
                    "Nunchaku"
                ],
                '0': [
                    "Gun", "Shotgun", "Rifle", "Carbine", "Machine Gun",
                    "Sniper rifle", "Musket"
                ],
                '2':
                ["Armor", "Uniform", "Camouflaje armor", "Bulletproof vest"],
                '3': [
                    "Microscope", "Tube", "Pipette", "Balance", "Beaker",
                    "Crucible"
                ],
                '4': ["Knive", "Balance", "Stove", "Oven", "Pan", "Pot"],
                '5': [
                    "Thermometer", "Cannula", "Enema", "Scissors",
                    "Stethoscope", "Bandage"
                ],
                '6': [
                    "Cables", "Voltimeter", "Generator", "Solar Panel",
                    "Boiler", "Engine"
                ]
            }

            adjectives = [
                "Premium", "Obsolete", "Glorious", "False", "Astonishing",
                "Splendid", "Pathetic", "Bizarre", "Sordid", "Studendous",
                "Sharp", "Overconfident", "Pleasant", "Sweet", "Last", "Curly",
                "Freezing", "Aberrant", "Profuse", "Dangerous", "Powerful"
            ]

            word = words[s.type]
            random.shuffle(word)
            word = word[0]
            random.shuffle(adjectives)
            adjective = adjectives[0]
            image = self.env.ref('game.stuff_template' + str(s.type)).image
            s.write({'name': adjective + " " + word, 'image': image})
            s.generate_properties()

    def generate_properties(self):
        for s in self:
            points = 0
            if s.player:
                if s.type == '0' or s.type == '1':
                    points = s.player.weapons_points
                else:
                    points = s.player.stuff_points
            points = points + 100
            base_points = {
                '0': 0,
                '1': 0,
                '2': 0,
                '3': 0,
                '4': 0,
                '5': 0,
                '6': 0
            }
            p = round(
                points * random.betavariate(4, 1) + 1
            )  # Betavariate en aquests parametres dona normalment numeros grans
            base_points[s.type] = p
            points = points - p
            while points > 0:
                p = round(points * random.random() + 1)
                a = str(random.randint(0, 6))
                base_points[a] = base_points[a] + p
                points = points - p
                print(points)
            s.write({
                'melee': base_points['1'],
                'shoot': base_points['0'],
                'armor': base_points['2'],
                'science': base_points['3'],
                'cook': base_points['4'],
                'medicine': base_points['5'],
                'energy': base_points['6']
            })

    @api.onchange('character')
    def _onchange_character(self):
        self.player = self.character.fortress.player.id

    def take_away(self):
        for s in self:
            s.write({'character': False})
示例#5
0
class AvatarMixin(models.AbstractModel):
    _name = 'avatar.mixin'
    _inherit = ['image.mixin']
    _description = "Avatar Mixin"
    _avatar_name_field = "name"

    # all image fields are base64 encoded and PIL-supported
    avatar_1920 = fields.Image("Avatar",
                               max_width=1920,
                               max_height=1920,
                               compute="_compute_avatar_1920")
    avatar_1024 = fields.Image("Avatar 1024",
                               max_width=1024,
                               max_height=1024,
                               compute="_compute_avatar_1024")
    avatar_512 = fields.Image("Avatar 512",
                              max_width=512,
                              max_height=512,
                              compute="_compute_avatar_512")
    avatar_256 = fields.Image("Avatar 256",
                              max_width=256,
                              max_height=256,
                              compute="_compute_avatar_256")
    avatar_128 = fields.Image("Avatar 128",
                              max_width=128,
                              max_height=128,
                              compute="_compute_avatar_128")

    def _compute_avatar(self, avatar_field, image_field):
        for record in self:
            avatar = record[image_field]
            if not avatar:
                if record.id and record[record._avatar_name_field]:
                    avatar = record._avatar_generate_svg()
                else:
                    avatar = record._avatar_get_placeholder()
            record[avatar_field] = avatar

    @api.depends(lambda self: [self._avatar_name_field, 'image_1920'])
    def _compute_avatar_1920(self):
        self._compute_avatar('avatar_1920', 'image_1920')

    @api.depends(lambda self: [self._avatar_name_field, 'image_1024'])
    def _compute_avatar_1024(self):
        self._compute_avatar('avatar_1024', 'image_1024')

    @api.depends(lambda self: [self._avatar_name_field, 'image_512'])
    def _compute_avatar_512(self):
        self._compute_avatar('avatar_512', 'image_512')

    @api.depends(lambda self: [self._avatar_name_field, 'image_256'])
    def _compute_avatar_256(self):
        self._compute_avatar('avatar_256', 'image_256')

    @api.depends(lambda self: [self._avatar_name_field, 'image_128'])
    def _compute_avatar_128(self):
        self._compute_avatar('avatar_128', 'image_128')

    def _avatar_generate_svg(self):
        initial = html_escape(self[self._avatar_name_field][0].upper())
        bgcolor = get_hsl_from_seed(self[self._avatar_name_field] +
                                    str(self.create_date.timestamp()))
        return b64encode((
            "<?xml version='1.0' encoding='UTF-8' ?>"
            "<svg height='180' width='180' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink'>"
            f"<rect fill='{bgcolor}' height='180' width='180'/>"
            f"<text fill='#ffffff' font-size='96' text-anchor='middle' x='90' y='125' font-family='sans-serif'>{initial}</text>"
            "</svg>").encode())

    def _avatar_get_placeholder_path(self):
        return "base/static/img/avatar_grey.png"

    def _avatar_get_placeholder(self):
        return b64encode(
            file_open(self._avatar_get_placeholder_path(), 'rb').read())
示例#6
0
class fortress(models.Model):
    _name = 'game.fortress'
    name = fields.Char()
    player = fields.Many2one('res.partner', ondelete='cascade')
    image = fields.Image(width=200)
    template = fields.Boolean()
    resources = fields.One2many('game.resource', 'fortress')
    available_resources = fields.Many2many('game.resource',
                                           compute='_get_available_resources')
    characters = fields.One2many('game.character', 'fortress')
    max_resources = fields.Integer(default=10)

    def create_new_character(self):
        for p in self:
            c_template = self.env.ref('game.character_template' +
                                      str(random.randint(1, 3)))
            c_template2 = self.env.ref('game.character_template' +
                                       str(random.randint(1, 12)))
            c = self.env['game.character'].create({
                'name':
                c_template2.name,
                'image':
                c_template.image,
                'fortress':
                p.id,
                'science':
                random.randint(1, 20),
                'construction':
                random.randint(1, 20),
                'mining':
                random.randint(1, 20),
                'war':
                random.randint(1, 20),
                'health':
                random.randint(1, 20)
            })

    def _get_available_resources(self):
        for f in self:
            print('aaa')
            r = self.env['game.resource'].search([('template', '=', True)])
            f.available_resources = [(6, 0, r.ids)]

    def grow_population(self):
        log = ''
        for f in self:
            n_characters = len(f.characters)
            if n_characters > 1:  # Al menys necessitem una parelleta
                probability = n_characters / 10000  # Amb 100 caracters, hi ha un 1% de probabilitat de naixer uno nou
                log = log + "Probability to grow: " + str(probability)
                if probability > random.random():
                    c_template = self.env.ref('game.character_template' +
                                              str(random.randint(1, 2)))
                    c_template2 = self.env.ref('game.character_template' +
                                               str(random.randint(1, 12)))
                    c = self.env['game.character'].create({
                        'name':
                        c_template2.name,
                        'image':
                        c_template.image,
                        'fortress':
                        f.id,
                        'science':
                        random.randint(1, 20),
                        'construction':
                        random.randint(1, 20),
                        'mining':
                        random.randint(1, 20),
                        'war':
                        random.randint(1, 20),
                        'health':
                        random.randint(1, 20)
                    })
                    log = log + 'Growing population ' + probability + " " + str(
                        c)
        print(log)
        return log
示例#7
0
class character(models.Model):
    _name = 'game.character'
    name = fields.Char()
    image = fields.Image(width=200)
    fortress = fields.Many2one('game.fortress', ondelete='cascade')
    player = fields.Many2one(related='fortress.player')
    science = fields.Float()
    construction = fields.Float()
    mining = fields.Float()
    war = fields.Float()
    health = fields.Float()
    age = fields.Integer(default=1)
    resource = fields.Many2one(
        'game.resource')  # Falta que no puga ser d'un altre fortress
    stuff = fields.One2many('game.stuff', 'character')
    unemployed = fields.Boolean(compute='_get_unemployed')
    resources_available = fields.One2many(related='fortress.resources',
                                          string='Resources available')

    def grow(self):
        for c in self:
            age = c.age + 1
            health = c.health
            # a partir de 100 anys, quasi segur que moren
            # 100 anys son 36500 dies, cada dia un minut de joc
            # un caracter dura com a molt 25 dies de joc
            # Funcio doble exponencial per a que dure menys de 25 dies
            p_mort = (1.00000000000001**(age**3.2) - 1) / 100
            # print(p_mort)
            if random.random() < p_mort:
                health = 0
                print('MORT!' + str(c.name))
            elif health < 100:
                health = health + 1
            c.write({'health': health, 'age': age})
            if c.resource:
                if not c.resource.inactive:
                    level = c.resource.level
                    k = c.resource.knowledge
                    if k == '1':  # Barracks
                        war = c.war + level
                        c.write({'war': war})
                    elif k == '2':  # Laboratory
                        science = c.science + level
                        c.write({'science': science})
                    elif k == '3':  # Mining
                        mining = c.mining + level
                        c.write({'mining': mining})
                    elif k == '4':
                        construction = c.construction + level
                        c.write({'construction': construction})
                    elif k == '5':
                        construction = c.construction + level
                        mining = c.mining + level
                        c.write({
                            'construction': construction,
                            'mining': mining
                        })

    def _get_unemployed(self):
        for c in self:
            if len(c.resource) == 0:
                c.unemployed = True
            else:
                c.unemployed = False

    @api.onchange('resource')
    def set_fortress(self):
        self.fortress = self.resource.fortress.id

    @api.onchange('name')
    def set_image(self):
        c_template = self.env.ref('game.character_template' +
                                  str(random.randint(1, 3)))
        self.image = c_template.image

    guns = fields.Many2many('game.stuff', compute='_get_stuff_type')
    armors = fields.Many2many('game.stuff', compute='_get_stuff_type')
    melees = fields.Many2many('game.stuff', compute='_get_stuff_type')

    gun_power = fields.Integer(compute='_get_stuff_type')
    melee_power = fields.Integer(compute='_get_stuff_type')
    armor_power = fields.Integer(compute='_get_stuff_type')

    def _get_stuff_type(self):
        for c in self:
            c.guns = c.stuff.filtered(lambda s: s.type == '0')
            c.armors = c.stuff.filtered(lambda s: s.type == '2')
            c.melees = c.stuff.filtered(lambda s: s.type == '1')

            best = c.stuff.filtered(
                lambda s: s.type == '0')  # Si va equipat en armes de foc
            if best:
                best = best.sorted(
                    key=lambda s: s.shoot,
                    reverse=True)[0].shoot  # La millor arma de foc
            else:
                best = 0
            c.gun_power = best

            best = c.stuff.filtered(
                lambda s: s.type == '1')  # Si va equipat en armes de melee
            if best:
                best = best.sorted(
                    key=lambda s: s.melee,
                    reverse=True)[0].melee  # La millor arma de melee
            else:
                best = 0
            c.melee_power = best

            best = c.stuff.filtered(
                lambda s: s.type == '2')  # Si va equipat en armadura
            if best:
                best = best.sorted(
                    key=lambda s: s.armor,
                    reverse=True)[0].armor  # La millor arma de foc
            else:
                best = 0
            c.armor_power = best

    def action_resurrect(self):
        print('RESUREECCCCCTTTT')
        for c in self.browse(self.env.context.get('active_ids')):
            #print(c.health)
            c.write({'health': 100})
示例#8
0
class LunchOrderWizard(models.TransientModel):
    _name = 'lunch.order.temp'
    _description = 'Lunch Order Temp'

    def _default_order_line(self):
        line_id = self.env.context.get('line_id')

        if line_id:
            last_time_ordered = self.env['lunch.order'].browse(line_id)
        else:
            last_time_ordered = self.env['lunch.order'].search(
                [('product_id', '=',
                  self.env.context.get('default_product_id', 0)),
                 ('user_id', '=',
                  self.env.context.get('default_user_id', self.env.user.id))],
                order="date desc, id desc",
                limit=1)
        return last_time_ordered

    currency_id = fields.Many2one(
        'res.currency', default=lambda self: self.env.company.currency_id)

    product_id = fields.Many2one('lunch.product', string='Product')
    product_description = fields.Text('Description',
                                      related='product_id.description')
    product_name = fields.Char('Product Name', related='product_id.name')
    product_category = fields.Many2one('lunch.product.category',
                                       related='product_id.category_id')
    topping_label_1 = fields.Char(
        related='product_id.category_id.topping_label_1')
    topping_label_2 = fields.Char(
        related='product_id.category_id.topping_label_2')
    topping_label_3 = fields.Char(
        related='product_id.category_id.topping_label_3')
    topping_quantity_1 = fields.Selection(
        related='product_id.category_id.topping_quantity_1')
    topping_quantity_2 = fields.Selection(
        related='product_id.category_id.topping_quantity_2')
    topping_quantity_3 = fields.Selection(
        related='product_id.category_id.topping_quantity_3')
    topping_ids_1 = fields.Many2many(
        'lunch.topping',
        'lunch_order_temp_topping',
        'order_id',
        'topping_id',
        string="Extra Garniture",
        domain=
        "[('category_id', '=', product_category), ('topping_category', '=', 1)]",
        default=lambda self: self._default_order_line().topping_ids_1)
    topping_ids_2 = fields.Many2many(
        'lunch.topping',
        'lunch_order_temp_topping',
        'order_id',
        'topping_id',
        string="Extra Garniture 2",
        domain=
        "[('category_id', '=', product_category), ('topping_category', '=', 2)]",
        default=lambda self: self._default_order_line().topping_ids_2)
    topping_ids_3 = fields.Many2many(
        'lunch.topping',
        'lunch_order_temp_topping',
        'order_id',
        'topping_id',
        string="Extra Garniture 3",
        domain=
        "[('category_id', '=', product_category), ('topping_category', '=', 3)]",
        default=lambda self: self._default_order_line().topping_ids_3)

    available_toppings_1 = fields.Boolean(
        help='Are extras available for this product',
        compute='_compute_available_toppings')
    available_toppings_2 = fields.Boolean(
        help='Are extras available for this product',
        compute='_compute_available_toppings')
    available_toppings_3 = fields.Boolean(
        help='Are extras available for this product',
        compute='_compute_available_toppings')

    image_1920 = fields.Image(related='product_id.image_1920')
    image_128 = fields.Image(related='product_id.image_128')

    quantity = fields.Float('Quantity', default=1)
    price_total = fields.Float('Total Price', compute='_compute_price_total')
    note = fields.Text('Special Instructions',
                       default=lambda self: self._default_order_line().note)

    user_id = fields.Many2one('res.users',
                              default=lambda self: self.env.user.id)
    edit = fields.Boolean(
        'Edit Mode',
        default=lambda self: bool(self.env.context.get('line_id')))

    @api.depends('product_id')
    def _compute_available_toppings(self):
        for wizard in self:
            wizard.available_toppings_1 = bool(
                wizard.env['lunch.topping'].search_count([
                    ('category_id', '=', wizard.product_category.id),
                    ('topping_category', '=', 1)
                ]))
            wizard.available_toppings_2 = bool(
                wizard.env['lunch.topping'].search_count([
                    ('category_id', '=', wizard.product_category.id),
                    ('topping_category', '=', 2)
                ]))
            wizard.available_toppings_3 = bool(
                wizard.env['lunch.topping'].search_count([
                    ('category_id', '=', wizard.product_category.id),
                    ('topping_category', '=', 3)
                ]))

    @api.constrains('topping_ids_1', 'topping_ids_2', 'topping_ids_3')
    def _check_topping_quantity(self):
        errors = {
            '1_more': _('You should order at least one %s'),
            '1': _('You have to order one and only one %s'),
        }
        for wizard in self:
            for index in range(1, 4):
                availability = wizard['available_toppings_%s' % index]
                quantity = wizard['topping_quantity_%s' % index]
                toppings = wizard['topping_ids_%s' % index].filtered(
                    lambda x: x.topping_category == index)
                label = wizard['topping_label_%s' % index]

                if availability and quantity != '0_more':
                    check = bool(
                        len(toppings) == 1 if quantity == '1' else toppings)
                    if not check:
                        raise ValidationError(errors[quantity] % label)

    @api.depends('product_id', 'topping_ids_1', 'topping_ids_2',
                 'topping_ids_3', 'quantity')
    def _compute_price_total(self):
        for wizard in self:
            wizard.price_total = wizard.quantity * (
                wizard.product_id.price + sum(
                    (wizard.topping_ids_1 | wizard.topping_ids_2
                     | wizard.topping_ids_3).mapped('price')))

    def _get_matching_lines(self):
        domain = [('user_id', '=', self.user_id.id),
                  ('product_id', '=', self.product_id.id),
                  ('date', '=', fields.Date.today()),
                  ('note', '=', self._get_note())]
        lines = self.env['lunch.order'].search(domain)
        return lines.filtered(lambda line:
                              (line.topping_ids_1 | line.topping_ids_2 | line.
                               topping_ids_3) == self.topping_ids_1)

    def _get_note(self):
        """
            returns self.note, but make sure that if it is an empty string it becomes False
        """
        return self.note if self.note else False

    def add_to_cart(self):
        self.ensure_one()
        line_id = self.env.context.get('line_id')

        matching_line = False
        matching_lines = self._get_matching_lines()

        if matching_lines:
            matching_line = matching_lines[0]
            quantity = 1

            if matching_line.id != line_id:
                if self.edit:
                    line = self.env['lunch.order'].browse(line_id)
                    quantity = line.quantity
                    line.sudo().unlink()
            else:
                quantity = 0

            matching_line.quantity += quantity
        else:
            if self.edit:
                line = self.env['lunch.order'].browse(line_id)

                line.topping_ids_1 = self.topping_ids_1
                line.topping_ids_2 = self.topping_ids_2
                line.topping_ids_3 = self.topping_ids_3
                line.note = self._get_note()
            else:
                self.env['lunch.order'].create({
                    'product_id':
                    self.product_id.id,
                    'topping_ids_1': [(6, 0, self.topping_ids_1.ids)],
                    'topping_ids_2': [(6, 0, self.topping_ids_2.ids)],
                    'topping_ids_3': [(6, 0, self.topping_ids_3.ids)],
                    'quantity':
                    self.quantity,
                    'note':
                    self._get_note()
                })
class HrAppraisal(models.Model):
    _name = "hr.appraisal"
    _inherit = ['mail.thread', 'mail.activity.mixin']
    _description = "Employee Appraisal"
    _order = 'state desc, id desc'
    _rec_name = 'employee_id'

    def _get_default_employee(self):
        if not self.env.user.has_group('hr_appraisal.group_hr_appraisal_user'):
            return self.env.user.employee_id

    active = fields.Boolean(default=True)
    employee_id = fields.Many2one('hr.employee',
                                  required=True,
                                  string='Employee',
                                  index=True,
                                  default=_get_default_employee)
    employee_user_id = fields.Many2one('res.users',
                                       related='employee_id.user_id')
    company_id = fields.Many2one('res.company',
                                 related='employee_id.company_id',
                                 store=True)
    department_id = fields.Many2one('hr.department',
                                    related='employee_id.department_id',
                                    string='Department',
                                    store=True)
    image_128 = fields.Image(related='employee_id.image_128')
    image_1920 = fields.Image(related='employee_id.image_1920')
    job_id = fields.Many2one('hr.job', related='employee_id.job_id')
    last_appraisal_id = fields.Many2one(
        'hr.appraisal', related='employee_id.last_appraisal_id')
    last_appraisal_date = fields.Date(
        related='employee_id.last_appraisal_date')
    employee_feedback_template = fields.Html(
        compute='_compute_feedback_templates')
    manager_feedback_template = fields.Html(
        compute='_compute_feedback_templates')

    date_close = fields.Date(
        string='Appraisal Deadline',
        required=True,
        default=lambda self: datetime.date.today().replace(
            day=1) + relativedelta(months=+1, days=-1))
    state = fields.Selection([('new', 'To Confirm'), ('pending', 'Confirmed'),
                              ('done', 'Done'), ('cancel', "Cancelled")],
                             string='Status',
                             tracking=True,
                             required=True,
                             copy=False,
                             default='new',
                             index=True,
                             group_expand='_group_expand_states')
    manager_ids = fields.Many2many(
        'hr.employee',
        'appraisal_manager_rel',
        'hr_appraisal_id',
        domain=
        "['|', ('company_id', '=', False), ('company_id', '=', company_id)]")
    meeting_id = fields.Many2one('calendar.event', string='Meeting')
    date_final_interview = fields.Date(string="Final Interview",
                                       index=True,
                                       tracking=True)
    waiting_feedback = fields.Boolean(
        string="Waiting Feedback from Employee/Managers",
        compute='_compute_waiting_feedback',
        tracking=True)
    employee_feedback = fields.Html(compute='_compute_feedbacks',
                                    store=True,
                                    readonly=False)
    manager_feedback = fields.Html(compute='_compute_feedbacks',
                                   store=True,
                                   readonly=False)
    employee_feedback_published = fields.Boolean(
        string="Employee Feedback Published", tracking=True)
    manager_feedback_published = fields.Boolean(
        string="Manager Feedback Published", tracking=True)
    can_see_employee_publish = fields.Boolean(
        compute='_compute_buttons_display')
    can_see_manager_publish = fields.Boolean(
        compute='_compute_buttons_display')
    assessment_note = fields.Many2one(
        'hr.appraisal.note', domain="[('company_id', '=', company_id)]")

    def _compute_buttons_display(self):
        new_appraisals = self.filtered(lambda a: a.state == 'new')
        new_appraisals.update({
            'can_see_employee_publish': False,
            'can_see_manager_publish': False,
        })
        user_employee = self.env.user.employee_id
        is_admin = self.env.user.user_has_groups(
            'hr_appraisal.group_hr_appraisal_manager')
        for appraisal in self - new_appraisals:
            appraisal.can_see_employee_publish = user_employee == appraisal.employee_id
            appraisal.can_see_manager_publish = user_employee in appraisal.manager_ids
            if is_admin and not appraisal.can_see_employee_publish and not appraisal.can_see_manager_publish:
                appraisal.can_see_employee_publish, appraisal.can_see_manager_publish = True, True

    @api.depends('employee_id.job_id')
    def _compute_feedbacks(self):
        for appraisal in self.filtered(lambda a: a.state == 'new'):
            appraisal.employee_feedback = appraisal.job_id.employee_feedback_template or appraisal.company_id.appraisal_employee_feedback_template
            appraisal.manager_feedback = appraisal.job_id.manager_feedback_template or appraisal.company_id.appraisal_manager_feedback_template

    @api.depends('employee_id.job_id')
    def _compute_feedback_templates(self):
        for appraisal in self:
            appraisal.employee_feedback_template = appraisal.job_id.employee_feedback_template or appraisal.company_id.appraisal_employee_feedback_template
            appraisal.manager_feedback_template = appraisal.job_id.manager_feedback_template or appraisal.company_id.appraisal_manager_feedback_template

    @api.depends('employee_feedback_published', 'manager_feedback_published')
    def _compute_waiting_feedback(self):
        for appraisal in self:
            appraisal.waiting_feedback = not appraisal.employee_feedback_published or not appraisal.manager_feedback_published

    def _group_expand_states(self, states, domain, order):
        return [key for key, val in self._fields['state'].selection]

    @api.onchange('employee_id')
    def _onchange_employee_id(self):
        self = self.sudo()  # fields are not on the employee public
        if self.employee_id:
            self.manager_ids = self.employee_id.parent_id

    def subscribe_employees(self):
        for appraisal in self:
            partners = appraisal.manager_ids.mapped(
                'related_partner_id'
            ) | appraisal.employee_id.related_partner_id
            appraisal.message_subscribe(partner_ids=partners.ids)

    def send_appraisal(self):
        for appraisal in self:
            employee_mail_template = appraisal.company_id.appraisal_confirm_employee_mail_template
            managers_mail_template = appraisal.company_id.appraisal_confirm_manager_mail_template
            mapped_data = {
                **{
                    appraisal.employee_id: employee_mail_template
                },
                **{
                    manager: managers_mail_template
                    for manager in appraisal.manager_ids
                }
            }
            for employee, mail_template in mapped_data.items():
                if not employee.work_email or not self.env.user.email:
                    continue
                ctx = {
                    'employee_to_name': employee.name,
                }
                RenderMixin = self.env['mail.render.mixin'].with_context(**ctx)
                subject = RenderMixin._render_template(
                    mail_template.subject,
                    'hr.appraisal',
                    appraisal.ids,
                    post_process=True)[appraisal.id]
                body = RenderMixin._render_template(
                    mail_template.body_html,
                    'hr.appraisal',
                    appraisal.ids,
                    post_process=True)[appraisal.id]
                # post the message
                mail_values = {
                    'email_from': self.env.user.email_formatted,
                    'author_id': self.env.user.partner_id.id,
                    'model': None,
                    'res_id': None,
                    'subject': subject,
                    'body_html': body,
                    'auto_delete': True,
                    'email_to': employee.work_email
                }
                try:
                    template = self.env.ref('mail.mail_notification_light',
                                            raise_if_not_found=True)
                except ValueError:
                    _logger.warning(
                        'QWeb template mail.mail_notification_light not found when sending appraisal confirmed mails. Sending without layouting.'
                    )
                else:
                    template_ctx = {
                        'message':
                        self.env['mail.message'].sudo().new(
                            dict(body=mail_values['body_html'],
                                 record_name=employee.name)),
                        'model_description':
                        self.env['ir.model']._get('hr.appraisal').display_name,
                        'company':
                        self.env.company,
                    }
                    body = template._render(template_ctx,
                                            engine='ir.qweb',
                                            minimal_qcontext=True)
                    mail_values['body_html'] = self.env[
                        'mail.render.mixin']._replace_local_links(body)
                self.env['mail.mail'].sudo().create(mail_values)

                if employee.user_id:
                    appraisal.activity_schedule(
                        'mail.mail_activity_data_todo',
                        appraisal.date_close,
                        summary=_('Appraisal Form to Fill'),
                        note=
                        _('Fill appraisal for <a href="#" data-oe-model="%s" data-oe-id="%s">%s</a>'
                          ) %
                        (appraisal.employee_id._name, appraisal.employee_id.id,
                         appraisal.employee_id.display_name),
                        user_id=employee.user_id.id)

    def action_cancel(self):
        self.write({'state': 'cancel', 'date_final_interview': False})
        self.mapped('meeting_id').unlink()
        self.activity_unlink([
            'mail.mail_activity_data_meeting', 'mail.mail_activity_data_todo'
        ])

    @api.model
    def create(self, vals):
        result = super(HrAppraisal, self).create(vals)
        if vals.get('state') and vals['state'] == 'pending':
            self.send_appraisal()

        result.employee_id.sudo().write({
            'next_appraisal_date':
            result.date_close,
        })
        result.subscribe_employees()
        return result

    def write(self, vals):
        if 'state' in vals and vals['state'] == 'pending':
            self.send_appraisal()
        result = super(HrAppraisal, self).write(vals)
        if vals.get('date_close'):
            self.mapped('employee_id').write(
                {'next_appraisal_date': vals.get('date_close')})
            self.activity_reschedule(['mail.mail_activity_data_todo'],
                                     date_deadline=vals['date_close'])
        return result

    def unlink(self):
        if any(appraisal.state not in ['new', 'cancel'] for appraisal in self):
            raise UserError(
                _("You cannot delete appraisal which is not in draft or canceled state"
                  ))
        return super(HrAppraisal, self).unlink()

    def read(self, fields=None, load='_classic_read'):
        check_feedback = set(fields) & {
            'manager_feedback', 'employee_feedback'
        }
        if check_feedback:
            fields = fields + [
                'can_see_employee_publish', 'can_see_manager_publish',
                'employee_feedback_published', 'manager_feedback_published'
            ]
        records = super().read(fields, load)
        if check_feedback:
            for appraisal in records:
                if not appraisal['can_see_employee_publish'] and not appraisal[
                        'employee_feedback_published']:
                    appraisal['employee_feedback'] = _('Unpublished')
                if not appraisal['can_see_manager_publish'] and not appraisal[
                        'manager_feedback_published']:
                    appraisal['manager_feedback'] = _('Unpublished')
        return records

    def action_calendar_event(self):
        self.ensure_one()
        partners = self.manager_ids.mapped(
            'related_partner_id'
        ) | self.employee_id.related_partner_id | self.env.user.partner_id
        action = self.env["ir.actions.actions"]._for_xml_id(
            "calendar.action_calendar_event")
        action['context'] = {
            'default_partner_ids': partners.ids,
            'search_default_mymeetings': 1
        }
        return action

    def action_confirm(self):
        self.activity_feedback(['mail.mail_activity_data_todo'])
        self.write({'state': 'pending'})

    def action_done(self):
        current_date = datetime.date.today()
        self.activity_feedback([
            'mail.mail_activity_data_meeting', 'mail.mail_activity_data_todo'
        ])
        self.write({'state': 'done'})
        for appraisal in self:
            appraisal.employee_id.write({
                'last_appraisal_id': appraisal.id,
                'last_appraisal_date': current_date,
                'next_appraisal_date': False
            })

    def action_open_last_appraisal(self):
        self.ensure_one()
        return {
            'view_mode': 'form',
            'res_model': 'hr.appraisal',
            'type': 'ir.actions.act_window',
            'target': 'current',
            'res_id': self.last_appraisal_id.id,
        }

    def action_open_goals(self):
        self.ensure_one()
        return {
            'name': _('%s Goals') % self.employee_id.name,
            'view_mode': 'kanban,tree,form',
            'res_model': 'hr.appraisal.goal',
            'type': 'ir.actions.act_window',
            'target': 'current',
            'domain': [('employee_id', '=', self.employee_id.id)],
            'context': {
                'default_employee_id': self.employee_id.id
            },
        }
示例#10
0
class LunchOrder(models.Model):
    _name = 'lunch.order'
    _description = 'Lunch Order'
    _order = 'id desc'
    _display_name = 'product_id'

    name = fields.Char(related='product_id.name',
                       string="Product Name",
                       readonly=True)  # to remove
    topping_ids_1 = fields.Many2many('lunch.topping',
                                     'lunch_order_topping',
                                     'order_id',
                                     'topping_id',
                                     string='Extras 1',
                                     domain=[('topping_category', '=', 1)])
    topping_ids_2 = fields.Many2many('lunch.topping',
                                     'lunch_order_topping',
                                     'order_id',
                                     'topping_id',
                                     string='Extras 2',
                                     domain=[('topping_category', '=', 2)])
    topping_ids_3 = fields.Many2many('lunch.topping',
                                     'lunch_order_topping',
                                     'order_id',
                                     'topping_id',
                                     string='Extras 3',
                                     domain=[('topping_category', '=', 3)])
    product_id = fields.Many2one('lunch.product',
                                 string="Product",
                                 required=True)
    category_id = fields.Many2one(string='Product Category',
                                  related='product_id.category_id',
                                  store=True)
    date = fields.Date('Order Date',
                       required=True,
                       readonly=True,
                       states={'new': [('readonly', False)]},
                       default=fields.Date.context_today)
    supplier_id = fields.Many2one(string='Vendor',
                                  related='product_id.supplier_id',
                                  store=True,
                                  index=True)
    user_id = fields.Many2one('res.users',
                              'User',
                              readonly=True,
                              states={'new': [('readonly', False)]},
                              default=lambda self: self.env.uid)
    note = fields.Text('Notes')
    price = fields.Float('Total Price',
                         compute='_compute_total_price',
                         readonly=True,
                         store=True,
                         digits='Account')
    active = fields.Boolean('Active', default=True)
    state = fields.Selection([('new', 'To Order'), ('ordered', 'Ordered'),
                              ('confirmed', 'Received'),
                              ('cancelled', 'Cancelled')],
                             'Status',
                             readonly=True,
                             index=True,
                             default='new')
    company_id = fields.Many2one('res.company',
                                 default=lambda self: self.env.company.id)
    currency_id = fields.Many2one(related='company_id.currency_id', store=True)
    quantity = fields.Float('Quantity', required=True, default=1)

    display_toppings = fields.Text('Extras',
                                   compute='_compute_display_toppings',
                                   store=True)

    product_description = fields.Html('Description',
                                      related='product_id.description')
    topping_label_1 = fields.Char(
        related='product_id.category_id.topping_label_1')
    topping_label_2 = fields.Char(
        related='product_id.category_id.topping_label_2')
    topping_label_3 = fields.Char(
        related='product_id.category_id.topping_label_3')
    topping_quantity_1 = fields.Selection(
        related='product_id.category_id.topping_quantity_1')
    topping_quantity_2 = fields.Selection(
        related='product_id.category_id.topping_quantity_2')
    topping_quantity_3 = fields.Selection(
        related='product_id.category_id.topping_quantity_3')
    image_1920 = fields.Image(compute='_compute_product_images')
    image_128 = fields.Image(compute='_compute_product_images')

    available_toppings_1 = fields.Boolean(
        help='Are extras available for this product',
        compute='_compute_available_toppings')
    available_toppings_2 = fields.Boolean(
        help='Are extras available for this product',
        compute='_compute_available_toppings')
    available_toppings_3 = fields.Boolean(
        help='Are extras available for this product',
        compute='_compute_available_toppings')

    @api.depends('product_id')
    def _compute_product_images(self):
        for line in self:
            line.image_1920 = line.product_id.image_1920 or line.category_id.image_1920
            line.image_128 = line.product_id.image_128 or line.category_id.image_128

    @api.depends('category_id')
    def _compute_available_toppings(self):
        for line in self:
            line.available_toppings_1 = bool(
                line.env['lunch.topping'].search_count([
                    ('category_id', '=', line.category_id.id),
                    ('topping_category', '=', 1)
                ]))
            line.available_toppings_2 = bool(
                line.env['lunch.topping'].search_count([
                    ('category_id', '=', line.category_id.id),
                    ('topping_category', '=', 2)
                ]))
            line.available_toppings_3 = bool(
                line.env['lunch.topping'].search_count([
                    ('category_id', '=', line.category_id.id),
                    ('topping_category', '=', 3)
                ]))

    def init(self):
        self._cr.execute(
            """CREATE INDEX IF NOT EXISTS lunch_order_user_product_date ON %s (user_id, product_id, date)"""
            % self._table)

    def _extract_toppings(self, values):
        """
            If called in api.multi then it will pop topping_ids_1,2,3 from values
        """
        if self.ids:
            # TODO This is not taking into account all the toppings for each individual order, this is usually not a problem
            # since in the interface you usually don't update more than one order at a time but this is a bug nonetheless
            topping_1 = values.pop('topping_ids_1')[0][
                2] if 'topping_ids_1' in values else self[:1].topping_ids_1.ids
            topping_2 = values.pop('topping_ids_2')[0][
                2] if 'topping_ids_2' in values else self[:1].topping_ids_2.ids
            topping_3 = values.pop('topping_ids_3')[0][
                2] if 'topping_ids_3' in values else self[:1].topping_ids_3.ids
        else:
            topping_1 = values['topping_ids_1'][0][
                2] if 'topping_ids_1' in values else []
            topping_2 = values['topping_ids_2'][0][
                2] if 'topping_ids_2' in values else []
            topping_3 = values['topping_ids_3'][0][
                2] if 'topping_ids_3' in values else []

        return topping_1 + topping_2 + topping_3

    @api.constrains('topping_ids_1', 'topping_ids_2', 'topping_ids_3')
    def _check_topping_quantity(self):
        errors = {
            '1_more': _('You should order at least one %s'),
            '1': _('You have to order one and only one %s'),
        }
        for line in self:
            for index in range(1, 4):
                availability = line['available_toppings_%s' % index]
                quantity = line['topping_quantity_%s' % index]
                toppings = line['topping_ids_%s' % index].filtered(
                    lambda x: x.topping_category == index)
                label = line['topping_label_%s' % index]

                if availability and quantity != '0_more':
                    check = bool(
                        len(toppings) == 1 if quantity == '1' else toppings)
                    if not check:
                        raise ValidationError(errors[quantity] % label)

    @api.model
    def create(self, values):
        lines = self._find_matching_lines({
            **values,
            'toppings':
            self._extract_toppings(values),
        })
        if lines:
            # YTI FIXME This will update multiple lines in the case there are multiple
            # matching lines which should not happen through the interface
            lines.update_quantity(1)
            return lines[:1]
        return super().create(values)

    def write(self, values):
        merge_needed = 'note' in values or 'topping_ids_1' in values or 'topping_ids_2' in values or 'topping_ids_3' in values

        if merge_needed:
            lines_to_deactivate = self.env['lunch.order']
            for line in self:
                # Only write on topping_ids_1 because they all share the same table
                # and we don't want to remove all the records
                # _extract_toppings will pop topping_ids_1, topping_ids_2 and topping_ids_3 from values
                # This also forces us to invalidate the cache for topping_ids_2 and topping_ids_3 that
                # could have changed through topping_ids_1 without the cache knowing about it
                toppings = self._extract_toppings(values)
                self.invalidate_cache(['topping_ids_2', 'topping_ids_3'])
                values['topping_ids_1'] = [(6, 0, toppings)]
                matching_lines = self._find_matching_lines({
                    'user_id':
                    values.get('user_id', line.user_id.id),
                    'product_id':
                    values.get('product_id', line.product_id.id),
                    'note':
                    values.get('note', line.note or False),
                    'toppings':
                    toppings,
                })
                if matching_lines:
                    lines_to_deactivate |= line
                    # YTI TODO Try to batch it, be careful there might be multiple matching
                    # lines for the same order hence quantity should not always be
                    # line.quantity, but rather a sum
                    matching_lines.update_quantity(line.quantity)
            lines_to_deactivate.write({'active': False})
            return super(LunchOrder, self - lines_to_deactivate).write(values)
        return super().write(values)

    @api.model
    def _find_matching_lines(self, values):
        domain = [
            ('user_id', '=',
             values.get('user_id',
                        self.default_get(['user_id'])['user_id'])),
            ('product_id', '=', values.get('product_id', False)),
            ('date', '=', fields.Date.today()),
            ('note', '=', values.get('note', False)),
        ]
        toppings = values.get('toppings', [])
        return self.search(domain).filtered(
            lambda line: (line.topping_ids_1 | line.topping_ids_2 | line.
                          topping_ids_3).ids == toppings)

    @api.depends('topping_ids_1', 'topping_ids_2', 'topping_ids_3',
                 'product_id', 'quantity')
    def _compute_total_price(self):
        for line in self:
            line.price = line.quantity * (line.product_id.price + sum(
                (line.topping_ids_1 | line.topping_ids_2
                 | line.topping_ids_3).mapped('price')))

    @api.depends('topping_ids_1', 'topping_ids_2', 'topping_ids_3')
    def _compute_display_toppings(self):
        for line in self:
            toppings = line.topping_ids_1 | line.topping_ids_2 | line.topping_ids_3
            line.display_toppings = ' + '.join(toppings.mapped('name'))

    def update_quantity(self, increment):
        for line in self.filtered(lambda line: line.state != 'confirmed'):
            if line.quantity <= -increment:
                # TODO: maybe unlink the order?
                line.active = False
            else:
                line.quantity += increment
        self._check_wallet()

    def add_to_cart(self):
        """
            This method currently does nothing, we currently need it in order to
            be able to reuse this model in place of a wizard
        """
        # YTI FIXME: Find a way to drop this.
        return True

    def _check_wallet(self):
        self.flush()
        for line in self:
            if self.env['lunch.cashmove'].get_wallet_balance(line.user_id) < 0:
                raise ValidationError(
                    _('Your wallet does not contain enough money to order that. To add some money to your wallet, please contact your lunch manager.'
                      ))

    def action_order(self):
        if self.filtered(lambda line: not line.product_id.active):
            raise ValidationError(_('Product is no longer available.'))
        self.write({'state': 'ordered'})
        self._check_wallet()

    def action_confirm(self):
        self.write({'state': 'confirmed'})

    def action_cancel(self):
        self.write({'state': 'cancelled'})
示例#11
0
class StudentContentVimeo(models.Model):
    _name = "s2u.student.content.vimeo"
    _inherit = 'image.mixin'
    _description = "Student Content Vimeo"
    _order = 'content_type_id, display_order, create_date desc, id desc'

    def _compute_temp_url(self):

        base_url = self.env['ir.config_parameter'].sudo().get_param(
            'web.base.url')

        for record in self:
            record.temp_url = '%s/student/vimeo/content/%d/%s.mp4' % (
                base_url, record.id, record.access_token)

    def _get_default_access_token(self):
        return str(uuid.uuid4())

    def unlink(self):

        for record in self:
            if not record.vimeo_uri:
                continue
            self.env['s2u.vimeo.services'].vimeo_delete_video(record.vimeo_uri)

        return super(StudentContentVimeo, self).unlink()

    def write(self, vals):

        if 'active' in vals:
            for record in self:
                if not vals['active']:
                    if record.vimeo_uri:
                        self.env['s2u.vimeo.services'].vimeo_delete_video(
                            record.vimeo_uri)
        return super(StudentContentVimeo, self).write(vals)

    def user_has_access(self):

        self.ensure_one()

        if self.user_ids:
            if self.env.user.id in self.user_ids.ids:
                return True

        if self.group_ids:
            groups = [g.id for g in self.group_ids]
            query = """
                        SELECT r.gid FROM res_groups_users_rel r  
                            WHERE r.gid IN %s AND r.uid = %s                                        
            """
            self.env.cr.execute(query, (tuple(groups), self.env.user.id))
            res = self.env.cr.fetchall()
            if res:
                return True
        return False

    def get_students(self):

        students = False
        self.env.cr.execute(
            """SELECT res_id FROM ir_model_data WHERE module='s2u_student' AND name='group_student'"""
        )
        res = self.env.cr.fetchall()
        if res:
            group_id = res[0][0]
            query = """
                   SELECT r.uid FROM res_groups_users_rel r  
                       WHERE r.gid = %s                                        
            """
            self.env.cr.execute(query, (group_id, ))
            res = self.env.cr.fetchall()
            if res:
                user_ids = [r[0] for r in res]
                students = self.env['res.users'].sudo().search([('id', 'in',
                                                                 user_ids)])
        return students

    def get_teacher_ids_of_user(self):

        if self.env.user.has_group('s2u_student.group_teacher'):
            return [self.env.user.id]
        else:
            self.env.cr.execute(
                """SELECT res_id FROM ir_model_data WHERE module='s2u_student' AND name='group_teacher'"""
            )
            res = self.env.cr.fetchall()
            group_id = res[0][0]
            query = """
                               SELECT r.uid FROM res_groups_users_rel r  
                                   WHERE r.gid = %s                                        
                        """
            self.env.cr.execute(query, (group_id, ))
            res = self.env.cr.fetchall()
            if res:
                user_ids = [r[0] for r in res]
                return user_ids
        return [0]

    def _get_content_levels(self):

        return [
            ('100', _('Level 100')),
            ('200', _('Level 200')),
            ('300', _('Level 300')),
            ('400', _('Level 400')),
        ]

    def _get_content_stars(self):

        return [
            ('0', _('None')),
            ('1', _('1 star')),
            ('2', _('2 stars')),
            ('3', _('3 stars')),
        ]

    # indirection to ease inheritance
    _content_level_selection = lambda self, *args, **kwargs: self._get_content_levels(
        *args, **kwargs)
    _content_stars_selection = lambda self, *args, **kwargs: self._get_content_stars(
        *args, **kwargs)

    company_id = fields.Many2one('res.company',
                                 string='Company',
                                 required=True,
                                 default=lambda self: self.env.user.company_id,
                                 readolny=True)
    teacher_id = fields.Many2one('res.users',
                                 string='Teacher',
                                 required=True,
                                 default=lambda self: self.env.user,
                                 readolny=True,
                                 states={'draft': [('readonly', False)]})
    name = fields.Char(string='Title',
                       required=True,
                       readonly=True,
                       states={'draft': [('readonly', False)]})
    description = fields.Text(string='Description',
                              readonly=True,
                              states={'draft': [('readonly', False)]})
    content_type = fields.Selection([
        ('video', 'Video'),
        ('page', 'Page'),
    ],
                                    required=True,
                                    default='video',
                                    string='Type',
                                    readonly=True,
                                    states={'draft': [('readonly', False)]})
    content_target = fields.Selection([
        ('elearning', 'e-Learning'),
        ('personal', 'Personal'),
    ],
                                      required=True,
                                      default='elearning',
                                      string='Target',
                                      readonly=True,
                                      states={'draft': [('readonly', False)]})
    group_ids = fields.Many2many('res.groups',
                                 's2u_res_groups_student_content_vimeo_rel',
                                 'content_id',
                                 'group_id',
                                 string='Access for groups')
    user_ids = fields.Many2many('res.users',
                                's2u_res_users_student_content_vimeo_rel',
                                'content_id',
                                'user_id',
                                string='Access for users')
    student_id = fields.Many2one('res.users',
                                 string='Student',
                                 readonly=True,
                                 states={'draft': [('readonly', False)]})
    project_id = fields.Many2one('project.project',
                                 string='Project',
                                 readonly=True,
                                 states={'draft': [('readonly', False)]})
    task_id = fields.Many2one('project.task', string='Task')
    vimeo_uri = fields.Char(string='Uri', copy=False)
    vimeo_html = fields.Text(string='Embedding', copy=False)
    vimeo_data = fields.Binary(string='Video',
                               readonly=True,
                               states={'draft': [('readonly', False)]},
                               copy=False)
    vimeo_filename = fields.Char(string='File Name', copy=False)
    vimeo_checksum = fields.Char(string="Checksum/SHA1", copy=False)
    vimeo_use_external_pull_link = fields.Boolean(string='External pull')
    vimeo_external_url = fields.Char("External url", copy=False)
    page_url = fields.Char(string='Page',
                           copy=False,
                           readonly=True,
                           states={'draft': [('readonly', False)]})
    active = fields.Boolean(default=True, copy=False)
    state = fields.Selection([
        ('draft', 'Draft'),
        ('sync', 'Synchronizing'),
        ('publish', 'Published'),
        ('offline', 'Offline'),
    ],
                             required=True,
                             default='draft',
                             string='State',
                             index=True,
                             copy=False)
    temp_url = fields.Char(string='Temp. url',
                           readonly=True,
                           compute='_compute_temp_url')
    access_token = fields.Char('Security Token',
                               copy=False,
                               default=_get_default_access_token)
    image_1920 = fields.Image(readonly=True,
                              states={'draft': [('readonly', False)]},
                              copy=False)
    tot_views = fields.Integer(string="# views", default=0)
    website_published = fields.Boolean(string="Website published",
                                       default=False)
    display_order = fields.Integer(string="Display order",
                                   required=True,
                                   default=1)
    level = fields.Selection(_content_level_selection,
                             required=True,
                             default='100',
                             string='Level',
                             copy=False)
    stars = fields.Selection(_content_stars_selection,
                             required=True,
                             default='0',
                             string='Stars',
                             copy=False)
    content_type_id = fields.Many2one('s2u.student.content.type',
                                      string='Category')

    def _create_task(self):

        self.ensure_one()

        if not self.project_id:
            return False

        content = '<p>%s</p><br/><br/><br/>' % self.description
        if self.content_type == 'page':
            url = '<a href="%s">%s</a>' % (self.page_url, self.name)
            content = '%s%s' % (content, url)

        students = []
        if self.user_ids:
            for u in self.user_ids:
                students.append(u.partner_id.id)

        if self.group_ids:
            groups = [g.id for g in self.group_ids]
            query = """
                        SELECT r.uid FROM res_groups_users_rel r  
                            WHERE r.gid IN %s                                        
            """
            self.env.cr.execute(query, (tuple(groups), ))
            res = self.env.cr.fetchall()
            users_in_groups = [r[0] for r in res]
            for u in self.env['res.users'].browse(users_in_groups):
                students.append(u.partner_id.id)

        students = list(set(students))
        for student in students:
            values = {
                'project_id': self.project_id.id,
                'name': self.name,
                'description': content,
                'partner_id': student,
                'user_id': self.teacher_id.id,
                's2u_content_id': self.id
            }

            task = self.env['project.task'].search([
                ('s2u_content_id', '=', self.id), ('partner_id', '=', student)
            ])
            if task:
                task.write(values)
            else:
                task = self.env['project.task'].create(values)

            add_followers_ids = [student]
            task.message_subscribe(add_followers_ids)

        return True

    def publish_content(self):

        self.ensure_one()

        if self.state == 'draft':
            if not self.group_ids and not self.user_ids:
                raise UserError(
                    _("Please select a group or user who have access to this content!"
                      ))

            if self.content_type == 'video':
                if self.vimeo_use_external_pull_link:
                    if self.vimeo_uri:
                        res = self.env[
                            's2u.vimeo.services'].vimeo_update_video(self)
                        if res:
                            self.write({'state': 'sync'})
                    else:
                        res = self.env[
                            's2u.vimeo.services'].vimeo_upload_video_pull(self)
                        if res:
                            self.write({
                                'vimeo_uri': res['uri'],
                                'vimeo_html': res['embed']['html'],
                                'state': 'sync',
                                'vimeo_checksum': False
                            })
                            self.env['s2u.vimeo.services'].vimeo_add_whitelist(
                                self.vimeo_uri, self.env['s2u.vimeo.services'].
                                get_whitelist_domain())
                            self.env['s2u.vimeo.services'].vimeo_change_folder(
                                self.vimeo_uri)
                            self.env[
                                's2u.vimeo.services'].vimeo_get_thumbnails(
                                    self.vimeo_uri)
                            self.write({'state': 'sync'})
                else:
                    if not self.vimeo_data:
                        raise UserError(_("No video present to publish!"))

                    content_data = self.env['ir.attachment'].sudo().search([
                        ('res_model', '=', 's2u.student.content.vimeo'),
                        ('res_field', '=', 'vimeo_data'),
                        ('res_id', '=', self.id)
                    ])
                    if not (content_data.checksum == self.vimeo_checksum):
                        res = self.env[
                            's2u.vimeo.services'].vimeo_upload_video_pull(self)
                        if res:
                            self.write({
                                'vimeo_uri': res['uri'],
                                'vimeo_html': res['embed']['html'],
                                'state': 'sync',
                                'vimeo_checksum': content_data.checksum
                            })
                            self.env['s2u.vimeo.services'].vimeo_add_whitelist(
                                self.vimeo_uri, self.env['s2u.vimeo.services'].
                                get_whitelist_domain())
                            self.env['s2u.vimeo.services'].vimeo_change_folder(
                                self.vimeo_uri)
                            self.env[
                                's2u.vimeo.services'].vimeo_get_thumbnails(
                                    self.vimeo_uri)
                    else:
                        res = self.env[
                            's2u.vimeo.services'].vimeo_update_video(self)
                        if res:
                            self.write({'state': 'sync'})

                return True

            if self.content_type == 'page':
                self.write({'state': 'publish', 'website_published': True})

                if self.content_target == 'personal':
                    self._create_task()

            return True

    def unpublish_content(self):

        self.ensure_one()

        if self.content_target == 'personal':
            task = self.env['project.task'].sudo().search([('s2u_content_id',
                                                            '=', self.id)])
            if task:
                task.write({'active': False})

        self.write({'state': 'draft', 'website_published': False})

    @api.model
    def cron_synchronize(self):
        """WARNING: meant for cron usage only - will commit() after each validation!"""

        _logger.info('Start synchronize with Vimeo ...')

        self._cr.commit()

        todos = self.search([('state', '=', 'sync')], limit=5)
        for t in todos:

            if t.content_type != 'video':
                continue

            try:
                res = self.env['s2u.vimeo.services'].vimeo_get_video(
                    t.vimeo_uri)
                if res and res['upload']['status'] == 'complete':
                    if not t.image_1920:
                        image = self.env[
                            's2u.vimeo.services'].vimeo_get_thumbnails(
                                t.vimeo_uri)
                        if image:
                            t.write({'image_1920': image})
                    t.write({'state': 'publish', 'website_published': True})

                    if t.content_target == 'personal':
                        t._create_task()

                self._cr.commit()
            except Exception as e:
                self._cr.rollback()
                _logger.error(
                    'Vimeo synchronization [%s] failed for object: [%s] with id: [%d]'
                    % (t.vimeo_uri, t.name, t.id))
示例#12
0
class User(models.Model):
    _inherit = ['res.users']

    hand_signature = fields.Image('Handwritten Signature',
                                  max_height=350,
                                  max_width=200)
示例#13
0
class SurveyQuestion(models.Model):
    """ Questions that will be asked in a survey.

        Each question can have one of more suggested answers (eg. in case of
        multi-answer checkboxes, radio buttons...).

        Technical note:

        survey.question is also the model used for the survey's pages (with the "is_page" field set to True).

        A page corresponds to a "section" in the interface, and the fact that it separates the survey in
        actual pages in the interface depends on the "questions_layout" parameter on the survey.survey model.
        Pages are also used when randomizing questions. The randomization can happen within a "page".

        Using the same model for questions and pages allows to put all the pages and questions together in a o2m field
        (see survey.survey.question_and_page_ids) on the view side and easily reorganize your survey by dragging the
        items around.

        It also removes on level of encoding by directly having 'Add a page' and 'Add a question'
        links on the tree view of questions, enabling a faster encoding.

        However, this has the downside of making the code reading a little bit more complicated.
        Efforts were made at the model level to create computed fields so that the use of these models
        still seems somewhat logical. That means:
        - A survey still has "page_ids" (question_and_page_ids filtered on is_page = True)
        - These "page_ids" still have question_ids (questions located between this page and the next)
        - These "question_ids" still have a "page_id"

        That makes the use and display of these information at view and controller levels easier to understand.
    """
    _name = 'survey.question'
    _description = 'Survey Question'
    _rec_name = 'title'
    _order = 'sequence,id'

    @api.model
    def default_get(self, fields):
        defaults = super(SurveyQuestion, self).default_get(fields)
        if (not fields or 'question_type' in fields):
            defaults['question_type'] = False if defaults.get('is_page') else 'simple_choice'
        return defaults

    # question generic data
    title = fields.Char('Title', required=True, translate=True)
    description = fields.Html(
        'Description', translate=True, sanitize=False,  # TDE TODO: sanitize but find a way to keep youtube iframe media stuff
        help="Use this field to add additional explanations about your question or to illustrate it with pictures or a video")
    question_placeholder = fields.Char("Placeholder", translate=True, compute="_compute_question_placeholder", store=True, readonly=False)
    background_image = fields.Image("Background Image", compute="_compute_background_image", store=True, readonly=False)
    background_image_url = fields.Char("Background Url", compute="_compute_background_image_url")
    survey_id = fields.Many2one('survey.survey', string='Survey', ondelete='cascade')
    scoring_type = fields.Selection(related='survey_id.scoring_type', string='Scoring Type', readonly=True)
    sequence = fields.Integer('Sequence', default=10)
    # page specific
    is_page = fields.Boolean('Is a page?')
    question_ids = fields.One2many('survey.question', string='Questions', compute="_compute_question_ids")
    questions_selection = fields.Selection(
        related='survey_id.questions_selection', readonly=True,
        help="If randomized is selected, add the number of random questions next to the section.")
    random_questions_count = fields.Integer(
        '# Questions Randomly Picked', default=1,
        help="Used on randomized sections to take X random questions from all the questions of that section.")
    # question specific
    page_id = fields.Many2one('survey.question', string='Page', compute="_compute_page_id", store=True)
    question_type = fields.Selection([
        ('simple_choice', 'Multiple choice: only one answer'),
        ('multiple_choice', 'Multiple choice: multiple answers allowed'),
        ('text_box', 'Multiple Lines Text Box'),
        ('char_box', 'Single Line Text Box'),
        ('numerical_box', 'Numerical Value'),
        ('date', 'Date'),
        ('datetime', 'Datetime'),
        ('matrix', 'Matrix')], string='Question Type',
        compute='_compute_question_type', readonly=False, store=True)
    is_scored_question = fields.Boolean(
        'Scored', compute='_compute_is_scored_question',
        readonly=False, store=True, copy=True,
        help="Include this question as part of quiz scoring. Requires an answer and answer score to be taken into account.")
    # -- scoreable/answerable simple answer_types: numerical_box / date / datetime
    answer_numerical_box = fields.Float('Correct numerical answer', help="Correct number answer for this question.")
    answer_date = fields.Date('Correct date answer', help="Correct date answer for this question.")
    answer_datetime = fields.Datetime('Correct datetime answer', help="Correct date and time answer for this question.")
    answer_score = fields.Float('Score', help="Score value for a correct answer to this question.")
    # -- char_box
    save_as_email = fields.Boolean(
        "Save as user email", compute='_compute_save_as_email', readonly=False, store=True, copy=True,
        help="If checked, this option will save the user's answer as its email address.")
    save_as_nickname = fields.Boolean(
        "Save as user nickname", compute='_compute_save_as_nickname', readonly=False, store=True, copy=True,
        help="If checked, this option will save the user's answer as its nickname.")
    # -- simple choice / multiple choice / matrix
    suggested_answer_ids = fields.One2many(
        'survey.question.answer', 'question_id', string='Types of answers', copy=True,
        help='Labels used for proposed choices: simple choice, multiple choice and columns of matrix')
    # -- matrix
    matrix_subtype = fields.Selection([
        ('simple', 'One choice per row'),
        ('multiple', 'Multiple choices per row')], string='Matrix Type', default='simple')
    matrix_row_ids = fields.One2many(
        'survey.question.answer', 'matrix_question_id', string='Matrix Rows', copy=True,
        help='Labels used for proposed choices: rows of matrix')
    # -- display & timing options
    is_time_limited = fields.Boolean("The question is limited in time",
        help="Currently only supported for live sessions.")
    time_limit = fields.Integer("Time limit (seconds)")
    # -- comments (simple choice, multiple choice, matrix (without count as an answer))
    comments_allowed = fields.Boolean('Show Comments Field')
    comments_message = fields.Char('Comment Message', translate=True, default=lambda self: _("If other, please specify:"))
    comment_count_as_answer = fields.Boolean('Comment Field is an Answer Choice')
    # question validation
    validation_required = fields.Boolean('Validate entry', compute='_compute_validation_required', readonly=False, store=True)
    validation_email = fields.Boolean('Input must be an email')
    validation_length_min = fields.Integer('Minimum Text Length', default=0)
    validation_length_max = fields.Integer('Maximum Text Length', default=0)
    validation_min_float_value = fields.Float('Minimum value', default=0.0)
    validation_max_float_value = fields.Float('Maximum value', default=0.0)
    validation_min_date = fields.Date('Minimum Date')
    validation_max_date = fields.Date('Maximum Date')
    validation_min_datetime = fields.Datetime('Minimum Datetime')
    validation_max_datetime = fields.Datetime('Maximum Datetime')
    validation_error_msg = fields.Char('Validation Error message', translate=True, default=lambda self: _("The answer you entered is not valid."))
    constr_mandatory = fields.Boolean('Mandatory Answer')
    constr_error_msg = fields.Char('Error message', translate=True, default=lambda self: _("This question requires an answer."))
    # answers
    user_input_line_ids = fields.One2many(
        'survey.user_input.line', 'question_id', string='Answers',
        domain=[('skipped', '=', False)], groups='survey.group_survey_user')

    # Conditional display
    is_conditional = fields.Boolean(
        string='Conditional Display', copy=False, help="""If checked, this question will be displayed only 
        if the specified conditional answer have been selected in a previous question""")
    triggering_question_id = fields.Many2one(
        'survey.question', string="Triggering Question", copy=False, compute="_compute_triggering_question_id",
        store=True, readonly=False, help="Question containing the triggering answer to display the current question.",
        domain="[('survey_id', '=', survey_id), \
                 '&', ('question_type', 'in', ['simple_choice', 'multiple_choice']), \
                 '|', \
                     ('sequence', '<', sequence), \
                     '&', ('sequence', '=', sequence), ('id', '<', id)]")
    triggering_answer_id = fields.Many2one(
        'survey.question.answer', string="Triggering Answer", copy=False, compute="_compute_triggering_answer_id",
        store=True, readonly=False, help="Answer that will trigger the display of the current question.",
        domain="[('question_id', '=', triggering_question_id)]")

    _sql_constraints = [
        ('positive_len_min', 'CHECK (validation_length_min >= 0)', 'A length must be positive!'),
        ('positive_len_max', 'CHECK (validation_length_max >= 0)', 'A length must be positive!'),
        ('validation_length', 'CHECK (validation_length_min <= validation_length_max)', 'Max length cannot be smaller than min length!'),
        ('validation_float', 'CHECK (validation_min_float_value <= validation_max_float_value)', 'Max value cannot be smaller than min value!'),
        ('validation_date', 'CHECK (validation_min_date <= validation_max_date)', 'Max date cannot be smaller than min date!'),
        ('validation_datetime', 'CHECK (validation_min_datetime <= validation_max_datetime)', 'Max datetime cannot be smaller than min datetime!'),
        ('positive_answer_score', 'CHECK (answer_score >= 0)', 'An answer score for a non-multiple choice question cannot be negative!'),
        ('scored_datetime_have_answers', "CHECK (is_scored_question != True OR question_type != 'datetime' OR answer_datetime is not null)",
            'All "Is a scored question = True" and "Question Type: Datetime" questions need an answer'),
        ('scored_date_have_answers', "CHECK (is_scored_question != True OR question_type != 'date' OR answer_date is not null)",
            'All "Is a scored question = True" and "Question Type: Date" questions need an answer')
    ]

    @api.depends('question_type')
    def _compute_question_placeholder(self):
        for question in self:
            if question.question_type in ('simple_choice', 'multiple_choice', 'matrix') \
                    or not question.question_placeholder:  # avoid CacheMiss errors
                question.question_placeholder = False

    @api.depends('is_page')
    def _compute_background_image(self):
        """ Background image is only available on sections. """
        for question in self.filtered(lambda q: not q.is_page):
            question.background_image = False

    @api.depends('survey_id.access_token', 'background_image', 'page_id', 'survey_id.background_image_url')
    def _compute_background_image_url(self):
        """ How the background url is computed:
        - For a question: it depends on the related section (see below)
        - For a section:
            - if a section has a background, then we create the background URL using this section's ID
            - if not, then we fallback on the survey background url """
        base_bg_url = "/survey/%s/%s/get_background_image"
        for question in self:
            if question.is_page:
                background_section_id = question.id if question.background_image else False
            else:
                background_section_id = question.page_id.id if question.page_id.background_image else False

            if background_section_id:
                question.background_image_url = base_bg_url % (
                    question.survey_id.access_token,
                    background_section_id
                )
            else:
                question.background_image_url = question.survey_id.background_image_url

    @api.depends('is_page')
    def _compute_question_type(self):
        for question in self:
            if not question.question_type or question.is_page:
                question.question_type = False

    @api.depends('survey_id.question_and_page_ids.is_page', 'survey_id.question_and_page_ids.sequence')
    def _compute_question_ids(self):
        """Will take all questions of the survey for which the index is higher than the index of this page
        and lower than the index of the next page."""
        for question in self:
            if question.is_page:
                next_page_index = False
                for page in question.survey_id.page_ids:
                    if page._index() > question._index():
                        next_page_index = page._index()
                        break

                question.question_ids = question.survey_id.question_ids.filtered(
                    lambda q: q._index() > question._index() and (not next_page_index or q._index() < next_page_index)
                )
            else:
                question.question_ids = self.env['survey.question']

    @api.depends('survey_id.question_and_page_ids.is_page', 'survey_id.question_and_page_ids.sequence')
    def _compute_page_id(self):
        """Will find the page to which this question belongs to by looking inside the corresponding survey"""
        for question in self:
            if question.is_page:
                question.page_id = None
            else:
                page = None
                for q in question.survey_id.question_and_page_ids.sorted():
                    if q == question:
                        break
                    if q.is_page:
                        page = q
                question.page_id = page

    @api.depends('question_type', 'validation_email')
    def _compute_save_as_email(self):
        for question in self:
            if question.question_type != 'char_box' or not question.validation_email:
                question.save_as_email = False

    @api.depends('question_type')
    def _compute_save_as_nickname(self):
        for question in self:
            if question.question_type != 'char_box':
                question.save_as_nickname = False

    @api.depends('question_type')
    def _compute_validation_required(self):
        for question in self:
            if not question.validation_required or question.question_type not in ['char_box', 'numerical_box', 'date', 'datetime']:
                question.validation_required = False

    @api.depends('is_conditional')
    def _compute_triggering_question_id(self):
        """ Used as an 'onchange' : Reset the triggering question if user uncheck 'Conditional Display'
            Avoid CacheMiss : set the value to False if the value is not set yet."""
        for question in self:
            if not question.is_conditional or question.triggering_question_id is None:
                question.triggering_question_id = False

    @api.depends('triggering_question_id')
    def _compute_triggering_answer_id(self):
        """ Used as an 'onchange' : Reset the triggering answer if user unset or change the triggering question
            or uncheck 'Conditional Display'.
            Avoid CacheMiss : set the value to False if the value is not set yet."""
        for question in self:
            if not question.triggering_question_id \
                    or question.triggering_question_id != question.triggering_answer_id.question_id\
                    or question.triggering_answer_id is None:
                question.triggering_answer_id = False

    @api.depends('question_type', 'scoring_type', 'answer_date', 'answer_datetime', 'answer_numerical_box')
    def _compute_is_scored_question(self):
        """ Computes whether a question "is scored" or not. Handles following cases:
          - inconsistent Boolean=None edge case that breaks tests => False
          - survey is not scored => False
          - 'date'/'datetime'/'numerical_box' question types w/correct answer => True
            (implied without user having to activate, except for numerical whose correct value is 0.0)
          - 'simple_choice / multiple_choice': set to True even if logic is a bit different (coming from answers)
          - question_type isn't scoreable (note: choice questions scoring logic handled separately) => False
        """
        for question in self:
            if question.is_scored_question is None or question.scoring_type == 'no_scoring':
                question.is_scored_question = False
            elif question.question_type == 'date':
                question.is_scored_question = bool(question.answer_date)
            elif question.question_type == 'datetime':
                question.is_scored_question = bool(question.answer_datetime)
            elif question.question_type == 'numerical_box' and question.answer_numerical_box:
                question.is_scored_question = True
            elif question.question_type in ['simple_choice', 'multiple_choice']:
                question.is_scored_question = True
            else:
                question.is_scored_question = False

    # ------------------------------------------------------------
    # VALIDATION
    # ------------------------------------------------------------

    def validate_question(self, answer, comment=None):
        """ Validate question, depending on question type and parameters
         for simple choice, text, date and number, answer is simply the answer of the question.
         For other multiple choices questions, answer is a list of answers (the selected choices
         or a list of selected answers per question -for matrix type-):
            - Simple answer : answer = 'example' or 2 or question_answer_id or 2019/10/10
            - Multiple choice : answer = [question_answer_id1, question_answer_id2, question_answer_id3]
            - Matrix: answer = { 'rowId1' : [colId1, colId2,...], 'rowId2' : [colId1, colId3, ...] }

         return dict {question.id (int): error (str)} -> empty dict if no validation error.
         """
        self.ensure_one()
        if isinstance(answer, str):
            answer = answer.strip()
        # Empty answer to mandatory question
        if self.constr_mandatory and not answer and self.question_type not in ['simple_choice', 'multiple_choice']:
            return {self.id: self.constr_error_msg}

        # because in choices question types, comment can count as answer
        if answer or self.question_type in ['simple_choice', 'multiple_choice']:
            if self.question_type == 'char_box':
                return self._validate_char_box(answer)
            elif self.question_type == 'numerical_box':
                return self._validate_numerical_box(answer)
            elif self.question_type in ['date', 'datetime']:
                return self._validate_date(answer)
            elif self.question_type in ['simple_choice', 'multiple_choice']:
                return self._validate_choice(answer, comment)
            elif self.question_type == 'matrix':
                return self._validate_matrix(answer)
        return {}

    def _validate_char_box(self, answer):
        # Email format validation
        # all the strings of the form "<something>@<anything>.<extension>" will be accepted
        if self.validation_email:
            if not tools.email_normalize(answer):
                return {self.id: _('This answer must be an email address')}

        # Answer validation (if properly defined)
        # Length of the answer must be in a range
        if self.validation_required:
            if not (self.validation_length_min <= len(answer) <= self.validation_length_max):
                return {self.id: self.validation_error_msg}
        return {}

    def _validate_numerical_box(self, answer):
        try:
            floatanswer = float(answer)
        except ValueError:
            return {self.id: _('This is not a number')}

        if self.validation_required:
            # Answer is not in the right range
            with tools.ignore(Exception):
                if not (self.validation_min_float_value <= floatanswer <= self.validation_max_float_value):
                    return {self.id: self.validation_error_msg}
        return {}

    def _validate_date(self, answer):
        isDatetime = self.question_type == 'datetime'
        # Checks if user input is a date
        try:
            dateanswer = fields.Datetime.from_string(answer) if isDatetime else fields.Date.from_string(answer)
        except ValueError:
            return {self.id: _('This is not a date')}
        if self.validation_required:
            # Check if answer is in the right range
            if isDatetime:
                min_date = fields.Datetime.from_string(self.validation_min_datetime)
                max_date = fields.Datetime.from_string(self.validation_max_datetime)
                dateanswer = fields.Datetime.from_string(answer)
            else:
                min_date = fields.Date.from_string(self.validation_min_date)
                max_date = fields.Date.from_string(self.validation_max_date)
                dateanswer = fields.Date.from_string(answer)

            if (min_date and max_date and not (min_date <= dateanswer <= max_date))\
                    or (min_date and not min_date <= dateanswer)\
                    or (max_date and not dateanswer <= max_date):
                return {self.id: self.validation_error_msg}
        return {}

    def _validate_choice(self, answer, comment):
        # Empty comment
        if self.constr_mandatory \
                and not answer \
                and not (self.comments_allowed and self.comment_count_as_answer and comment):
            return {self.id: self.constr_error_msg}
        return {}

    def _validate_matrix(self, answers):
        # Validate that each line has been answered
        if self.constr_mandatory and len(self.matrix_row_ids) != len(answers):
            return {self.id: self.constr_error_msg}
        return {}

    def _index(self):
        """We would normally just use the 'sequence' field of questions BUT, if the pages and questions are
        created without ever moving records around, the sequence field can be set to 0 for all the questions.

        However, the order of the recordset is always correct so we can rely on the index method."""
        self.ensure_one()
        return list(self.survey_id.question_and_page_ids).index(self)

    # ------------------------------------------------------------
    # STATISTICS / REPORTING
    # ------------------------------------------------------------

    def _prepare_statistics(self, user_input_lines):
        """ Compute statistical data for questions by counting number of vote per choice on basis of filter """
        all_questions_data = []
        for question in self:
            question_data = {'question': question, 'is_page': question.is_page}

            if question.is_page:
                all_questions_data.append(question_data)
                continue

            # fetch answer lines, separate comments from real answers
            all_lines = user_input_lines.filtered(lambda line: line.question_id == question)
            if question.question_type in ['simple_choice', 'multiple_choice', 'matrix']:
                answer_lines = all_lines.filtered(
                    lambda line: line.answer_type == 'suggestion' or (
                        line.skipped and not line.answer_type) or (
                        line.answer_type == 'char_box' and question.comment_count_as_answer)
                    )
                comment_line_ids = all_lines.filtered(lambda line: line.answer_type == 'char_box')
            else:
                answer_lines = all_lines
                comment_line_ids = self.env['survey.user_input.line']
            skipped_lines = answer_lines.filtered(lambda line: line.skipped)
            done_lines = answer_lines - skipped_lines
            question_data.update(
                answer_line_ids=answer_lines,
                answer_line_done_ids=done_lines,
                answer_input_done_ids=done_lines.mapped('user_input_id'),
                answer_input_skipped_ids=skipped_lines.mapped('user_input_id'),
                comment_line_ids=comment_line_ids)
            question_data.update(question._get_stats_summary_data(answer_lines))

            # prepare table and graph data
            table_data, graph_data = question._get_stats_data(answer_lines)
            question_data['table_data'] = table_data
            question_data['graph_data'] = json.dumps(graph_data)

            all_questions_data.append(question_data)
        return all_questions_data

    def _get_stats_data(self, user_input_lines):
        if self.question_type == 'simple_choice':
            return self._get_stats_data_answers(user_input_lines)
        elif self.question_type == 'multiple_choice':
            table_data, graph_data = self._get_stats_data_answers(user_input_lines)
            return table_data, [{'key': self.title, 'values': graph_data}]
        elif self.question_type == 'matrix':
            return self._get_stats_graph_data_matrix(user_input_lines)
        return [line for line in user_input_lines], []

    def _get_stats_data_answers(self, user_input_lines):
        """ Statistics for question.answer based questions (simple choice, multiple
        choice.). A corner case with a void record survey.question.answer is added
        to count comments that should be considered as valid answers. This small hack
        allow to have everything available in the same standard structure. """
        suggested_answers = [answer for answer in self.mapped('suggested_answer_ids')]
        if self.comment_count_as_answer:
            suggested_answers += [self.env['survey.question.answer']]

        count_data = dict.fromkeys(suggested_answers, 0)
        for line in user_input_lines:
            if line.suggested_answer_id in count_data\
               or (line.value_char_box and self.comment_count_as_answer):
                count_data[line.suggested_answer_id] += 1

        table_data = [{
            'value': _('Other (see comments)') if not sug_answer else sug_answer.value,
            'suggested_answer': sug_answer,
            'count': count_data[sug_answer]
            }
            for sug_answer in suggested_answers]
        graph_data = [{
            'text': _('Other (see comments)') if not sug_answer else sug_answer.value,
            'count': count_data[sug_answer]
            }
            for sug_answer in suggested_answers]

        return table_data, graph_data

    def _get_stats_graph_data_matrix(self, user_input_lines):
        suggested_answers = self.mapped('suggested_answer_ids')
        matrix_rows = self.mapped('matrix_row_ids')

        count_data = dict.fromkeys(itertools.product(matrix_rows, suggested_answers), 0)
        for line in user_input_lines:
            if line.matrix_row_id and line.suggested_answer_id:
                count_data[(line.matrix_row_id, line.suggested_answer_id)] += 1

        table_data = [{
            'row': row,
            'columns': [{
                'suggested_answer': sug_answer,
                'count': count_data[(row, sug_answer)]
            } for sug_answer in suggested_answers],
        } for row in matrix_rows]
        graph_data = [{
            'key': sug_answer.value,
            'values': [{
                'text': row.value,
                'count': count_data[(row, sug_answer)]
                }
                for row in matrix_rows
            ]
        } for sug_answer in suggested_answers]

        return table_data, graph_data

    def _get_stats_summary_data(self, user_input_lines):
        stats = {}
        if self.question_type in ['simple_choice', 'multiple_choice']:
            stats.update(self._get_stats_summary_data_choice(user_input_lines))
        elif self.question_type == 'numerical_box':
            stats.update(self._get_stats_summary_data_numerical(user_input_lines))

        if self.question_type in ['numerical_box', 'date', 'datetime']:
            stats.update(self._get_stats_summary_data_scored(user_input_lines))
        return stats

    def _get_stats_summary_data_choice(self, user_input_lines):
        right_inputs, partial_inputs = self.env['survey.user_input'], self.env['survey.user_input']
        right_answers = self.suggested_answer_ids.filtered(lambda label: label.is_correct)
        if self.question_type == 'multiple_choice':
            for user_input, lines in tools.groupby(user_input_lines, operator.itemgetter('user_input_id')):
                user_input_answers = self.env['survey.user_input.line'].concat(*lines).filtered(lambda l: l.answer_is_correct).mapped('suggested_answer_id')
                if user_input_answers and user_input_answers < right_answers:
                    partial_inputs += user_input
                elif user_input_answers:
                    right_inputs += user_input
        else:
            right_inputs = user_input_lines.filtered(lambda line: line.answer_is_correct).mapped('user_input_id')
        return {
            'right_answers': right_answers,
            'right_inputs_count': len(right_inputs),
            'partial_inputs_count': len(partial_inputs),
        }

    def _get_stats_summary_data_numerical(self, user_input_lines):
        all_values = user_input_lines.filtered(lambda line: not line.skipped).mapped('value_numerical_box')
        lines_sum = sum(all_values)
        return {
            'numerical_max': max(all_values, default=0),
            'numerical_min': min(all_values, default=0),
            'numerical_average': round(lines_sum / (len(all_values) or 1), 2),
        }

    def _get_stats_summary_data_scored(self, user_input_lines):
        return {
            'common_lines': collections.Counter(
                user_input_lines.filtered(lambda line: not line.skipped).mapped('value_%s' % self.question_type)
            ).most_common(5),
            'right_inputs_count': len(user_input_lines.filtered(lambda line: line.answer_is_correct).mapped('user_input_id'))
        }
示例#14
0
class Track(models.Model):
    _name = "event.track"
    _description = 'Event Track'
    _order = 'priority, date'
    _inherit = ['mail.thread', 'mail.activity.mixin', 'website.seo.metadata', 'website.published.mixin']

    @api.model
    def _get_default_stage_id(self):
        return self.env['event.track.stage'].search([], limit=1).id

    # description
    name = fields.Char('Title', required=True, translate=True)
    event_id = fields.Many2one('event.event', 'Event', required=True)
    active = fields.Boolean(default=True)
    user_id = fields.Many2one('res.users', 'Responsible', tracking=True, default=lambda self: self.env.user)
    company_id = fields.Many2one('res.company', related='event_id.company_id')
    tag_ids = fields.Many2many('event.track.tag', string='Tags')
    description = fields.Html(translate=html_translate, sanitize_attributes=False, sanitize_form=False)
    color = fields.Integer('Color')
    priority = fields.Selection([
        ('0', 'Low'), ('1', 'Medium'),
        ('2', 'High'), ('3', 'Highest')],
        'Priority', required=True, default='1')
    # management
    stage_id = fields.Many2one(
        'event.track.stage', string='Stage', ondelete='restrict',
        index=True, copy=False, default=_get_default_stage_id,
        group_expand='_read_group_stage_ids',
        required=True, tracking=True)
    legend_blocked = fields.Char(related='stage_id.legend_blocked',
        string='Kanban Blocked Explanation', readonly=True)
    legend_done = fields.Char(related='stage_id.legend_done',
        string='Kanban Valid Explanation', readonly=True)
    legend_normal = fields.Char(related='stage_id.legend_normal',
        string='Kanban Ongoing Explanation', readonly=True)
    kanban_state = fields.Selection([
        ('normal', 'Grey'),
        ('done', 'Green'),
        ('blocked', 'Red')], string='Kanban State',
        copy=False, default='normal', required=True,
        help="A track's kanban state indicates special situations affecting it:\n"
             " * Grey is the default situation\n"
             " * Red indicates something is preventing the progress of this track\n"
             " * Green indicates the track is ready to be pulled to the next stage")
    kanban_state_label = fields.Char(
        string='Kanban State Label', compute='_compute_kanban_state_label', store=True,
        tracking=True)
    partner_id = fields.Many2one('res.partner', 'Contact', help="Contact of the track, may be different from speaker.")
    # speaker information
    partner_name = fields.Char(
        string='Name', compute='_compute_partner_name',
        readonly=False, store=True, tracking=10,
        help='Speaker name is used for public display and may vary from contact name')
    partner_email = fields.Char(
        string='Email', compute='_compute_partner_email',
        readonly=False, store=True, tracking=20,
        help='Speaker email is used for public display and may vary from contact email')
    partner_phone = fields.Char(
        string='Phone', compute='_compute_partner_phone',
        readonly=False, store=True, tracking=30,
        help='Speaker phone is used for public display and may vary from contact phone')
    partner_biography = fields.Html(
        string='Biography', compute='_compute_partner_biography',
        readonly=False, store=True)
    partner_function = fields.Char(
        'Job Position', compute='_compute_partner_function',
        store=True, readonly=False)
    partner_company_name = fields.Char(
        'Company Name', compute='_compute_partner_company_name',
        readonly=False, store=True)
    partner_tag_line = fields.Char(
        'Tag Line', compute='_compute_partner_tag_line',
        help='Description of the partner (name, function and company name)')
    image = fields.Image(
        string="Speaker Photo", compute="_compute_partner_image",
        readonly=False, store=True,
        max_width=256, max_height=256)
    # contact information
    contact_email = fields.Char(
        string='Contact Email', compute='_compute_contact_email',
        readonly=False, store=True, tracking=20,
        help="Contact email is private and used internally")
    contact_phone = fields.Char(
        string='Contact Phone', compute='_compute_contact_phone',
        readonly=False, store=True, tracking=30,
        help="Contact phone is private and used internally")
    location_id = fields.Many2one('event.track.location', 'Location')
    # time information
    date = fields.Datetime('Track Date')
    date_end = fields.Datetime('Track End Date', compute='_compute_end_date', store=True)
    duration = fields.Float('Duration', default=0.5, help="Track duration in hours.")
    is_track_live = fields.Boolean(
        'Is Track Live', compute='_compute_track_time_data',
        help="Track has started and is ongoing")
    is_track_soon = fields.Boolean(
        'Is Track Soon', compute='_compute_track_time_data',
        help="Track begins soon")
    is_track_today = fields.Boolean(
        'Is Track Today', compute='_compute_track_time_data',
        help="Track begins today")
    is_track_upcoming = fields.Boolean(
        'Is Track Upcoming', compute='_compute_track_time_data',
        help="Track is not yet started")
    is_track_done = fields.Boolean(
        'Is Track Done', compute='_compute_track_time_data',
        help="Track is finished")
    track_start_remaining = fields.Integer(
        'Minutes before track starts', compute='_compute_track_time_data',
        help="Remaining time before track starts (seconds)")
    track_start_relative = fields.Integer(
        'Minutes compare to track start', compute='_compute_track_time_data',
        help="Relative time compared to track start (seconds)")
    # frontend description
    website_image = fields.Image(string="Website Image", max_width=1024, max_height=1024)
    website_image_url = fields.Char(
        string='Image URL', compute='_compute_website_image_url',
        compute_sudo=True, store=False)
    # wishlist / visitors management
    event_track_visitor_ids = fields.One2many(
        'event.track.visitor', 'track_id', string="Track Visitors",
        groups="event.group_event_user")
    is_reminder_on = fields.Boolean('Is Reminder On', compute='_compute_is_reminder_on')
    wishlist_visitor_ids = fields.Many2many(
        'website.visitor', string="Visitor Wishlist",
        compute="_compute_wishlist_visitor_ids", compute_sudo=True,
        search="_search_wishlist_visitor_ids",
        groups="event.group_event_user")
    wishlist_visitor_count = fields.Integer(
        string="# Wishlisted",
        compute="_compute_wishlist_visitor_ids", compute_sudo=True,
        groups="event.group_event_user")
    wishlisted_by_default = fields.Boolean(
        string='Always Wishlisted',
        help="""If set, the talk will be set as favorite for each attendee registered to the event.""")
    # Call to action
    website_cta = fields.Boolean('Magic Button',
                                 help="Display a Call to Action button to your Attendees while they watch your Track.")
    website_cta_title = fields.Char('Button Title')
    website_cta_url = fields.Char('Button Target URL')
    website_cta_delay = fields.Integer('Button appears')
    # time information for CTA
    is_website_cta_live = fields.Boolean(
        'Is CTA Live', compute='_compute_cta_time_data',
        help="CTA button is available")
    website_cta_start_remaining = fields.Integer(
        'Minutes before CTA starts', compute='_compute_cta_time_data',
        help="Remaining time before CTA starts (seconds)")

    @api.depends('name')
    def _compute_website_url(self):
        super(Track, self)._compute_website_url()
        for track in self:
            if track.id:
                track.website_url = '/event/%s/track/%s' % (slug(track.event_id), slug(track))

    # STAGES

    @api.depends('stage_id', 'kanban_state')
    def _compute_kanban_state_label(self):
        for track in self:
            if track.kanban_state == 'normal':
                track.kanban_state_label = track.stage_id.legend_normal
            elif track.kanban_state == 'blocked':
                track.kanban_state_label = track.stage_id.legend_blocked
            else:
                track.kanban_state_label = track.stage_id.legend_done

    # SPEAKER

    @api.depends('partner_id')
    def _compute_partner_name(self):
        for track in self:
            if track.partner_id and not track.partner_name:
                track.partner_name = track.partner_id.name

    @api.depends('partner_id')
    def _compute_partner_email(self):
        for track in self:
            if track.partner_id and not track.partner_email:
                track.partner_email = track.partner_id.email

    @api.depends('partner_id')
    def _compute_partner_phone(self):
        for track in self:
            if track.partner_id and not track.partner_phone:
                track.partner_phone = track.partner_id.phone

    @api.depends('partner_id')
    def _compute_partner_biography(self):
        for track in self:
            if not track.partner_biography:
                track.partner_biography = track.partner_id.website_description
            elif track.partner_id and is_html_empty(track.partner_biography) and \
                not is_html_empty(track.partner_id.website_description):
                track.partner_biography = track.partner_id.website_description

    @api.depends('partner_id')
    def _compute_partner_function(self):
        for track in self:
            if track.partner_id and not track.partner_function:
                track.partner_function = track.partner_id.function

    @api.depends('partner_id', 'partner_id.company_type')
    def _compute_partner_company_name(self):
        for track in self:
            if track.partner_id.company_type == 'company':
                track.partner_company_name = track.partner_id.name
            elif not track.partner_company_name:
                track.partner_company_name = track.partner_id.parent_id.name

    @api.depends('partner_name', 'partner_function', 'partner_company_name')
    def _compute_partner_tag_line(self):
        for track in self:
            if not track.partner_name:
                track.partner_tag_line = False
                continue

            tag_line = track.partner_name
            if track.partner_function:
                if track.partner_company_name:
                    tag_line = _('%(name)s, %(function)s at %(company)s',
                                 name=track.partner_name,
                                 function=track.partner_function,
                                 company=track.partner_company_name
                                )
                else:
                    tag_line = '%s, %s' % (track.partner_name, track.partner_function)
            elif track.partner_company_name:
                tag_line = _('%(name)s from %(company)s',
                             name=tag_line,
                             company=track.partner_company_name
                            )
            track.partner_tag_line = tag_line

    @api.depends('partner_id')
    def _compute_partner_image(self):
        for track in self:
            if not track.image:
                track.image = track.partner_id.image_256

    # CONTACT

    @api.depends('partner_id', 'partner_id.email')
    def _compute_contact_email(self):
        for track in self:
            if track.partner_id:
                track.contact_email = track.partner_id.email

    @api.depends('partner_id', 'partner_id.phone')
    def _compute_contact_phone(self):
        for track in self:
            if track.partner_id:
                track.contact_phone = track.partner_id.phone

    # TIME

    @api.depends('date', 'duration')
    def _compute_end_date(self):
        for track in self:
            if track.date:
                delta = timedelta(minutes=60 * track.duration)
                track.date_end = track.date + delta
            else:
                track.date_end = False


    # FRONTEND DESCRIPTION

    @api.depends('image', 'partner_id.image_256')
    def _compute_website_image_url(self):
        for track in self:
            if track.website_image:
                track.website_image_url = self.env['website'].image_url(track, 'website_image', size=1024)
            else:
                track.website_image_url = '/website_event_track/static/src/img/event_track_default_%d.jpeg' % (track.id % 2)

    # WISHLIST / VISITOR MANAGEMENT

    @api.depends('wishlisted_by_default', 'event_track_visitor_ids.visitor_id',
                 'event_track_visitor_ids.partner_id', 'event_track_visitor_ids.is_wishlisted',
                 'event_track_visitor_ids.is_blacklisted')
    @api.depends_context('uid')
    def _compute_is_reminder_on(self):
        current_visitor = self.env['website.visitor']._get_visitor_from_request(force_create=False)
        if self.env.user._is_public() and not current_visitor:
            for track in self:
                track.is_reminder_on = track.wishlisted_by_default
        else:
            if self.env.user._is_public():
                domain = [('visitor_id', '=', current_visitor.id)]
            elif current_visitor:
                domain = [
                    '|',
                    ('partner_id', '=', self.env.user.partner_id.id),
                    ('visitor_id', '=', current_visitor.id)
                ]
            else:
                domain = [('partner_id', '=', self.env.user.partner_id.id)]

            event_track_visitors = self.env['event.track.visitor'].sudo().search_read(
                expression.AND([
                    domain,
                    [('track_id', 'in', self.ids)]
                ]), fields=['track_id', 'is_wishlisted', 'is_blacklisted']
            )

            wishlist_map = {
                track_visitor['track_id'][0]: {
                    'is_wishlisted': track_visitor['is_wishlisted'],
                    'is_blacklisted': track_visitor['is_blacklisted']
                } for track_visitor in event_track_visitors
            }
            for track in self:
                if wishlist_map.get(track.id):
                    track.is_reminder_on = wishlist_map.get(track.id)['is_wishlisted'] or (track.wishlisted_by_default and not wishlist_map[track.id]['is_blacklisted'])
                else:
                    track.is_reminder_on = track.wishlisted_by_default

    @api.depends('event_track_visitor_ids.visitor_id', 'event_track_visitor_ids.is_wishlisted')
    def _compute_wishlist_visitor_ids(self):
        results = self.env['event.track.visitor']._read_group(
            [('track_id', 'in', self.ids), ('is_wishlisted', '=', True)],
            ['track_id', 'visitor_id:array_agg'],
            ['track_id']
        )
        visitor_ids_map = {result['track_id'][0]: result['visitor_id'] for result in results}
        for track in self:
            track.wishlist_visitor_ids = visitor_ids_map.get(track.id, [])
            track.wishlist_visitor_count = len(visitor_ids_map.get(track.id, []))

    def _search_wishlist_visitor_ids(self, operator, operand):
        if operator == "not in":
            raise NotImplementedError("Unsupported 'Not In' operation on track wishlist visitors")

        track_visitors = self.env['event.track.visitor'].sudo().search([
            ('visitor_id', operator, operand),
            ('is_wishlisted', '=', True)
        ])
        return [('id', 'in', track_visitors.track_id.ids)]

    # TIME

    @api.depends('date', 'date_end')
    def _compute_track_time_data(self):
        """ Compute start and remaining time for track itself. Do everything in
        UTC as we compute only time deltas here. """
        now_utc = utc.localize(fields.Datetime.now().replace(microsecond=0))
        for track in self:
            if not track.date:
                track.is_track_live = track.is_track_soon = track.is_track_today = track.is_track_upcoming = track.is_track_done = False
                track.track_start_relative = track.track_start_remaining = 0
                continue
            date_begin_utc = utc.localize(track.date, is_dst=False)
            date_end_utc = utc.localize(track.date_end, is_dst=False)
            track.is_track_live = date_begin_utc <= now_utc < date_end_utc
            track.is_track_soon = (date_begin_utc - now_utc).total_seconds() < 30*60 if date_begin_utc > now_utc else False
            track.is_track_today = date_begin_utc.date() == now_utc.date()
            track.is_track_upcoming = date_begin_utc > now_utc
            track.is_track_done = date_end_utc <= now_utc
            if date_begin_utc >= now_utc:
                track.track_start_relative = int((date_begin_utc - now_utc).total_seconds())
                track.track_start_remaining = track.track_start_relative
            else:
                track.track_start_relative = int((now_utc - date_begin_utc).total_seconds())
                track.track_start_remaining = 0

    @api.depends('date', 'date_end', 'website_cta', 'website_cta_delay')
    def _compute_cta_time_data(self):
        """ Compute start and remaining time for track itself. Do everything in
        UTC as we compute only time deltas here. """
        now_utc = utc.localize(fields.Datetime.now().replace(microsecond=0))
        for track in self:
            if not track.website_cta:
                track.is_website_cta_live = track.website_cta_start_remaining = False
                continue

            date_begin_utc = utc.localize(track.date, is_dst=False) + timedelta(minutes=track.website_cta_delay or 0)
            date_end_utc = utc.localize(track.date_end, is_dst=False)
            track.is_website_cta_live = date_begin_utc <= now_utc <= date_end_utc
            if date_begin_utc >= now_utc:
                td = date_begin_utc - now_utc
                track.website_cta_start_remaining = int(td.total_seconds())
            else:
                track.website_cta_start_remaining = 0

    # ------------------------------------------------------------
    # CRUD
    # ------------------------------------------------------------

    @api.model_create_multi
    def create(self, vals_list):
        for values in vals_list:
            if values.get('website_cta_url'):
                values['website_cta_url'] = self.env['res.partner']._clean_website(values['website_cta_url'])

        tracks = super(Track, self).create(vals_list)

        for track in tracks:
            email_values = {} if self.env.user.email else {'email_from': self.env.company.catchall_formatted}
            track.event_id.message_post_with_view(
                'website_event_track.event_track_template_new',
                values={
                    'track': track,
                    'is_html_empty': is_html_empty,
                },
                subtype_id=self.env.ref('website_event_track.mt_event_track').id,
                **email_values,
            )
            track._synchronize_with_stage(track.stage_id)

        return tracks

    def write(self, vals):
        if vals.get('website_cta_url'):
            vals['website_cta_url'] = self.env['res.partner']._clean_website(vals['website_cta_url'])
        if 'stage_id' in vals and 'kanban_state' not in vals:
            vals['kanban_state'] = 'normal'
        if vals.get('stage_id'):
            stage = self.env['event.track.stage'].browse(vals['stage_id'])
            self._synchronize_with_stage(stage)
        res = super(Track, self).write(vals)
        return res

    @api.model
    def _read_group_stage_ids(self, stages, domain, order):
        """ Always display all stages """
        return stages.search([], order=order)

    def _synchronize_with_stage(self, stage):
        if stage.is_fully_accessible:
            self.is_published = True
        elif stage.is_cancel:
            self.is_published = False

    # ------------------------------------------------------------
    # MESSAGING
    # ------------------------------------------------------------

    def _message_get_default_recipients(self):
        return {
            track.id: {
                'partner_ids': [],
                'email_to': track.contact_email or track.partner_email,
                'email_cc': False
            } for track in self
        }

    def _message_get_suggested_recipients(self):
        recipients = super(Track, self)._message_get_suggested_recipients()
        for track in self:
            if track.partner_id:
                if track.partner_id not in recipients:
                    track._message_add_suggested_recipient(recipients, partner=track.partner_id, reason=_('Contact'))
            else:
                #  Priority: contact information then speaker information
                if track.contact_email and track.contact_email != track.partner_id.email:
                    track._message_add_suggested_recipient(recipients, email=track.contact_email, reason=_('Contact Email'))
                if not track.contact_email and track.partner_email and track.partner_email != track.partner_id.email:
                    track._message_add_suggested_recipient(recipients, email=track.partner_email, reason=_('Speaker Email'))
        return recipients

    def _message_post_after_hook(self, message, msg_vals):
        #  OVERRIDE
        #  If no partner is set on track when sending a message, then we create one from suggested contact selected.
        #  If one or more have been created from chatter (Suggested Recipients) we search for the expected one and write the partner_id on track.
        if msg_vals.get('partner_ids') and not self.partner_id:
            #  Contact(s) created from chatter set on track : we verify if at least one is the expected contact
            #  linked to the track. (created from contact_email if any, then partner_email if any)
            main_email = self.contact_email or self.partner_email
            if main_email:
                new_partner = message.partner_ids.filtered(lambda partner: partner.email == main_email)
                if new_partner:
                    main_email_string = 'contact_email' if self.contact_email else 'partner_email'
                    self.search([
                        ('partner_id', '=', False),
                        (main_email_string, '=', new_partner.email),
                        ('stage_id.is_cancel', '=', False),
                    ]).write({'partner_id': new_partner.id})
        return super(Track, self)._message_post_after_hook(message, msg_vals)

    def _track_template(self, changes):
        res = super(Track, self)._track_template(changes)
        track = self[0]
        if 'stage_id' in changes and track.stage_id.mail_template_id:
            res['stage_id'] = (track.stage_id.mail_template_id, {
                'composition_mode': 'comment',
                'auto_delete_message': True,
                'subtype_id': self.env['ir.model.data']._xmlid_to_res_id('mail.mt_note'),
                'email_layout_xmlid': 'mail.mail_notification_light'
            })
        return res

    def _track_subtype(self, init_values):
        self.ensure_one()
        if 'kanban_state' in init_values and self.kanban_state == 'blocked':
            return self.env.ref('website_event_track.mt_track_blocked')
        elif 'kanban_state' in init_values and self.kanban_state == 'done':
            return self.env.ref('website_event_track.mt_track_ready')
        return super(Track, self)._track_subtype(init_values)

    # ------------------------------------------------------------
    # ACTION
    # ------------------------------------------------------------

    def open_track_speakers_list(self):
        return {
            'name': _('Speakers'),
            'domain': [('id', 'in', self.mapped('partner_id').ids)],
            'view_mode': 'kanban,form',
            'res_model': 'res.partner',
            'view_id': False,
            'type': 'ir.actions.act_window',
        }

    def get_backend_menu_id(self):
        return self.env.ref('event.event_main_menu').id

    # ------------------------------------------------------------
    # TOOLS
    # ------------------------------------------------------------

    def _get_event_track_visitors(self, force_create=False):
        self.ensure_one()

        force_visitor_create = self.env.user._is_public()
        visitor_sudo = self.env['website.visitor']._get_visitor_from_request(force_create=force_visitor_create)
        if visitor_sudo:
            visitor_sudo._update_visitor_last_visit()

        if self.env.user._is_public():
            domain = [('visitor_id', '=', visitor_sudo.id)]
        elif visitor_sudo:
            domain = [
                '|',
                ('partner_id', '=', self.env.user.partner_id.id),
                ('visitor_id', '=', visitor_sudo.id)
            ]
        else:
            domain = [('partner_id', '=', self.env.user.partner_id.id)]

        track_visitors = self.env['event.track.visitor'].sudo().search(
            expression.AND([domain, [('track_id', 'in', self.ids)]])
        )
        missing = self - track_visitors.track_id
        if missing and force_create:
            track_visitors += self.env['event.track.visitor'].sudo().create([{
                'visitor_id': visitor_sudo.id,
                'partner_id': self.env.user.partner_id.id if not self.env.user._is_public() else False,
                'track_id': track.id,
            } for track in missing])

        return track_visitors

    def _get_track_suggestions(self, restrict_domain=None, limit=None):
        """ Returns the next tracks suggested after going to the current one
        given by self. Tracks always belong to the same event.

        Heuristic is

          * live first;
          * then ordered by start date, finished being sent to the end;
          * wishlisted (manually or by default);
          * tag matching with current track;
          * location matching with current track;
          * finally a random to have an "equivalent wave" randomly given;

        :param restrict_domain: an additional domain to restrict candidates;
        :param limit: number of tracks to return;
        """
        self.ensure_one()

        base_domain = [
            '&',
            ('event_id', '=', self.event_id.id),
            ('id', '!=', self.id),
        ]
        if restrict_domain:
            base_domain = expression.AND([
                base_domain,
                restrict_domain
            ])

        track_candidates = self.search(base_domain, limit=None, order='date asc')
        if not track_candidates:
            return track_candidates

        track_candidates = track_candidates.sorted(
            lambda track:
                (track.is_published,
                 track.track_start_remaining == 0  # First get the tracks that started less than 10 minutes ago ...
                 and track.track_start_relative < (10 * 60)
                 and not track.is_track_done,  # ... AND not finished
                 track.track_start_remaining > 0,  # Then the one that will begin later (the sooner come first)
                 -1 * track.track_start_remaining,
                 track.is_reminder_on,
                 not track.wishlisted_by_default,
                 len(track.tag_ids & self.tag_ids),
                 track.location_id == self.location_id,
                 randint(0, 20),
                ), reverse=True
        )

        return track_candidates[:limit]
示例#15
0
class Track(models.Model):
    _name = "event.track"
    _description = 'Event Track'
    _order = 'priority, date'
    _inherit = [
        'mail.thread', 'mail.activity.mixin', 'website.seo.metadata',
        'website.published.mixin'
    ]

    @api.model
    def _get_default_stage_id(self):
        return self.env['event.track.stage'].search([], limit=1).id

    name = fields.Char('Title', required=True, translate=True)
    active = fields.Boolean(default=True)
    user_id = fields.Many2one('res.users',
                              'Responsible',
                              tracking=True,
                              default=lambda self: self.env.user)
    company_id = fields.Many2one('res.company', related='event_id.company_id')
    partner_id = fields.Many2one('res.partner', 'Speaker')
    partner_name = fields.Char('Name')
    partner_email = fields.Char('Email')
    partner_phone = fields.Char('Phone')
    partner_biography = fields.Html('Biography')
    tag_ids = fields.Many2many('event.track.tag', string='Tags')
    stage_id = fields.Many2one('event.track.stage',
                               string='Stage',
                               ondelete='restrict',
                               index=True,
                               copy=False,
                               default=_get_default_stage_id,
                               group_expand='_read_group_stage_ids',
                               required=True,
                               tracking=True)
    kanban_state = fields.Selection(
        [('normal', 'Grey'), ('done', 'Green'), ('blocked', 'Red')],
        string='Kanban State',
        copy=False,
        default='normal',
        required=True,
        tracking=True,
        help=
        "A track's kanban state indicates special situations affecting it:\n"
        " * Grey is the default situation\n"
        " * Red indicates something is preventing the progress of this track\n"
        " * Green indicates the track is ready to be pulled to the next stage")
    description = fields.Html(translate=html_translate,
                              sanitize_attributes=False)
    date = fields.Datetime('Track Date')
    date_end = fields.Datetime('Track End Date',
                               compute='_compute_end_date',
                               store=True)
    duration = fields.Float('Duration',
                            default=1.5,
                            help="Track duration in hours.")
    location_id = fields.Many2one('event.track.location', 'Room')
    event_id = fields.Many2one('event.event', 'Event', required=True)
    color = fields.Integer('Color Index')
    priority = fields.Selection([('0', 'Low'), ('1', 'Medium'), ('2', 'High'),
                                 ('3', 'Highest')],
                                'Priority',
                                required=True,
                                default='1')
    image = fields.Image("Image",
                         related='partner_id.image_128',
                         store=True,
                         readonly=False)

    @api.depends('name')
    def _compute_website_url(self):
        super(Track, self)._compute_website_url()
        for track in self:
            if track.id:
                track.website_url = '/event/%s/track/%s' % (slug(
                    track.event_id), slug(track))

    @api.onchange('partner_id')
    def _onchange_partner_id(self):
        if self.partner_id:
            self.partner_name = self.partner_id.name
            self.partner_email = self.partner_id.email
            self.partner_phone = self.partner_id.phone
            self.partner_biography = self.partner_id.website_description

    @api.depends('date', 'duration')
    def _compute_end_date(self):
        for track in self:
            if track.date:
                delta = timedelta(minutes=60 * track.duration)
                track.date_end = track.date + delta
            else:
                track.date_end = False

    @api.model
    def create(self, vals):
        track = super(Track, self).create(vals)

        track.event_id.message_post_with_view(
            'website_event_track.event_track_template_new',
            values={'track': track},
            subject=track.name,
            subtype_id=self.env.ref('website_event_track.mt_event_track').id,
        )

        return track

    def write(self, vals):
        if 'stage_id' in vals and 'kanban_state' not in vals:
            vals['kanban_state'] = 'normal'
        res = super(Track, self).write(vals)
        if vals.get('partner_id'):
            self.message_subscribe([vals['partner_id']])
        return res

    @api.model
    def _read_group_stage_ids(self, stages, domain, order):
        """ Always display all stages """
        return stages.search([], order=order)

    def _track_template(self, changes):
        res = super(Track, self)._track_template(changes)
        track = self[0]
        if 'stage_id' in changes and track.stage_id.mail_template_id:
            res['stage_id'] = (track.stage_id.mail_template_id, {
                'composition_mode':
                'comment',
                'auto_delete_message':
                True,
                'subtype_id':
                self.env['ir.model.data'].xmlid_to_res_id('mail.mt_note'),
                'email_layout_xmlid':
                'mail.mail_notification_light'
            })
        return res

    def _track_subtype(self, init_values):
        self.ensure_one()
        if 'kanban_state' in init_values and self.kanban_state == 'blocked':
            return self.env.ref('website_event_track.mt_track_blocked')
        elif 'kanban_state' in init_values and self.kanban_state == 'done':
            return self.env.ref('website_event_track.mt_track_ready')
        return super(Track, self)._track_subtype(init_values)

    def _message_get_suggested_recipients(self):
        recipients = super(Track, self)._message_get_suggested_recipients()
        for track in self:
            if track.partner_email and track.partner_email != track.partner_id.email:
                track._message_add_suggested_recipient(
                    recipients,
                    email=track.partner_email,
                    reason=_('Speaker Email'))
        return recipients

    def _message_post_after_hook(self, message, msg_vals):
        if self.partner_email and not self.partner_id:
            # we consider that posting a message with a specified recipient (not a follower, a specific one)
            # on a document without customer means that it was created through the chatter using
            # suggested recipients. This heuristic allows to avoid ugly hacks in JS.
            new_partner = message.partner_ids.filtered(
                lambda partner: partner.email == self.partner_email)
            if new_partner:
                self.search([
                    ('partner_id', '=', False),
                    ('partner_email', '=', new_partner.email),
                    ('stage_id.is_cancel', '=', False),
                ]).write({'partner_id': new_partner.id})
        return super(Track, self)._message_post_after_hook(message, msg_vals)

    def open_track_speakers_list(self):
        return {
            'name': _('Speakers'),
            'domain': [('id', 'in', self.mapped('partner_id').ids)],
            'view_mode': 'kanban,form',
            'res_model': 'res.partner',
            'view_id': False,
            'type': 'ir.actions.act_window',
        }
示例#16
0
class GithubOrganization(models.Model):
    _name = "github.organization"
    _inherit = ["abstract.github.model"]
    _order = "name"
    _description = "Github organization"

    _github_type = "organization"
    _github_login_field = "login"

    # Columns Section
    name = fields.Char(string="Organization Name", required=True, readonly=True)

    image = fields.Image(string="Image", readonly=True)

    description = fields.Char(string="Description", readonly=True)

    email = fields.Char(string="Email", readonly=True)

    website_url = fields.Char(string="Website URL", readonly=True)

    location = fields.Char(string="Location", readonly=True)

    ignored_repository_names = fields.Text(
        string="Ignored Repositories",
        help="Set here repository names"
        " you want to ignore. One repository per line."
        " If set, the repositories will be created, but branches"
        " synchronization and source code download will be disabled."
        " Exemple:\n"
        "purchase-workflow\nOCB\nOpenUpgrade\n",
    )

    member_ids = fields.Many2many(
        string="Members",
        comodel_name="res.partner",
        relation="github_organization_partner_rel",
        column1="organization_id",
        column2="partner_id",
        readonly=True,
    )

    member_qty = fields.Integer(
        string="Number of Members", compute="_compute_member_qty", store=True
    )

    repository_ids = fields.One2many(
        string="Repositories",
        comodel_name="github.repository",
        ondelete="cascade",
        inverse_name="organization_id",
        readonly=True,
    )

    repository_qty = fields.Integer(
        string="Number of Repositories", compute="_compute_repository_qty", store=True
    )

    team_ids = fields.One2many(
        string="Teams",
        comodel_name="github.team",
        inverse_name="organization_id",
        readonly=True,
    )

    team_qty = fields.Integer(
        string="Number of Teams", compute="_compute_team_qty", store=True
    )

    organization_serie_ids = fields.One2many(
        string="Organization Series",
        comodel_name="github.organization.serie",
        inverse_name="organization_id",
    )

    organization_serie_qty = fields.Integer(
        string="Number of Series", store=True, compute="_compute_organization_serie_qty"
    )

    coverage_url_pattern = fields.Char(string="Coverage URL Pattern")

    ci_url_pattern = fields.Char(string="CI URL Pattern")

    # Overloadable Section
    @api.model
    def get_conversion_dict(self):
        res = super().get_conversion_dict()
        res.update(
            {
                "name": "name",
                "description": "description",
                "location": "location",
                "email": "email",
                "website_url": "blog",
            }
        )
        return res

    @api.model
    def get_odoo_data_from_github(self, data):
        res = super().get_odoo_data_from_github(data)
        if "avatar_url" in data:
            res.update({"image": self.get_base64_image_from_github(data["avatar_url"])})
        return res

    def full_update(self):
        self.button_sync_member()
        self.button_sync_repository()
        self.button_sync_team()

    @api.model
    def cron_update_organization_team(self):
        organizations = self.search([])
        organizations.full_update()
        organizations.mapped("team_ids").full_update()
        return True

    # Compute Section
    @api.depends("member_ids", "member_ids.organization_ids")
    def _compute_member_qty(self):
        for organization in self:
            organization.member_qty = len(organization.member_ids)

    @api.depends("repository_ids.organization_id")
    def _compute_repository_qty(self):
        for organization in self:
            organization.repository_qty = len(organization.repository_ids)

    @api.depends("team_ids.organization_id")
    def _compute_team_qty(self):
        for organization in self:
            organization.team_qty = len(organization.team_ids)

    @api.depends("organization_serie_ids.organization_id")
    def _compute_organization_serie_qty(self):
        for organization in self:
            organization.organization_serie_qty = len(
                organization.organization_serie_ids
            )

    # Action section
    def button_sync_member(self):
        github_member = self.get_github_connector("organization_members")
        partner_obj = self.env["res.partner"]
        for organization in self:
            member_ids = []
            for data in github_member.list([organization.github_login]):
                partner = partner_obj.get_from_id_or_create(data)
                member_ids.append(partner.id)
            organization.member_ids = member_ids

    def button_sync_repository(self):
        repository_obj = self.env["github.repository"]
        github_repo = self.get_github_connector("organization_repositories")
        for organization in self:
            repository_ids = []
            for data in github_repo.list([organization.github_login]):
                repository = repository_obj.get_from_id_or_create(data)
                repository_ids.append(repository.id)
            organization.repository_ids = repository_ids

    def button_sync_team(self):
        team_obj = self.env["github.team"]
        github_team = self.get_github_connector("organization_teams")
        for organization in self:
            team_ids = []
            for data in github_team.list([organization.github_login]):
                team = team_obj.get_from_id_or_create(
                    data, {"organization_id": organization.id}
                )
                team_ids.append(team.id)
            organization.team_ids = team_ids

    def action_github_repository(self):
        self.ensure_one()
        action = self.env.ref("github_connector.action_github_repository").read()[0]
        action["context"] = dict(self.env.context)
        action["context"].pop("group_by", None)
        action["context"]["search_default_organization_id"] = self.id
        return action

    def action_github_team(self):
        self.ensure_one()
        action = self.env.ref("github_connector.action_github_team").read()[0]
        action["context"] = dict(self.env.context)
        action["context"].pop("group_by", None)
        action["context"]["search_default_organization_id"] = self.id
        return action

    def action_res_partner(self):
        self.ensure_one()
        action = self.env.ref("github_connector.action_res_partner").read()[0]
        action["context"] = dict(self.env.context)
        action["context"].pop("group_by", None)
        action["context"]["search_default_organization_ids"] = self.id
        return action
示例#17
0
class MailGroup(models.Model):
    """This model represents a mailing list.

    Users send emails to an alias to create new group messages or reply to existing
    group messages. Moderation can be activated on groups. In that case email have to
    be validated or rejected.
    """
    _name = 'mail.group'
    _description = 'Mailing List'
    # TDE CHECK: use blaclist mixin
    _inherit = ['mail.alias.mixin']

    @api.model
    def default_get(self, fields):
        res = super(MailGroup, self).default_get(fields)
        if not res.get('alias_contact') and (not fields or 'alias_contact' in fields):
            res['alias_contact'] = 'everyone' if res.get('access_mode') == 'public' else 'followers'
        return res

    def _get_default_image(self):
        image_path = modules.get_resource_path('mail', 'static/src/img', 'groupdefault.png')
        with tools.file_open(image_path, 'rb') as fd:
            return base64.b64encode(fd.read())

    name = fields.Char('Name', required=True)
    alias_name = fields.Char('Alias Name', copy=False, related='alias_id.alias_name', readonly=False)
    description = fields.Text('Description')
    image_128 = fields.Image('Image', max_width=128, max_height=128, default=_get_default_image)
    # Messages
    mail_group_message_ids = fields.One2many('mail.group.message', 'mail_group_id', string='Pending Messages')
    mail_group_message_last_month_count = fields.Integer('Messages Per Month', compute='_compute_mail_group_message_last_month_count')
    mail_group_message_moderation_count = fields.Integer('Messages Count', help='Messages that need an action', compute='_compute_mail_group_message_moderation_count')
    # Members
    member_ids = fields.One2many('mail.group.member', 'mail_group_id', string='Members')
    member_partner_ids = fields.Many2many('res.partner', string='Partners Member', compute='_compute_member_partner_ids', search='_search_member_partner_ids')
    member_count = fields.Integer('Members Count', compute='_compute_member_count')
    # Moderation
    is_moderator = fields.Boolean(string='Moderator', help='Current user is a moderator of the group', compute='_compute_is_moderator')
    moderation = fields.Boolean(string='Moderate this group')
    moderation_rule_count = fields.Integer(string='Moderated emails count', compute='_compute_moderation_rule_count')
    moderation_rule_ids = fields.One2many('mail.group.moderation', 'mail_group_id', string='Moderated Emails')
    moderator_ids = fields.Many2many('res.users', 'mail_group_moderator_rel', string='Moderators',
                                     domain=lambda self: [('groups_id', 'in', self.env.ref('base.group_user').id)])
    moderation_notify = fields.Boolean(
        string='Automatic notification',
        help='People receive an automatic notification about their message being waiting for moderation.')
    moderation_notify_msg = fields.Text(string='Notification message')
    moderation_guidelines = fields.Boolean(
        string='Send guidelines to new subscribers',
        help='Newcomers on this moderated group will automatically receive the guidelines.')
    moderation_guidelines_msg = fields.Text(string='Guidelines')
    # ACLs
    access_mode = fields.Selection([
        ('public', 'Everyone'),
        ('members', 'Members only'),
        ('groups', 'Selected group of users'),
        ], string='Privacy', required=True, default='public')
    access_group_id = fields.Many2one('res.groups', string='Authorized Group',
                                      default=lambda self: self.env.ref('base.group_user'))
    # UI
    can_manage_members = fields.Boolean('Can Manage', help='Can manage the members', compute='_compute_can_manage_members')

    @api.depends('mail_group_message_ids.create_date', 'mail_group_message_ids.moderation_status')
    def _compute_mail_group_message_last_month_count(self):
        month_date = datetime.today() - relativedelta.relativedelta(months=1)
        messages_data = self.env['mail.group.message'].read_group([
            ('create_date', '>=', fields.Datetime.to_string(month_date)),
            ('moderation_status', '=', 'accepted'),
        ], ['mail_group_id'], ['mail_group_id'])

        # { mail_discusison_id: number_of_mail_group_message_last_month_count }
        messages_data = {
            message['mail_group_id'][0]: message['mail_group_id_count']
            for message in messages_data
        }

        for group in self:
            group.mail_group_message_last_month_count = messages_data.get(group.id, 0)

    @api.depends('mail_group_message_ids.moderation_status')
    def _compute_mail_group_message_moderation_count(self):
        results = self.env['mail.group.message'].read_group(
            [('moderation_status', '=', 'pending_moderation')],
            ['mail_group_id'],
            ['mail_group_id'],
        )
        result_per_group = {
            result['mail_group_id'][0]: result['mail_group_id_count']
            for result in results
        }

        for group in self:
            group.mail_group_message_moderation_count = result_per_group.get(group.id)

    @api.depends('member_ids')
    def _compute_member_count(self):
        for group in self:
            group.member_count = len(group.member_ids)

    @api.depends('member_ids')
    def _compute_member_partner_ids(self):
        for group in self:
            group.member_partner_ids = group.member_ids.partner_id

    def _search_member_partner_ids(self, operator, operand):
        return [(
            'member_ids',
            'in',
            self.env['mail.group.member'].sudo()._search([
                ('partner_id', operator, operand)
            ])
        )]

    @api.depends('moderator_ids')
    @api.depends_context('uid')
    def _compute_is_moderator(self):
        for group in self:
            group.is_moderator = self.env.user.id in group.moderator_ids.ids

    @api.depends('moderation_rule_ids')
    def _compute_moderation_rule_count(self):
        for group in self:
            group.moderation_rule_count = len(group.moderation_rule_ids)

    @api.depends('is_moderator')
    @api.depends_context('uid')
    def _compute_can_manage_members(self):
        is_admin = self.env.user.has_group('mail_group.group_mail_group_manager')
        for group in self:
            group.can_manage_members = is_admin or group.is_moderator

    @api.onchange('access_mode')
    def _onchange_access_mode(self):
        if self.access_mode == 'public':
            self.alias_contact = 'everyone'
        else:
            self.alias_contact = 'followers'

    @api.onchange('moderation')
    def _onchange_moderation(self):
        if self.moderation and self.env.user not in self.moderator_ids:
            self.moderator_ids |= self.env.user

    # CONSTRAINTS

    @api.constrains('moderator_ids')
    def _check_moderator_email(self):
        if any(not moderator.email for group in self for moderator in group.moderator_ids):
            raise ValidationError(_('Moderators must have an email address.'))

    @api.constrains('moderation_notify', 'moderation_notify_msg')
    def _check_moderation_notify(self):
        if any(group.moderation_notify and not group.moderation_notify_msg for group in self):
            raise ValidationError(_('The notification message is missing.'))

    @api.constrains('moderation_guidelines', 'moderation_guidelines_msg')
    def _check_moderation_guidelines(self):
        if any(group.moderation_guidelines and not group.moderation_guidelines_msg for group in self):
            raise ValidationError(_('The guidelines description is missing.'))

    @api.constrains('moderator_ids', 'moderation')
    def _check_moderator_existence(self):
        if any(not group.moderator_ids for group in self if group.moderation):
            raise ValidationError(_('Moderated group must have moderators.'))

    @api.constrains('access_mode', 'access_group_id')
    def _check_access_mode(self):
        if any(group.access_mode == 'groups' and not group.access_group_id for group in self):
            raise ValidationError(_('The "Authorized Group" is missing.'))

    def _alias_get_creation_values(self):
        """Return the default values for the automatically created alias."""
        values = super(MailGroup, self)._alias_get_creation_values()
        values['alias_model_id'] = self.env['ir.model']._get('mail.group').id
        values['alias_force_thread_id'] = self.id
        values['alias_defaults'] = literal_eval(self.alias_defaults or '{}')
        return values

    # ------------------------------------------------------------
    # MAILING
    # ------------------------------------------------------------

    def _alias_get_error_message(self, message, message_dict, alias):
        self.ensure_one()

        if alias.alias_contact == 'followers':
            # Members only
            author_id = message_dict.get('author_id', None)
            email_from = message_dict.get('email_from')
            if not self._find_member(email_from, author_id):
                return _('Only members can send email to the mailing list.')
            # Skip the verification because the partner is in the member list
            return

        return super(MailGroup, self)._alias_get_error_message(message, message_dict, alias)

    @api.model
    def message_new(self, msg_dict, custom_values=None):
        """Add the method to make the mail gateway flow work with this model."""
        return

    @api.model
    def message_update(self, msg_dict, update_vals=None):
        """Add the method to make the mail gateway flow work with this model."""
        return

    @api.returns('mail.message', lambda value: value.id)
    def message_post(self, body='', subject=None, email_from=None, author_id=None, **kwargs):
        """ Custom posting process. This model does not inherit from ``mail.thread``
        but uses mailgateway so few methods should be defined.

        This custom posting process works as follow

          * create a ``mail.message`` based on incoming email;
          * create linked ``mail.group.message`` that encapsulates message in a
            format used in mail groups;
          * apply moderation rules;

        :return message: newly-created mail.message
        """
        self.ensure_one()
        # First create the <mail.message>
        Mailthread = self.env['mail.thread']
        values = dict((key, val) for key, val in kwargs.items() if key in self.env['mail.message']._fields)
        author_id, email_from = Mailthread._message_compute_author(author_id, email_from, raise_exception=True)
        values.update({
            'author_id': author_id,
            'body': body or '',
            'email_from': email_from,
            'model': self._name,
            'partner_ids': [],
            'reply_to': self.env['mail.message']._get_reply_to(values),
            'res_id': self.id,
            'subject': subject,
        })
        # ensure message ID so that replies go to the right thread
        if not values.get('message_id'):
            values['message_id'] = generate_tracking_message_id('%s-mail.group' % self.id)

        attachments = kwargs.get('attachments') or []
        attachment_ids = kwargs.get('attachment_ids') or []
        attachement_values = Mailthread._message_post_process_attachments(attachments, attachment_ids, values)
        values.update(attachement_values)

        mail_message = Mailthread._message_create(values)

        # Find the <mail.group.message> parent
        group_message_parent_id = False
        if mail_message.parent_id:
            group_message_parent = self.env['mail.group.message'].search(
                [('mail_message_id', '=', mail_message.parent_id.id)])
            group_message_parent_id = group_message_parent.id if group_message_parent else False

        moderation_status = 'pending_moderation' if self.moderation else 'accepted'

        # Create the group message associated
        group_message = self.env['mail.group.message'].create({
            'mail_group_id': self.id,
            'mail_message_id': mail_message.id,
            'moderation_status': moderation_status,
            'group_message_parent_id': group_message_parent_id,
        })

        # Check the moderation rule to determine if we should accept or reject the email
        email_normalized = email_normalize(email_from)
        moderation_rule = self.env['mail.group.moderation'].search([
            ('mail_group_id', '=', self.id),
            ('email', '=', email_normalized),
        ], limit=1)

        if not self.moderation:
            self._notify_members(group_message)

        elif moderation_rule and moderation_rule.status == 'allow':
            group_message.action_moderate_accept()

        elif moderation_rule and moderation_rule.status == 'ban':
            group_message.action_moderate_reject()

        elif self.moderation_notify:
            self.env['mail.mail'].sudo().create({
                'author_id': self.env.user.partner_id.id,
                'auto_delete': True,
                'body_html': group_message.mail_group_id.moderation_notify_msg,
                'email_from': self.env.user.company_id.catchall_formatted or self.env.user.company_id.email_formatted,
                'email_to': email_from,
                'subject': 'Re: %s' % (subject or ''),
                'state': 'outgoing'
            })

        return mail_message

    def action_send_guidelines(self, members=None):
        """ Send guidelines to given members. """
        self.ensure_one()

        if not self.env.is_admin() and not self.is_moderator:
            raise UserError(_('Only an administrator or a moderator can send guidelines to group members.'))

        if not self.moderation_guidelines_msg:
            raise UserError(_('The guidelines description is empty.'))

        template = self.env.ref('mail_group.mail_group_send_guidelines', raise_if_not_found=False)
        if not template:
            raise UserError(_('View "mail_group.mail_group_send_guidelines" was not found. No email has been sent. Please contact an administrator to fix this issue.'))

        banned_emails = self.env['mail.group.moderation'].sudo().search([
            ('status', '=', 'ban'),
            ('mail_group_id', '=', self.id),
        ]).mapped('email')

        if members is None:
            members = self.member_ids
        members = members.filtered(lambda member: member.email_normalized not in banned_emails)

        mails_values = []
        for member in members:
            company = member.partner_id.company_id or self.env.company
            mails_values.append({
                'author_id': self.env.user.partner_id.id,
                'auto_delete': True,
                'body_html': template._render({'group': self, 'member': member}, engine='ir.qweb', minimal_qcontext=True),
                'email_from': company.catchall_formatted or company.email_formatted,
                'email_to': member.email,
                'model': 'mail.group',
                'res_id': self.id,
                'subject': _('Guidelines of group %s', self.name),
            })

        mails = self.env['mail.mail'].sudo().create(mails_values)

        _logger.info('Send guidelines to %i members', len(mails))

    def _notify_members(self, message):
        """Send the given message to all members of the mail group (except the author)."""
        self.ensure_one()

        if message.mail_group_id != self:
            raise UserError(_('The group of the message do not match.'))

        if not message.mail_message_id.reply_to:
            _logger.error('The alias or the catchall domain is missing, group might not work properly.')

        base_url = self.get_base_url()
        body = self.env['mail.render.mixin']._replace_local_links(message.body)
        access_token = self._generate_group_access_token()
        mail_values = []
        for batch_members in tools.split_every(GROUP_SEND_BATCH_SIZE, self.member_ids):
            for member in batch_members:
                if member.email_normalized == message.email_from_normalized:
                    # Do not send the email to his author
                    continue

                # SMTP headers related to the subscription
                email_url_encoded = urls.url_quote(member.email_normalized)
                headers = {
                    'List-Archive': f'<{base_url}/groups/{slug(self)}>',
                    'List-Subscribe': f'<{base_url}/groups?email={email_url_encoded}>',
                    'List-Unsubscribe': f'<{base_url}/groups?unsubscribe&email={email_url_encoded}>',
                }

                # Add the footer (member specific) in the body
                template_values = {
                    'mailto': f'{self.alias_name}@{self.alias_domain}',
                    'group_url': f'{base_url}/groups/{slug(self)}',
                    'unsub_url':  f'{base_url}/groups?unsubscribe&group_id={self.id}&token={access_token}'
                }
                template = self.env.ref('mail_group.mail_group_footer')
                footer = template._render(template_values, engine='ir.qweb', minimal_qcontext=True)
                member_body = tools.append_content_to_html(body, footer)

                mail_values.append({
                    'auto_delete': True,
                    'attachment_ids': message.attachment_ids.ids,
                    'body_html': member_body,
                    'email_from': message.email_from,
                    'email_to': member.email,
                    'headers': json.dumps(headers),
                    'mail_message_id': message.mail_message_id.id,
                    'message_id': message.mail_message_id.message_id,
                    'model': 'mail.group',
                    'reply_to': message.mail_message_id.reply_to,
                    'res_id': self.id,
                    'subject': message.subject,
                })

            if mail_values:
                self.env['mail.mail'].sudo().create(mail_values)

    @api.model
    def _cron_notify_moderators(self):
        moderated_groups = self.env['mail.group'].search([('moderation', '=', True)])
        return moderated_groups._notify_moderators()

    def _notify_moderators(self):
        """Push a notification (Inbox / Email) to the moderators whose an action is waiting."""
        template = self.env.ref('mail_group.mail_group_notify_moderation', raise_if_not_found=False)
        if not template:
            _logger.warning('Template "mail_group.mail_group_notify_moderation" was not found. Cannot send reminder notifications.')
            return

        results = self.env['mail.group.message'].read_group(
            [('moderation_status', '=', 'pending_moderation'),
             ('mail_group_id', 'in', self.ids)
            ],
            ['mail_group_id'],
            ['mail_group_id'],
        )
        groups = self.browse([result['mail_group_id'][0] for result in results])
        moderators_to_notify = groups.mapped('moderator_ids')

        MailThread = self.env['mail.thread'].with_context(mail_notify_author=True)
        for moderator in moderators_to_notify:
            body = template._render({'record': moderator.partner_id}, engine='ir.qweb', minimal_qcontext=True)
            email_from = moderator.company_id.catchall_formatted or moderator.company_id.email_formatted
            MailThread.message_notify(
                partner_ids=moderator.partner_id.ids,
                subject=_('Messages are pending moderation'), body=body,
                email_from=email_from,
            )

    # ------------------------------------------------------------
    # MEMBERSHIP
    # ------------------------------------------------------------

    def _join_group(self, email, partner_id=None):
        self.ensure_one()

        if partner_id:
            partner = self.env['res.partner'].browse(partner_id).exists()
            email = partner.email

        existing_member = self._find_member(email, partner_id)
        if existing_member:
            raise UserError(_('The email "%s" is already registered in this group.', email))

        member = self.env['mail.group.member'].create({
            'partner_id': partner_id,
            'email': email,
            'mail_group_id': self.id,
        })

        if self.moderation_guidelines:
            # Automatically send the guidelines to the new member
            self.action_send_guidelines(member)

    def _leave_group(self, email, partner_id=None):
        self.ensure_one()
        member = self._find_member(email, partner_id)
        if member:
            member.unlink()

    def _send_subscribe_confirmation_email(self, email):
        """Send an email to the given address to subscribe / unsubscribe to the mailing list."""
        self.ensure_one()
        confirm_action_url = self._generate_action_url(email, 'subscribe')

        template = self.env.ref('mail_group.mail_template_list_subscribe')
        template.with_context(token_url=confirm_action_url).send_mail(
            self.id,
            force_send=True,
            email_values={
                'author_id': self.create_uid.partner_id.id,
                'auto_delete': True,
                'email_from': self.env.company.email_formatted,
                'email_to': email,
                'message_type': 'user_notification',
            },
            notif_layout='mail.mail_notification_light',
        )
        _logger.info('Subscription email sent to %s.', email)

    def _send_unsubscribe_confirmation_email(self, email, action):
        """Send an email to the given address to subscribe / unsubscribe to the mailing list."""
        self.ensure_one()
        confirm_action_url = self._generate_action_url(email, 'unsubscribe')

        template = self.env.ref('mail_group.mail_template_list_unsubscribe')
        template.with_context(token_url=confirm_action_url).send_mail(
            self.id,
            force_send=True,
            email_values={
                'author_id': self.create_uid.partner_id.id,
                'auto_delete': True,
                'email_from': self.env.company.email_formatted,
                'email_to': email,
                'message_type': 'user_notification',
            },
            notif_layout='mail.mail_notification_light',
        )
        _logger.info('Unsubscription email sent to %s.', email)

    def _generate_action_url(self, email, action):
        """Generate the confirmation URL to subscribe / unsubscribe from the mailing list."""
        if action not in ['subscribe', 'unsubscribe']:
            raise ValueError(_('Invalid action for URL generation (%s)', action))
        self.ensure_one()

        confirm_action_url = '/groups/subscribe?%s' % urls.url_encode({
            'group_id': self.id,
            'email': email,
            'token': self._generate_action_token(email, action),
            'action': action,
        })
        base_url = self.get_base_url()
        confirm_action_url = urls.url_join(base_url, confirm_action_url)
        return confirm_action_url

    def _generate_action_token(self, email, action):
        """Generate an action token to be able to subscribe / unsubscribe from the mailing list."""
        if action not in ['subscribe', 'unsubscribe']:
            raise ValueError(_('Invalid action for URL generation (%s)', action))
        self.ensure_one()

        email_normalized = email_normalize(email)
        if not email_normalized:
            raise UserError(_('Email %s is invalid', email))

        data = (self.id, email_normalized, action)
        return hmac(self.env(su=True), 'mail_group-email-subscription', data)

    def _generate_group_access_token(self):
        """Generate an action token to be able to subscribe / unsubscribe from the mailing list."""
        self.ensure_one()
        return hmac(self.env(su=True), 'mail_group-access-token-portal', self.id)

    def _find_member(self, email, partner_id=None):
        """Return the <mail.group.member> corresponding to the given email address."""
        self.ensure_one()

        result = self._find_members(email, partner_id)
        return result.get(self.id)

    def _find_members(self, email, partner_id):
        """Get all the members record corresponding to the email / partner_id.

        Can be called in batch and return a dictionary
            {'group_id': <mail.group.member>}
        """
        domain = [('email_normalized', '=', email_normalize(email))]
        if partner_id:
            domain = expression.OR([domain, [('partner_id', '=', partner_id)]])
        domain = expression.AND([domain, [('mail_group_id', 'in', self.ids)]])
        members_data = self.env['mail.group.member'].sudo().search(domain)
        return {
            member.mail_group_id.id: member
            for member in members_data
        }
示例#18
0
class HrEmployeePublic(models.Model):
    _name = "hr.employee.public"
    _inherit = ["hr.employee.base"]
    _description = 'Public Employee'
    _order = 'name'
    _auto = False
    _log_access = True  # Include magic fields

    # Fields coming from hr.employee.base
    create_date = fields.Datetime(readonly=True)
    name = fields.Char(readonly=True)
    active = fields.Boolean(readonly=True)
    department_id = fields.Many2one(readonly=True)
    job_id = fields.Many2one(readonly=True)
    job_title = fields.Char(readonly=True)
    company_id = fields.Many2one(readonly=True)
    address_id = fields.Many2one(readonly=True)
    mobile_phone = fields.Char(readonly=True)
    work_phone = fields.Char(readonly=True)
    work_email = fields.Char(readonly=True)
    work_location = fields.Char(readonly=True)
    user_id = fields.Many2one(readonly=True)
    resource_id = fields.Many2one(readonly=True)
    resource_calendar_id = fields.Many2one(readonly=True)
    tz = fields.Selection(readonly=True)
    color = fields.Integer(readonly=True)
    employee_type = fields.Selection(readonly=True)

    # hr.employee.public specific fields
    child_ids = fields.One2many('hr.employee.public',
                                'parent_id',
                                string='Direct subordinates',
                                readonly=True)
    image_1920 = fields.Image("Original Image",
                              compute='_compute_image',
                              compute_sudo=True)
    image_1024 = fields.Image("Image 1024",
                              compute='_compute_image',
                              compute_sudo=True)
    image_512 = fields.Image("Image 512",
                             compute='_compute_image',
                             compute_sudo=True)
    image_256 = fields.Image("Image 256",
                             compute='_compute_image',
                             compute_sudo=True)
    image_128 = fields.Image("Image 128",
                             compute='_compute_image',
                             compute_sudo=True)
    parent_id = fields.Many2one('hr.employee.public', 'Manager', readonly=True)
    coach_id = fields.Many2one('hr.employee.public', 'Coach', readonly=True)
    user_partner_id = fields.Many2one(related='user_id.partner_id',
                                      related_sudo=False,
                                      string="User's partner")

    def _compute_image(self):
        for employee in self:
            # We have to be in sudo to have access to the images
            employee_id = self.sudo().env['hr.employee'].browse(employee.id)
            employee.image_1920 = employee_id.image_1920
            employee.image_1024 = employee_id.image_1024
            employee.image_512 = employee_id.image_512
            employee.image_256 = employee_id.image_256
            employee.image_128 = employee_id.image_128

    @api.model
    def _get_fields(self):
        return ','.join(
            'emp.%s' % name for name, field in self._fields.items()
            if field.store and field.type not in ['many2many', 'one2many'])

    def init(self):
        tools.drop_view_if_exists(self.env.cr, self._table)
        self.env.cr.execute("""CREATE or REPLACE VIEW %s as (
            SELECT
                %s
            FROM hr_employee emp
        )""" % (self._table, self._get_fields()))
示例#19
0
class resource(models.Model):
    _name = 'game.resource'
    name = fields.Char()
    produccions = fields.Many2many('game.raw')
    raws_stored = fields.One2many('game.raws_resource', 'resource')
    costs = fields.One2many('game.cost', 'resource')
    durations = fields.One2many('game.duration', 'resource')
    costs_child = fields.One2many(related='parent.costs')
    durations_child = fields.One2many(related='parent.durations')
    image = fields.Image(width=200)
    fortress = fields.Many2one('game.fortress', ondelete='cascade')

    level = fields.Integer()
    template = fields.Boolean()
    parent = fields.Many2one('game.resource',
                             domain="[('template', '=', True)]")
    minutes_left = fields.Integer()
    const_percent = fields.Float(
    )  #compute='_get_const_percent') # sera computed el % de contruccio
    # En cas de ser barracks o academy o laboratory
    knowledge = fields.Selection([('0', 'None'), ('1', 'Militar'),
                                  ('2', 'Scientific'), ('3', 'Mining'),
                                  ('4', 'Construction'), ('5', 'All Skills')])
    characters = fields.One2many('game.character', 'resource')
    researches = fields.One2many('game.research', 'resource', readonly=True)
    inactive = fields.Boolean(default=False)
    current_productionsk = fields.Char(compute='_get_productions')
    characters_productions = fields.Float(compute='_get_productions')
    color = fields.Integer(compute='_get_color')
    # El resource pot ser a medida. L'usuari el crea definint el que consumeix i el que produeix
    custom_production = fields.Many2one('game.raw')
    production_spend = fields.Selection([('0', 'Nothing'),
                                         ('1', 'Construccio'),
                                         ('2', 'Armes Blanques'),
                                         ('3', 'Armes Foc'), ('4', 'Nutricio'),
                                         ('5', 'Tecnologia'),
                                         ('6', 'Medicina'), ('7', 'Energia')],
                                        default='0')

    def _get_color(self):
        for i in self:
            if i.template:
                i.color = 4
            else:
                i.color = 3
            if i.inactive:
                i.color = 1

    @api.depends('minutes_left')
    def _get_const_percent(self):
        for r in self:
            total_time = r.durations_child.filtered(
                lambda s: s.level == r.level).minutes
            if total_time > 0:
                r.const_percent = 100 - (r.minutes_left / total_time) * 100

    def build_it(self):
        for r in self:
            fortress = self.env['game.fortress'].browse(
                self.env.context['fortress'])
            if fortress.max_resources > len(fortress.resources):
                r.create({
                    'name':
                    r.name,
                    'image':
                    r.image,
                    'fortress':
                    self.env.context['fortress'],
                    'level':
                    1,
                    'template':
                    False,
                    'parent':
                    r.id,
                    'minutes_left':
                    r.durations.filtered(lambda s: s.level == 1).minutes,
                    'knowledge':
                    r.knowledge,
                    'produccions': [(6, 0, r.produccions.ids)],
                    'production_spend':
                    r.production_spend
                })

    def assign_to_resource(self):
        for r in self:
            character = self.env['game.character'].browse(
                self.env.context['character'])
            character.write({'resource': r.id})
        return {
            'type': 'ir.actions.client',
            'tag': 'reload',
        }

    def _get_productions(self):
        for r in self:
            r.current_productionsk = " "  # r.produccions.mapped(lambda p: str(p.name)+" "+str((r.level*1440)/p.production_cost))
            characters_increment = 0
            if r.characters:
                characters_p = sum(cc.mining for cc in r.characters) + 1
                characters_increment = math.log(characters_p,
                                                1.05)  # Augment logaritmic
            r.characters_productions = characters_increment

    def produce(self):
        log = 'produce: '
        for r in self:
            # Produccions i gastos en les produccions
            productions = r.produccions
            c_productions = r.characters_productions
            for p in productions:  # La llista de les produccions d'aquest recurs
                raws = r.fortress.player.raws.filtered(
                    lambda r: r.raw.id == p.id
                )  # El raws del player que es d'aquesta produccio
                for i in raws:
                    q = (
                        1440 * r.level
                    ) / p.production_cost  # Costa més quan millor és el material
                    log = log + " " + str(
                        p.name) + " Res Production:   " + str(q)
                    # ara cal calcular la produccio dels characters:
                    q = q + (c_productions / p.production_cost
                             )  # Costa més quan millor és el material
                    log = log + " " + str(
                        p.name) + " Total production: " + str(
                            q) + " Cost: " + str(p.production_cost)
                    if r.production_spend != '0':
                        p_spend = r.production_spend
                        raws_needed = {
                            '0': 0,
                            '1': i.raw.construccio,
                            '2': i.raw.armesblanques,
                            '3': i.raw.armesfoc,
                            '4': i.raw.nutricio,
                            '5': i.raw.tecnologia,
                            '6': i.raw.medicina,
                            '7': i.raw.energia
                        }
                        raws_needed = raws_needed[
                            p_spend] * i.raw.production_cost * q
                        log = log + " " + 'Raws needed: ' + str(
                            p_spend) + " " + str(raws_needed)
                        r._spend(raws_needed)

                    i.write({'quantity': q + i.quantity})
            # Els consums en cas de que no produisca res
            if (len(productions) == 0 and r.production_spend != '0'):
                raws_needed = 2**r.level * (
                    len(r.characters) + 1
                )  # El consum depen del nivell i dels ocupants
                log = log + " " + 'Non productive raws needed: ' + str(
                    raws_needed)
                r._spend(raws_needed)
        return log

    def research(self):
        log = ''
        for r in self:
            #print(r.researches)
            for research in r.researches.filtered(
                    lambda s: s.minutes_left > 0):
                research.write({'minutes_left': research.minutes_left - 1})
                log = log + 'Updating researches'
                log = log + research.do_research()
        return log

    def _spend(self, raws_needed):
        for r in self:
            raws_stored = r.raws_stored
            p_spend = r.production_spend
            for ra in raws_stored:
                r_properties = {
                    '0': 0,
                    '1': ra.raw.construccio,
                    '2': ra.raw.armesblanques,
                    '3': ra.raw.armesfoc,
                    '4': ra.raw.nutricio + (
                        r.fortress.player.food_points**2
                    ),  # Els punts de nutrició del player fan que siga més eficient el RAW
                    '5': ra.raw.tecnologia,
                    '6': ra.raw.medicina,
                    '7': ra.raw.energia
                }
                print("Propietats del RAW: " + str(r_properties))
                raw_potential = 1.1**r_properties[p_spend]

                raw_spend = raws_needed / raw_potential
                print(str(raw_potential) + " Raw Spend: " + str(raw_spend))
                if raw_spend < ra.quantity:
                    ra.write({'quantity': ra.quantity - raw_spend})
                    raws_needed = 0
                else:
                    ra.write({'quantity': 0})
                    raws_needed = raws_needed - raw_potential * ra.quantity
            print(raws_needed)
            if raws_needed > 0:
                r.write({'inactive': True})

    def level_up(self):
        for r in self:
            if r.minutes_left == 0:
                level = r.level + 1
                minutes_left = r.durations_child.filtered(
                    lambda d: d.level == level)
                if minutes_left:
                    minutes_left = minutes_left[0].minutes
                    r.write({'level': level, 'minutes_left': minutes_left})

    def new_research(self):
        for r in self:
            type = self.env.context['type']
            self.env['game.research'].create({
                'resource': r.id,
                'type': type,
                'minutes_left': 1440 / r.level
            })

    @api.constrains('researches')
    def _check_researches(self):
        for r in self:
            if len(r.researches) > 0 and r.knowledge != '2':
                raise ValidationError("Your cannot research in this resource")
示例#20
0
class ProductImage(models.Model):
    _name = 'product.image'
    _description = "Product Image"
    _inherit = ['image.mixin']
    _order = 'sequence, id'

    name = fields.Char("Name", required=True)
    sequence = fields.Integer(default=10, index=True)

    image_1920 = fields.Image()

    product_tmpl_id = fields.Many2one('product.template',
                                      "Product Template",
                                      index=True,
                                      ondelete='cascade')
    product_variant_id = fields.Many2one('product.product',
                                         "Product Variant",
                                         index=True,
                                         ondelete='cascade')
    video_url = fields.Char('Video URL',
                            help='URL of a video for showcasing your product.')
    embed_code = fields.Html(compute="_compute_embed_code", sanitize=False)

    can_image_1024_be_zoomed = fields.Boolean(
        "Can Image 1024 be zoomed",
        compute='_compute_can_image_1024_be_zoomed',
        store=True)

    @api.depends('image_1920', 'image_1024')
    def _compute_can_image_1024_be_zoomed(self):
        for image in self:
            image.can_image_1024_be_zoomed = image.image_1920 and tools.is_image_size_above(
                image.image_1920, image.image_1024)

    @api.onchange('video_url')
    def _onchange_video_url(self):
        if not self.image_1920:
            self.image_1920 = get_video_thumbnail(self.video_url) or False

    @api.depends('video_url')
    def _compute_embed_code(self):
        for image in self:
            image.embed_code = get_video_embed_code(image.video_url) or False

    @api.constrains('video_url')
    def _check_valid_video_url(self):
        for image in self:
            if image.video_url and not image.embed_code:
                raise ValidationError(
                    _(
                        "Provided video URL for '%s' is not valid. Please enter a valid video URL.",
                        image.name))

    @api.model_create_multi
    def create(self, vals_list):
        """
            We don't want the default_product_tmpl_id from the context
            to be applied if we have a product_variant_id set to avoid
            having the variant images to show also as template images.
            But we want it if we don't have a product_variant_id set.
        """
        context_without_template = self.with_context({
            k: v
            for k, v in self.env.context.items()
            if k != 'default_product_tmpl_id'
        })
        normal_vals = []
        variant_vals_list = []

        for vals in vals_list:
            if vals.get('product_variant_id'
                        ) and 'default_product_tmpl_id' in self.env.context:
                variant_vals_list.append(vals)
            else:
                normal_vals.append(vals)

        return super().create(normal_vals) + super(
            ProductImage, context_without_template).create(variant_vals_list)
示例#21
0
class character_template(models.Model):
    _name = 'game.character.template'
    image = fields.Image(width=200)
    name = fields.Char()
示例#22
0
class ImLivechatChannel(models.Model):
    """ Livechat Channel
        Define a communication channel, which can be accessed with 'script_external' (script tag to put on
        external website), 'script_internal' (code to be integrated with odoo website) or via 'web_page' link.
        It provides rating tools, and access rules for anonymous people.
    """

    _name = 'im_livechat.channel'
    _inherit = ['rating.parent.mixin']
    _description = 'Livechat Channel'
    _rating_satisfaction_days = 7  # include only last 7 days to compute satisfaction

    def _default_image(self):
        image_path = modules.get_module_resource('im_livechat',
                                                 'static/src/img',
                                                 'default.png')
        return base64.b64encode(open(image_path, 'rb').read())

    def _default_user_ids(self):
        return [(6, 0, [self._uid])]

    # attribute fields
    name = fields.Char('Name', required=True, help="The name of the channel")
    button_text = fields.Char(
        'Text of the Button',
        default='Have a Question? Chat with us.',
        help="Default text displayed on the Livechat Support Button")
    default_message = fields.Char(
        'Welcome Message',
        default='How may I help you?',
        help=
        "This is an automated 'welcome' message that your visitor will see when they initiate a new conversation."
    )
    input_placeholder = fields.Char(
        'Chat Input Placeholder',
        help='Text that prompts the user to initiate the chat.')

    # computed fields
    web_page = fields.Char(
        'Web Page',
        compute='_compute_web_page_link',
        store=False,
        readonly=True,
        help=
        "URL to a static page where you client can discuss with the operator of the channel."
    )
    are_you_inside = fields.Boolean(string='Are you inside the matrix?',
                                    compute='_are_you_inside',
                                    store=False,
                                    readonly=True)
    script_external = fields.Text('Script (external)',
                                  compute='_compute_script_external',
                                  store=False,
                                  readonly=True)
    nbr_channel = fields.Integer('Number of conversation',
                                 compute='_compute_nbr_channel',
                                 store=False,
                                 readonly=True)

    image_128 = fields.Image("Image",
                             max_width=128,
                             max_height=128,
                             default=_default_image)

    # relationnal fields
    user_ids = fields.Many2many('res.users',
                                'im_livechat_channel_im_user',
                                'channel_id',
                                'user_id',
                                string='Operators',
                                default=_default_user_ids)
    channel_ids = fields.One2many('mail.channel', 'livechat_channel_id',
                                  'Sessions')
    rule_ids = fields.One2many('im_livechat.channel.rule', 'channel_id',
                               'Rules')

    def _are_you_inside(self):
        for channel in self:
            channel.are_you_inside = bool(
                self.env.uid in [u.id for u in channel.user_ids])

    def _compute_script_external(self):
        view = self.env['ir.model.data'].get_object('im_livechat',
                                                    'external_loader')
        values = {
            "url":
            self.env['ir.config_parameter'].sudo().get_param('web.base.url'),
            "dbname": self._cr.dbname,
        }
        for record in self:
            values["channel_id"] = record.id
            record.script_external = view.render(values)

    def _compute_web_page_link(self):
        base_url = self.env['ir.config_parameter'].sudo().get_param(
            'web.base.url')
        for record in self:
            record.web_page = "%s/im_livechat/support/%i" % (base_url,
                                                             record.id)

    @api.depends('channel_ids')
    def _compute_nbr_channel(self):
        for record in self:
            record.nbr_channel = len(record.channel_ids)

    # --------------------------
    # Action Methods
    # --------------------------
    def action_join(self):
        self.ensure_one()
        return self.write({'user_ids': [(4, self._uid)]})

    def action_quit(self):
        self.ensure_one()
        return self.write({'user_ids': [(3, self._uid)]})

    def action_view_rating(self):
        """ Action to display the rating relative to the channel, so all rating of the
            sessions of the current channel
            :returns : the ir.action 'action_view_rating' with the correct domain
        """
        self.ensure_one()
        action = self.env['ir.actions.act_window'].for_xml_id(
            'im_livechat', 'rating_rating_action_view_livechat_rating')
        action['domain'] = [('parent_res_id', '=', self.id),
                            ('parent_res_model', '=', 'im_livechat.channel')]
        return action

    # --------------------------
    # Channel Methods
    # --------------------------
    def _get_available_users(self):
        """ get available user of a given channel
            :retuns : return the res.users having their im_status online
        """
        self.ensure_one()
        return self.user_ids.filtered(lambda user: user.im_status == 'online')

    def _get_mail_channel(self,
                          anonymous_name,
                          previous_operator_id=None,
                          user_id=None,
                          country_id=None):
        """ Return a mail.channel given a livechat channel. It creates one with a connected operator, or return false otherwise
            :param anonymous_name : the name of the anonymous person of the channel
            :param previous_operator_id : partner_id.id of the previous operator that this visitor had in the past
            :param user_id : the id of the logged in visitor, if any
            :param country_code : the country of the anonymous person of the channel
            :type anonymous_name : str
            :return : channel header
            :rtype : dict

            If this visitor already had an operator within the last 7 days (information stored with the 'im_livechat_previous_operator_pid' cookie),
            the system will first try to assign that operator if he's available (to improve user experience).
        """
        self.ensure_one()

        operator = False
        if previous_operator_id:
            available_users = self._get_available_users()
            # previous_operator_id is the partner_id of the previous operator, need to convert to user
            if previous_operator_id in available_users.mapped(
                    'partner_id').ids:
                operator = next(
                    available_user for available_user in available_users
                    if available_user.partner_id.id == previous_operator_id)
        if not operator:
            operator = self._get_random_operator()
        if not operator:
            # no one available
            return False

        operator_partner_id = operator.partner_id.id
        # partner to add to the mail.channel
        channel_partner_to_add = [(4, operator_partner_id)]
        visitor_user = False
        if user_id:
            visitor_user = self.env['res.users'].browse(user_id)
            if visitor_user and visitor_user.active:  # valid session user (not public)
                channel_partner_to_add.append((4, visitor_user.partner_id.id))
        # create the session, and add the link with the given channel
        mail_channel = self.env["mail.channel"].with_context(
            mail_create_nosubscribe=False).sudo().create({
                'channel_partner_ids':
                channel_partner_to_add,
                'livechat_operator_id':
                operator_partner_id,
                'livechat_channel_id':
                self.id,
                'anonymous_name':
                False if user_id else anonymous_name,
                'country_id':
                country_id,
                'channel_type':
                'livechat',
                'name':
                ', '.join([
                    visitor_user.name if visitor_user else anonymous_name,
                    operator.livechat_username
                    if operator.livechat_username else operator.name
                ]),
                'public':
                'private',
                'email_send':
                False,
            })
        mail_channel._broadcast([operator_partner_id])
        return mail_channel.sudo().channel_info()[0]

    def _get_random_operator(self):
        """ Return a random operator from the available users of the channel that have the lowest number of active livechats.
        A livechat is considered 'active' if it has at least one message within the 30 minutes.

        (Some annoying conversions have to be made on the fly because this model holds 'res.users' as available operators
        and the mail_channel model stores the partner_id of the randomly selected operator)

        :return : user
        :rtype : res.users
        """
        operators = self._get_available_users()
        if len(operators) == 0:
            return False

        self.env.cr.execute(
            """SELECT COUNT(DISTINCT c.id), c.livechat_operator_id
            FROM mail_channel c
            LEFT OUTER JOIN mail_message_mail_channel_rel r ON c.id = r.mail_channel_id
            LEFT OUTER JOIN mail_message m ON r.mail_message_id = m.id
            WHERE m.create_date > ((now() at time zone 'UTC') - interval '30 minutes')
            AND c.channel_type = 'livechat'
            AND c.livechat_operator_id in %s
            GROUP BY c.livechat_operator_id
            ORDER BY COUNT(DISTINCT c.id) asc""",
            (tuple(operators.mapped('partner_id').ids), ))
        active_channels = self.env.cr.dictfetchall()

        # If inactive operator(s), return one of them
        active_channel_operator_ids = [
            active_channel['livechat_operator_id']
            for active_channel in active_channels
        ]
        inactive_operators = [
            operator for operator in operators
            if operator.partner_id.id not in active_channel_operator_ids
        ]
        if inactive_operators:
            return random.choice(inactive_operators)

        # If no inactive operator, active_channels is not empty as len(operators) > 0 (see above).
        # Get the less active operator using the active_channels first element's count (since they are sorted 'ascending')
        lowest_number_of_conversations = active_channels[0]['count']
        less_active_operator = random.choice([
            active_channel['livechat_operator_id']
            for active_channel in active_channels
            if active_channel['count'] == lowest_number_of_conversations
        ])

        # convert the selected 'partner_id' to its corresponding res.users
        return next(operator for operator in operators
                    if operator.partner_id.id == less_active_operator)

    def _get_channel_infos(self):
        self.ensure_one()

        return {
            'button_text': self.button_text,
            'input_placeholder': self.input_placeholder,
            'default_message': self.default_message,
            "channel_name": self.name,
            "channel_id": self.id,
        }

    def get_livechat_info(self, username='******'):
        self.ensure_one()

        info = {}
        info['available'] = len(self._get_available_users()) > 0
        info['server_url'] = self.env['ir.config_parameter'].sudo().get_param(
            'web.base.url')
        if info['available']:
            info['options'] = self._get_channel_infos()
            info['options']["default_username"] = username
        return info
示例#23
0
class LunchProductReport(models.Model):
    _name = "lunch.product.report"
    _description = 'Product report'
    _auto = False
    _order = 'is_favorite desc, is_new desc, last_order_date asc, product_id asc'

    id = fields.Integer('ID')
    product_id = fields.Many2one('lunch.product', 'Product')
    name = fields.Char('Product Name', related='product_id.name')
    category_id = fields.Many2one('lunch.product.category', 'Product Category')
    description = fields.Text('Description', related='product_id.description')
    price = fields.Float('Price')
    supplier_id = fields.Many2one('lunch.supplier', 'Vendor')
    company_id = fields.Many2one('res.company')
    currency_id = fields.Many2one('res.currency',
                                  related='company_id.currency_id')
    is_favorite = fields.Boolean('Favorite')
    user_id = fields.Many2one('res.users')
    is_new = fields.Boolean('New')
    active = fields.Boolean('Active')
    last_order_date = fields.Date('Last Order Date')
    image_128 = fields.Image(compute="_compute_image_128")

    # This field is used only for searching
    is_available_at = fields.Many2one('lunch.location',
                                      'Product Availability',
                                      compute='_compute_is_available_at',
                                      search='_search_is_available_at')

    def _compute_image_128(self):
        for product_r in self:
            product = product_r.product_id
            category = product_r.category_id
            if product.image_128:
                product_r.image_128 = product.image_128
            elif category.image_128:
                product_r.image_128 = category.image_128
            else:
                product_r.image_128 = False

    def _compute_is_available_at(self):
        """
            Is available_at is always false when browsing it
            this field is there only to search (see _search_is_available_at)
        """
        for product in self:
            product.is_available_at = False

    def _search_is_available_at(self, operator, value):
        supported_operators = ['in', 'not in', '=', '!=']

        if not operator in supported_operators:
            return expression.TRUE_DOMAIN

        if isinstance(value, int):
            value = [value]

        if operator in expression.NEGATIVE_TERM_OPERATORS:
            return expression.AND([[
                ('supplier_id.available_location_ids', 'not in', value)
            ], [('supplier_id.available_location_ids', '!=', False)]])

        return expression.OR([[
            ('supplier_id.available_location_ids', 'in', value)
        ], [('supplier_id.available_location_ids', '=', False)]])

    def write(self, values):
        if 'is_favorite' in values:
            if values['is_favorite']:
                commands = [(4, product_id)
                            for product_id in self.mapped('product_id').ids]
            else:
                commands = [(3, product_id)
                            for product_id in self.mapped('product_id').ids]
            self.env.user.write({
                'favorite_lunch_product_ids': commands,
            })

    def init(self):
        tools.drop_view_if_exists(self._cr, self._table)

        self._cr.execute(
            """
            CREATE or REPLACE view %s AS (
                SELECT
                    row_number() over (ORDER BY users.id,product.id) AS id,
                    product.id AS product_id,
                    product.category_id,
                    product.price,
                    product.supplier_id,
                    product.company_id,
                    product.active,
                    users.id AS user_id,
                    fav.user_id IS NOT NULL AS is_favorite,
                    product.new_until >= current_date AS is_new,
                    orders.last_order_date
                FROM lunch_product product
                INNER JOIN res_users users ON product.company_id IS NULL OR users.company_id = product.company_id -- multi company
                INNER JOIN res_groups_users_rel groups ON groups.uid = users.id -- only generate for internal users
                LEFT JOIN LATERAL (select max(date) AS last_order_date FROM lunch_order where user_id=users.id and product_id=product.id) AS orders ON TRUE
                LEFT JOIN LATERAL (select user_id FROM lunch_product_favorite_user_rel where user_id=users.id and product_id=product.id) AS fav ON TRUE
                WHERE users.active AND product.active AND groups.gid = %%s --only take into account active products and users
            );
        """ % self._table, (self.env.ref('base.group_user').id, ))
示例#24
0
class FleetVehicleModel(models.Model):
    _name = 'fleet.vehicle.model'
    _description = 'Model of a vehicle'
    _order = 'name asc'

    name = fields.Char('Model name', required=True)
    brand_id = fields.Many2one('fleet.vehicle.model.brand',
                               'Manufacturer',
                               required=True,
                               help='Manufacturer of the vehicle')
    category_id = fields.Many2one('fleet.vehicle.model.category', 'Category')
    vendors = fields.Many2many('res.partner',
                               'fleet_vehicle_model_vendors',
                               'model_id',
                               'partner_id',
                               string='Vendors')
    image_128 = fields.Image(related='brand_id.image_128', readonly=True)
    active = fields.Boolean(default=True)
    vehicle_type = fields.Selection([('car', 'Car'), ('bike', 'Bike')],
                                    default='car',
                                    required=True)
    transmission = fields.Selection([('manual', 'Manual'),
                                     ('automatic', 'Automatic')],
                                    'Transmission',
                                    help='Transmission Used by the vehicle')
    vehicle_count = fields.Integer(compute='_compute_vehicle_count')

    @api.depends('name', 'brand_id')
    def name_get(self):
        res = []
        for record in self:
            name = record.name
            if record.brand_id.name:
                name = record.brand_id.name + '/' + name
            res.append((record.id, name))
        return res

    def _compute_vehicle_count(self):
        group = self.env['fleet.vehicle'].read_group(
            [('model_id', 'in', self.ids)],
            ['id', 'model_id'],
            groupby='model_id',
            lazy=False,
        )
        count_by_model = {
            entry['model_id'][0]: entry['__count']
            for entry in group
        }
        for model in self:
            model.vehicle_count = count_by_model.get(model.id, 0)

    def action_model_vehicle(self):
        self.ensure_one()
        view = {
            'type': 'ir.actions.act_window',
            'view_mode': 'kanban,tree,form',
            'res_model': 'fleet.vehicle',
            'name': _('Vehicles'),
            'context': {
                'search_default_model_id': self.id,
                'default_model_id': self.id
            }
        }

        return view
示例#25
0
class FleetVehicle(models.Model):
    _inherit = ['mail.thread', 'mail.activity.mixin']
    _name = 'fleet.vehicle'
    _description = 'Vehicle'
    _order = 'license_plate asc, acquisition_date asc'

    def _get_default_state(self):
        state = self.env.ref('fleet.fleet_vehicle_state_registered',
                             raise_if_not_found=False)
        return state if state and state.id else False

    name = fields.Char(compute="_compute_vehicle_name", store=True)
    description = fields.Text("Vehicle Description")
    active = fields.Boolean('Active', default=True, tracking=True)
    company_id = fields.Many2one('res.company',
                                 'Company',
                                 default=lambda self: self.env.company)
    currency_id = fields.Many2one('res.currency',
                                  related='company_id.currency_id')
    license_plate = fields.Char(
        tracking=True,
        help='License plate number of the vehicle (i = plate number for a car)'
    )
    vin_sn = fields.Char(
        'Chassis Number',
        help='Unique number written on the vehicle motor (VIN/SN number)',
        copy=False)
    driver_id = fields.Many2one('res.partner',
                                'Driver',
                                tracking=True,
                                help='Driver of the vehicle',
                                copy=False)
    future_driver_id = fields.Many2one(
        'res.partner',
        'Future Driver',
        tracking=True,
        help='Next Driver of the vehicle',
        copy=False,
        domain=
        "['|', ('company_id', '=', False), ('company_id', '=', company_id)]")
    model_id = fields.Many2one('fleet.vehicle.model',
                               'Model',
                               tracking=True,
                               required=True,
                               help='Model of the vehicle')
    manager_id = fields.Many2one(
        'res.users',
        'Fleet Manager',
        domain=lambda self: [('groups_id', 'in',
                              self.env.ref('fleet.fleet_group_manager').id)])

    brand_id = fields.Many2one('fleet.vehicle.model.brand',
                               'Brand',
                               related="model_id.brand_id",
                               store=True,
                               readonly=False)
    log_drivers = fields.One2many('fleet.vehicle.assignation.log',
                                  'vehicle_id',
                                  string='Assignment Logs')
    log_services = fields.One2many('fleet.vehicle.log.services', 'vehicle_id',
                                   'Services Logs')
    log_contracts = fields.One2many('fleet.vehicle.log.contract', 'vehicle_id',
                                    'Contracts')
    contract_count = fields.Integer(compute="_compute_count_all",
                                    string='Contract Count')
    service_count = fields.Integer(compute="_compute_count_all",
                                   string='Services')
    odometer_count = fields.Integer(compute="_compute_count_all",
                                    string='Odometer')
    history_count = fields.Integer(compute="_compute_count_all",
                                   string="Drivers History Count")
    next_assignation_date = fields.Date(
        'Assignment Date',
        help=
        'This is the date at which the car will be available, if not set it means available instantly'
    )
    acquisition_date = fields.Date(
        'Immatriculation Date',
        required=False,
        default=fields.Date.today,
        help='Date when the vehicle has been immatriculated')
    first_contract_date = fields.Date(string="First Contract Date",
                                      default=fields.Date.today)
    color = fields.Char(help='Color of the vehicle')
    state_id = fields.Many2one('fleet.vehicle.state',
                               'State',
                               default=_get_default_state,
                               group_expand='_read_group_stage_ids',
                               tracking=True,
                               help='Current state of the vehicle',
                               ondelete="set null")
    location = fields.Char(help='Location of the vehicle (garage, ...)')
    seats = fields.Integer('Seats Number',
                           help='Number of seats of the vehicle')
    model_year = fields.Char('Model Year', help='Year of the model')
    doors = fields.Integer('Doors Number',
                           help='Number of doors of the vehicle',
                           compute="_compute_doors",
                           store=True,
                           readonly=True)
    tag_ids = fields.Many2many('fleet.vehicle.tag',
                               'fleet_vehicle_vehicle_tag_rel',
                               'vehicle_tag_id',
                               'tag_id',
                               'Tags',
                               copy=False)
    odometer = fields.Float(
        compute='_get_odometer',
        inverse='_set_odometer',
        string='Last Odometer',
        help='Odometer measure of the vehicle at the moment of this log')
    odometer_unit = fields.Selection([('kilometers', 'km'), ('miles', 'mi')],
                                     'Odometer Unit',
                                     default='kilometers',
                                     help='Unit of the odometer ',
                                     required=True)
    transmission = fields.Selection([('manual', 'Manual'),
                                     ('automatic', 'Automatic')],
                                    'Transmission',
                                    help='Transmission Used by the vehicle')
    fuel_type = fields.Selection([
        ('gasoline', 'Gasoline'),
        ('diesel', 'Diesel'),
        ('lpg', 'LPG'),
        ('electric', 'Electric'),
        ('hybrid', 'Hybrid'),
        ('plug_in_hybrid_diesel', 'Plug-in Hybrid Diesel'),
        ('plug_in_hybrid_gasoline', 'Plug-in Hybrid Gasoline'),
        ('full_hybrid_gasoline', 'Full Hybrid Gasoline'),
        ('cng', 'CNG'),
        ('hydrogen', 'Hydrogen'),
    ],
                                 'Fuel Type',
                                 help='Fuel Used by the vehicle')
    horsepower = fields.Integer()
    horsepower_tax = fields.Float('Horsepower Taxation')
    power = fields.Integer('Power', help='Power in kW of the vehicle')
    co2 = fields.Float('CO2 Emissions', help='CO2 emissions of the vehicle')
    image_128 = fields.Image(related='model_id.image_128', readonly=True)
    contract_renewal_due_soon = fields.Boolean(
        compute='_compute_contract_reminder',
        search='_search_contract_renewal_due_soon',
        string='Has Contracts to renew')
    contract_renewal_overdue = fields.Boolean(
        compute='_compute_contract_reminder',
        search='_search_get_overdue_contract_reminder',
        string='Has Contracts Overdue')
    contract_renewal_name = fields.Text(
        compute='_compute_contract_reminder',
        string='Name of contract to renew soon')
    contract_renewal_total = fields.Text(
        compute='_compute_contract_reminder',
        string='Total of contracts due or overdue minus one')
    car_value = fields.Float(string="Catalog Value (VAT Incl.)",
                             help='Value of the bought vehicle')
    net_car_value = fields.Float(string="Purchase Value",
                                 help="Purchase value of the vehicle")
    residual_value = fields.Float()
    plan_to_change_car = fields.Boolean(related='driver_id.plan_to_change_car',
                                        store=True,
                                        readonly=False)
    plan_to_change_bike = fields.Boolean(
        related='driver_id.plan_to_change_bike', store=True, readonly=False)
    vehicle_type = fields.Selection(related='model_id.vehicle_type')
    frame_type = fields.Selection([('diamant', 'Diamant'),
                                   ('trapez', 'Trapez'), ('wave', 'Wave')],
                                  help="Frame type of the bike")
    electric_assistance = fields.Boolean()
    frame_size = fields.Float()

    @api.depends('vehicle_type')
    def _compute_doors(self):
        for record in self:
            record.doors = 5 if record.model_id.vehicle_type == 'car' else 0

    @api.depends('model_id.brand_id.name', 'model_id.name', 'license_plate')
    def _compute_vehicle_name(self):
        for record in self:
            record.name = (record.model_id.brand_id.name or '') + '/' + (
                record.model_id.name or '') + '/' + (record.license_plate
                                                     or _('No Plate'))

    def _get_odometer(self):
        FleetVehicalOdometer = self.env['fleet.vehicle.odometer']
        for record in self:
            vehicle_odometer = FleetVehicalOdometer.search(
                [('vehicle_id', '=', record.id)], limit=1, order='value desc')
            if vehicle_odometer:
                record.odometer = vehicle_odometer.value
            else:
                record.odometer = 0

    def _set_odometer(self):
        for record in self:
            if record.odometer:
                date = fields.Date.context_today(record)
                data = {
                    'value': record.odometer,
                    'date': date,
                    'vehicle_id': record.id
                }
                self.env['fleet.vehicle.odometer'].create(data)

    def _compute_count_all(self):
        Odometer = self.env['fleet.vehicle.odometer']
        LogService = self.env['fleet.vehicle.log.services']
        LogContract = self.env['fleet.vehicle.log.contract']
        for record in self:
            record.odometer_count = Odometer.search_count([('vehicle_id', '=',
                                                            record.id)])
            record.service_count = LogService.search_count([
                ('vehicle_id', '=', record.id), ('active', '=', record.active)
            ])
            record.contract_count = LogContract.search_count([
                ('vehicle_id', '=', record.id), ('state', '!=', 'closed'),
                ('active', '=', record.active)
            ])
            record.history_count = self.env[
                'fleet.vehicle.assignation.log'].search_count([
                    ('vehicle_id', '=', record.id)
                ])

    @api.depends('log_contracts')
    def _compute_contract_reminder(self):
        params = self.env['ir.config_parameter'].sudo()
        delay_alert_contract = int(
            params.get_param('hr_fleet.delay_alert_contract', default=30))
        for record in self:
            overdue = False
            due_soon = False
            total = 0
            name = ''
            for element in record.log_contracts:
                if element.state in ('open',
                                     'expired') and element.expiration_date:
                    current_date_str = fields.Date.context_today(record)
                    due_time_str = element.expiration_date
                    current_date = fields.Date.from_string(current_date_str)
                    due_time = fields.Date.from_string(due_time_str)
                    diff_time = (due_time - current_date).days
                    if diff_time < 0:
                        overdue = True
                        total += 1
                    if diff_time < delay_alert_contract:
                        due_soon = True
                        total += 1
                    if overdue or due_soon:
                        log_contract = self.env[
                            'fleet.vehicle.log.contract'].search(
                                [('vehicle_id', '=', record.id),
                                 ('state', 'in', ('open', 'expired'))],
                                limit=1,
                                order='expiration_date asc')
                        if log_contract:
                            # we display only the name of the oldest overdue/due soon contract
                            name = log_contract.name

            record.contract_renewal_overdue = overdue
            record.contract_renewal_due_soon = due_soon
            record.contract_renewal_total = total - 1  # we remove 1 from the real total for display purposes
            record.contract_renewal_name = name

    def _get_analytic_name(self):
        # This function is used in fleet_account and is overrided in l10n_be_hr_payroll_fleet
        return self.license_plate or _('No plate')

    def _search_contract_renewal_due_soon(self, operator, value):
        params = self.env['ir.config_parameter'].sudo()
        delay_alert_contract = int(
            params.get_param('hr_fleet.delay_alert_contract', default=30))
        res = []
        assert operator in ('=', '!=', '<>') and value in (
            True, False), 'Operation not supported'
        if (operator == '=' and value is True) or (operator in ('<>', '!=')
                                                   and value is False):
            search_operator = 'in'
        else:
            search_operator = 'not in'
        today = fields.Date.context_today(self)
        datetime_today = fields.Datetime.from_string(today)
        limit_date = fields.Datetime.to_string(datetime_today + relativedelta(
            days=+delay_alert_contract))
        res_ids = self.env['fleet.vehicle.log.contract'].search([
            ('expiration_date', '>', today),
            ('expiration_date', '<', limit_date),
            ('state', 'in', ['open', 'expired'])
        ]).mapped('id')
        res.append(('id', search_operator, res_ids))
        return res

    def _search_get_overdue_contract_reminder(self, operator, value):
        res = []
        assert operator in ('=', '!=', '<>') and value in (
            True, False), 'Operation not supported'
        if (operator == '=' and value is True) or (operator in ('<>', '!=')
                                                   and value is False):
            search_operator = 'in'
        else:
            search_operator = 'not in'
        today = fields.Date.context_today(self)
        res_ids = self.env['fleet.vehicle.log.contract'].search([
            ('expiration_date', '!=', False), ('expiration_date', '<', today),
            ('state', 'in', ['open', 'expired'])
        ]).mapped('id')
        res.append(('id', search_operator, res_ids))
        return res

    @api.model
    def create(self, vals):
        # Fleet administrator may not have rights to create the plan_to_change_car value when the driver_id is a res.user
        # This trick is used to prevent access right error.
        ptc_value = 'plan_to_change_car' in vals.keys() and {
            'plan_to_change_car': vals.pop('plan_to_change_car')
        }
        res = super(FleetVehicle, self).create(vals)
        if ptc_value:
            res.sudo().write(ptc_value)
        if 'driver_id' in vals and vals['driver_id']:
            res.create_driver_history(vals['driver_id'])
        if 'future_driver_id' in vals and vals['future_driver_id']:
            state_waiting_list = self.env.ref(
                'fleet.fleet_vehicle_state_waiting_list',
                raise_if_not_found=False)
            states = res.mapped('state_id').ids
            if not state_waiting_list or state_waiting_list.id not in states:
                future_driver = self.env['res.partner'].browse(
                    vals['future_driver_id'])
                if self.vehicle_type == 'bike':
                    future_driver.sudo().write({'plan_to_change_bike': True})
                if self.vehicle_type == 'car':
                    future_driver.sudo().write({'plan_to_change_car': True})
        return res

    def write(self, vals):
        if 'driver_id' in vals and vals['driver_id']:
            driver_id = vals['driver_id']
            for vehicle in self.filtered(
                    lambda v: v.driver_id.id != driver_id):
                vehicle.create_driver_history(driver_id)
                if vehicle.driver_id:
                    vehicle.activity_schedule(
                        'mail.mail_activity_data_todo',
                        user_id=vehicle.manager_id.id or self.env.user.id,
                        note=_('Specify the End date of %s') %
                        vehicle.driver_id.name)

        if 'future_driver_id' in vals and vals['future_driver_id']:
            state_waiting_list = self.env.ref(
                'fleet.fleet_vehicle_state_waiting_list',
                raise_if_not_found=False)
            states = self.mapped(
                'state_id').ids if 'state_id' not in vals else [
                    vals['state_id']
                ]
            if not state_waiting_list or state_waiting_list.id not in states:
                future_driver = self.env['res.partner'].browse(
                    vals['future_driver_id'])
                if self.vehicle_type == 'bike':
                    future_driver.sudo().write({'plan_to_change_bike': True})
                if self.vehicle_type == 'car':
                    future_driver.sudo().write({'plan_to_change_car': True})

        res = super(FleetVehicle, self).write(vals)
        return res

    def create_driver_history(self, driver_id):
        for vehicle in self:
            self.env['fleet.vehicle.assignation.log'].create({
                'vehicle_id':
                vehicle.id,
                'driver_id':
                driver_id,
                'date_start':
                fields.Date.today(),
            })

    def action_accept_driver_change(self):
        # Find all the vehicles for which the driver is the future_driver_id
        # remove their driver_id and close their history using current date
        vehicles = self.search([('driver_id', 'in',
                                 self.mapped('future_driver_id').ids)])
        vehicles.write({'driver_id': False})

        for vehicle in self:
            if vehicle.vehicle_type == 'bike':
                vehicle.future_driver_id.sudo().write(
                    {'plan_to_change_bike': False})
            if vehicle.vehicle_type == 'car':
                vehicle.future_driver_id.sudo().write(
                    {'plan_to_change_car': False})
            vehicle.driver_id = vehicle.future_driver_id
            vehicle.future_driver_id = False

    def toggle_active(self):
        self.env['fleet.vehicle.log.contract'].with_context(
            active_test=False).search([('vehicle_id', 'in', self.ids)
                                       ]).toggle_active()
        self.env['fleet.vehicle.log.services'].with_context(
            active_test=False).search([('vehicle_id', 'in', self.ids)
                                       ]).toggle_active()
        super(FleetVehicle, self).toggle_active()

    @api.model
    def _read_group_stage_ids(self, stages, domain, order):
        return self.env['fleet.vehicle.state'].search([], order=order)

    @api.model
    def read_group(self,
                   domain,
                   fields,
                   groupby,
                   offset=0,
                   limit=None,
                   orderby=False,
                   lazy=True):
        if 'co2' in fields:
            fields.remove('co2')
        return super(FleetVehicle,
                     self).read_group(domain, fields, groupby, offset, limit,
                                      orderby, lazy)

    @api.model
    def _name_search(self,
                     name,
                     args=None,
                     operator='ilike',
                     limit=100,
                     name_get_uid=None):
        args = args or []
        if operator == 'ilike' and not (name or '').strip():
            domain = []
        else:
            domain = [
                '|', ('name', operator, name),
                ('driver_id.name', operator, name)
            ]
        return self._search(expression.AND([domain, args]),
                            limit=limit,
                            access_rights_uid=name_get_uid)

    def return_action_to_open(self):
        """ This opens the xml view specified in xml_id for the current vehicle """
        self.ensure_one()
        xml_id = self.env.context.get('xml_id')
        if xml_id:

            res = self.env['ir.actions.act_window']._for_xml_id('fleet.%s' %
                                                                xml_id)
            res.update(context=dict(self.env.context,
                                    default_vehicle_id=self.id,
                                    group_by=False),
                       domain=[('vehicle_id', '=', self.id)])
            return res
        return False

    def act_show_log_cost(self):
        """ This opens log view to view and add new log for this vehicle, groupby default to only show effective costs
            @return: the costs log view
        """
        self.ensure_one()
        copy_context = dict(self.env.context)
        copy_context.pop('group_by', None)
        res = self.env['ir.actions.act_window']._for_xml_id(
            'fleet.fleet_vehicle_costs_action')
        res.update(context=dict(copy_context,
                                default_vehicle_id=self.id,
                                search_default_parent_false=True),
                   domain=[('vehicle_id', '=', self.id)])
        return res

    def _track_subtype(self, init_values):
        self.ensure_one()
        if 'driver_id' in init_values or 'future_driver_id' in init_values:
            return self.env.ref('fleet.mt_fleet_driver_updated')
        return super(FleetVehicle, self)._track_subtype(init_values)

    def open_assignation_logs(self):
        self.ensure_one()
        return {
            'type': 'ir.actions.act_window',
            'name': 'Assignment Logs',
            'view_mode': 'tree',
            'res_model': 'fleet.vehicle.assignation.log',
            'domain': [('vehicle_id', '=', self.id)],
            'context': {
                'default_driver_id': self.driver_id.id,
                'default_vehicle_id': self.id
            }
        }
示例#26
0
class Lang(models.Model):
    _name = "res.lang"
    _description = "Languages"
    _order = "active desc,name"

    _disallowed_datetime_patterns = list(tools.DATETIME_FORMATS_MAP)
    _disallowed_datetime_patterns.remove(
        '%y')  # this one is in fact allowed, just not good practice

    name = fields.Char(required=True)
    code = fields.Char(string='Locale Code',
                       required=True,
                       help='This field is used to set/get locales for user')
    iso_code = fields.Char(
        string='ISO code',
        help='This ISO code is the name of po files to use for translations')
    url_code = fields.Char('URL Code',
                           required=True,
                           help='The Lang Code displayed in the URL')
    active = fields.Boolean()
    direction = fields.Selection([('ltr', 'Left-to-Right'),
                                  ('rtl', 'Right-to-Left')],
                                 required=True,
                                 default='ltr')
    date_format = fields.Char(string='Date Format',
                              required=True,
                              default=DEFAULT_DATE_FORMAT)
    time_format = fields.Char(string='Time Format',
                              required=True,
                              default=DEFAULT_TIME_FORMAT)
    week_start = fields.Selection([('1', 'Monday'), ('2', 'Tuesday'),
                                   ('3', 'Wednesday'), ('4', 'Thursday'),
                                   ('5', 'Friday'), ('6', 'Saturday'),
                                   ('7', 'Sunday')],
                                  string='First Day of Week',
                                  required=True,
                                  default='7')
    grouping = fields.Char(
        string='Separator Format',
        required=True,
        default='[]',
        help=
        "The Separator Format should be like [,n] where 0 < n :starting from Unit digit. "
        "-1 will end the separation. e.g. [3,2,-1] will represent 106500 to be 1,06,500; "
        "[1,2,-1] will represent it to be 106,50,0;[3] will represent it as 106,500. "
        "Provided ',' as the thousand separator in each case.")
    decimal_point = fields.Char(string='Decimal Separator',
                                required=True,
                                default='.',
                                trim=False)
    thousands_sep = fields.Char(string='Thousands Separator',
                                default=',',
                                trim=False)

    @api.depends('code', 'flag_image')
    def _compute_field_flag_image_url(self):
        for lang in self:
            if lang.flag_image:
                lang.flag_image_url = f"/web/image/res.lang/{lang.id}/flag_image"
            else:
                lang.flag_image_url = f"/base/static/img/country_flags/{lang.code.lower().rsplit('_')[-1]}.png"

    flag_image = fields.Image("Image")
    flag_image_url = fields.Char(compute=_compute_field_flag_image_url)

    _sql_constraints = [
        ('name_uniq', 'unique(name)',
         'The name of the language must be unique !'),
        ('code_uniq', 'unique(code)',
         'The code of the language must be unique !'),
        ('url_code_uniq', 'unique(url_code)',
         'The URL code of the language must be unique !'),
    ]

    @api.constrains('active')
    def _check_active(self):
        # do not check during installation
        if self.env.registry.ready and not self.search_count([]):
            raise ValidationError(_('At least one language must be active.'))

    @api.constrains('time_format', 'date_format')
    def _check_format(self):
        for lang in self:
            for pattern in lang._disallowed_datetime_patterns:
                if (lang.time_format and pattern in lang.time_format) or \
                        (lang.date_format and pattern in lang.date_format):
                    raise ValidationError(
                        _('Invalid date/time format directive specified. '
                          'Please refer to the list of allowed directives, '
                          'displayed when you edit a language.'))

    @api.constrains('grouping')
    def _check_grouping(self):
        warning = _(
            'The Separator Format should be like [,n] where 0 < n :starting from Unit digit. '
            '-1 will end the separation. e.g. [3,2,-1] will represent 106500 to be 1,06,500;'
            '[1,2,-1] will represent it to be 106,50,0;[3] will represent it as 106,500. '
            'Provided as the thousand separator in each case.')
        for lang in self:
            try:
                if any(not isinstance(x, int)
                       for x in json.loads(lang.grouping)):
                    raise ValidationError(warning)
            except Exception:
                raise ValidationError(warning)

    def _register_hook(self):
        # check that there is at least one active language
        if not self.search_count([]):
            _logger.error("No language is active.")

    # TODO remove me after v14
    def load_lang(self, lang, lang_name=None):
        _logger.warning(
            "Call to deprecated method load_lang, use _create_lang or _activate_lang instead"
        )
        language = self._activate_lang(lang) or self._create_lang(
            lang, lang_name)
        return language.id

    def _activate_lang(self, code):
        """ Activate languages
        :param code: code of the language to activate
        :return: the language matching 'code' activated
        """
        lang = self.with_context(active_test=False).search([('code', '=', code)
                                                            ])
        if lang and not lang.active:
            lang.active = True
        return lang

    def _create_lang(self, lang, lang_name=None):
        """ Create the given language and make it active. """
        # create the language with locale information
        fail = True
        iso_lang = tools.get_iso_codes(lang)
        for ln in tools.get_locales(lang):
            try:
                locale.setlocale(locale.LC_ALL, str(ln))
                fail = False
                break
            except locale.Error:
                continue
        if fail:
            lc = locale.getdefaultlocale()[0]
            msg = 'Unable to get information for locale %s. Information from the default locale (%s) have been used.'
            _logger.warning(msg, lang, lc)

        if not lang_name:
            lang_name = lang

        def fix_xa0(s):
            """Fix badly-encoded non-breaking space Unicode character from locale.localeconv(),
               coercing to utf-8, as some platform seem to output localeconv() in their system
               encoding, e.g. Windows-1252"""
            if s == '\xa0':
                return '\xc2\xa0'
            return s

        def fix_datetime_format(format):
            """Python's strftime supports only the format directives
               that are available on the platform's libc, so in order to
               be 100% cross-platform we map to the directives required by
               the C standard (1989 version), always available on platforms
               with a C standard implementation."""
            # For some locales, nl_langinfo returns a D_FMT/T_FMT that contains
            # unsupported '%-' patterns, e.g. for cs_CZ
            format = format.replace('%-', '%')
            for pattern, replacement in tools.DATETIME_FORMATS_MAP.items():
                format = format.replace(pattern, replacement)
            return str(format)

        conv = locale.localeconv()
        lang_info = {
            'code': lang,
            'iso_code': iso_lang,
            'name': lang_name,
            'active': True,
            'date_format':
            fix_datetime_format(locale.nl_langinfo(locale.D_FMT)),
            'time_format':
            fix_datetime_format(locale.nl_langinfo(locale.T_FMT)),
            'decimal_point': fix_xa0(str(conv['decimal_point'])),
            'thousands_sep': fix_xa0(str(conv['thousands_sep'])),
            'grouping': str(conv.get('grouping', [])),
        }
        try:
            return self.create(lang_info)
        finally:
            tools.resetlocale()

    @api.model
    def install_lang(self):
        """

        This method is called from odoo/addons/base/data/res_lang_data.xml to load
        some language and set it as the default for every partners. The
        language is set via tools.config by the '_initialize_db' method on the
        'db' object. This is a fragile solution and something else should be
        found.

        """
        # config['load_language'] is a comma-separated list or None
        lang_code = (tools.config.get('load_language')
                     or 'en_US').split(',')[0]
        lang = self._activate_lang(lang_code) or self._create_lang(lang_code)
        IrDefault = self.env['ir.default']
        default_value = IrDefault.get('res.partner', 'lang')
        if default_value is None:
            IrDefault.set('res.partner', 'lang', lang_code)
            # set language of main company, created directly by db bootstrap SQL
            partner = self.env.company.partner_id
            if not partner.lang:
                partner.write({'lang': lang_code})
        return True

    @tools.ormcache('code')
    def _lang_get_id(self, code):
        return self.with_context(active_test=True).search([('code', '=', code)
                                                           ]).id

    @tools.ormcache('url_code')
    def _lang_get_code(self, url_code):
        return self.with_context(active_test=True).search(
            [('url_code', '=', url_code)]).code or url_code

    def _lang_get(self, code):
        """ Return the language using this code if it is active """
        return self.browse(self._lang_get_id(code))

    @tools.ormcache('self.code', 'monetary')
    def _data_get(self, monetary=False):
        conv = locale.localeconv()
        thousands_sep = self.thousands_sep or conv[
            monetary and 'mon_thousands_sep' or 'thousands_sep']
        decimal_point = self.decimal_point
        grouping = self.grouping
        return grouping, thousands_sep, decimal_point

    @api.model
    @tools.ormcache()
    def get_available(self):
        """ Return the available languages as a list of (code, url_code, name,
            active) sorted by name.
        """
        langs = self.with_context(active_test=False).search([])
        return langs.get_sorted()

    def get_sorted(self):
        return sorted([(lang.code, lang.url_code, lang.name, lang.active,
                        lang.flag_image_url) for lang in self],
                      key=itemgetter(2))

    @tools.ormcache('self.id')
    def _get_cached_values(self):
        self.ensure_one()
        return {
            'id': self.id,
            'code': self.code,
            'url_code': self.url_code,
            'name': self.name,
        }

    def _get_cached(self, field):
        return self._get_cached_values()[field]

    @api.model
    @tools.ormcache('code')
    def _lang_code_to_urlcode(self, code):
        for c, urlc, name, *_ in self.get_available():
            if c == code:
                return urlc
        return self._lang_get(code).url_code

    @api.model
    @tools.ormcache()
    def get_installed(self):
        """ Return the installed languages as a list of (code, name) sorted by name. """
        langs = self.with_context(active_test=True).search([])
        return sorted([(lang.code, lang.name) for lang in langs],
                      key=itemgetter(1))

    def toggle_active(self):
        super().toggle_active()
        # Automatically load translation
        active_lang = [lang.code for lang in self.filtered(lambda l: l.active)]
        if active_lang:
            mods = self.env['ir.module.module'].search([('state', '=',
                                                         'installed')])
            mods._update_translations(active_lang)

    @api.model_create_multi
    def create(self, vals_list):
        self.clear_caches()
        for vals in vals_list:
            if not vals.get('url_code'):
                vals['url_code'] = vals.get('iso_code') or vals['code']
        return super(Lang, self).create(vals_list)

    def write(self, vals):
        lang_codes = self.mapped('code')
        if 'code' in vals and any(code != vals['code'] for code in lang_codes):
            raise UserError(_("Language code cannot be modified."))
        if vals.get('active') == False:
            if self.env['res.users'].search_count([('lang', 'in', lang_codes)
                                                   ]):
                raise UserError(
                    _("Cannot deactivate a language that is currently used by users."
                      ))
            if self.env['res.partner'].search_count([('lang', 'in', lang_codes)
                                                     ]):
                raise UserError(
                    _("Cannot deactivate a language that is currently used by contacts."
                      ))
            # delete linked ir.default specifying default partner's language
            self.env['ir.default'].discard_values('res.partner', 'lang',
                                                  lang_codes)

        res = super(Lang, self).write(vals)
        self.flush()
        self.clear_caches()
        return res

    def unlink(self):
        for language in self:
            if language.code == 'en_US':
                raise UserError(_("Base Language 'en_US' can not be deleted."))
            ctx_lang = self._context.get('lang')
            if ctx_lang and (language.code == ctx_lang):
                raise UserError(
                    _("You cannot delete the language which is the user's preferred language."
                      ))
            if language.active:
                raise UserError(
                    _("You cannot delete the language which is Active!\nPlease de-activate the language first."
                      ))
            self.env['ir.translation'].search([('lang', '=', language.code)
                                               ]).unlink()
        self.clear_caches()
        return super(Lang, self).unlink()

    def format(self, percent, value, grouping=False, monetary=False):
        """ Format() will return the language-specific output for float values"""
        self.ensure_one()
        if percent[0] != '%':
            raise ValueError(
                _("format() must be given exactly one %char format specifier"))

        formatted = percent % value

        # floats and decimal ints need special action!
        if grouping:
            lang_grouping, thousands_sep, decimal_point = self._data_get(
                monetary)
            eval_lang_grouping = ast.literal_eval(lang_grouping)

            if percent[-1] in 'eEfFgG':
                parts = formatted.split('.')
                parts[0] = intersperse(parts[0], eval_lang_grouping,
                                       thousands_sep)[0]

                formatted = decimal_point.join(parts)

            elif percent[-1] in 'diu':
                formatted = intersperse(formatted, eval_lang_grouping,
                                        thousands_sep)[0]

        return formatted
示例#27
0
class LunchProductCategory(models.Model):
    """ Category of the product such as pizza, sandwich, pasta, chinese, burger... """
    _name = 'lunch.product.category'
    _inherit = 'image.mixin'
    _description = 'Lunch Product Category'

    @api.model
    def _default_image(self):
        image_path = get_module_resource('lunch', 'static/img', 'lunch.png')
        return base64.b64encode(open(image_path, 'rb').read())

    name = fields.Char('Product Category', required=True, translate=True)
    company_id = fields.Many2one('res.company')
    currency_id = fields.Many2one('res.currency',
                                  related='company_id.currency_id')
    topping_label_1 = fields.Char('Extra 1 Label',
                                  required=True,
                                  default='Extras')
    topping_label_2 = fields.Char('Extra 2 Label',
                                  required=True,
                                  default='Beverages')
    topping_label_3 = fields.Char('Extra 3 Label',
                                  required=True,
                                  default='Extra Label 3')
    topping_ids_1 = fields.One2many('lunch.topping',
                                    'category_id',
                                    domain=[('topping_category', '=', 1)])
    topping_ids_2 = fields.One2many('lunch.topping',
                                    'category_id',
                                    domain=[('topping_category', '=', 2)])
    topping_ids_3 = fields.One2many('lunch.topping',
                                    'category_id',
                                    domain=[('topping_category', '=', 3)])
    topping_quantity_1 = fields.Selection([('0_more', 'None or More'),
                                           ('1_more', 'One or More'),
                                           ('1', 'Only One')],
                                          'Extra 1 Quantity',
                                          default='0_more',
                                          required=True)
    topping_quantity_2 = fields.Selection([('0_more', 'None or More'),
                                           ('1_more', 'One or More'),
                                           ('1', 'Only One')],
                                          'Extra 2 Quantity',
                                          default='0_more',
                                          required=True)
    topping_quantity_3 = fields.Selection([('0_more', 'None or More'),
                                           ('1_more', 'One or More'),
                                           ('1', 'Only One')],
                                          'Extra 3 Quantity',
                                          default='0_more',
                                          required=True)
    product_count = fields.Integer(
        compute='_compute_product_count',
        help="The number of products related to this category")
    active = fields.Boolean(string='Active', default=True)
    image_1920 = fields.Image(default=_default_image)

    def _compute_product_count(self):
        product_data = self.env['lunch.product'].read_group(
            [('category_id', 'in', self.ids)], ['category_id'],
            ['category_id'])
        data = {
            product['category_id'][0]: product['category_id_count']
            for product in product_data
        }
        for category in self:
            category.product_count = data.get(category.id, 0)

    @api.model
    def create(self, vals):
        for topping in vals.get('topping_ids_2', []):
            topping[2].update({'topping_category': 2})
        for topping in vals.get('topping_ids_3', []):
            topping[2].update({'topping_category': 3})
        return super(LunchProductCategory, self).create(vals)

    def write(self, vals):
        for topping in vals.get('topping_ids_2', []):
            topping_values = topping[2]
            if topping_values:
                topping_values.update({'topping_category': 2})
        for topping in vals.get('topping_ids_3', []):
            topping_values = topping[2]
            if topping_values:
                topping_values.update({'topping_category': 3})
        return super(LunchProductCategory, self).write(vals)

    def toggle_active(self):
        """ Archiving related lunch product """
        res = super().toggle_active()
        Product = self.env['lunch.product'].with_context(active_test=False)
        all_products = Product.search([('category_id', 'in', self.ids)])
        for category in self:
            all_products.filtered(lambda p: p.category_id == category and p.
                                  active != category.active).toggle_active()
        return res
示例#28
0
文件: product.py 项目: yomas33/odoo
class ProductProduct(models.Model):
    _name = "product.product"
    _description = "Product"
    _inherits = {'product.template': 'product_tmpl_id'}
    _inherit = ['mail.thread', 'mail.activity.mixin']
    _order = 'default_code, name, id'

    # price: total price, context dependent (partner, pricelist, quantity)
    price = fields.Float('Price',
                         compute='_compute_product_price',
                         digits='Product Price',
                         inverse='_set_product_price')
    # price_extra: catalog extra value only, sum of variant extra attributes
    price_extra = fields.Float(
        'Variant Price Extra',
        compute='_compute_product_price_extra',
        digits='Product Price',
        help="This is the sum of the extra price of all attributes")
    # lst_price: catalog value + extra, context dependent (uom)
    lst_price = fields.Float(
        'Public Price',
        compute='_compute_product_lst_price',
        digits='Product Price',
        inverse='_set_product_lst_price',
        help=
        "The sale price is managed from the product template. Click on the 'Configure Variants' button to set the extra attribute prices."
    )

    default_code = fields.Char('Internal Reference', index=True)
    code = fields.Char('Reference', compute='_compute_product_code')
    partner_ref = fields.Char('Customer Ref', compute='_compute_partner_ref')

    active = fields.Boolean(
        'Active',
        default=True,
        help=
        "If unchecked, it will allow you to hide the product without removing it."
    )
    product_tmpl_id = fields.Many2one('product.template',
                                      'Product Template',
                                      auto_join=True,
                                      index=True,
                                      ondelete="cascade",
                                      required=True)
    barcode = fields.Char(
        'Barcode',
        copy=False,
        help="International Article Number used for product identification.")
    product_template_attribute_value_ids = fields.Many2many(
        'product.template.attribute.value',
        relation='product_variant_combination',
        string="Attribute Values",
        ondelete='restrict')
    combination_indices = fields.Char(compute='_compute_combination_indices',
                                      store=True,
                                      index=True)
    is_product_variant = fields.Boolean(compute='_compute_is_product_variant')

    standard_price = fields.Float(
        'Cost',
        company_dependent=True,
        digits='Product Price',
        groups="base.group_user",
        help=
        """In Standard Price & AVCO: value of the product (automatically computed in AVCO).
        In FIFO: value of the last unit that left the stock (automatically computed).
        Used to value the product when the purchase cost is not known (e.g. inventory adjustment).
        Used to compute margins on sale orders.""")
    volume = fields.Float('Volume', digits='Volume')
    weight = fields.Float('Weight', digits='Stock Weight')

    pricelist_item_count = fields.Integer(
        "Number of price rules", compute="_compute_variant_item_count")

    packaging_ids = fields.One2many(
        'product.packaging',
        'product_id',
        'Product Packages',
        help="Gives the different ways to package the same product.")

    # all image fields are base64 encoded and PIL-supported

    # all image_variant fields are technical and should not be displayed to the user
    image_variant_1920 = fields.Image("Variant Image",
                                      max_width=1920,
                                      max_height=1920)

    # resized fields stored (as attachment) for performance
    image_variant_1024 = fields.Image("Variant Image 1024",
                                      related="image_variant_1920",
                                      max_width=1024,
                                      max_height=1024,
                                      store=True)
    image_variant_512 = fields.Image("Variant Image 512",
                                     related="image_variant_1920",
                                     max_width=512,
                                     max_height=512,
                                     store=True)
    image_variant_256 = fields.Image("Variant Image 256",
                                     related="image_variant_1920",
                                     max_width=256,
                                     max_height=256,
                                     store=True)
    image_variant_128 = fields.Image("Variant Image 128",
                                     related="image_variant_1920",
                                     max_width=128,
                                     max_height=128,
                                     store=True)
    can_image_variant_1024_be_zoomed = fields.Boolean(
        "Can Variant Image 1024 be zoomed",
        compute='_compute_can_image_variant_1024_be_zoomed',
        store=True)

    # Computed fields that are used to create a fallback to the template if
    # necessary, it's recommended to display those fields to the user.
    image_1920 = fields.Image("Image",
                              compute='_compute_image_1920',
                              inverse='_set_image_1920')
    image_1024 = fields.Image("Image 1024", compute='_compute_image_1024')
    image_512 = fields.Image("Image 512", compute='_compute_image_512')
    image_256 = fields.Image("Image 256", compute='_compute_image_256')
    image_128 = fields.Image("Image 128", compute='_compute_image_128')
    can_image_1024_be_zoomed = fields.Boolean(
        "Can Image 1024 be zoomed",
        compute='_compute_can_image_1024_be_zoomed')

    @api.depends('image_variant_1920', 'image_variant_1024')
    def _compute_can_image_variant_1024_be_zoomed(self):
        for record in self:
            record.can_image_variant_1024_be_zoomed = record.image_variant_1920 and tools.is_image_size_above(
                record.image_variant_1920, record.image_variant_1024)

    def _compute_image_1920(self):
        """Get the image from the template if no image is set on the variant."""
        for record in self:
            record.image_1920 = record.image_variant_1920 or record.product_tmpl_id.image_1920

    def _set_image_1920(self):
        for record in self:
            if (
                    # We are trying to remove an image even though it is already
                    # not set, remove it from the template instead.
                    not record.image_1920 and not record.image_variant_1920 or
                    # We are trying to add an image, but the template image is
                    # not set, write on the template instead.
                    record.image_1920 and not record.product_tmpl_id.image_1920
                    or
                    # There is only one variant, always write on the template.
                    self.search_count([
                        ('product_tmpl_id', '=', record.product_tmpl_id.id),
                        ('active', '=', True),
                    ]) <= 1):
                record.image_variant_1920 = False
                record.product_tmpl_id.image_1920 = record.image_1920
            else:
                record.image_variant_1920 = record.image_1920

    def _compute_image_1024(self):
        """Get the image from the template if no image is set on the variant."""
        for record in self:
            record.image_1024 = record.image_variant_1024 or record.product_tmpl_id.image_1024

    def _compute_image_512(self):
        """Get the image from the template if no image is set on the variant."""
        for record in self:
            record.image_512 = record.image_variant_512 or record.product_tmpl_id.image_512

    def _compute_image_256(self):
        """Get the image from the template if no image is set on the variant."""
        for record in self:
            record.image_256 = record.image_variant_256 or record.product_tmpl_id.image_256

    def _compute_image_128(self):
        """Get the image from the template if no image is set on the variant."""
        for record in self:
            record.image_128 = record.image_variant_128 or record.product_tmpl_id.image_128

    def _compute_can_image_1024_be_zoomed(self):
        """Get the image from the template if no image is set on the variant."""
        for record in self:
            record.can_image_1024_be_zoomed = record.can_image_variant_1024_be_zoomed if record.image_variant_1920 else record.product_tmpl_id.can_image_1024_be_zoomed

    def init(self):
        """Ensure there is at most one active variant for each combination.

        There could be no variant for a combination if using dynamic attributes.
        """
        self.env.cr.execute(
            "CREATE UNIQUE INDEX IF NOT EXISTS product_product_combination_unique ON %s (product_tmpl_id, combination_indices) WHERE active is true"
            % self._table)

    _sql_constraints = [
        ('barcode_uniq', 'unique(barcode)',
         "A barcode can only be assigned to one product !"),
    ]

    def _get_invoice_policy(self):
        return False

    @api.depends('product_template_attribute_value_ids')
    def _compute_combination_indices(self):
        for product in self:
            product.combination_indices = product.product_template_attribute_value_ids._ids2str(
            )

    def _compute_is_product_variant(self):
        for product in self:
            product.is_product_variant = True

    @api.depends_context('pricelist', 'partner', 'quantity', 'uom', 'date',
                         'no_variant_attributes_price_extra')
    def _compute_product_price(self):
        prices = {}
        pricelist_id_or_name = self._context.get('pricelist')
        if pricelist_id_or_name:
            pricelist = None
            partner = self.env.context.get('partner', False)
            quantity = self.env.context.get('quantity', 1.0)

            # Support context pricelists specified as list, display_name or ID for compatibility
            if isinstance(pricelist_id_or_name, list):
                pricelist_id_or_name = pricelist_id_or_name[0]
            if isinstance(pricelist_id_or_name, str):
                pricelist_name_search = self.env[
                    'product.pricelist'].name_search(pricelist_id_or_name,
                                                     operator='=',
                                                     limit=1)
                if pricelist_name_search:
                    pricelist = self.env['product.pricelist'].browse(
                        [pricelist_name_search[0][0]])
            elif isinstance(pricelist_id_or_name, int):
                pricelist = self.env['product.pricelist'].browse(
                    pricelist_id_or_name)

            if pricelist:
                quantities = [quantity] * len(self)
                partners = [partner] * len(self)
                prices = pricelist.get_products_price(self, quantities,
                                                      partners)

        for product in self:
            product.price = prices.get(product.id, 0.0)

    def _set_product_price(self):
        for product in self:
            if self._context.get('uom'):
                value = self.env['uom.uom'].browse(
                    self._context['uom'])._compute_price(
                        product.price, product.uom_id)
            else:
                value = product.price
            value -= product.price_extra
            product.write({'list_price': value})

    def _set_product_lst_price(self):
        for product in self:
            if self._context.get('uom'):
                value = self.env['uom.uom'].browse(
                    self._context['uom'])._compute_price(
                        product.lst_price, product.uom_id)
            else:
                value = product.lst_price
            value -= product.price_extra
            product.write({'list_price': value})

    def _compute_product_price_extra(self):
        for product in self:
            product.price_extra = sum(
                product.product_template_attribute_value_ids.mapped(
                    'price_extra'))

    @api.depends('list_price', 'price_extra')
    @api.depends_context('uom')
    def _compute_product_lst_price(self):
        to_uom = None
        if 'uom' in self._context:
            to_uom = self.env['uom.uom'].browse(self._context['uom'])

        for product in self:
            if to_uom:
                list_price = product.uom_id._compute_price(
                    product.list_price, to_uom)
            else:
                list_price = product.list_price
            product.lst_price = list_price + product.price_extra

    @api.depends_context('partner_id')
    def _compute_product_code(self):
        for product in self:
            for supplier_info in product.seller_ids:
                if supplier_info.name.id == product._context.get('partner_id'):
                    product.code = supplier_info.product_code or product.default_code
                    break
            else:
                product.code = product.default_code

    @api.depends_context('partner_id')
    def _compute_partner_ref(self):
        for product in self:
            for supplier_info in product.seller_ids:
                if supplier_info.name.id == product._context.get('partner_id'):
                    product_name = supplier_info.product_name or product.default_code or product.name
                    product.partner_ref = '%s%s' % (product.code
                                                    and '[%s] ' % product.code
                                                    or '', product_name)
                    break
            else:
                product.partner_ref = product.display_name

    def _compute_variant_item_count(self):
        for product in self:
            domain = [
                '|', '&', ('product_tmpl_id', '=', product.product_tmpl_id.id),
                ('applied_on', '=', '1_product'), '&',
                ('product_id', '=', product.id),
                ('applied_on', '=', '0_product_variant')
            ]
            product.pricelist_item_count = self.env[
                'product.pricelist.item'].search_count(domain)

    @api.onchange('uom_id', 'uom_po_id')
    def _onchange_uom(self):
        if self.uom_id and self.uom_po_id and self.uom_id.category_id != self.uom_po_id.category_id:
            self.uom_po_id = self.uom_id

    @api.model_create_multi
    def create(self, vals_list):
        products = super(
            ProductProduct,
            self.with_context(create_product_product=True)).create(vals_list)
        # `_get_variant_id_for_combination` depends on existing variants
        self.clear_caches()
        return products

    def write(self, values):
        res = super(ProductProduct, self).write(values)
        if 'product_template_attribute_value_ids' in values:
            # `_get_variant_id_for_combination` depends on `product_template_attribute_value_ids`
            self.clear_caches()
        if 'active' in values:
            # prefetched o2m have to be reloaded (because of active_test)
            # (eg. product.template: product_variant_ids)
            self.flush()
            self.invalidate_cache()
            # `_get_first_possible_variant_id` depends on variants active state
            self.clear_caches()
        return res

    def unlink(self):
        unlink_products = self.env['product.product']
        unlink_templates = self.env['product.template']
        for product in self:
            # If there is an image set on the variant and no image set on the
            # template, move the image to the template.
            if product.image_variant_1920 and not product.product_tmpl_id.image_1920:
                product.product_tmpl_id.image_1920 = product.image_variant_1920
            # Check if product still exists, in case it has been unlinked by unlinking its template
            if not product.exists():
                continue
            # Check if the product is last product of this template...
            other_products = self.search([('product_tmpl_id', '=',
                                           product.product_tmpl_id.id),
                                          ('id', '!=', product.id)])
            # ... and do not delete product template if it's configured to be created "on demand"
            if not other_products and not product.product_tmpl_id.has_dynamic_attributes(
            ):
                unlink_templates |= product.product_tmpl_id
            unlink_products |= product
        res = super(ProductProduct, unlink_products).unlink()
        # delete templates after calling super, as deleting template could lead to deleting
        # products due to ondelete='cascade'
        unlink_templates.unlink()
        # `_get_variant_id_for_combination` depends on existing variants
        self.clear_caches()
        return res

    def _unlink_or_archive(self, check_access=True):
        """Unlink or archive products.
        Try in batch as much as possible because it is much faster.
        Use dichotomy when an exception occurs.
        """

        # Avoid access errors in case the products is shared amongst companies
        # but the underlying objects are not. If unlink fails because of an
        # AccessError (e.g. while recomputing fields), the 'write' call will
        # fail as well for the same reason since the field has been set to
        # recompute.
        if check_access:
            self.check_access_rights('unlink')
            self.check_access_rule('unlink')
            self.check_access_rights('write')
            self.check_access_rule('write')
            self = self.sudo()

        try:
            with self.env.cr.savepoint(), tools.mute_logger('odoo.sql_db'):
                self.unlink()
        except Exception:
            # We catch all kind of exceptions to be sure that the operation
            # doesn't fail.
            if len(self) > 1:
                self[:len(self) // 2]._unlink_or_archive(check_access=False)
                self[len(self) // 2:]._unlink_or_archive(check_access=False)
            else:
                if self.active:
                    # Note: this can still fail if something is preventing
                    # from archiving.
                    # This is the case from existing stock reordering rules.
                    self.write({'active': False})

    @api.returns('self', lambda value: value.id)
    def copy(self, default=None):
        """Variants are generated depending on the configuration of attributes
        and values on the template, so copying them does not make sense.

        For convenience the template is copied instead and its first variant is
        returned.
        """
        return self.product_tmpl_id.copy(default=default).product_variant_id

    @api.model
    def _search(self,
                args,
                offset=0,
                limit=None,
                order=None,
                count=False,
                access_rights_uid=None):
        # TDE FIXME: strange
        if self._context.get('search_default_categ_id'):
            args.append((('categ_id', 'child_of',
                          self._context['search_default_categ_id'])))
        return super(ProductProduct,
                     self)._search(args,
                                   offset=offset,
                                   limit=limit,
                                   order=order,
                                   count=count,
                                   access_rights_uid=access_rights_uid)

    def name_get(self):
        # TDE: this could be cleaned a bit I think

        def _name_get(d):
            name = d.get('name', '')
            code = self._context.get('display_default_code', True) and d.get(
                'default_code', False) or False
            if code:
                name = '[%s] %s' % (code, name)
            return (d['id'], name)

        partner_id = self._context.get('partner_id')
        if partner_id:
            partner_ids = [
                partner_id, self.env['res.partner'].browse(
                    partner_id).commercial_partner_id.id
            ]
        else:
            partner_ids = []
        company_id = self.env.context.get('company_id')

        # all user don't have access to seller and partner
        # check access and use superuser
        self.check_access_rights("read")
        self.check_access_rule("read")

        result = []

        # Prefetch the fields used by the `name_get`, so `browse` doesn't fetch other fields
        # Use `load=False` to not call `name_get` for the `product_tmpl_id`
        self.sudo().read(['name', 'default_code', 'product_tmpl_id'],
                         load=False)

        product_template_ids = self.sudo().mapped('product_tmpl_id').ids

        if partner_ids:
            supplier_info = self.env['product.supplierinfo'].sudo().search([
                ('product_tmpl_id', 'in', product_template_ids),
                ('name', 'in', partner_ids),
            ])
            # Prefetch the fields used by the `name_get`, so `browse` doesn't fetch other fields
            # Use `load=False` to not call `name_get` for the `product_tmpl_id` and `product_id`
            supplier_info.sudo().read([
                'product_tmpl_id', 'product_id', 'product_name', 'product_code'
            ],
                                      load=False)
            supplier_info_by_template = {}
            for r in supplier_info:
                supplier_info_by_template.setdefault(r.product_tmpl_id,
                                                     []).append(r)
        for product in self.sudo():
            variant = product.product_template_attribute_value_ids._get_combination_name(
            )

            name = variant and "%s (%s)" % (product.name,
                                            variant) or product.name
            sellers = []
            if partner_ids:
                product_supplier_info = supplier_info_by_template.get(
                    product.product_tmpl_id, [])
                sellers = [
                    x for x in product_supplier_info
                    if x.product_id and x.product_id == product
                ]
                if not sellers:
                    sellers = [
                        x for x in product_supplier_info if not x.product_id
                    ]
                # Filter out sellers based on the company. This is done afterwards for a better
                # code readability. At this point, only a few sellers should remain, so it should
                # not be a performance issue.
                if company_id:
                    sellers = [
                        x for x in sellers
                        if x.company_id.id in [company_id, False]
                    ]
            if sellers:
                for s in sellers:
                    seller_variant = s.product_name and (
                        variant and "%s (%s)" %
                        (s.product_name, variant) or s.product_name) or False
                    mydict = {
                        'id': product.id,
                        'name': seller_variant or name,
                        'default_code': s.product_code or product.default_code,
                    }
                    temp = _name_get(mydict)
                    if temp not in result:
                        result.append(temp)
            else:
                mydict = {
                    'id': product.id,
                    'name': name,
                    'default_code': product.default_code,
                }
                result.append(_name_get(mydict))
        return result

    @api.model
    def _name_search(self,
                     name,
                     args=None,
                     operator='ilike',
                     limit=100,
                     name_get_uid=None):
        if not args:
            args = []
        if name:
            positive_operators = ['=', 'ilike', '=ilike', 'like', '=like']
            product_ids = []
            if operator in positive_operators:
                product_ids = self._search([('default_code', '=', name)] +
                                           args,
                                           limit=limit,
                                           access_rights_uid=name_get_uid)
                if not product_ids:
                    product_ids = self._search([('barcode', '=', name)] + args,
                                               limit=limit,
                                               access_rights_uid=name_get_uid)
            if not product_ids and operator not in expression.NEGATIVE_TERM_OPERATORS:
                # Do not merge the 2 next lines into one single search, SQL search performance would be abysmal
                # on a database with thousands of matching products, due to the huge merge+unique needed for the
                # OR operator (and given the fact that the 'name' lookup results come from the ir.translation table
                # Performing a quick memory merge of ids in Python will give much better performance
                product_ids = self._search(args +
                                           [('default_code', operator, name)],
                                           limit=limit)
                if not limit or len(product_ids) < limit:
                    # we may underrun the limit because of dupes in the results, that's fine
                    limit2 = (limit - len(product_ids)) if limit else False
                    product2_ids = self._search(
                        args + [('name', operator, name),
                                ('id', 'not in', product_ids)],
                        limit=limit2,
                        access_rights_uid=name_get_uid)
                    product_ids.extend(product2_ids)
            elif not product_ids and operator in expression.NEGATIVE_TERM_OPERATORS:
                domain = expression.OR([
                    [
                        '&', ('default_code', operator, name),
                        ('name', operator, name)
                    ],
                    [
                        '&', ('default_code', '=', False),
                        ('name', operator, name)
                    ],
                ])
                domain = expression.AND([args, domain])
                product_ids = self._search(domain,
                                           limit=limit,
                                           access_rights_uid=name_get_uid)
            if not product_ids and operator in positive_operators:
                ptrn = re.compile('(\[(.*?)\])')
                res = ptrn.search(name)
                if res:
                    product_ids = self._search(
                        [('default_code', '=', res.group(2))] + args,
                        limit=limit,
                        access_rights_uid=name_get_uid)
            # still no results, partner in context: search on supplier info as last hope to find something
            if not product_ids and self._context.get('partner_id'):
                suppliers_ids = self.env['product.supplierinfo']._search(
                    [('name', '=', self._context.get('partner_id')), '|',
                     ('product_code', operator, name),
                     ('product_name', operator, name)],
                    access_rights_uid=name_get_uid)
                if suppliers_ids:
                    product_ids = self._search(
                        [('product_tmpl_id.seller_ids', 'in', suppliers_ids)],
                        limit=limit,
                        access_rights_uid=name_get_uid)
        else:
            product_ids = self._search(args,
                                       limit=limit,
                                       access_rights_uid=name_get_uid)
        return models.lazy_name_get(
            self.browse(product_ids).with_user(name_get_uid))

    @api.model
    def view_header_get(self, view_id, view_type):
        res = super(ProductProduct, self).view_header_get(view_id, view_type)
        if self._context.get('categ_id'):
            return _('Products: ') + self.env['product.category'].browse(
                self._context['categ_id']).name
        return res

    def open_pricelist_rules(self):
        self.ensure_one()
        domain = [
            '|', '&', ('product_tmpl_id', '=', self.product_tmpl_id.id),
            ('applied_on', '=', '1_product'), '&',
            ('product_id', '=', self.id),
            ('applied_on', '=', '0_product_variant')
        ]
        return {
            'name':
            _('Price Rules'),
            'view_mode':
            'tree,form',
            'views': [(self.env.ref(
                'product.product_pricelist_item_tree_view_from_product').id,
                       'tree'), (False, 'form')],
            'res_model':
            'product.pricelist.item',
            'type':
            'ir.actions.act_window',
            'target':
            'current',
            'domain':
            domain,
            'context': {
                'default_product_id': self.id,
                'default_applied_on': '0_product_variant',
            }
        }

    def open_product_template(self):
        """ Utility method used to add an "Open Template" button in product views """
        self.ensure_one()
        return {
            'type': 'ir.actions.act_window',
            'res_model': 'product.template',
            'view_mode': 'form',
            'res_id': self.product_tmpl_id.id,
            'target': 'new'
        }

    def _prepare_sellers(self, params=False):
        return self.seller_ids.filtered(lambda s: s.name.active).sorted(
            lambda s: (s.sequence, -s.min_qty, s.price))

    def _select_seller(self,
                       partner_id=False,
                       quantity=0.0,
                       date=None,
                       uom_id=False,
                       params=False):
        self.ensure_one()
        if date is None:
            date = fields.Date.context_today(self)
        precision = self.env['decimal.precision'].precision_get(
            'Product Unit of Measure')

        res = self.env['product.supplierinfo']
        sellers = self._prepare_sellers(params)
        sellers = sellers.filtered(lambda s: not s.company_id or s.company_id.
                                   id == self.env.company.id)
        for seller in sellers:
            # Set quantity in UoM of seller
            quantity_uom_seller = quantity
            if quantity_uom_seller and uom_id and uom_id != seller.product_uom:
                quantity_uom_seller = uom_id._compute_quantity(
                    quantity_uom_seller, seller.product_uom)

            if seller.date_start and seller.date_start > date:
                continue
            if seller.date_end and seller.date_end < date:
                continue
            if partner_id and seller.name not in [
                    partner_id, partner_id.parent_id
            ]:
                continue
            if float_compare(quantity_uom_seller,
                             seller.min_qty,
                             precision_digits=precision) == -1:
                continue
            if seller.product_id and seller.product_id != self:
                continue
            if not res or res.name == seller.name:
                res |= seller
        return res.sorted('price')[:1]

    def price_compute(self,
                      price_type,
                      uom=False,
                      currency=False,
                      company=None):
        # TDE FIXME: delegate to template or not ? fields are reencoded here ...
        # compatibility about context keys used a bit everywhere in the code
        if not uom and self._context.get('uom'):
            uom = self.env['uom.uom'].browse(self._context['uom'])
        if not currency and self._context.get('currency'):
            currency = self.env['res.currency'].browse(
                self._context['currency'])

        products = self
        if price_type == 'standard_price':
            # standard_price field can only be seen by users in base.group_user
            # Thus, in order to compute the sale price from the cost for users not in this group
            # We fetch the standard price as the superuser
            products = self.with_company(company or self.env.company).sudo()

        prices = dict.fromkeys(self.ids, 0.0)
        for product in products:
            prices[product.id] = product[price_type] or 0.0
            if price_type == 'list_price':
                prices[product.id] += product.price_extra
                # we need to add the price from the attributes that do not generate variants
                # (see field product.attribute create_variant)
                if self._context.get('no_variant_attributes_price_extra'):
                    # we have a list of price_extra that comes from the attribute values, we need to sum all that
                    prices[product.id] += sum(
                        self._context.get('no_variant_attributes_price_extra'))

            if uom:
                prices[product.id] = product.uom_id._compute_price(
                    prices[product.id], uom)

            # Convert from current user company currency to asked one
            # This is right cause a field cannot be in more than one currency
            if currency:
                prices[product.id] = product.currency_id._convert(
                    prices[product.id], currency, product.company_id,
                    fields.Date.today())

        return prices

    @api.model
    def get_empty_list_help(self, help):
        self = self.with_context(empty_list_help_document_name=_("product"), )
        return super(ProductProduct, self).get_empty_list_help(help)

    def get_product_multiline_description_sale(self):
        """ Compute a multiline description of this product, in the context of sales
                (do not use for purchases or other display reasons that don't intend to use "description_sale").
            It will often be used as the default description of a sale order line referencing this product.
        """
        name = self.display_name
        if self.description_sale:
            name += '\n' + self.description_sale

        return name

    def _is_variant_possible(self, parent_combination=None):
        """Return whether the variant is possible based on its own combination,
        and optionally a parent combination.

        See `_is_combination_possible` for more information.

        This will always exclude variants for templates that have `no_variant`
        attributes because the variant itself will not be the full combination.

        :param parent_combination: combination from which `self` is an
            optional or accessory product.
        :type parent_combination: recordset `product.template.attribute.value`

        :return: ẁhether the variant is possible based on its own combination
        :rtype: bool
        """
        self.ensure_one()
        return self.product_tmpl_id._is_combination_possible(
            self.product_template_attribute_value_ids,
            parent_combination=parent_combination)

    def toggle_active(self):
        """ Archiving related product.template if there is only one active product.product """
        with_one_active = self.filtered(lambda product: len(
            product.product_tmpl_id.product_variant_ids) == 1)
        for product in with_one_active:
            product.product_tmpl_id.toggle_active()
        return super(ProductProduct, self - with_one_active).toggle_active()
示例#29
0
class Track(models.Model):
    _name = "event.track"
    _inherit = ['event.track']

    # status management
    is_accepted = fields.Boolean('Is Accepted',
                                 related='stage_id.is_accepted',
                                 readonly=True)
    # speaker
    partner_biography = fields.Html(string='Biography',
                                    compute='_compute_partner_biography',
                                    readonly=False,
                                    store=True)
    partner_function = fields.Char('Job Position',
                                   related='partner_id.function',
                                   compute_sudo=True,
                                   readonly=True)
    partner_company_name = fields.Char('Company Name',
                                       related='partner_id.parent_name',
                                       compute_sudo=True,
                                       readonly=True)
    image = fields.Image(string="Speaker Photo",
                         compute="_compute_speaker_image",
                         readonly=False,
                         store=True,
                         max_width=256,
                         max_height=256)
    # frontend description
    website_image = fields.Image(string="Website Image",
                                 max_width=1024,
                                 max_height=1024)
    website_image_url = fields.Char(string='Image URL',
                                    compute='_compute_website_image_url',
                                    compute_sudo=True,
                                    store=False)
    # wishlist / visitors management
    event_track_visitor_ids = fields.One2many('event.track.visitor',
                                              'track_id',
                                              string="Track Visitors",
                                              groups="event.group_event_user")
    is_reminder_on = fields.Boolean('Is Reminder On',
                                    compute='_compute_is_reminder_on')
    wishlist_visitor_ids = fields.Many2many(
        'website.visitor',
        string="Visitor Wishlist",
        compute="_compute_wishlist_visitor_ids",
        compute_sudo=True,
        search="_search_wishlist_visitor_ids",
        groups="event.group_event_user")
    wishlist_visitor_count = fields.Integer(
        string="# Wishlisted",
        compute="_compute_wishlist_visitor_ids",
        compute_sudo=True,
        groups="event.group_event_user")
    wishlisted_by_default = fields.Boolean(
        string='Always Wishlisted',
        help=
        """If set, the talk will be starred for each attendee registered to the event. The attendee won't be able to un-star this talk."""
    )

    # SPEAKER

    @api.depends('partner_id')
    def _compute_partner_biography(self):
        for track in self:
            if not track.partner_biography:
                track.partner_biography = track.partner_id.website_description
            elif track.partner_id and is_html_empty(track.partner_biography) and \
                not is_html_empty(track.partner_id.website_description):
                track.partner_biography = track.partner_id.website_description

    @api.depends('partner_id')
    def _compute_speaker_image(self):
        for track in self:
            if not track.image:
                track.image = track.partner_id.image_256

    # FRONTEND DESCRIPTION

    @api.depends('image', 'partner_id.image_256')
    def _compute_website_image_url(self):
        for track in self:
            if track.website_image:
                track.website_image_url = self.env['website'].image_url(
                    track, 'website_image', size=1024)
            else:
                track.website_image_url = '/website_event_track/static/src/img/event_track_default_%d.jpeg' % (
                    track.id % 2)

    # WISHLIST / VISITOR MANAGEMENT

    @api.depends('wishlisted_by_default', 'event_track_visitor_ids.visitor_id',
                 'event_track_visitor_ids.partner_id',
                 'event_track_visitor_ids.is_wishlisted',
                 'event_track_visitor_ids.is_blacklisted')
    @api.depends_context('uid')
    def _compute_is_reminder_on(self):
        current_visitor = self.env[
            'website.visitor']._get_visitor_from_request(force_create=False)
        if self.env.user._is_public() and not current_visitor:
            for track in self:
                track.is_reminder_on = track.wishlisted_by_default
        else:
            if self.env.user._is_public():
                domain = [('visitor_id', '=', current_visitor.id)]
            elif current_visitor:
                domain = [
                    '|', ('partner_id', '=', self.env.user.partner_id.id),
                    ('visitor_id', '=', current_visitor.id)
                ]
            else:
                domain = [('partner_id', '=', self.env.user.partner_id.id)]

            event_track_visitors = self.env['event.track.visitor'].sudo(
            ).search_read(
                expression.AND([domain, [('track_id', 'in', self.ids)]]),
                fields=['track_id', 'is_wishlisted', 'is_blacklisted'])

            wishlist_map = {
                track_visitor['track_id'][0]: {
                    'is_wishlisted': track_visitor['is_wishlisted'],
                    'is_blacklisted': track_visitor['is_blacklisted']
                }
                for track_visitor in event_track_visitors
            }
            for track in self:
                if wishlist_map.get(track.id):
                    track.is_reminder_on = wishlist_map.get(
                        track.id)['is_wishlisted'] or (
                            track.wishlisted_by_default
                            and not wishlist_map[track.id]['is_blacklisted'])
                else:
                    track.is_reminder_on = track.wishlisted_by_default

    @api.depends('event_track_visitor_ids.visitor_id',
                 'event_track_visitor_ids.is_wishlisted')
    def _compute_wishlist_visitor_ids(self):
        results = self.env['event.track.visitor'].read_group(
            [('track_id', 'in', self.ids), ('is_wishlisted', '=', True)],
            ['track_id', 'visitor_id:array_agg'], ['track_id'])
        visitor_ids_map = {
            result['track_id'][0]: result['visitor_id']
            for result in results
        }
        for track in self:
            track.wishlist_visitor_ids = visitor_ids_map.get(track.id, [])
            track.wishlist_visitor_count = len(
                visitor_ids_map.get(track.id, []))

    def _search_wishlist_visitor_ids(self, operator, operand):
        if operator == "not in":
            raise NotImplementedError(
                "Unsupported 'Not In' operation on track wishlist visitors")

        track_visitors = self.env['event.track.visitor'].sudo().search([
            ('visitor_id', operator, operand), ('is_wishlisted', '=', True)
        ])
        return [('id', 'in', track_visitors.track_id.ids)]

    # ------------------------------------------------------------
    # CRUD
    # -----------------------------------------------------------

    @api.model_create_multi
    def create(self, vals_list):
        tracks = super(Track, self).create(vals_list)

        for track in tracks:
            track._synchronize_with_stage(track.stage_id)

        return tracks

    def write(self, vals):
        if vals.get('stage_id'):
            stage = self.env['event.track.stage'].browse(vals['stage_id'])
            self._synchronize_with_stage(stage)
        return super(Track, self).write(vals)

    def _synchronize_with_stage(self, stage):
        if stage.is_done:
            self.is_published = True

    # ------------------------------------------------------------
    # TOOLS
    # ------------------------------------------------------------

    def _get_event_track_visitors(self, force_create=False):
        self.ensure_one()

        force_visitor_create = self.env.user._is_public()
        visitor_sudo = self.env['website.visitor']._get_visitor_from_request(
            force_create=force_visitor_create)
        if visitor_sudo:
            visitor_sudo._update_visitor_last_visit()

        if self.env.user._is_public():
            domain = [('visitor_id', '=', visitor_sudo.id)]
        elif visitor_sudo:
            domain = [
                '|', ('partner_id', '=', self.env.user.partner_id.id),
                ('visitor_id', '=', visitor_sudo.id)
            ]
        else:
            domain = [('partner_id', '=', self.env.user.partner_id.id)]

        track_visitors = self.env['event.track.visitor'].sudo().search(
            expression.AND([domain, [('track_id', 'in', self.ids)]]))
        missing = self - track_visitors.track_id
        if missing and force_create:
            track_visitors += self.env['event.track.visitor'].sudo().create([{
                'visitor_id':
                visitor_sudo.id,
                'partner_id':
                self.env.user.partner_id.id
                if not self.env.user._is_public() else False,
                'track_id':
                track.id,
            } for track in missing])

        return track_visitors
示例#30
0
class HrEmployeePrivate(models.Model):
    """
    NB: Any field only available on the model hr.employee (i.e. not on the
    hr.employee.public model) should have `groups="hr.group_hr_user"` on its
    definition to avoid being prefetched when the user hasn't access to the
    hr.employee model. Indeed, the prefetch loads the data for all the fields
    that are available according to the group defined on them.
    """
    _name = "hr.employee"
    _description = "Employee"
    _order = 'name'
    _inherit = [
        'hr.employee.base', 'mail.thread', 'mail.activity.mixin',
        'resource.mixin', 'image.mixin'
    ]
    _mail_post_access = 'read'

    # resource and user
    # required on the resource, make sure required="True" set in the view
    name = fields.Char(string="Employee Name",
                       related='resource_id.name',
                       store=True,
                       readonly=False,
                       tracking=True)
    user_id = fields.Many2one('res.users',
                              'User',
                              related='resource_id.user_id',
                              store=True,
                              readonly=False)
    user_partner_id = fields.Many2one(related='user_id.partner_id',
                                      related_sudo=False,
                                      string="User's partner")
    active = fields.Boolean('Active',
                            related='resource_id.active',
                            default=True,
                            store=True,
                            readonly=False)
    company_id = fields.Many2one('res.company', required=True)
    # private partner
    address_home_id = fields.Many2one(
        'res.partner',
        'Address',
        help=
        'Enter here the private address of the employee, not the one linked to your company.',
        groups="hr.group_hr_user",
        tracking=True,
        domain=
        "['|', ('company_id', '=', False), ('company_id', '=', company_id)]")
    is_address_home_a_company = fields.Boolean(
        'The employee address has a company linked',
        compute='_compute_is_address_home_a_company',
    )
    private_email = fields.Char(related='address_home_id.email',
                                string="Private Email",
                                groups="hr.group_hr_user")
    lang = fields.Selection(related='address_home_id.lang',
                            string="Lang",
                            groups="hr.group_hr_user",
                            readonly=False)
    country_id = fields.Many2one('res.country',
                                 'Nationality (Country)',
                                 groups="hr.group_hr_user",
                                 tracking=True)
    gender = fields.Selection([('male', 'Male'), ('female', 'Female'),
                               ('other', 'Other')],
                              groups="hr.group_hr_user",
                              tracking=True)
    marital = fields.Selection([('single', 'Single'), ('married', 'Married'),
                                ('cohabitant', 'Legal Cohabitant'),
                                ('widower', 'Widower'),
                                ('divorced', 'Divorced')],
                               string='Marital Status',
                               groups="hr.group_hr_user",
                               default='single',
                               tracking=True)
    spouse_complete_name = fields.Char(string="Spouse Complete Name",
                                       groups="hr.group_hr_user",
                                       tracking=True)
    spouse_birthdate = fields.Date(string="Spouse Birthdate",
                                   groups="hr.group_hr_user",
                                   tracking=True)
    children = fields.Integer(string='Number of Children',
                              groups="hr.group_hr_user",
                              tracking=True)
    place_of_birth = fields.Char('Place of Birth',
                                 groups="hr.group_hr_user",
                                 tracking=True)
    country_of_birth = fields.Many2one('res.country',
                                       string="Country of Birth",
                                       groups="hr.group_hr_user",
                                       tracking=True)
    birthday = fields.Date('Date of Birth',
                           groups="hr.group_hr_user",
                           tracking=True)
    ssnid = fields.Char('SSN No',
                        help='Social Security Number',
                        groups="hr.group_hr_user",
                        tracking=True)
    sinid = fields.Char('SIN No',
                        help='Social Insurance Number',
                        groups="hr.group_hr_user",
                        tracking=True)
    identification_id = fields.Char(string='Identification No',
                                    groups="hr.group_hr_user",
                                    tracking=True)
    passport_id = fields.Char('Passport No',
                              groups="hr.group_hr_user",
                              tracking=True)
    bank_account_id = fields.Many2one(
        'res.partner.bank',
        'Bank Account Number',
        domain=
        "[('partner_id', '=', address_home_id), '|', ('company_id', '=', False), ('company_id', '=', company_id)]",
        groups="hr.group_hr_user",
        tracking=True,
        help='Employee bank salary account')
    permit_no = fields.Char('Work Permit No',
                            groups="hr.group_hr_user",
                            tracking=True)
    visa_no = fields.Char('Visa No', groups="hr.group_hr_user", tracking=True)
    visa_expire = fields.Date('Visa Expire Date',
                              groups="hr.group_hr_user",
                              tracking=True)
    additional_note = fields.Text(string='Additional Note',
                                  groups="hr.group_hr_user",
                                  tracking=True)
    certificate = fields.Selection([
        ('graduate', 'Graduate'),
        ('bachelor', 'Bachelor'),
        ('master', 'Master'),
        ('doctor', 'Doctor'),
        ('other', 'Other'),
    ],
                                   'Certificate Level',
                                   default='other',
                                   groups="hr.group_hr_user",
                                   tracking=True)
    study_field = fields.Char("Field of Study",
                              groups="hr.group_hr_user",
                              tracking=True)
    study_school = fields.Char("School",
                               groups="hr.group_hr_user",
                               tracking=True)
    emergency_contact = fields.Char("Emergency Contact",
                                    groups="hr.group_hr_user",
                                    tracking=True)
    emergency_phone = fields.Char("Emergency Phone",
                                  groups="hr.group_hr_user",
                                  tracking=True)
    km_home_work = fields.Integer(string="Home-Work Distance",
                                  groups="hr.group_hr_user",
                                  tracking=True)

    job_id = fields.Many2one(tracking=True)
    image_1920 = fields.Image()
    phone = fields.Char(related='address_home_id.phone',
                        related_sudo=False,
                        readonly=False,
                        string="Private Phone",
                        groups="hr.group_hr_user")
    # employee in company
    child_ids = fields.One2many('hr.employee',
                                'parent_id',
                                string='Direct subordinates')
    category_ids = fields.Many2many('hr.employee.category',
                                    'employee_category_rel',
                                    'emp_id',
                                    'category_id',
                                    groups="hr.group_hr_manager",
                                    string='Tags')
    # misc
    notes = fields.Text('Notes', groups="hr.group_hr_user")
    color = fields.Integer('Color Index', default=0, groups="hr.group_hr_user")
    barcode = fields.Char(string="Badge ID",
                          help="ID used for employee identification.",
                          groups="hr.group_hr_user",
                          copy=False)
    pin = fields.Char(
        string="PIN",
        groups="hr.group_hr_user",
        copy=False,
        help=
        "PIN used to Check In/Out in the Kiosk Mode of the Attendance application (if enabled in Configuration) and to change the cashier in the Point of Sale application."
    )
    departure_reason_id = fields.Many2one("hr.departure.reason",
                                          string="Departure Reason",
                                          groups="hr.group_hr_user",
                                          copy=False,
                                          tracking=True,
                                          ondelete='restrict')
    departure_description = fields.Text(string="Additional Information",
                                        groups="hr.group_hr_user",
                                        copy=False,
                                        tracking=True)
    departure_date = fields.Date(string="Departure Date",
                                 groups="hr.group_hr_user",
                                 copy=False,
                                 tracking=True)
    message_main_attachment_id = fields.Many2one(groups="hr.group_hr_user")

    _sql_constraints = [
        ('barcode_uniq', 'unique (barcode)',
         "The Badge ID must be unique, this one is already assigned to another employee."
         ),
        ('user_uniq', 'unique (user_id, company_id)',
         "A user cannot be linked to multiple employees in the same company.")
    ]

    def _get_placeholder_filename(self, field=None):
        image_fields = [
            'image_%s' % size for size in [1920, 1024, 512, 256, 128]
        ]
        if field in image_fields:
            return 'hr/static/src/img/default_image.png'
        return super()._get_placeholder_filename(field=field)

    def name_get(self):
        if self.check_access_rights('read', raise_exception=False):
            return super(HrEmployeePrivate, self).name_get()
        return self.env['hr.employee.public'].browse(self.ids).name_get()

    def _read(self, fields):
        if self.check_access_rights('read', raise_exception=False):
            return super(HrEmployeePrivate, self)._read(fields)

        res = self.env['hr.employee.public'].browse(self.ids).read(fields)
        for r in res:
            record = self.browse(r['id'])
            record._update_cache({k: v
                                  for k, v in r.items() if k in fields},
                                 validate=False)

    def read(self, fields, load='_classic_read'):
        if self.check_access_rights('read', raise_exception=False):
            return super(HrEmployeePrivate, self).read(fields, load=load)
        private_fields = set(fields).difference(
            self.env['hr.employee.public']._fields.keys())
        if private_fields:
            raise AccessError(
                _('The fields "%s" you try to read is not available on the public employee profile.'
                  ) % (','.join(private_fields)))
        return self.env['hr.employee.public'].browse(self.ids).read(fields,
                                                                    load=load)

    @api.model
    def load_views(self, views, options=None):
        if self.check_access_rights('read', raise_exception=False):
            return super(HrEmployeePrivate, self).load_views(views,
                                                             options=options)
        return self.env['hr.employee.public'].load_views(views,
                                                         options=options)

    @api.model
    def _search(self,
                args,
                offset=0,
                limit=None,
                order=None,
                count=False,
                access_rights_uid=None):
        """
            We override the _search because it is the method that checks the access rights
            This is correct to override the _search. That way we enforce the fact that calling
            search on an hr.employee returns a hr.employee recordset, even if you don't have access
            to this model, as the result of _search (the ids of the public employees) is to be
            browsed on the hr.employee model. This can be trusted as the ids of the public
            employees exactly match the ids of the related hr.employee.
        """
        if self.check_access_rights('read', raise_exception=False):
            return super(HrEmployeePrivate,
                         self)._search(args,
                                       offset=offset,
                                       limit=limit,
                                       order=order,
                                       count=count,
                                       access_rights_uid=access_rights_uid)
        ids = self.env['hr.employee.public']._search(
            args,
            offset=offset,
            limit=limit,
            order=order,
            count=count,
            access_rights_uid=access_rights_uid)
        if not count and isinstance(ids, Query):
            # the result is expected from this table, so we should link tables
            ids = super(HrEmployeePrivate,
                        self.sudo())._search([('id', 'in', ids)])
        return ids

    def get_formview_id(self, access_uid=None):
        """ Override this method in order to redirect many2one towards the right model depending on access_uid """
        if access_uid:
            self_sudo = self.with_user(access_uid)
        else:
            self_sudo = self

        if self_sudo.check_access_rights('read', raise_exception=False):
            return super(HrEmployeePrivate,
                         self).get_formview_id(access_uid=access_uid)
        # Hardcode the form view for public employee
        return self.env.ref('hr.hr_employee_public_view_form').id

    def get_formview_action(self, access_uid=None):
        """ Override this method in order to redirect many2one towards the right model depending on access_uid """
        res = super(HrEmployeePrivate,
                    self).get_formview_action(access_uid=access_uid)
        if access_uid:
            self_sudo = self.with_user(access_uid)
        else:
            self_sudo = self

        if not self_sudo.check_access_rights('read', raise_exception=False):
            res['res_model'] = 'hr.employee.public'

        return res

    @api.constrains('pin')
    def _verify_pin(self):
        for employee in self:
            if employee.pin and not employee.pin.isdigit():
                raise ValidationError(
                    _("The PIN must be a sequence of digits."))

    @api.onchange('user_id')
    def _onchange_user(self):
        if self.user_id:
            self.update(self._sync_user(self.user_id, (bool(self.image_1920))))
            if not self.name:
                self.name = self.user_id.name

    @api.onchange('resource_calendar_id')
    def _onchange_timezone(self):
        if self.resource_calendar_id and not self.tz:
            self.tz = self.resource_calendar_id.tz

    def _sync_user(self, user, employee_has_image=False):
        vals = dict(
            work_email=user.email,
            user_id=user.id,
        )
        if not employee_has_image:
            vals['image_1920'] = user.image_1920
        if user.tz:
            vals['tz'] = user.tz
        return vals

    @api.model
    def create(self, vals):
        if vals.get('user_id'):
            user = self.env['res.users'].browse(vals['user_id'])
            vals.update(self._sync_user(user, bool(vals.get('image_1920'))))
            vals['name'] = vals.get('name', user.name)
        employee = super(HrEmployeePrivate, self).create(vals)
        url = '/web#%s' % url_encode(
            {
                'action': 'hr.plan_wizard_action',
                'active_id': employee.id,
                'active_model': 'hr.employee',
                'menu_id': self.env.ref('hr.menu_hr_root').id,
            })
        employee._message_log(body=_(
            '<b>Congratulations!</b> May I recommend you to setup an <a href="%s">onboarding plan?</a>'
        ) % (url))
        if employee.department_id:
            self.env['mail.channel'].sudo().search([
                ('subscription_department_ids', 'in',
                 employee.department_id.id)
            ])._subscribe_users_automatically()
        return employee

    def write(self, vals):
        if 'address_home_id' in vals:
            account_id = vals.get('bank_account_id') or self.bank_account_id.id
            if account_id:
                self.env['res.partner.bank'].browse(
                    account_id).partner_id = vals['address_home_id']
        if vals.get('user_id'):
            # Update the profile pictures with user, except if provided
            vals.update(
                self._sync_user(self.env['res.users'].browse(vals['user_id']),
                                (bool(self.image_1920))))
        res = super(HrEmployeePrivate, self).write(vals)
        if vals.get('department_id') or vals.get('user_id'):
            department_id = vals['department_id'] if vals.get(
                'department_id') else self[:1].department_id.id
            # When added to a department or changing user, subscribe to the channels auto-subscribed by department
            self.env['mail.channel'].sudo().search([
                ('subscription_department_ids', 'in', department_id)
            ])._subscribe_users_automatically()
        return res

    def unlink(self):
        resources = self.mapped('resource_id')
        super(HrEmployeePrivate, self).unlink()
        return resources.unlink()

    def toggle_active(self):
        res = super(HrEmployeePrivate, self).toggle_active()
        unarchived_employees = self.filtered(lambda employee: employee.active)
        unarchived_employees.write({
            'departure_reason_id': False,
            'departure_description': False,
            'departure_date': False
        })
        archived_addresses = unarchived_employees.mapped(
            'address_home_id').filtered(lambda addr: not addr.active)
        archived_addresses.toggle_active()
        if len(self) == 1 and not self.active:
            return {
                'type': 'ir.actions.act_window',
                'name': _('Register Departure'),
                'res_model': 'hr.departure.wizard',
                'view_mode': 'form',
                'target': 'new',
                'context': {
                    'active_id': self.id
                },
                'views': [[False, 'form']]
            }
        return res

    @api.onchange('company_id')
    def _onchange_company_id(self):
        if self._origin:
            return {
                'warning': {
                    'title':
                    _("Warning"),
                    'message':
                    _("To avoid multi company issues (loosing the access to your previous contracts, leaves, ...), you should create another employee in the new company instead."
                      )
                }
            }

    def generate_random_barcode(self):
        for employee in self:
            employee.barcode = '041' + "".join(
                choice(digits) for i in range(9))

    @api.depends('address_home_id.parent_id')
    def _compute_is_address_home_a_company(self):
        """Checks that chosen address (res.partner) is not linked to a company.
        """
        for employee in self:
            try:
                employee.is_address_home_a_company = employee.address_home_id.parent_id.id is not False
            except AccessError:
                employee.is_address_home_a_company = False

    # ---------------------------------------------------------
    # Business Methods
    # ---------------------------------------------------------

    @api.model
    def get_import_templates(self):
        return [{
            'label': _('Import Template for Employees'),
            'template': '/hr/static/xls/hr_employee.xls'
        }]

    def _post_author(self):
        """
        When a user updates his own employee's data, all operations are performed
        by super user. However, tracking messages should not be posted as OdooBot
        but as the actual user.
        This method is used in the overrides of `_message_log` and `message_post`
        to post messages as the correct user.
        """
        real_user = self.env.context.get('binary_field_real_user')
        if self.env.is_superuser() and real_user:
            self = self.with_user(real_user)
        return self

    # ---------------------------------------------------------
    # Messaging
    # ---------------------------------------------------------

    def _message_log(self, **kwargs):
        return super(HrEmployeePrivate,
                     self._post_author())._message_log(**kwargs)

    @api.returns('mail.message', lambda value: value.id)
    def message_post(self, **kwargs):
        return super(HrEmployeePrivate,
                     self._post_author()).message_post(**kwargs)

    def _sms_get_partner_fields(self):
        return ['user_partner_id']

    def _sms_get_number_fields(self):
        return ['mobile_phone']