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
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
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):
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)
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())
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
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
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
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))
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