Example #1
0
class IrRule(models.Model):
    _inherit = "ir.rule"

    @api.model
    @tools.conditional(
        "xml" not in config["dev_mode"],
        tools.ormcache(
            "self.env.uid",
            "self.env.su",
            "model_name",
            "mode",
            "tuple(self._compute_domain_context_values())",
        ),
    )
    def _compute_domain(self, model_name, mode="read"):
        """Inject extra domain for restricting partners when the user
        has the group 'Sales / User: Own Documents Only'.
        """
        res = super()._compute_domain(model_name, mode=mode)
        user = self.env.user
        group1 = "sales_team.group_sale_salesman"
        group2 = "sales_team_security.group_sale_team_manager"
        group3 = "sales_team.group_sale_salesman_all_leads"
        if model_name == "res.partner" and not self.env.su:
            if user.has_group(group1) and not user.has_group(group3):
                extra_domain = [
                    "|",
                    ("message_partner_ids", "in", user.partner_id.ids),
                    "|",
                    ("id", "=", user.partner_id.id),
                ]
                if user.has_group(group2):
                    extra_domain += [
                        "|",
                        ("team_id", "=", user.sale_team_id.id),
                        ("team_id", "=", False),
                    ]
                else:
                    extra_domain += [
                        "|",
                        ("user_id", "=", user.id),
                        "&",
                        ("user_id", "=", False),
                        "|",
                        ("team_id", "=", False),
                        ("team_id", "=", user.sale_team_id.id),
                    ]
                extra_domain = expression.normalize_domain(extra_domain)
                res = expression.AND([extra_domain] + [res])
        return res
Example #2
0
class IrRule(models.Model):
    _inherit = 'ir.rule'

    @api.model
    @tools.conditional(
        'xml' not in config['dev_mode'],
        tools.ormcache(
            'self._uid', 'model_name', 'mode',
            'tuple(self._context.get(k) for k in self._compute_domain_keys())'
        )
    )
    def _compute_domain(self, model_name, mode="read"):
        """Inject extra domain for restricting partners when the user
        has the group 'Sales / User: Own Documents Only'.
        """
        res = super()._compute_domain(model_name, mode=mode)
        user = self.env.user
        group1 = "sales_team.group_sale_salesman"
        group2 = "sales_team_security.group_sale_team_manager"
        group3 = "sales_team.group_sale_salesman_all_leads"
        if model_name == "res.partner" and user.id != SUPERUSER_ID:
            if user.has_group(group1) and not user.has_group(group3):
                extra_domain = [
                    '|',
                    ('message_partner_ids', 'in', user.partner_id.ids),
                    '|',
                    ('id', '=', user.partner_id.id),
                ]
                if user.has_group(group2):
                    extra_domain += [
                        "|",
                        ("team_id", "=", user.sale_team_id.id),
                        ("team_id", "=", False),
                    ]
                else:
                    extra_domain += [
                        "|",
                        ("user_id", "=", user.id),
                        "&",
                        ("user_id", "=", False),
                        "|",
                        ("team_id", "=", False),
                        ("team_id", "=", user.sale_team_id.id),
                    ]
                extra_domain = expression.normalize_domain(extra_domain)
                res = expression.AND([extra_domain] + [res])
        return res
Example #3
0
class IrQWeb(models.AbstractModel, QWeb):
    """ Base QWeb rendering engine
    * to customize ``t-field`` rendering, subclass ``ir.qweb.field`` and
      create new models called :samp:`ir.qweb.field.{widget}`
    Beware that if you need extensions or alterations which could be
    incompatible with other subsystems, you should create a local object
    inheriting from ``ir.qweb`` and customize that.
    """

    _name = 'ir.qweb'

    @api.model
    def render(self, id_or_xml_id, values=None, **options):
        """ render(id_or_xml_id, values, **options)

        Render the template specified by the given name.

        :param id_or_xml_id: name or etree (see get_template)
        :param dict values: template values to be used for rendering
        :param options: used to compile the template (the dict available for the rendering is frozen)
            * ``load`` (function) overrides the load method
            * ``profile`` (float) profile the rendering (use astor lib) (filter
              profile line with time ms >= profile)
        """
        for method in dir(self):
            if method.startswith('render_'):
                _logger.warning("Unused method '%s' is found in ir.qweb." %
                                method)

        context = dict(self.env.context,
                       dev_mode='qweb' in tools.config['dev_mode'])
        context.update(options)

        return super(IrQWeb, self).render(id_or_xml_id,
                                          values=values,
                                          **context)

    def default_values(self):
        """ attributes add to the values for each computed template
        """
        default = super(IrQWeb, self).default_values()
        default.update(
            request=request,
            cache_assets=round(time() / 180),
            true=True,
            false=False
        )  # true and false added for backward compatibility to remove after v10
        return default

    # assume cache will be invalidated by third party on write to ir.ui.view
    def _get_template_cache_keys(self):
        """ Return the list of context keys to use for caching ``_get_template``. """
        return [
            'lang', 'inherit_branding', 'editable', 'translatable',
            'edit_translations', 'website_id'
        ]

    # apply ormcache_context decorator unless in dev mode...
    @tools.conditional(
        'xml' not in tools.config['dev_mode'],
        tools.ormcache(
            'id_or_xml_id',
            'tuple(options.get(k) for k in self._get_template_cache_keys())'),
    )
    def compile(self, id_or_xml_id, options):
        return super(IrQWeb, self).compile(id_or_xml_id, options=options)

    def load(self, name, options):
        lang = options.get('lang', 'en_US')
        env = self.env
        if lang != env.context.get('lang'):
            env = env(context=dict(env.context, lang=lang))

        template = env['ir.ui.view'].read_template(name)

        # QWeb's `read_template` will check if one of the first children of
        # what we send to it has a "t-name" attribute having `name` as value
        # to consider it has found it. As it'll never be the case when working
        # with view ids or children view or children primary views, force it here.
        def is_child_view(view_name):
            view_id = self.env['ir.ui.view'].get_view_id(view_name)
            view = self.env['ir.ui.view'].browse(view_id)
            return view.inherit_id is not None

        if isinstance(name, pycompat.integer_types) or is_child_view(name):
            for node in etree.fromstring(template):
                if node.get('t-name'):
                    node.set('t-name', str(name))
                    return node.getparent()
            return None  # trigger "template not found" in QWeb
        else:
            return template

    # order

    def _directives_eval_order(self):
        directives = super(IrQWeb, self)._directives_eval_order()
        directives.insert(directives.index('call'), 'lang')
        directives.insert(directives.index('field'), 'call-assets')
        return directives

    # compile directives

    def _compile_directive_lang(self, el, options):
        lang = el.attrib.pop('t-lang', 'en_US')
        if el.get('t-call-options'):
            el.set('t-call-options',
                   el.get('t-call-options')[0:-1] + u', "lang": %s}' % lang)
        else:
            el.set('t-call-options', u'{"lang": %s}' % lang)
        return self._compile_node(el, options)

    def _compile_directive_call_assets(self, el, options):
        """ This special 't-call' tag can be used in order to aggregate/minify javascript and css assets"""
        if len(el):
            raise SyntaxError("t-call-assets cannot contain children nodes")

        # self._get_asset(xmlid, options, css=css, js=js, debug=values.get('debug'), async=async, values=values)
        return [
            self._append(
                ast.Call(
                    func=ast.Attribute(value=ast.Name(id='self',
                                                      ctx=ast.Load()),
                                       attr='_get_asset',
                                       ctx=ast.Load()),
                    args=[
                        ast.Str(el.get('t-call-assets')),
                        ast.Name(id='options', ctx=ast.Load()),
                    ],
                    keywords=[
                        ast.keyword('css',
                                    self._get_attr_bool(el.get('t-css',
                                                               True))),
                        ast.keyword('js',
                                    self._get_attr_bool(el.get('t-js', True))),
                        ast.keyword(
                            'debug',
                            ast.Call(func=ast.Attribute(value=ast.Name(
                                id='values', ctx=ast.Load()),
                                                        attr='get',
                                                        ctx=ast.Load()),
                                     args=[ast.Str('debug')],
                                     keywords=[],
                                     starargs=None,
                                     kwargs=None)),
                        ast.keyword(
                            'async',
                            self._get_attr_bool(el.get('async', False))),
                        ast.keyword('values',
                                    ast.Name(id='values', ctx=ast.Load())),
                    ],
                    starargs=None,
                    kwargs=None))
        ]

    # for backward compatibility to remove after v10
    def _compile_widget_options(self, el, directive_type):
        field_options = super(IrQWeb, self)._compile_widget_options(
            el, directive_type)

        if ('t-%s-options' % directive_type) in el.attrib:
            if tools.config['dev_mode']:
                _logger.warning(
                    "Use new syntax t-options instead of t-%s-options" %
                    directive_type)
            if not field_options:
                field_options = el.attrib.pop('t-%s-options' % directive_type)

        if field_options and 'monetary' in field_options:
            try:
                options = "{'widget': 'monetary'"
                for k, v in json.loads(field_options).items():
                    if k in ('display_currency', 'from_currency'):
                        options = "%s, '%s': %s" % (options, k, v)
                    else:
                        options = "%s, '%s': '%s'" % (options, k, v)
                options = "%s}" % options
                field_options = options
                _logger.warning(
                    "Use new syntax for '%s' monetary widget t-options (python dict instead of deprecated JSON syntax)."
                    % etree.tostring(el))
            except ValueError:
                pass

        return field_options

    # end backward

    # method called by computing code

    @tools.conditional(
        # in non-xml-debug mode we want assets to be cached forever, and the admin can force a cache clear
        # by restarting the server after updating the source code (or using the "Clear server cache" in debug tools)
        'xml' not in tools.config['dev_mode'],
        tools.ormcache_context('xmlid',
                               'options.get("lang", "en_US")',
                               'css',
                               'js',
                               'debug',
                               'async',
                               keys=("website_id", )),
    )
    def _get_asset(self,
                   xmlid,
                   options,
                   css=True,
                   js=True,
                   debug=False,
                   async=False,
                   values=None):
Example #4
0
class IrRule(models.Model):
    _name = 'ir.rule'
    _description = 'Record Rule'
    _order = 'model_id DESC,id'
    _MODES = ['read', 'write', 'create', 'unlink']

    name = fields.Char(index=True)
    active = fields.Boolean(
        default=True,
        help=
        "If you uncheck the active field, it will disable the record rule without deleting it (if you delete a native record rule, it may be re-created when you reload the module)."
    )
    model_id = fields.Many2one('ir.model',
                               string='Model',
                               index=True,
                               required=True,
                               ondelete="cascade")
    groups = fields.Many2many('res.groups',
                              'rule_group_rel',
                              'rule_group_id',
                              'group_id',
                              ondelete='restrict')
    domain_force = fields.Text(string='Domain')
    perm_read = fields.Boolean(string='Apply for Read', default=True)
    perm_write = fields.Boolean(string='Apply for Write', default=True)
    perm_create = fields.Boolean(string='Apply for Create', default=True)
    perm_unlink = fields.Boolean(string='Apply for Delete', default=True)

    _sql_constraints = [
        ('no_access_rights',
         'CHECK (perm_read!=False or perm_write!=False or perm_create!=False or perm_unlink!=False)',
         'Rule must have at least one checked access right !'),
    ]

    @api.model
    def _eval_context(self):
        """Returns a dictionary to use as evaluation context for
           ir.rule domains.
           Note: company_ids contains the ids of the activated companies
           by the user with the switch company menu. These companies are
           filtered and trusted.
        """
        # use an empty context for 'user' to make the domain evaluation
        # independent from the context
        return {
            'user': self.env.user.with_context({}),
            'time': time,
            'company_ids': self.env.companies.ids,
            'company_id': self.env.company.id,
        }

    @api.depends('groups')
    def _compute_global(self):
        for rule in self:
            rule['global'] = not rule.groups

    @api.constrains('model_id')
    def _check_model_name(self):
        # Don't allow rules on rules records (this model).
        if any(rule.model_id.model == self._name for rule in self):
            raise ValidationError(
                _('Rules can not be applied on the Record Rules model.'))

    def _compute_domain_keys(self):
        """ Return the list of context keys to use for caching ``_compute_domain``. """
        return ['allowed_company_ids']

    def _get_failing(self, for_records, mode='read'):
        """ Returns the rules for the mode for the current user which fail on
        the specified records.

        Can return any global rule and/or all local rules (since local rules
        are OR-ed together, the entire group succeeds or fails, while global
        rules get AND-ed and can each fail)
        """
        Model = for_records.browse(()).sudo()
        eval_context = self._eval_context()

        all_rules = self._get_rules(Model._name, mode=mode).sudo()

        # first check if the group rules fail for any record (aka if
        # searching on (records, group_rules) filters out some of the records)
        group_rules = all_rules.filtered(
            lambda r: r.groups and r.groups & self.env.user.groups_id)
        group_domains = expression.OR([
            safe_eval(r.domain_force, eval_context) if r.domain_force else []
            for r in group_rules
        ])
        # if all records get returned, the group rules are not failing
        if Model.search_count(
                expression.AND([[('id', 'in', for_records.ids)],
                                group_domains])) == len(for_records):
            group_rules = self.browse(())

        # failing rules are previously selected group rules or any failing global rule
        def is_failing(r, ids=for_records.ids):
            dom = safe_eval(r.domain_force,
                            eval_context) if r.domain_force else []
            return Model.search_count(
                expression.AND([[('id', 'in', ids)],
                                expression.normalize_domain(dom)])) < len(ids)

        return all_rules.filtered(lambda r: r in group_rules or
                                  (not r.groups and is_failing(r))).with_user(
                                      self.env.user)

    def _get_rules(self, model_name, mode='read'):
        """ Returns all the rules matching the model for the mode for the
        current user.
        """
        if mode not in self._MODES:
            raise ValueError('Invalid mode: %r' % (mode, ))

        if self.env.su:
            return self.browse(())

        query = """ SELECT r.id FROM ir_rule r JOIN ir_model m ON (r.model_id=m.id)
                    WHERE m.model=%s AND r.active AND r.perm_{mode}
                    AND (r.id IN (SELECT rule_group_id FROM rule_group_rel rg
                                  JOIN res_groups_users_rel gu ON (rg.group_id=gu.gid)
                                  WHERE gu.uid=%s)
                         OR r.global)
                    ORDER BY r.id
                """.format(mode=mode)
        self._cr.execute(query, (model_name, self._uid))
        return self.browse(row[0] for row in self._cr.fetchall())

    @api.model
    @tools.conditional(
        'xml' not in config['dev_mode'],
        tools.ormcache('self.env.uid', 'self.env.su', 'model_name', 'mode',
                       'tuple(self._compute_domain_context_values())'),
    )
    def _compute_domain(self, model_name, mode="read"):
        rules = self._get_rules(model_name, mode=mode)
        if not rules:
            return

        # browse user and rules as SUPERUSER_ID to avoid access errors!
        eval_context = self._eval_context()
        user_groups = self.env.user.groups_id
        global_domains = []  # list of domains
        group_domains = []  # list of domains
        for rule in rules.sudo():
            # evaluate the domain for the current user
            dom = safe_eval(rule.domain_force,
                            eval_context) if rule.domain_force else []
            dom = expression.normalize_domain(dom)
            if not rule.groups:
                global_domains.append(dom)
            elif rule.groups & user_groups:
                group_domains.append(dom)

        # combine global domains and group domains
        if not group_domains:
            return expression.AND(global_domains)
        return expression.AND(global_domains + [expression.OR(group_domains)])

    def _compute_domain_context_values(self):
        for k in self._compute_domain_keys():
            v = self._context.get(k)
            if isinstance(v, list):
                # currently this could be a frozenset (to avoid depending on
                # the order of allowed_company_ids) but it seems safer if
                # possibly slightly more miss-y to use a tuple
                v = tuple(v)
            yield v

    @api.model
    def clear_cache(self):
        warnings.warn(
            "Deprecated IrRule.clear_cache(), use IrRule.clear_caches() instead",
            DeprecationWarning)
        self.clear_caches()

    def unlink(self):
        res = super(IrRule, self).unlink()
        self.clear_caches()
        return res

    @api.model_create_multi
    def create(self, vals_list):
        res = super(IrRule, self).create(vals_list)
        # DLE P33: tests
        self.env.flush_all()
        self.clear_caches()
        return res

    def write(self, vals):
        res = super(IrRule, self).write(vals)
        # DLE P33: tests
        # - odoo/addons/test_access_rights/tests/test_feedback.py
        # - odoo/addons/test_access_rights/tests/test_ir_rules.py
        # - odoo/addons/base/tests/test_orm.py (/home/dle/src/odoo/master-nochange-fp/odoo/addons/base/tests/test_orm.py)
        self.env.flush_all()
        self.clear_caches()
        return res

    def _make_access_error(self, operation, records):
        _logger.info(
            'Access Denied by record rules for operation: %s on record ids: %r, uid: %s, model: %s',
            operation, records.ids[:6], self._uid, records._name)

        model = records._name
        description = self.env['ir.model']._get(model).name or model
        msg_heads = {
            # Messages are declared in extenso so they are properly exported in translation terms
            'read':
            _("Due to security restrictions, you are not allowed to access '%(document_kind)s' (%(document_model)s) records.",
              document_kind=description,
              document_model=model),
            'write':
            _("Due to security restrictions, you are not allowed to modify '%(document_kind)s' (%(document_model)s) records.",
              document_kind=description,
              document_model=model),
            'create':
            _("Due to security restrictions, you are not allowed to create '%(document_kind)s' (%(document_model)s) records.",
              document_kind=description,
              document_model=model),
            'unlink':
            _("Due to security restrictions, you are not allowed to delete '%(document_kind)s' (%(document_model)s) records.",
              document_kind=description,
              document_model=model)
        }
        operation_error = msg_heads[operation]
        resolution_info = _(
            "Contact your administrator to request access if necessary.")

        if not self.env.user.has_group(
                'base.group_no_one') or not self.env.user.has_group(
                    'base.group_user'):
            return AccessError(f"{operation_error}\n\n{resolution_info}")

        # This extended AccessError is only displayed in debug mode.
        # Note that by default, public and portal users do not have
        # the group "base.group_no_one", even if debug mode is enabled,
        # so it is relatively safe here to include the list of rules and record names.
        rules = self._get_failing(records, mode=operation).sudo()

        records_sudo = records[:6].sudo()
        company_related = any('company_id' in (r.domain_force or '')
                              for r in rules)

        def get_record_description(rec):
            # If the user has access to the company of the record, add this
            # information in the description to help them to change company
            if company_related and 'company_id' in rec and rec.company_id in self.env.user.company_ids:
                return f'{rec.display_name} (id={rec.id}, company={rec.company_id.display_name})'
            return f'{rec.display_name} (id={rec.id})'

        records_description = ', '.join(
            get_record_description(rec) for rec in records_sudo)
        failing_records = _("Records: %s", records_description)

        user_description = f'{self.env.user.name} (id={self.env.user.id})'
        failing_user = _("User: %s", user_description)

        rules_description = '\n'.join(f'- {rule.name}' for rule in rules)
        failing_rules = _(
            "This restriction is due to the following rules:\n%s",
            rules_description)
        if company_related:
            failing_rules += "\n\n" + _(
                'Note: this might be a multi-company issue.')

        # clean up the cache of records prefetched with display_name above
        for record in records[:6]:
            record._cache.clear()

        msg = f"{operation_error}\n\n{failing_records}\n{failing_user}\n\n{failing_rules}\n\n{resolution_info}"
        return AccessError(msg)
Example #5
0
class IrQWeb(models.AbstractModel, QWeb):
    """ Base QWeb rendering engine
    * to customize ``t-field`` rendering, subclass ``ir.qweb.field`` and
      create new models called :samp:`ir.qweb.field.{widget}`
    Beware that if you need extensions or alterations which could be
    incompatible with other subsystems, you should create a local object
    inheriting from ``ir.qweb`` and customize that.
    """

    _name = 'ir.qweb'

    @api.model
    def render(self, id_or_xml_id, values=None, **options):
        """ render(id_or_xml_id, values, **options)

        Render the template specified by the given name.

        :param id_or_xml_id: name or etree (see get_template)
        :param dict values: template values to be used for rendering
        :param options: used to compile the template (the dict available for the rendering is frozen)
            * ``load`` (function) overrides the load method
            * ``profile`` (float) profile the rendering (use astor lib) (filter
              profile line with time ms >= profile)
        """
        for method in dir(self):
            if method.startswith('render_'):
                _logger.warning("Unused method '%s' is found in ir.qweb." %
                                method)

        context = dict(self.env.context,
                       dev_mode='qweb' in tools.config['dev_mode'])
        context.update(options)

        return super(IrQWeb, self).render(id_or_xml_id,
                                          values=values,
                                          **context)

    def default_values(self):
        """ attributes add to the values for each computed template
        """
        default = super(IrQWeb, self).default_values()
        default.update(
            request=request,
            cache_assets=round(time() / 180),
            true=True,
            false=False
        )  # true and false added for backward compatibility to remove after v10
        return default

    # assume cache will be invalidated by third party on write to ir.ui.view
    def _get_template_cache_keys(self):
        """ Return the list of context keys to use for caching ``_get_template``. """
        return [
            'lang', 'inherit_branding', 'editable', 'translatable',
            'edit_translations', 'website_id'
        ]

    # apply ormcache_context decorator unless in dev mode...
    @tools.conditional(
        'xml' not in tools.config['dev_mode'],
        tools.ormcache(
            'id_or_xml_id',
            'tuple(options.get(k) for k in self._get_template_cache_keys())'),
    )
    def compile(self, id_or_xml_id, options):
        return super(IrQWeb, self).compile(id_or_xml_id, options=options)

    def load(self, name, options):
        lang = options.get('lang', 'en_US')
        env = self.env
        if lang != env.context.get('lang'):
            env = env(context=dict(env.context, lang=lang))

        template = env['ir.ui.view'].read_template(name)

        # QWeb's `read_template` will check if one of the first children of
        # what we send to it has a "t-name" attribute having `name` as value
        # to consider it has found it. As it'll never be the case when working
        # with view ids or children view or children primary views, force it here.
        def is_child_view(view_name):
            view_id = self.env['ir.ui.view'].get_view_id(view_name)
            view = self.env['ir.ui.view'].browse(view_id)
            return view.inherit_id is not None

        if isinstance(name, pycompat.integer_types) or is_child_view(name):
            for node in etree.fromstring(template):
                if node.get('t-name'):
                    node.set('t-name', str(name))
                    return node.getparent()
            return None  # trigger "template not found" in QWeb
        else:
            return template

    # order

    def _directives_eval_order(self):
        directives = super(IrQWeb, self)._directives_eval_order()
        directives.insert(directives.index('call'), 'lang')
        directives.insert(directives.index('field'), 'call-assets')
        return directives

    # compile directives

    def _compile_directive_lang(self, el, options):
        lang = el.attrib.pop('t-lang', 'en_US')
        if el.get('t-call-options'):
            el.set('t-call-options',
                   el.get('t-call-options')[0:-1] + u', "lang": %s}' % lang)
        else:
            el.set('t-call-options', u'{"lang": %s}' % lang)
        return self._compile_node(el, options)

    def _compile_directive_call_assets(self, el, options):
        """ This special 't-call' tag can be used in order to aggregate/minify javascript and css assets"""
        if len(el):
            raise SyntaxError("t-call-assets cannot contain children nodes")

        # nodes = self._get_asset(xmlid, options, css=css, js=js, debug=values.get('debug'), async=async, values=values)
        #
        # for index, (tagName, t_attrs, content) in enumerate(nodes):
        #     if index:
        #         append('\n        ')
        #     append('<')
        #     append(tagName)
        #
        #     self._post_processing_att(tagName, t_attrs, options)
        #     for name, value in t_attrs.items():
        #         if value or isinstance(value, string_types)):
        #             append(u' ')
        #             append(name)
        #             append(u'="')
        #             append(escape(pycompat.to_text((value)))
        #             append(u'"')
        #
        #     if not content and tagName in self._void_elements:
        #         append('/>')
        #     else:
        #         append('>')
        #         if content:
        #           append(content)
        #         append('</')
        #         append(tagName)
        #         append('>')
        #
        space = el.getprevious() is not None and el.getprevious(
        ).tail or el.getparent().text
        sep = u'\n' + space.rsplit('\n').pop()
        return [
            ast.Assign(
                targets=[ast.Name(id='nodes', ctx=ast.Store())],
                value=ast.Call(
                    func=ast.Attribute(value=ast.Name(id='self',
                                                      ctx=ast.Load()),
                                       attr='_get_asset_nodes',
                                       ctx=ast.Load()),
                    args=[
                        ast.Str(el.get('t-call-assets')),
                        ast.Name(id='options', ctx=ast.Load()),
                    ],
                    keywords=[
                        ast.keyword('css',
                                    self._get_attr_bool(el.get('t-css',
                                                               True))),
                        ast.keyword('js',
                                    self._get_attr_bool(el.get('t-js', True))),
                        ast.keyword(
                            'debug',
                            ast.Call(func=ast.Attribute(value=ast.Name(
                                id='values', ctx=ast.Load()),
                                                        attr='get',
                                                        ctx=ast.Load()),
                                     args=[ast.Str('debug')],
                                     keywords=[],
                                     starargs=None,
                                     kwargs=None)),
                        ast.keyword(
                            '_async',
                            self._get_attr_bool(el.get('async', False))),
                        ast.keyword('values',
                                    ast.Name(id='values', ctx=ast.Load())),
                    ],
                    starargs=None,
                    kwargs=None)),
            ast.For(
                target=ast.Tuple(elts=[
                    ast.Name(id='index', ctx=ast.Store()),
                    ast.Tuple(elts=[
                        ast.Name(id='tagName', ctx=ast.Store()),
                        ast.Name(id='t_attrs', ctx=ast.Store()),
                        ast.Name(id='content', ctx=ast.Store())
                    ],
                              ctx=ast.Store())
                ],
                                 ctx=ast.Store()),
                iter=ast.Call(func=ast.Name(id='enumerate', ctx=ast.Load()),
                              args=[ast.Name(id='nodes', ctx=ast.Load())],
                              keywords=[],
                              starargs=None,
                              kwargs=None),
                body=[
                    ast.If(test=ast.Name(id='index', ctx=ast.Load()),
                           body=[self._append(ast.Str(sep))],
                           orelse=[]),
                    self._append(ast.Str(u'<')),
                    self._append(ast.Name(id='tagName', ctx=ast.Load())),
                ] + self._append_attributes() + [
                    ast.If(test=ast.BoolOp(
                        op=ast.And(),
                        values=[
                            ast.UnaryOp(ast.Not(),
                                        ast.Name(id='content', ctx=ast.Load()),
                                        lineno=0,
                                        col_offset=0),
                            ast.Compare(
                                left=ast.Name(id='tagName', ctx=ast.Load()),
                                ops=[ast.In()],
                                comparators=[
                                    ast.Attribute(value=ast.Name(
                                        id='self', ctx=ast.Load()),
                                                  attr='_void_elements',
                                                  ctx=ast.Load())
                                ]),
                        ]),
                           body=[self._append(ast.Str(u'/>'))],
                           orelse=[
                               self._append(ast.Str(u'>')),
                               ast.If(test=ast.Name(id='content',
                                                    ctx=ast.Load()),
                                      body=[
                                          self._append(
                                              ast.Name(id='content',
                                                       ctx=ast.Load()))
                                      ],
                                      orelse=[]),
                               self._append(ast.Str(u'</')),
                               self._append(
                                   ast.Name(id='tagName', ctx=ast.Load())),
                               self._append(ast.Str(u'>')),
                           ])
                ],
                orelse=[])
        ]

    # for backward compatibility to remove after v10
    def _compile_widget_options(self, el, directive_type):
        field_options = super(IrQWeb, self)._compile_widget_options(
            el, directive_type)

        if ('t-%s-options' % directive_type) in el.attrib:
            if tools.config['dev_mode']:
                _logger.warning(
                    "Use new syntax t-options instead of t-%s-options" %
                    directive_type)
            if not field_options:
                field_options = el.attrib.pop('t-%s-options' % directive_type)

        if field_options and 'monetary' in field_options:
            try:
                options = "{'widget': 'monetary'"
                for k, v in json.loads(field_options).items():
                    if k in ('display_currency', 'from_currency'):
                        options = "%s, '%s': %s" % (options, k, v)
                    else:
                        options = "%s, '%s': '%s'" % (options, k, v)
                options = "%s}" % options
                field_options = options
                _logger.warning(
                    "Use new syntax for '%s' monetary widget t-options (python dict instead of deprecated JSON syntax)."
                    % etree.tostring(el))
            except ValueError:
                pass

        return field_options

    # end backward

    # method called by computing code

    def get_asset_bundle(self, xmlid, files, remains=None, env=None):
        return AssetsBundle(xmlid, files, remains=remains, env=env)

    # compatibility to remove after v11 - DEPRECATED
    @tools.conditional(
        'xml' not in tools.config['dev_mode'],
        tools.ormcache_context('xmlid',
                               'options.get("lang", "en_US")',
                               'css',
                               'js',
                               'debug',
                               'kw.get("async")',
                               'async_load',
                               keys=("website_id", )),
    )
    def _get_asset(self,
                   xmlid,
                   options,
                   css=True,
                   js=True,
                   debug=False,
                   async_load=False,
                   values=None,
                   **kw):
        if 'async' in kw:
            async_load = kw['async']
        files, remains = self._get_asset_content(xmlid, options)
        asset = self.get_asset_bundle(xmlid, files, remains, env=self.env)
        return asset.to_html(css=css,
                             js=js,
                             debug=debug,
                             async_load=async_load,
                             url_for=(values
                                      or {}).get('url_for', lambda url: url))

    @tools.conditional(
        # in non-xml-debug mode we want assets to be cached forever, and the admin can force a cache clear
        # by restarting the server after updating the source code (or using the "Clear server cache" in debug tools)
        'xml' not in tools.config['dev_mode'],
        tools.ormcache_context('xmlid',
                               'options.get("lang", "en_US")',
                               'css',
                               'js',
                               'debug',
                               'kw.get("async")',
                               'async_load',
                               keys=("website_id", )),
    )
    def _get_asset_nodes(self,
                         xmlid,
                         options,
                         css=True,
                         js=True,
                         debug=False,
                         async_load=False,
                         values=None,
                         **kw):
        if 'async' in kw:
            async_load = kw['async']
        files, remains = self._get_asset_content(xmlid, options)
        asset = self.get_asset_bundle(xmlid, files, env=self.env)
        remains = [
            node for node in remains
            if (css and node[0] == 'link') or (js and node[0] != 'link')
        ]
        return remains + asset.to_node(
            css=css, js=js, debug=debug, async_load=async_load)

    @tools.ormcache_context('xmlid',
                            'options.get("lang", "en_US")',
                            keys=("website_id", ))
    def _get_asset_content(self, xmlid, options):
        options = dict(options,
                       inherit_branding=False,
                       inherit_branding_auto=False,
                       edit_translations=False,
                       translatable=False,
                       rendering_bundle=True)

        env = self.env(context=options)

        def can_aggregate(url):
            return not urls.url_parse(url).scheme and not urls.url_parse(
                url).netloc and not url.startswith('/web/content')

        # TODO: This helper can be used by any template that wants to embedd the backend.
        #       It is currently necessary because the ir.ui.view bundle inheritance does not
        #       match the module dependency graph.
        def get_modules_order():
            if request:
                from odoo.addons.web.controllers.main import module_boot
                return json.dumps(module_boot())
            return '[]'

        template = env['ir.qweb'].render(
            xmlid, {"get_modules_order": get_modules_order})

        files = []
        remains = []
        for el in html.fragments_fromstring(template):
            if isinstance(el, html.HtmlElement):
                href = el.get('href', '')
                src = el.get('src', '')
                atype = el.get('type')
                media = el.get('media')

                if can_aggregate(href) and (
                        el.tag == 'style' or
                    (el.tag == 'link' and el.get('rel') == 'stylesheet')):
                    if href.endswith('.sass'):
                        atype = 'text/sass'
                    elif href.endswith('.less'):
                        atype = 'text/less'
                    if atype not in ('text/less', 'text/sass'):
                        atype = 'text/css'
                    path = [segment for segment in href.split('/') if segment]
                    filename = get_resource_path(*path) if path else None
                    files.append({
                        'atype': atype,
                        'url': href,
                        'filename': filename,
                        'content': el.text,
                        'media': media
                    })
                elif can_aggregate(src) and el.tag == 'script':
                    atype = 'text/javascript'
                    path = [segment for segment in src.split('/') if segment]
                    filename = get_resource_path(*path) if path else None
                    files.append({
                        'atype': atype,
                        'url': src,
                        'filename': filename,
                        'content': el.text,
                        'media': media
                    })
                else:
                    remains.append((el.tag, OrderedDict(el.attrib), el.text))
            else:
                # the other cases are ignored
                pass

        return (files, remains)

    def _get_field(self, record, field_name, expression, tagName,
                   field_options, options, values):
        field = record._fields[field_name]

        # adds template compile options for rendering fields
        field_options['template_options'] = options

        # adds generic field options
        field_options['tagName'] = tagName
        field_options['expression'] = expression
        field_options['type'] = field_options.get('widget', field.type)
        inherit_branding = options.get(
            'inherit_branding',
            options.get('inherit_branding_auto')
            and record.check_access_rights('write', False))
        field_options['inherit_branding'] = inherit_branding
        translate = options.get('edit_translations') and options.get(
            'translatable') and field.translate
        field_options['translate'] = translate

        # field converter
        model = 'ir.qweb.field.' + field_options['type']
        converter = self.env[model] if model in self.env else self.env[
            'ir.qweb.field']

        # get content
        content = converter.record_to_html(record, field_name, field_options)
        attributes = converter.attributes(record, field_name, field_options,
                                          values)

        return (attributes, content, inherit_branding or translate)

    def _get_widget(self, value, expression, tagName, field_options, options,
                    values):
        # adds template compile options for rendering fields
        field_options['template_options'] = options

        field_options['type'] = field_options['widget']
        field_options['tagName'] = tagName
        field_options['expression'] = expression

        # field converter
        model = 'ir.qweb.field.' + field_options['type']
        converter = self.env[model] if model in self.env else self.env[
            'ir.qweb.field']

        # get content
        content = converter.value_to_html(value, field_options)
        attributes = OrderedDict()
        attributes['data-oe-type'] = field_options['type']
        attributes['data-oe-expression'] = field_options['expression']

        return (attributes, content, None)

    # compile expression add safe_eval

    def _compile_expr(self, expr):
        """ Compiles a purported Python expression to ast, verifies that it's safe
        (according to safe_eval's semantics) and alter its variable references to
        access values data instead
        """
        # string must be stripped otherwise whitespace before the start for
        # formatting purpose are going to break parse/compile
        st = ast.parse(expr.strip(), mode='eval')
        assert_valid_codeobj(
            _SAFE_OPCODES,
            compile(st, '<>',
                    'eval'),  # could be expr, but eval *should* be fine
            expr)

        # ast.Expression().body -> expr
        return Contextifier(_BUILTINS).visit(st).body

    def _get_attr_bool(self, attr, default=False):
        if attr:
            if attr is True:
                return ast.Name(id='True', ctx=ast.Load())
            attr = attr.lower()
            if attr in ('false', '0'):
                return ast.Name(id='False', ctx=ast.Load())
            elif attr in ('true', '1'):
                return ast.Name(id='True', ctx=ast.Load())
        return ast.Name(id=str(attr if attr is False else default),
                        ctx=ast.Load())
Example #6
0
class IrQWeb(models.AbstractModel, QWeb):
    """ Base QWeb rendering engine
    * to customize ``t-field`` rendering, subclass ``ir.qweb.field`` and
      create new models called :samp:`ir.qweb.field.{widget}`
    Beware that if you need extensions or alterations which could be
    incompatible with other subsystems, you should create a local object
    inheriting from ``ir.qweb`` and customize that.
    """

    _name = 'ir.qweb'
    _description = 'Qweb'

    _available_objects = dict(_BUILTINS)
    _empty_lines = re.compile(r'\n\s*\n')

    @QwebTracker.wrap_render
    @api.model
    def _render(self, template, values=None, **options):
        """ render(template, values, **options)

        Render the template specified by the given name.

        :param template: etree, xml_id, template name (see _get_template)
            * Call the method ``load`` is not an etree.
        :param dict values: template values to be used for rendering
        :param options: used to compile the template (the dict available for the rendering is frozen)
            * ``load`` (function) overrides the load method

        :returns: bytes marked as markup-safe (decode to :class:`markupsafe.Markup`
                  instead of `str`)
        :rtype: MarkupSafe
        """
        compile_options = dict(self.env.context,
                               dev_mode='qweb' in tools.config['dev_mode'])
        compile_options.update(options)

        result = super()._render(template, values=values, **compile_options)

        if not values or not values.get('__keep_empty_lines'):
            result = markupsafe.Markup(
                IrQWeb._empty_lines.sub('\n', result.strip()))

        if 'data-pagebreak=' not in result:
            return result

        fragments = html.fragments_fromstring(result)

        for fragment in fragments:
            for row in fragment.iterfind('.//tr[@data-pagebreak]'):
                table = next(row.iterancestors('table'))
                newtable = html.Element('table', attrib=dict(table.attrib))
                thead = table.find('thead')
                if thead:
                    newtable.append(copy.deepcopy(thead))
                # TODO: copy caption & tfoot as well?
                # TODO: move rows in a tbody if row.getparent() is one?

                pos = row.get('data-pagebreak')
                assert pos in ('before', 'after')
                for sibling in row.getparent().iterchildren('tr'):
                    if sibling is row:
                        if pos == 'after':
                            newtable.append(sibling)
                        break
                    newtable.append(sibling)

                table.addprevious(newtable)
                table.addprevious(
                    html.Element('div',
                                 attrib={'style': 'page-break-after: always'}))

        return markupsafe.Markup(''.join(
            html.tostring(f).decode() for f in fragments))

    # assume cache will be invalidated by third party on write to ir.ui.view
    def _get_template_cache_keys(self):
        """ Return the list of context keys to use for caching ``_get_template``. """
        return [
            'lang', 'inherit_branding', 'editable', 'translatable',
            'edit_translations', 'website_id', 'profile'
        ]

    # apply ormcache_context decorator unless in dev mode...
    @tools.conditional(
        'xml' not in tools.config['dev_mode'],
        tools.ormcache(
            'id_or_xml_id',
            'tuple(options.get(k) for k in self._get_template_cache_keys())'),
    )
    @QwebTracker.wrap_compile
    def _compile(self, id_or_xml_id, options):
        try:
            id_or_xml_id = int(id_or_xml_id)
        except:
            pass
        return super()._compile(id_or_xml_id, options=options)

    def _load(self, name, options):
        lang = options.get('lang', get_lang(self.env).code)
        env = self.env
        if lang != env.context.get('lang'):
            env = env(context=dict(env.context, lang=lang))

        view_id = self.env['ir.ui.view'].get_view_id(name)
        template = env['ir.ui.view'].sudo()._read_template(view_id)

        # QWeb's `_read_template` will check if one of the first children of
        # what we send to it has a "t-name" attribute having `name` as value
        # to consider it has found it. As it'll never be the case when working
        # with view ids or children view or children primary views, force it here.
        def is_child_view(view_name):
            view_id = self.env['ir.ui.view'].get_view_id(view_name)
            view = self.env['ir.ui.view'].sudo().browse(view_id)
            return view.inherit_id is not None

        if isinstance(name, int) or is_child_view(name):
            view = etree.fromstring(template)
            for node in view:
                if node.get('t-name'):
                    node.set('t-name', str(name))
            return (view, view_id)
        else:
            return (template, view_id)

    # order

    def _directives_eval_order(self):
        directives = super()._directives_eval_order()
        directives.insert(directives.index('foreach'), 'groups')
        directives.insert(directives.index('call'), 'lang')
        directives.insert(directives.index('field'), 'call-assets')
        return directives

    # compile

    def _compile_node(self, el, options, indent):
        if el.get("groups"):
            el.set("t-groups", el.attrib.pop("groups"))
        return super()._compile_node(el, options, indent)

    # compile directives

    @QwebTracker.wrap_compile_directive
    def _compile_directive(self, el, options, directive, indent):
        return super()._compile_directive(el, options, directive, indent)

    def _compile_directive_groups(self, el, options, indent):
        """Compile `t-groups` expressions into a python code as a list of
        strings.

        The code will contain the condition `if self.user_has_groups(groups)`
        part that wrap the rest of the compiled code of this element.
        """
        groups = el.attrib.pop('t-groups')
        code = self._flushText(options, indent)
        code.append(
            self._indent(f"if self.user_has_groups({repr(groups)}):", indent))
        code.extend(
            self._compile_directives(el, options, indent + 1) +
            self._flushText(options, indent + 1)
            or [self._indent('pass', indent + 1)])
        return code

    def _compile_directive_lang(self, el, options, indent):
        el.attrib['t-options-lang'] = el.attrib.pop('t-lang')
        return self._compile_node(el, options, indent)

    def _compile_directive_call_assets(self, el, options, indent):
        """ This special 't-call' tag can be used in order to aggregate/minify javascript and css assets"""
        if len(el):
            raise SyntaxError("t-call-assets cannot contain children nodes")

        code = self._flushText(options, indent)
        code.append(
            self._indent(
                dedent("""
            t_call_assets_nodes = self._get_asset_nodes(%(xmlid)s, css=%(css)s, js=%(js)s, debug=values.get("debug"), async_load=%(async_load)s, defer_load=%(defer_load)s, lazy_load=%(lazy_load)s, media=%(media)s)
            for index, (tagName, attrs, content) in enumerate(t_call_assets_nodes):
                if index:
                    yield '\\n        '
                yield '<'
                yield tagName
            """).strip() % {
                    'xmlid': repr(el.get('t-call-assets')),
                    'css': self._compile_bool(el.get('t-css', True)),
                    'js': self._compile_bool(el.get('t-js', True)),
                    'async_load': self._compile_bool(
                        el.get('async_load', False)),
                    'defer_load': self._compile_bool(
                        el.get('defer_load', False)),
                    'lazy_load': self._compile_bool(el.get('lazy_load',
                                                           False)),
                    'media':
                    repr(el.get('media')) if el.get('media') else False,
                }, indent))
        code.extend(self._compile_attributes(options, indent + 1))
        code.append(
            self._indent(
                dedent("""
                if not content and tagName in self._void_elements:
                    yield '/>'
                else:
                    yield '>'
                    if content:
                      yield content
                    yield '</'
                    yield tagName
                    yield '>'
                """).strip(), indent + 1))

        return code

    # method called by computing code

    def get_asset_bundle(self,
                         bundle_name,
                         files,
                         env=None,
                         css=True,
                         js=True):
        return AssetsBundle(bundle_name, files, env=env, css=css, js=js)

    def _get_asset_nodes(self,
                         bundle,
                         css=True,
                         js=True,
                         debug=False,
                         async_load=False,
                         defer_load=False,
                         lazy_load=False,
                         media=None):
        """Generates asset nodes.
        If debug=assets, the assets will be regenerated when a file which composes them has been modified.
        Else, the assets will be generated only once and then stored in cache.
        """
        if debug and 'assets' in debug:
            return self._generate_asset_nodes(bundle, css, js, debug,
                                              async_load, defer_load,
                                              lazy_load, media)
        else:
            return self._generate_asset_nodes_cache(bundle, css, js, debug,
                                                    async_load, defer_load,
                                                    lazy_load, media)

    @tools.conditional(
        # in non-xml-debug mode we want assets to be cached forever, and the admin can force a cache clear
        # by restarting the server after updating the source code (or using the "Clear server cache" in debug tools)
        'xml' not in tools.config['dev_mode'],
        tools.ormcache_context('bundle',
                               'css',
                               'js',
                               'debug',
                               'async_load',
                               'defer_load',
                               'lazy_load',
                               keys=("website_id", "lang")),
    )
    def _generate_asset_nodes_cache(self,
                                    bundle,
                                    css=True,
                                    js=True,
                                    debug=False,
                                    async_load=False,
                                    defer_load=False,
                                    lazy_load=False,
                                    media=None):
        return self._generate_asset_nodes(bundle, css, js, debug, async_load,
                                          defer_load, lazy_load, media)

    def _generate_asset_nodes(self,
                              bundle,
                              css=True,
                              js=True,
                              debug=False,
                              async_load=False,
                              defer_load=False,
                              lazy_load=False,
                              media=None):
        nodeAttrs = None
        if css and media:
            nodeAttrs = {
                'media': media,
            }
        files, remains = self._get_asset_content(bundle, nodeAttrs)
        asset = self.get_asset_bundle(bundle,
                                      files,
                                      env=self.env,
                                      css=css,
                                      js=js)
        remains = [
            node for node in remains
            if (css and node[0] == 'link') or (js and node[0] == 'script')
        ]
        return remains + asset.to_node(css=css,
                                       js=js,
                                       debug=debug,
                                       async_load=async_load,
                                       defer_load=defer_load,
                                       lazy_load=lazy_load)

    def _get_asset_link_urls(self, bundle):
        asset_nodes = self._get_asset_nodes(bundle, js=False)
        return [node[1]['href'] for node in asset_nodes if node[0] == 'link']

    @tools.ormcache_context('bundle',
                            'nodeAttrs and nodeAttrs.get("media")',
                            keys=("website_id", "lang"))
    def _get_asset_content(self, bundle, nodeAttrs=None):
        asset_paths = self.env['ir.asset']._get_asset_paths(bundle=bundle,
                                                            css=True,
                                                            js=True)

        files = []
        remains = []
        for path, *_ in asset_paths:
            ext = path.split('.')[-1]
            is_js = ext in SCRIPT_EXTENSIONS
            is_css = ext in STYLE_EXTENSIONS
            if not is_js and not is_css:
                continue

            mimetype = 'text/javascript' if is_js else 'text/%s' % ext
            if can_aggregate(path):
                segments = [segment for segment in path.split('/') if segment]
                files.append({
                    'atype':
                    mimetype,
                    'url':
                    path,
                    'filename':
                    get_resource_path(*segments) if segments else None,
                    'content':
                    '',
                    'media':
                    nodeAttrs and nodeAttrs.get('media'),
                })
            else:
                if is_js:
                    tag = 'script'
                    attributes = {
                        "type": mimetype,
                        "src": path,
                    }
                else:
                    tag = 'link'
                    attributes = {
                        "type": mimetype,
                        "rel": "stylesheet",
                        "href": path,
                        'media': nodeAttrs and nodeAttrs.get('media'),
                    }
                remains.append((tag, attributes, ''))

        return (files, remains)

    def _get_field(self, record, field_name, expression, tagName,
                   field_options, options, values):
        field = record._fields[field_name]

        # adds template compile options for rendering fields
        field_options['template_options'] = options

        # adds generic field options
        field_options['tagName'] = tagName
        field_options['expression'] = expression
        field_options['type'] = field_options.get('widget', field.type)
        inherit_branding = options.get(
            'inherit_branding',
            options.get('inherit_branding_auto')
            and record.check_access_rights('write', False))
        field_options['inherit_branding'] = inherit_branding
        translate = options.get('edit_translations') and options.get(
            'translatable') and field.translate
        field_options['translate'] = translate

        # field converter
        model = 'ir.qweb.field.' + field_options['type']
        converter = self.env[model] if model in self.env else self.env[
            'ir.qweb.field']

        # get content (the return values from fields are considered to be markup safe)
        content = converter.record_to_html(record, field_name, field_options)
        attributes = converter.attributes(record, field_name, field_options,
                                          values)

        return (attributes, content, inherit_branding or translate)

    def _get_widget(self, value, expression, tagName, field_options, options,
                    values):
        # adds template compile options for rendering fields
        field_options['template_options'] = options

        field_options['type'] = field_options['widget']
        field_options['tagName'] = tagName
        field_options['expression'] = expression

        # field converter
        model = 'ir.qweb.field.' + field_options['type']
        converter = self.env[model] if model in self.env else self.env[
            'ir.qweb.field']

        # get content (the return values from widget are considered to be markup safe)
        content = converter.value_to_html(value, field_options)
        attributes = {}
        attributes['data-oe-type'] = field_options['type']
        attributes['data-oe-expression'] = field_options['expression']

        return (attributes, content, None)

    def _prepare_values(self, values, options):
        """ Prepare the context that will be sent to the evaluated function.

        :param values: template values to be used for rendering
        :param options: frozen dict of compilation parameters.
        """
        check_values(values)
        values['true'] = True
        values['false'] = False
        if 'request' not in values:
            values['request'] = request
        return super()._prepare_values(values, options)

    def _compile_expr(self, expr, raise_on_missing=False):
        """ Compiles a purported Python expression to compiled code, verifies
        that it's safe (according to safe_eval's semantics) and alter its
        variable references to access values data instead

        :param expr: string
        """
        namespace_expr = super()._compile_expr(
            expr, raise_on_missing=raise_on_missing)
        assert_valid_codeobj(_SAFE_QWEB_OPCODES,
                             compile(namespace_expr, '<>', 'eval'), expr)
        return namespace_expr
Example #7
0
class IrRule(models.Model):
    _name = 'ir.rule'
    _order = 'model_id DESC'
    _MODES = ['read', 'write', 'create', 'unlink']

    name = fields.Char(index=True)
    active = fields.Boolean(
        default=True,
        help=
        "If you uncheck the active field, it will disable the record rule without deleting it (if you delete a native record rule, it may be re-created when you reload the module)."
    )
    model_id = fields.Many2one('ir.model',
                               string='Object',
                               index=True,
                               required=True,
                               ondelete="cascade")
    groups = fields.Many2many('res.groups', 'rule_group_rel', 'rule_group_id',
                              'group_id')
    domain_force = fields.Text(string='Domain')
    perm_read = fields.Boolean(string='Apply for Read', default=True)
    perm_write = fields.Boolean(string='Apply for Write', default=True)
    perm_create = fields.Boolean(string='Apply for Create', default=True)
    perm_unlink = fields.Boolean(string='Apply for Delete', default=True)

    _sql_constraints = [
        ('no_access_rights',
         'CHECK (perm_read!=False or perm_write!=False or perm_create!=False or perm_unlink!=False)',
         'Rule must have at least one checked access right !'),
    ]

    def _eval_context_for_combinations(self):
        """Returns a dictionary to use as evaluation context for
           ir.rule domains, when the goal is to obtain python lists
           that are easier to parse and combine, but not to
           actually execute them."""
        return {'user': tools.unquote('user'), 'time': tools.unquote('time')}

    @api.model
    def _eval_context(self):
        """Returns a dictionary to use as evaluation context for
           ir.rule domains."""
        return {'user': self.env.user, 'time': time}

    @api.depends('groups')
    def _compute_global(self):
        for rule in self:
            rule['global'] = not rule.groups

    @api.constrains('model_id')
    def _check_model_transience(self):
        if any(self.env[rule.model_id.model].is_transient() for rule in self):
            raise ValidationError(
                _('Rules can not be applied on Transient models.'))

    @api.constrains('model_id')
    def _check_model_name(self):
        # Don't allow rules on rules records (this model).
        if any(rule.model_id.model == self._name for rule in self):
            raise ValidationError(
                _('Rules can not be applied on the Record Rules model.'))

    def _compute_domain_keys(self):
        """ Return the list of context keys to use for caching ``_compute_domain``. """
        return []

    @api.model
    @tools.conditional(
        'xml' not in config['dev_mode'],
        tools.ormcache(
            'self._uid', 'model_name', 'mode',
            'tuple(self._context.get(k) for k in self._compute_domain_keys())'
        ),
    )
    def _compute_domain(self, model_name, mode="read"):
        if mode not in self._MODES:
            raise ValueError('Invalid mode: %r' % (mode, ))

        if self._uid == SUPERUSER_ID:
            return None

        query = """ SELECT r.id FROM ir_rule r JOIN ir_model m ON (r.model_id=m.id)
                    WHERE m.model=%s AND r.active AND r.perm_{mode}
                    AND (r.id IN (SELECT rule_group_id FROM rule_group_rel rg
                                  JOIN res_groups_users_rel gu ON (rg.group_id=gu.gid)
                                  WHERE gu.uid=%s)
                         OR r.global)
                """.format(mode=mode)
        self._cr.execute(query, (model_name, self._uid))
        rule_ids = [row[0] for row in self._cr.fetchall()]
        if not rule_ids:
            return []

        # browse user and rules as SUPERUSER_ID to avoid access errors!
        eval_context = self._eval_context()
        user_groups = self.env.user.groups_id
        global_domains = []  # list of domains
        group_domains = []  # list of domains
        for rule in self.browse(rule_ids).sudo():
            # evaluate the domain for the current user
            dom = safe_eval(rule.domain_force,
                            eval_context) if rule.domain_force else []
            dom = expression.normalize_domain(dom)
            if not rule.groups:
                global_domains.append(dom)
            elif rule.groups & user_groups:
                group_domains.append(dom)

        # combine global domains and group domains
        return expression.AND(global_domains + [expression.OR(group_domains)])

    @api.model
    def clear_cache(self):
        """ Deprecated, use `clear_caches` instead. """
        self.clear_caches()

    @api.model
    def domain_get(self, model_name, mode='read'):
        dom = self._compute_domain(model_name, mode)
        if dom:
            # _where_calc is called as superuser. This means that rules can
            # involve objects on which the real uid has no acces rights.
            # This means also there is no implicit restriction (e.g. an object
            # references another object the user can't see).
            query = self.env[model_name].sudo()._where_calc(dom,
                                                            active_test=False)
            return query.where_clause, query.where_clause_params, query.tables
        return [], [], ['"%s"' % self.env[model_name]._table]

    @api.multi
    def unlink(self):
        res = super(IrRule, self).unlink()
        self.clear_caches()
        return res

    @api.model_create_multi
    def create(self, vals_list):
        res = super(IrRule, self).create(vals_list)
        self.clear_caches()
        return res

    @api.multi
    def write(self, vals):
        res = super(IrRule, self).write(vals)
        self.clear_caches()
        return res
Example #8
0
class IrRule(models.Model):
    _name = 'ir.rule'
    _description = 'Record Rule'
    _order = 'model_id DESC,id'
    _MODES = ['read', 'write', 'create', 'unlink']

    name = fields.Char(index=True)
    active = fields.Boolean(default=True, help="If you uncheck the active field, it will disable the record rule without deleting it (if you delete a native record rule, it may be re-created when you reload the module).")
    model_id = fields.Many2one('ir.model', string='Object', index=True, required=True, ondelete="cascade")
    groups = fields.Many2many('res.groups', 'rule_group_rel', 'rule_group_id', 'group_id')
    domain_force = fields.Text(string='Domain')
    perm_read = fields.Boolean(string='Apply for Read', default=True)
    perm_write = fields.Boolean(string='Apply for Write', default=True)
    perm_create = fields.Boolean(string='Apply for Create', default=True)
    perm_unlink = fields.Boolean(string='Apply for Delete', default=True)

    _sql_constraints = [
        ('no_access_rights',
         'CHECK (perm_read!=False or perm_write!=False or perm_create!=False or perm_unlink!=False)',
         'Rule must have at least one checked access right !'),
    ]

    def _eval_context_for_combinations(self):
        """Returns a dictionary to use as evaluation context for
           ir.rule domains, when the goal is to obtain python lists
           that are easier to parse and combine, but not to
           actually execute them."""
        return {'user': tools.unquote('user'),
                'time': tools.unquote('time')}

    @api.model
    def _eval_context(self):
        """Returns a dictionary to use as evaluation context for
           ir.rule domains.
           Note: company_ids contains the ids of the activated companies
           by the user with the switch company menu. These companies are
           filtered and trusted.
        """
        # use an empty context for 'user' to make the domain evaluation
        # independent from the context
        return {
            'user': self.env.user.with_context({}),
            'time': time,
            'company_ids': self.env.companies.ids,
            'company_id': self.env.company.id,
        }

    @api.depends('groups')
    def _compute_global(self):
        for rule in self:
            rule['global'] = not rule.groups

    @api.constrains('model_id')
    def _check_model_transience(self):
        if any(self.env[rule.model_id.model].is_transient() for rule in self):
            raise ValidationError(_('Rules can not be applied on Transient models.'))

    @api.constrains('model_id')
    def _check_model_name(self):
        # Don't allow rules on rules records (this model).
        if any(rule.model_id.model == self._name for rule in self):
            raise ValidationError(_('Rules can not be applied on the Record Rules model.'))

    def _compute_domain_keys(self):
        """ Return the list of context keys to use for caching ``_compute_domain``. """
        return ['allowed_company_ids']

    def _get_failing(self, for_records, mode='read'):
        """ Returns the rules for the mode for the current user which fail on
        the specified records.

        Can return any global rule and/or all local rules (since local rules
        are OR-ed together, the entire group succeeds or fails, while global
        rules get AND-ed and can each fail)
        """
        Model = for_records.browse(()).sudo()
        eval_context = self._eval_context()

        all_rules = self._get_rules(Model._name, mode=mode).sudo()

        # first check if the group rules fail for any record (aka if
        # searching on (records, group_rules) filters out some of the records)
        group_rules = all_rules.filtered(lambda r: r.groups and r.groups & self.env.user.groups_id)
        group_domains = expression.OR([
            safe_eval(r.domain_force, eval_context) if r.domain_force else []
            for r in group_rules
        ])
        # if all records get returned, the group rules are not failing
        if Model.search_count(expression.AND([[('id', 'in', for_records.ids)], group_domains])) == len(for_records):
            group_rules = self.browse(())

        # failing rules are previously selected group rules or any failing global rule
        def is_failing(r, ids=for_records.ids):
            dom = safe_eval(r.domain_force, eval_context) if r.domain_force else []
            return Model.search_count(expression.AND([
                [('id', 'in', ids)],
                expression.normalize_domain(dom)
            ])) < len(ids)

        return all_rules.filtered(lambda r: r in group_rules or (not r.groups and is_failing(r))).with_user(self.env.user)

    def _get_rules(self, model_name, mode='read'):
        """ Returns all the rules matching the model for the mode for the
        current user.
        """
        if mode not in self._MODES:
            raise ValueError('Invalid mode: %r' % (mode,))

        if self.env.su:
            return self.browse(())

        query = """ SELECT r.id FROM ir_rule r JOIN ir_model m ON (r.model_id=m.id)
                    WHERE m.model=%s AND r.active AND r.perm_{mode}
                    AND (r.id IN (SELECT rule_group_id FROM rule_group_rel rg
                                  JOIN res_groups_users_rel gu ON (rg.group_id=gu.gid)
                                  WHERE gu.uid=%s)
                         OR r.global)
                    ORDER BY r.id
                """.format(mode=mode)
        self._cr.execute(query, (model_name, self._uid))
        return self.browse(row[0] for row in self._cr.fetchall())

    @api.model
    @tools.conditional(
        'xml' not in config['dev_mode'],
        tools.ormcache('self.env.uid', 'self.env.su', 'model_name', 'mode',
                       'tuple(self._compute_domain_context_values())'),
    )
    def _compute_domain(self, model_name, mode="read"):
        rules = self._get_rules(model_name, mode=mode)
        if not rules:
            return

        # browse user and rules as SUPERUSER_ID to avoid access errors!
        eval_context = self._eval_context()
        user_groups = self.env.user.groups_id
        global_domains = []                     # list of domains
        group_domains = []                      # list of domains
        for rule in rules.sudo():
            # evaluate the domain for the current user
            dom = safe_eval(rule.domain_force, eval_context) if rule.domain_force else []
            dom = expression.normalize_domain(dom)
            if not rule.groups:
                global_domains.append(dom)
            elif rule.groups & user_groups:
                group_domains.append(dom)

        # combine global domains and group domains
        if not group_domains:
            return expression.AND(global_domains)
        return expression.AND(global_domains + [expression.OR(group_domains)])

    def _compute_domain_context_values(self):
        for k in self._compute_domain_keys():
            v = self._context.get(k)
            if isinstance(v, list):
                # currently this could be a frozenset (to avoid depending on
                # the order of allowed_company_ids) but it seems safer if
                # possibly slightly more miss-y to use a tuple
                v = tuple(v)
            yield v

    @api.model
    def clear_cache(self):
        """ Deprecated, use `clear_caches` instead. """
        self.clear_caches()

    @api.model
    def domain_get(self, model_name, mode='read'):
        dom = self._compute_domain(model_name, mode)
        if dom:
            # _where_calc is called as superuser. This means that rules can
            # involve objects on which the real uid has no acces rights.
            # This means also there is no implicit restriction (e.g. an object
            # references another object the user can't see).
            query = self.env[model_name].sudo()._where_calc(dom, active_test=False)
            return query.where_clause, query.where_clause_params, query.tables
        return [], [], ['"%s"' % self.env[model_name]._table]

    def unlink(self):
        res = super(IrRule, self).unlink()
        self.clear_caches()
        return res

    @api.model_create_multi
    def create(self, vals_list):
        res = super(IrRule, self).create(vals_list)
        # DLE P33: tests
        self.flush()
        self.clear_caches()
        return res

    def write(self, vals):
        res = super(IrRule, self).write(vals)
        # DLE P33: tests
        # - odoo/addons/test_access_rights/tests/test_feedback.py
        # - odoo/addons/test_access_rights/tests/test_ir_rules.py
        # - odoo/addons/base/tests/test_orm.py (/home/dle/src/odoo/master-nochange-fp/odoo/addons/base/tests/test_orm.py)
        self.flush()
        self.clear_caches()
        return res

    def _make_access_error(self, operation, records):
        _logger.info('Access Denied by record rules for operation: %s on record ids: %r, uid: %s, model: %s', operation, records.ids[:6], self._uid, records._name)

        model = records._name
        description = self.env['ir.model']._get(model).name or model
        if not self.env.user.has_group('base.group_no_one'):
            return AccessError(_('The requested operation cannot be completed due to security restrictions. Please contact your system administrator.\n\n(Document type: "%(document_kind)s" (%(document_model)s), Operation: %(operation)s)') % {
                'document_kind': description,
                'document_model': model,
                'operation': operation,
            })

        # This extended AccessError is only displayed in debug mode.
        # Note that by default, public and portal users do not have
        # the group "base.group_no_one", even if debug mode is enabled,
        # so it is relatively safe here to include the list of rules and
        # record names.
        rules = self._get_failing(records, mode=operation).sudo()
        error = AccessError(_("""The requested operation ("%(operation)s" on "%(document_kind)s" (%(document_model)s)) was rejected because of the following rules:
%(rules_list)s
%(multi_company_warning)s
(Records: %(example_records)s, User: %(user_id)s)""") % {
            'operation': operation,
            'document_kind': description,
            'document_model': model,
            'rules_list': '\n'.join('- %s' % rule.name for rule in rules),
            'multi_company_warning': ('\n' + _('Note: this might be a multi-company issue.') + '\n') if any(
                'company_id' in (r.domain_force or []) for r in rules) else '',
            'example_records': ' - '.join(['%s (id=%s)' % (rec.display_name, rec.id) for rec in records[:6].sudo()]),
            'user_id': '%s (id=%s)' % (self.env.user.name, self.env.user.id),
        })
        # clean up the cache of records prefetched with display_name above
        for record in records[:6]:
            record._cache.clear()
        return error
Example #9
0
class IrQWeb(models.AbstractModel, QWeb):
    """ Base QWeb rendering engine
    * to customize ``t-field`` rendering, subclass ``ir.qweb.field`` and
      create new models called :samp:`ir.qweb.field.{widget}`
    Beware that if you need extensions or alterations which could be
    incompatible with other subsystems, you should create a local object
    inheriting from ``ir.qweb`` and customize that.
    """

    _name = 'ir.qweb'
    _description = 'Qweb'

    @api.model
    def _render(self, id_or_xml_id, values=None, **options):
        """ render(id_or_xml_id, values, **options)

        Render the template specified by the given name.

        :param id_or_xml_id: name or etree (see get_template)
        :param dict values: template values to be used for rendering
        :param options: used to compile the template (the dict available for the rendering is frozen)
            * ``load`` (function) overrides the load method
            * ``profile`` (float) profile the rendering (use astor lib) (filter
              profile line with time ms >= profile)
        """

        context = dict(self.env.context,
                       dev_mode='qweb' in tools.config['dev_mode'])
        context.update(options)

        result = super(IrQWeb, self)._render(id_or_xml_id,
                                             values=values,
                                             **context)

        if b'data-pagebreak=' not in result:
            return result

        fragments = html.fragments_fromstring(result.decode('utf-8'))

        for fragment in fragments:
            for row in fragment.iterfind('.//tr[@data-pagebreak]'):
                table = next(row.iterancestors('table'))
                newtable = html.Element('table', attrib=dict(table.attrib))
                thead = table.find('thead')
                if thead:
                    newtable.append(copy.deepcopy(thead))
                # TODO: copy caption & tfoot as well?
                # TODO: move rows in a tbody if row.getparent() is one?

                pos = row.get('data-pagebreak')
                assert pos in ('before', 'after')
                for sibling in row.getparent().iterchildren('tr'):
                    if sibling is row:
                        if pos == 'after':
                            newtable.append(sibling)
                        break
                    newtable.append(sibling)

                table.addprevious(newtable)
                table.addprevious(
                    html.Element('div',
                                 attrib={'style': 'page-break-after: always'}))

        return MarkupSafeBytes(b''.join(html.tostring(f) for f in fragments))

    def default_values(self):
        """ attributes add to the values for each computed template
        """
        default = super(IrQWeb, self).default_values()
        default.update(
            request=request,
            cache_assets=round(time() / 180),
            true=True,
            false=False
        )  # true and false added for backward compatibility to remove after v10
        return default

    # assume cache will be invalidated by third party on write to ir.ui.view
    def _get_template_cache_keys(self):
        """ Return the list of context keys to use for caching ``_get_template``. """
        return [
            'lang', 'inherit_branding', 'editable', 'translatable',
            'edit_translations', 'website_id'
        ]

    # apply ormcache_context decorator unless in dev mode...
    @tools.conditional(
        'xml' not in tools.config['dev_mode'],
        tools.ormcache(
            'id_or_xml_id',
            'tuple(options.get(k) for k in self._get_template_cache_keys())'),
    )
    def compile(self, id_or_xml_id, options):
        try:
            id_or_xml_id = int(id_or_xml_id)
        except:
            pass
        return super(IrQWeb, self).compile(id_or_xml_id, options=options)

    def _load(self, name, options):
        lang = options.get('lang', get_lang(self.env).code)
        env = self.env
        if lang != env.context.get('lang'):
            env = env(context=dict(env.context, lang=lang))

        view_id = self.env['ir.ui.view'].get_view_id(name)
        template = env['ir.ui.view'].sudo()._read_template(view_id)

        # QWeb's `_read_template` will check if one of the first children of
        # what we send to it has a "t-name" attribute having `name` as value
        # to consider it has found it. As it'll never be the case when working
        # with view ids or children view or children primary views, force it here.
        def is_child_view(view_name):
            view_id = self.env['ir.ui.view'].get_view_id(view_name)
            view = self.env['ir.ui.view'].sudo().browse(view_id)
            return view.inherit_id is not None

        if isinstance(name, int) or is_child_view(name):
            view = etree.fromstring(template)
            for node in view:
                if node.get('t-name'):
                    node.set('t-name', str(name))
            return view
        else:
            return template

    # order

    def _directives_eval_order(self):
        directives = super(IrQWeb, self)._directives_eval_order()
        directives.insert(directives.index('call'), 'lang')
        directives.insert(directives.index('field'), 'call-assets')
        return directives

    # compile directives

    def _compile_directive_lang(self, el, options):
        lang = el.attrib.pop('t-lang', get_lang(self.env).code)
        if el.get('t-call-options'):
            el.set('t-call-options',
                   el.get('t-call-options')[0:-1] + u', "lang": %s}' % lang)
        else:
            el.set('t-call-options', u'{"lang": %s}' % lang)
        return self._compile_node(el, options)

    def _compile_directive_call_assets(self, el, options):
        """ This special 't-call' tag can be used in order to aggregate/minify javascript and css assets"""
        if len(el):
            raise SyntaxError("t-call-assets cannot contain children nodes")

        # nodes = self._get_asset_nodes(bundle, options, css=css, js=js, debug=values.get('debug'), async=async, values=values)
        #
        # for index, (tagName, t_attrs, content) in enumerate(nodes):
        #     if index:
        #         append('\n        ')
        #     append('<')
        #     append(tagName)
        #
        #     self._post_processing_att(tagName, t_attrs, options)
        #     for name, value in t_attrs.items():
        #         if value or isinstance(value, string_types)):
        #             append(u' ')
        #             append(name)
        #             append(u'="')
        #             append(escape(pycompat.to_text((value)))
        #             append(u'"')
        #
        #     if not content and tagName in self._void_elements:
        #         append('/>')
        #     else:
        #         append('>')
        #         if content:
        #           append(content)
        #         append('</')
        #         append(tagName)
        #         append('>')
        #
        space = el.getprevious() is not None and el.getprevious(
        ).tail or el.getparent().text
        sep = u'\n' + space.rsplit('\n').pop()
        return [
            ast.Assign(
                targets=[ast.Name(id='nodes', ctx=ast.Store())],
                value=ast.Call(
                    func=ast.Attribute(value=ast.Name(id='self',
                                                      ctx=ast.Load()),
                                       attr='_get_asset_nodes',
                                       ctx=ast.Load()),
                    args=[
                        ast.Str(el.get('t-call-assets')),
                        ast.Name(id='options', ctx=ast.Load()),
                    ],
                    keywords=[
                        ast.keyword('css',
                                    self._get_attr_bool(el.get('t-css',
                                                               True))),
                        ast.keyword('js',
                                    self._get_attr_bool(el.get('t-js', True))),
                        ast.keyword(
                            'debug',
                            ast.Call(func=ast.Attribute(value=ast.Name(
                                id='values', ctx=ast.Load()),
                                                        attr='get',
                                                        ctx=ast.Load()),
                                     args=[ast.Str('debug')],
                                     keywords=[],
                                     starargs=None,
                                     kwargs=None)),
                        ast.keyword(
                            'async_load',
                            self._get_attr_bool(el.get('async_load', False))),
                        ast.keyword(
                            'defer_load',
                            self._get_attr_bool(el.get('defer_load', False))),
                        ast.keyword(
                            'lazy_load',
                            self._get_attr_bool(el.get('lazy_load', False))),
                        ast.keyword('media', ast.Constant(el.get('media'))),
                    ],
                    starargs=None,
                    kwargs=None)),
            ast.For(
                target=ast.Tuple(elts=[
                    ast.Name(id='index', ctx=ast.Store()),
                    ast.Tuple(elts=[
                        ast.Name(id='tagName', ctx=ast.Store()),
                        ast.Name(id='t_attrs', ctx=ast.Store()),
                        ast.Name(id='content', ctx=ast.Store())
                    ],
                              ctx=ast.Store())
                ],
                                 ctx=ast.Store()),
                iter=ast.Call(func=ast.Name(id='enumerate', ctx=ast.Load()),
                              args=[ast.Name(id='nodes', ctx=ast.Load())],
                              keywords=[],
                              starargs=None,
                              kwargs=None),
                body=[
                    ast.If(test=ast.Name(id='index', ctx=ast.Load()),
                           body=[self._append(ast.Str(sep))],
                           orelse=[]),
                    self._append(ast.Str(u'<')),
                    self._append(ast.Name(id='tagName', ctx=ast.Load())),
                ] + self._append_attributes() + [
                    ast.If(test=ast.BoolOp(
                        op=ast.And(),
                        values=[
                            ast.UnaryOp(ast.Not(),
                                        ast.Name(id='content', ctx=ast.Load()),
                                        lineno=0,
                                        col_offset=0),
                            ast.Compare(
                                left=ast.Name(id='tagName', ctx=ast.Load()),
                                ops=[ast.In()],
                                comparators=[
                                    ast.Attribute(value=ast.Name(
                                        id='self', ctx=ast.Load()),
                                                  attr='_void_elements',
                                                  ctx=ast.Load())
                                ]),
                        ]),
                           body=[self._append(ast.Str(u'/>'))],
                           orelse=[
                               self._append(ast.Str(u'>')),
                               ast.If(test=ast.Name(id='content',
                                                    ctx=ast.Load()),
                                      body=[
                                          self._append(
                                              ast.Name(id='content',
                                                       ctx=ast.Load()))
                                      ],
                                      orelse=[]),
                               self._append(ast.Str(u'</')),
                               self._append(
                                   ast.Name(id='tagName', ctx=ast.Load())),
                               self._append(ast.Str(u'>')),
                           ])
                ],
                orelse=[])
        ]

    # method called by computing code

    def get_asset_bundle(self,
                         bundle_name,
                         files,
                         env=None,
                         css=True,
                         js=True):
        return AssetsBundle(bundle_name, files, env=env, css=css, js=js)

    def _get_asset_nodes(self,
                         bundle,
                         options,
                         css=True,
                         js=True,
                         debug=False,
                         async_load=False,
                         defer_load=False,
                         lazy_load=False,
                         media=None):
        """Generates asset nodes.
        If debug=assets, the assets will be regenerated when a file which composes them has been modified.
        Else, the assets will be generated only once and then stored in cache.
        """
        if debug and 'assets' in debug:
            return self._generate_asset_nodes(bundle, options, css, js, debug,
                                              async_load, defer_load,
                                              lazy_load, media)
        else:
            return self._generate_asset_nodes_cache(bundle, options, css, js,
                                                    debug, async_load,
                                                    defer_load, lazy_load,
                                                    media)

    @tools.conditional(
        # in non-xml-debug mode we want assets to be cached forever, and the admin can force a cache clear
        # by restarting the server after updating the source code (or using the "Clear server cache" in debug tools)
        'xml' not in tools.config['dev_mode'],
        tools.ormcache_context('bundle',
                               'options.get("lang", "en_US")',
                               'css',
                               'js',
                               'debug',
                               'async_load',
                               'defer_load',
                               'lazy_load',
                               keys=("website_id", )),
    )
    def _generate_asset_nodes_cache(self,
                                    bundle,
                                    options,
                                    css=True,
                                    js=True,
                                    debug=False,
                                    async_load=False,
                                    defer_load=False,
                                    lazy_load=False,
                                    media=None):
        return self._generate_asset_nodes(bundle, options, css, js, debug,
                                          async_load, defer_load, lazy_load,
                                          media)

    def _generate_asset_nodes(self,
                              bundle,
                              options,
                              css=True,
                              js=True,
                              debug=False,
                              async_load=False,
                              defer_load=False,
                              lazy_load=False,
                              media=None):
        nodeAttrs = None
        if css and media:
            nodeAttrs = {
                'media': media,
            }
        files, remains = self._get_asset_content(bundle, options, nodeAttrs)
        asset = self.get_asset_bundle(bundle,
                                      files,
                                      env=self.env,
                                      css=css,
                                      js=js)
        remains = [
            node for node in remains
            if (css and node[0] == 'link') or (js and node[0] == 'script')
        ]
        return remains + asset.to_node(css=css,
                                       js=js,
                                       debug=debug,
                                       async_load=async_load,
                                       defer_load=defer_load,
                                       lazy_load=lazy_load)

    def _get_asset_link_urls(self, bundle, options):
        asset_nodes = self._get_asset_nodes(bundle, options, js=False)
        return [node[1]['href'] for node in asset_nodes if node[0] == 'link']

    @tools.ormcache_context('bundle',
                            'options.get("lang", "en_US")',
                            keys=("website_id", ))
    def _get_asset_content(self, bundle, options, nodeAttrs=None):
        options = dict(options,
                       inherit_branding=False,
                       inherit_branding_auto=False,
                       edit_translations=False,
                       translatable=False,
                       rendering_bundle=True)

        options['website_id'] = self.env.context.get('website_id')

        asset_paths = self.env['ir.asset']._get_asset_paths(bundle=bundle,
                                                            css=True,
                                                            js=True)

        files = []
        remains = []
        for path, *_ in asset_paths:
            ext = path.split('.')[-1]
            is_js = ext in SCRIPT_EXTENSIONS
            is_css = ext in STYLE_EXTENSIONS
            if not is_js and not is_css:
                continue

            mimetype = 'text/javascript' if is_js else 'text/%s' % ext
            if can_aggregate(path):
                segments = [segment for segment in path.split('/') if segment]
                files.append({
                    'atype':
                    mimetype,
                    'url':
                    path,
                    'filename':
                    get_resource_path(*segments) if segments else None,
                    'content':
                    '',
                    'media':
                    nodeAttrs and nodeAttrs.get('media'),
                })
            else:
                if is_js:
                    tag = 'script'
                    attributes = {
                        "type": mimetype,
                        "src": path,
                    }
                else:
                    tag = 'link'
                    attributes = {
                        "type": mimetype,
                        "rel": "stylesheet",
                        "href": path,
                        'media': nodeAttrs and nodeAttrs.get('media'),
                    }
                remains.append((tag, attributes, ''))

        return (files, remains)

    def _get_field(self, record, field_name, expression, tagName,
                   field_options, options, values):
        field = record._fields[field_name]

        # adds template compile options for rendering fields
        field_options['template_options'] = options

        # adds generic field options
        field_options['tagName'] = tagName
        field_options['expression'] = expression
        field_options['type'] = field_options.get('widget', field.type)
        inherit_branding = options.get(
            'inherit_branding',
            options.get('inherit_branding_auto')
            and record.check_access_rights('write', False))
        field_options['inherit_branding'] = inherit_branding
        translate = options.get('edit_translations') and options.get(
            'translatable') and field.translate
        field_options['translate'] = translate

        # field converter
        model = 'ir.qweb.field.' + field_options['type']
        converter = self.env[model] if model in self.env else self.env[
            'ir.qweb.field']

        # get content
        content = converter.record_to_html(record, field_name, field_options)
        attributes = converter.attributes(record, field_name, field_options,
                                          values)

        return (attributes, content, inherit_branding or translate)

    def _get_widget(self, value, expression, tagName, field_options, options,
                    values):
        # adds template compile options for rendering fields
        field_options['template_options'] = options

        field_options['type'] = field_options['widget']
        field_options['tagName'] = tagName
        field_options['expression'] = expression

        # field converter
        model = 'ir.qweb.field.' + field_options['type']
        converter = self.env[model] if model in self.env else self.env[
            'ir.qweb.field']

        # get content
        content = converter.value_to_html(value, field_options)
        attributes = OrderedDict()
        attributes['data-oe-type'] = field_options['type']
        attributes['data-oe-expression'] = field_options['expression']

        return (attributes, content, None)

    # compile expression add safe_eval

    def _compile_expr(self, expr):
        """ Compiles a purported Python expression to ast, verifies that it's safe
        (according to safe_eval's semantics) and alter its variable references to
        access values data instead
        """
        # string must be stripped otherwise whitespace before the start for
        # formatting purpose are going to break parse/compile
        st = ast.parse(expr.strip(), mode='eval')
        assert_valid_codeobj(
            _SAFE_OPCODES,
            compile(st, '<>',
                    'eval'),  # could be expr, but eval *should* be fine
            expr)

        # ast.Expression().body -> expr
        return Contextifier(_BUILTINS).visit(st).body

    def _get_attr_bool(self, attr, default=False):
        if attr:
            if attr is True:
                return ast.Constant(True)
            attr = attr.lower()
            if attr in ('false', '0'):
                return ast.Constant(False)
            elif attr in ('true', '1'):
                return ast.Constant(True)
        return ast.Constant(attr if attr is False else bool(default))
Example #10
0
class MultiApprovalType(models.Model):
    _inherit = 'multi.approval.type'
    _order = 'priority'

    apply_for_model = fields.Boolean('Apply for Model?')
    is_configured = fields.Boolean('Configured?', copy=False)
    approve_python_code = fields.Text('Approved Action')
    refuse_python_code = fields.Text('Refused Action')

    domain = fields.Text(default='[]', string='Domain')
    model_id = fields.Selection(selection='_list_all_models', string='Model')
    view_id = fields.Many2one('ir.ui.view', string='Extension View')
    is_free_create = fields.Boolean('Free Create?')
    hide_button = fields.Boolean('Hide Buttons from Model View?')
    state_field_id = fields.Many2one('ir.model.fields',
                                     string="State / Stage Field")
    state_field = fields.Char()
    company_id = fields.Many2one(comodel_name='res.company', string='Company')
    group_ids = fields.Many2many('res.groups',
                                 'ma_type_group_rel',
                                 'ma_type_id',
                                 'group_id',
                                 string="Groups")
    is_global = fields.Boolean(compute="_compute_global", store=True)
    priority = fields.Integer(default=100)

    @api.model
    def _get_default_description(self):
        return '''
        Hi,
        </br> </br>
        Please review my request.</br> 
        Click <a target="__blank__" href="{record_url}"> {record.display_name}</a> to view more !
        </br> </br>
        Thanks,
        '''

    request_tmpl = fields.Html(default=_get_default_description)

    @api.constrains('approve_python_code', 'refuse_python_code')
    def _check_python_code(self):
        for rec in self.sudo().filtered('approve_python_code'):
            try:
                msg = test_python_expr(expr=rec.approve_python_code.strip(),
                                       mode="exec")
            except:
                raise ValidationError(_('Invalid python syntax'))
            if msg:
                raise ValidationError(msg)
        for rec in self.sudo().filtered('refuse_python_code'):
            try:
                msg = test_python_expr(expr=rec.refuse_python_code.strip(),
                                       mode="exec")
            except:
                raise ValidationError(_('Invalid python syntax'))
            if msg:
                raise ValidationError(msg)

    @api.depends('group_ids')
    def _compute_global(self):
        for rule in self:
            rule.is_global = not rule.group_ids

    @api.model
    def _get_types(self, model_name):
        type_ids = self._fetch_types(model_name)
        res = self.browse(type_ids)
        return res

    @api.model
    @tools.conditional(
        'xml' not in config['dev_mode'],
        tools.ormcache('self.env.uid', 'self.env.user.company_id.id',
                       'self.env.su', 'model_name'),
    )
    def _fetch_types(self, model_name):
        """
        Returns all the types matching the model
        """
        # if self.env.su:
        #    return self.browse(())
        query = """
            SELECT t.id FROM multi_approval_type t
            WHERE t.model_id='{model_name}'
                AND t.active
                AND t.is_configured
                AND (t.id IN (
                        SELECT ma_type_id
                        FROM ma_type_group_rel mg
                        JOIN res_groups_users_rel gu
                            ON (mg.group_id=gu.gid)
                        WHERE gu.uid={uid})
                    OR t.is_global)
                AND (t.company_id ISNULL
                    OR t.company_id={company_id})
            ORDER BY t.priority, t.id
        """.format(model_name=model_name,
                   uid=self.env.uid,
                   company_id=self.env.user.company_id.id)
        self._cr.execute(query)
        res = [row[0] for row in self._cr.fetchall()]
        return res

    def _compute_domain_keys(self):
        """
        Return the list of context keys to use for caching ``_compute_domain``.
        """
        return ['allowed_company_ids']

    def _compute_domain_context_values(self):
        for k in self._compute_domain_keys():
            v = self._context.get(k)
            if isinstance(v, list):
                v = tuple(v)
            yield v

    @api.model
    @tools.conditional(
        'xml' not in config['dev_mode'],
        tools.ormcache('self.env.uid', 'self.env.user.company_id.id',
                       'self.env.su', 'model_name',
                       'tuple(self._compute_domain_context_values())'),
    )
    def _compute_domain(self, model_name):
        rules = self._get_types(model_name)
        if not rules:
            return []

        # browse user and rules as SUPERUSER_ID to avoid access errors!
        all_domains = []  # list of domains
        for rule in rules.sudo():
            # evaluate the domain for the current user
            dom = safe_eval(rule.domain) if rule.domain else []
            dom = expression.normalize_domain(dom)
            all_domains.append(dom)

        # combine global domains and group domains
        return expression.OR(all_domains)

    @api.model
    def domain_get(self, model_name, get_not=False):
        dom = self._compute_domain(model_name)
        if dom and get_not:
            return expression.AND(['!'] + [dom])
        return dom

    def unlink(self):
        res = super(MultiApprovalType, self).unlink()
        self.clear_caches()
        return res

    @api.model_create_multi
    def create(self, vals_list):
        res = super(MultiApprovalType, self).create(vals_list)
        self.flush()
        self.clear_caches()
        return res

    def write(self, vals):
        res = super(MultiApprovalType, self).write(vals)
        self.flush()
        self.clear_caches()
        return res

    @api.onchange('apply_for_model')
    def _reset_values(self):
        if self.apply_for_model:
            self.document_opt = 'Optional'
            self.contact_opt = 'None'
            self.date_opt = 'None'
            self.period_opt = 'None'
            self.item_opt = 'None'
            self.quantity_opt = 'None'
            self.amount_opt = 'None'
            self.reference_opt = 'None'
            self.payment_opt = 'None'
            self.location_opt = 'None'

    def action_archive(self):
        res = super(MultiApprovalType, self).action_archive()
        # Untick "Is Config?"
        # Delete view
        for r in self:
            r = r.sudo()
            vals = {
                'is_configured': False,
                'state_field_id': False,
                'state_field': False
            }
            view = None
            if r.view_id:
                vals.update({'view_id': False})
                view = r.view_id
            r.write(vals)
            if view:
                # search
                args = [('view_id', '=', view.id), ('id', '!=', r.id)]
                exist = self.search(args, limit=1)
                if not exist:
                    view.unlink()
        return res

    @api.model
    def _list_all_models(self):
        self._cr.execute("SELECT model, name FROM ir_model ORDER BY name")
        return self._cr.fetchall()

    def open_submitted_request(self):
        self.ensure_one()
        view_id = self.env.ref('multi_level_approval.multi_approval_view_form',
                               False)
        res = {
            'name': _('Submitted Requests'),
            'view_mode': 'tree,form',
            'res_model': 'multi.approval',
            'view_id': False,
            'type': 'ir.actions.act_window',
            'domain': [('type_id', '=', self.id), ('state', '=', 'Submitted')],
        }
        if not self.apply_for_model:
            res.update({'context': {
                'default_type_id': self.id,
            }})
        return res

    def _domain(self):
        self.ensure_one()
        dmain = safe_eval(self.domain)
        return dmain

    def create_fields(self, f_names, model_id):
        ResField = self.env['ir.model.fields']
        for f_name, ttype in f_names.items():
            field_record = ResField._get(self.model_id, f_name)
            if not field_record:
                f_vals = {
                    'name': f_name,
                    'field_description': f_name,
                    'ttype': ttype,
                    'copied': False,
                    'model_id': model_id
                }
                ResField.create(f_vals)

    def get_compute_val(self, f_name):
        vals = '''
for rec in self:
  rec['{f_name}'] = rec.env['multi.approval.type'].compute_need_approval(rec)
        '''.format(f_name=f_name)
        return vals

    def create_compute_field(self, f_name, model_id):
        ResField = self.env['ir.model.fields']
        compute_f = ResField._get(self.model_id, f_name)
        compute_val = self.get_compute_val(f_name)
        if not compute_f:
            f_vals = {
                'name': f_name,
                'field_description': f_name,
                'ttype': 'boolean',
                'copied': False,
                'store': False,
                'model_id': model_id,
                #'depends': 'create_date',
                'compute': compute_val
            }
            ResField.create(f_vals)
        else:
            # Update compute function
            compute_f.write({'compute': compute_val})

    def get_default_view(self):
        view_id = self.env['ir.ui.view'].default_view(self.model_id, 'form')
        return view_id

    def get_existed_view(self):
        self.ensure_one()
        args = [('model_id', '=', self.model_id), ('view_id', '!=', False)]
        exist = self.search(args, limit=1)
        return exist.view_id

    def create_view(self, f_name, f_name1, f_name2):
        '''
        1. Find a base view
        2. Check if it has a header path already?
        3. If not, create new header path
        4. Insert 2 button inside the header path
            - Request Approval: if there is no request yet
            - View Approval: if already has some
        '''
        if self.view_id:
            return False
        existed_view = self.get_existed_view()
        if existed_view:
            self.view_id = existed_view
            return False
        IrView = self.env['ir.ui.view']
        model_id = self.env['ir.model']._get_id(self.model_id)
        view_id = self.get_default_view()
        if not view_id:
            raise Warning(_('This model has no form view !'))
        view_content = self.env[self.model_id]._fields_view_get(view_id)
        view_arch = etree.fromstring(view_content['arch'])
        node = IrView.locate_node(
            view_arch,
            E.xpath(expr="//form/header"),
        )
        wiz_act = self.env.ref(
            'multi_level_approval_configuration.request_approval_action',
            False)
        wiz_view_act = self.env.ref(
            'multi_level_approval_configuration.action_open_request', False)
        wiz_rework_act = self.env.ref(
            'multi_level_approval_configuration.rework_approval_action', False)
        if not wiz_act or not wiz_view_act or not wiz_rework_act:
            raise Warning(_('Not found the action !'))

        f_node = E.field(name=f_name, invisible="1")
        f1_node = E.field(name=f_name1, invisible="1")
        f2_node = E.field(name=f_name2, invisible="1")
        btn_req_node = E.button(
            {
                'class': "oe_highlight",
                'approval_btn': '1'
            },
            name=str(wiz_act.id),
            type="action",
            string="Request Approval",
            groups="multi_level_approval.group_approval_user",
            attrs=str({
                'invisible': ['|', (f_name, '=', False), (f_name2, '=', True)]
            }),
        )
        btn_vie_node = E.button(
            {'approval_btn': '1'},
            name=str(wiz_view_act.id),
            type="action",
            string="View Approval",
            groups="multi_level_approval.group_approval_user",
            attrs=str({'invisible': [(f_name2, '=', False)]}))
        btn_refuse_node = E.button(
            {'approval_btn': '1'},
            name=str(wiz_rework_act.id),
            type="action",
            string="Rework",
            groups="multi_level_approval.group_approval_user",
            attrs=str({'invisible': [(f_name1, '!=', 'refused')]}))

        # div is insert right after the header
        div_node1 = E.div(
            'This document need to be approved !', {
                'class': "alert alert-info",
                'style': 'margin-bottom:0px;',
                'role': 'alert'
            },
            attrs=str({
                'invisible':
                ['|', (f_name, '=', False), (f_name1, '!=', False)]
            }))
        div_node2 = E.div(
            'This document has been approved !', {
                'class': "alert alert-info",
                'style': 'margin-bottom:0px;',
                'role': 'alert'
            },
            attrs=str({
                'invisible':
                ['|', (f_name, '=', False), (f_name1, '!=', 'approved')]
            }))
        div_node3 = E.div(
            'This document has been refused !', {
                'class': "alert alert-danger",
                'style': 'margin-bottom:0px;',
                'role': 'alert'
            },
            attrs=str({
                'invisible':
                ['|', (f_name, '=', False), (f_name1, '!=', 'refused')]
            }))
        div_node = E.div(div_node1, div_node2, div_node3)

        # Create header tag if there is not yet
        if node is None:
            header_node = E.header(f_node, f1_node, f2_node, btn_req_node,
                                   btn_vie_node, btn_refuse_node)
            # find a sheet
            sheet_node = IrView.locate_node(
                view_arch,
                E.xpath(expr="//form/sheet"),
            )
            expr = "//form/sheet"
            position = "before"
            if sheet_node is None:
                expr = "//form"
                position = "inside"
            xml = E.xpath(header_node, div_node, expr=expr, position=position)
        else:
            xml0 = E.xpath(f_node,
                           f1_node,
                           f2_node,
                           btn_req_node,
                           btn_vie_node,
                           btn_refuse_node,
                           expr="//form/header",
                           position="inside")
            xml1 = E.xpath(div_node, expr="//form/header", position="after")
            xml = E.data(xml0, xml1)
        xml_content = etree.tostring(xml,
                                     pretty_print=True,
                                     encoding="unicode")
        new_view_name = 'approval_view_' + fields.Datetime.now().strftime(
            '%Y%m%d%H%M%S')
        new_view = IrView.create({
            'name': new_view_name,
            'model': self.model_id,
            'inherit_id': view_id,
            'arch': xml_content
        })

        # create new field for model
        self.env['ir.model.data'].create({
            'module': 'multi_level_approval_configuration',
            'name': new_view_name,
            'model': 'ir.ui.view',
            'noupdate': True,
            'res_id': new_view.id,
        })
        self.view_id = new_view

    def check_state_field(self):
        '''
        if no state field is detected, return a window action
        '''
        if self.state_field_id:
            if not self.state_field:
                self.state_field = self.state_field_id.name
            return False
        dmain = self._domain()
        if not dmain:
            raise Warning(_('Domain is required !'))
        potential_f = [
            'state', 'state_id', 'stage', 'stage_id', 'status', 'status_id'
        ]
        state_field = ''
        dmain_fields = []
        for d in dmain:
            if not d or not isinstance(d, (list, tuple)):
                continue
            dmain_fields += [d[0]]
            if d[0] in potential_f:
                state_field = d[0]
                break
        if state_field:
            ResField = self.env['ir.model.fields']
            field_record = ResField._get(self.model_id, state_field)
            self.write({
                'state_field_id': field_record.id,
                'state_field': state_field
            })
        else:
            view = self.env.ref(
                'multi_level_approval_configuration.multi_approval_type_view_form_popup'
            )
            ResModel = self.env['ir.model']
            model_id = ResModel._get_id(self.model_id)
            ctx = {'dmain_fields': dmain_fields, 'res_model_id': model_id}
            return {
                'name': _('Select State Field'),
                'type': 'ir.actions.act_window',
                'view_type': 'form',
                'view_mode': 'form',
                'res_model': 'multi.approval.type',
                'views': [(view.id, 'form')],
                'view_id': view.id,
                'res_id': self.id,
                'target': 'new',
                'context': ctx,
            }

    def action_configure(self):
        self.ensure_one()
        self = self.sudo()
        if not self.active:
            return False
        ResModel = self.env['ir.model']

        # Check state / stage field
        ret_act = self.check_state_field()
        if ret_act:
            return ret_act

        # Create new fields
        f_name_dict = {
            'x_review_result': 'char',
            'x_has_request_approval': 'boolean'
        }
        f_names = ['x_review_result', 'x_has_request_approval']
        model_id = ResModel._get_id(self.model_id)
        self.create_fields(f_name_dict, model_id)

        # Create compute field
        compute_field = 'x_need_approval'
        self.create_compute_field(compute_field, model_id)

        # create extension view
        self.create_view(compute_field, f_names[0], f_names[1])
        self.is_configured = True

    @api.model
    def check_boo(self, rec, dmain):
        '''
        This function is used for 13.0.1.0 only
        '''
        dmain = [('id', '=', rec.id)] + dmain
        res = rec.search_count(dmain)
        if res:
            return True
        return False

    @api.model
    def compute_need_approval(self, rec):
        dmain = self.domain_get(rec._name)
        if not dmain or isinstance(rec.id, models.NewId):
            return False
        dmain = [('id', '=', rec.id)] + dmain
        res = rec.search_count(dmain)
        if res:
            return True
        return False

    @api.model
    def update_x_field(self, obj, fi, val=True):
        if hasattr(obj, fi):
            obj.sudo().write({fi: val})
        else:
            raise Warning(_('Something wrong !'))

    @api.model
    def open_request(self):
        ctx = self._context
        model_name = ctx.get('active_model')
        res_id = ctx.get('active_id')
        origin_ref = '{model},{res_id}'.format(model=model_name, res_id=res_id)
        return {
            'name': 'My Requests',
            'type': 'ir.actions.act_window',
            'res_model': 'multi.approval',
            'view_type': 'list',
            'view_mode': 'list,form',
            'target': 'current',
            'domain': [('origin_ref', '=', origin_ref)],
        }

    @api.model
    def _get_eval_context(self, record=None):
        """ evaluation context to pass to safe_eval """
        if not record:
            raise Warning(_('Something is wrong !'))

        def log(message, level="info"):
            message = str(message)
            with self.pool.cursor() as cr:
                cr.execute(
                    """
                    INSERT INTO ir_logging(create_date, create_uid, type, dbname, name, level, message, path, line, func)
                    VALUES (NOW() at time zone 'UTC', %s, %s, %s, %s, %s, %s, %s, %s, %s)
                """, (self.env.uid, 'server', self._cr.dbname, __name__, level,
                      message, "approval", record.id, record.name))

        model_name = record._name
        model = self.env[model_name]
        ctx = self._context.copy()
        ctx.update({'run_python_code': 1})
        eval_context = {
            'uid': self._uid,
            'user': self.env.user,
            # orm
            'env': self.env,
            'model': model,
            # Exceptions
            'Warning': Warning,
            # record
            'record': record.with_context(ctx),
            # helpers
            'log': log,
        }
        return eval_context

    def run(self, record=None, action='approve'):
        """
        """
        res = False
        for rec in self:
            if record.x_need_approval:
                res = rec._run(record, action)
        return res

    def _run(self, record, action):
        self.ensure_one()
        res = False
        func_name = 'get_action_%s_code' % action
        if hasattr(self, func_name):
            func = getattr(self, func_name)
            python_code = func(action)
            if not python_code:
                return res
            eval_context = self._get_eval_context(record)
            res = self.exec_func(python_code, eval_context)
        return res

    def get_action_approve_code(self, action):
        return self.approve_python_code

    def get_action_refuse_code(self, action):
        return self.refuse_python_code

    @api.model
    def exec_func(self, python_code='', eval_context=None):
        try:
            safe_eval(python_code.strip(),
                      eval_context,
                      mode="exec",
                      nocopy=True)  # nocopy allows to return 'action'
        except Exception as e:
            raise Warning(ustr(e))
        except:
            raise Warning(
                _('''
Approval Type is not configured properly, contact your administrator for help!
'''))
        if 'action' in eval_context:
            return eval_context['action']

    @api.model
    @tools.ormcache()
    def _get_applied_models(self):
        args = [('is_configured', '=', True)]
        types = self.search(args)
        model_names = types.mapped('model_id')
        return model_names

    @api.model
    def check_rule(self, records, vals):
        '''
        1. Get approval type if possible
        2. check (not x_review_result and x_need_approval)
        3. prevent from editing the fields in domain
        '''
        if self.env.su or self._context.get('run_python_code'):
            return True
        # Find the approval type
        model_name = records._name
        if model_name in ('multi.approval.type', 'ir.module.module'):
            return True
        available_models = self._get_applied_models()
        if model_name not in available_models:
            return True
        approval_types = self._get_types(model_name)
        if not approval_types:
            return True
        approval_type = approval_types[0]
        for rec in records:
            if not rec.x_need_approval or rec.x_review_result == 'approved':
                continue
            if rec.x_review_result == 'refused':
                raise Warning(self._make_err_msg(True))
            # Could not update state field
            if approval_type.state_field and approval_type.state_field in vals:
                raise Warning(self._make_err_msg())
        return True

    def _make_err_msg(self, refused=False):
        error = _('This document need to be approved by manager !')
        if refused:
            error = _('This document has been refused by manager !')
        return error

    @api.model
    def filter_type(self, types, model_name, res_id):
        for t in types:
            args = t._domain() or []
            args = expression.AND([args] + [[('id', '=', res_id)]])
            existed = self.env[model_name]._search(args, limit=1)
            if existed:
                return t
        return self