summaryrefslogtreecommitdiff
path: root/odoo/addons/base/ir/ir_values.py
diff options
context:
space:
mode:
Diffstat (limited to 'odoo/addons/base/ir/ir_values.py')
-rw-r--r--odoo/addons/base/ir/ir_values.py418
1 files changed, 418 insertions, 0 deletions
diff --git a/odoo/addons/base/ir/ir_values.py b/odoo/addons/base/ir/ir_values.py
new file mode 100644
index 00000000000..46d533b465b
--- /dev/null
+++ b/odoo/addons/base/ir/ir_values.py
@@ -0,0 +1,418 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import api, fields, models, tools, _
+from odoo.exceptions import AccessError, MissingError
+from odoo.tools import pickle
+
+EXCLUDED_FIELDS = set((
+ 'report_sxw_content', 'report_rml_content', 'report_sxw', 'report_rml',
+ 'report_sxw_content_data', 'report_rml_content_data', 'search_view', ))
+
+#: Possible slots to bind an action to with :meth:`~.set_action`
+ACTION_SLOTS = [
+ "client_action_multi", # sidebar wizard action
+ "client_print_multi", # sidebar report printing button
+ "client_action_relate", # sidebar related link
+ "tree_but_open", # double-click on item in tree view
+ "tree_but_action", # deprecated: same as tree_but_open
+]
+
+
+class IrValues(models.Model):
+ """Holds internal model-specific action bindings and user-defined default
+ field values. definitions. This is a legacy internal model, mixing
+ two different concepts, and will likely be updated or replaced in a
+ future version by cleaner, separate models. You should not depend
+ explicitly on it.
+
+ The purpose of each ``ir.values`` entry depends on its type, defined
+ by the ``key`` column:
+
+ * 'default': user-defined default values, used when creating new
+ records of this model:
+ * 'action': binding of an action to a particular *action slot* of
+ this model, making the action easily available in the user
+ interface for this model.
+
+ The ``key2`` column acts as a qualifier, further refining the type
+ of the entry. The possible values are:
+
+ * for 'default' entries: an optional condition restricting the
+ cases where this particular default value will be applicable,
+ or ``False`` for no condition
+ * for 'action' entries: the ``key2`` qualifier is one of the available
+ action slots, defining how this action can be invoked:
+
+ * ``'client_print_multi'`` for report printing actions that will
+ be available on views displaying items from this model
+ * ``'client_action_multi'`` for assistants (wizards) actions
+ that will be available in views displaying objects of this model
+ * ``'client_action_relate'`` for links towards related documents
+ that should be available in views displaying objects of this model
+ * ``'tree_but_open'`` for actions that will be triggered when
+ double-clicking an item from this model in a hierarchical tree view
+
+ Each entry is specific to a model (``model`` column), and for ``'actions'``
+ type, may even be made specific to a given record of that model when the
+ ``res_id`` column contains a record ID (``False`` means it's global for
+ all records).
+
+ The content of the entry is defined by the ``value`` column, which may either
+ contain an arbitrary value, or a reference string defining the action that
+ should be executed.
+
+ .. rubric:: Usage: default values
+
+ The ``'default'`` entries are usually defined manually by the
+ users, and set by their UI clients calling :meth:`~.set_default`.
+ These default values are then automatically used by the
+ ORM every time a new record is about to be created, i.e. when
+ :meth:`~odoo.models.Model.default_get`
+ or :meth:`~odoo.models.Model.create` are called.
+
+ .. rubric:: Usage: action bindings
+
+ Business applications will usually bind their actions during
+ installation, and Odoo UI clients will apply them as defined,
+ based on the list of actions included in the result of
+ :meth:`~odoo.models.Model.fields_view_get`,
+ or directly returned by explicit calls to :meth:`~.get_actions`.
+ """
+ _name = 'ir.values'
+
+ name = fields.Char(required=True)
+ model = fields.Char(string='Model Name', index=True, required=True,
+ help="Model to which this entry applies")
+
+ # TODO: model_id and action_id should be read-write function fields
+ model_id = fields.Many2one('ir.model', string='Model (change only)',
+ help="Model to which this entry applies - "
+ "helper field for setting a model, will "
+ "automatically set the correct model name")
+ action_id = fields.Many2one('ir.actions.actions', string='Action (change only)',
+ help="Action bound to this entry - "
+ "helper field for binding an action, will "
+ "automatically set the correct reference")
+
+ value = fields.Text(help="Default value (pickled) or reference to an action")
+ value_unpickle = fields.Text(string='Default value or action reference',
+ compute='_value_unpickle', inverse='_value_pickle')
+ key = fields.Selection([('action', 'Action'), ('default', 'Default')],
+ string='Type', index=True, required=True, default='action',
+ help="- Action: an action attached to one slot of the given model\n"
+ "- Default: a default value for a model field")
+ key2 = fields.Char(string='Qualifier', index=True, default='tree_but_open',
+ help="For actions, one of the possible action slots: \n"
+ " - client_action_multi\n"
+ " - client_print_multi\n"
+ " - client_action_relate\n"
+ " - tree_but_open\n"
+ "For defaults, an optional condition")
+ res_id = fields.Integer(string='Record ID', index=True,
+ help="Database identifier of the record to which this applies. "
+ "0 = for all records")
+ user_id = fields.Many2one('res.users', string='User', ondelete='cascade', index=True,
+ help="If set, action binding only applies for this user.")
+ company_id = fields.Many2one('res.company', string='Company', ondelete='cascade', index=True,
+ help="If set, action binding only applies for this company")
+
+ @api.depends('key', 'value')
+ def _value_unpickle(self):
+ for record in self:
+ value = record.value
+ if record.key == 'default' and value:
+ # default values are pickled on the fly
+ with tools.ignore(Exception):
+ value = str(pickle.loads(value))
+ record.value_unpickle = value
+
+ def _value_pickle(self):
+ context = dict(self._context)
+ context.pop(self.CONCURRENCY_CHECK_FIELD, None)
+ for record in self.with_context(context):
+ value = record.value_unpickle
+ if record.key == 'default':
+ value = pickle.dumps(value)
+ record.value = value
+
+ @api.onchange('model_id')
+ def onchange_object_id(self):
+ if self.model_id:
+ self.model = self.model_id.model
+
+ @api.onchange('action_id')
+ def onchange_action_id(self):
+ if self.action_id:
+ self.value_unpickle = self.action_id
+
+ @api.model_cr_context
+ def _auto_init(self):
+ res = super(IrValues, self)._auto_init()
+ self._cr.execute("SELECT indexname FROM pg_indexes WHERE indexname = 'ir_values_key_model_key2_res_id_user_id_idx'")
+ if not self._cr.fetchone():
+ self._cr.execute("CREATE INDEX ir_values_key_model_key2_res_id_user_id_idx ON ir_values (key, model, key2, res_id, user_id)")
+ return res
+
+ @api.model
+ def create(self, vals):
+ self.clear_caches()
+ return super(IrValues, self).create(vals)
+
+ @api.multi
+ def write(self, vals):
+ self.clear_caches()
+ return super(IrValues, self).write(vals)
+
+ @api.multi
+ def unlink(self):
+ self.clear_caches()
+ return super(IrValues, self).unlink()
+
+ @api.model
+ @api.returns('self', lambda value: value.id)
+ def set_default(self, model, field_name, value, for_all_users=True, company_id=False, condition=False):
+ """Defines a default value for the given model and field_name. Any previous
+ default for the same scope (model, field_name, value, for_all_users, company_id, condition)
+ will be replaced and lost in the process.
+
+ Defaults can be later retrieved via :meth:`~.get_defaults`, which will return
+ the highest priority default for any given field. Defaults that are more specific
+ have a higher priority, in the following order (highest to lowest):
+
+ * specific to user and company
+ * specific to user only
+ * specific to company only
+ * global to everyone
+
+ :param string model: model name
+ :param string field_name: field name to which the default applies
+ :param value: the default field value to set
+ :type value: any serializable Python value
+ :param bool for_all_users: whether the default should apply to everybody or only
+ the user calling the method
+ :param int company_id: optional ID of the company to which the default should
+ apply. If omitted, the default will be global. If True
+ is passed, the current user's company will be used.
+ :param string condition: optional condition specification that can be used to
+ restrict the applicability of the default values
+ (e.g. based on another field's value). This is an
+ opaque string as far as the API is concerned, but client
+ stacks typically use single-field conditions in the
+ form ``'key=stringified_value'``.
+ (Currently, the condition is trimmed to 200 characters,
+ so values that share the same first 200 characters always
+ match)
+ :return: the newly created ir.values entry
+ """
+ if isinstance(value, unicode):
+ value = value.encode('utf8')
+ if company_id is True:
+ # should be company-specific, need to get company id
+ company_id = self.env.user.company_id.id
+
+ # remove existing defaults for the same scope
+ search_criteria = [
+ ('key', '=', 'default'),
+ ('key2', '=', condition and condition[:200]),
+ ('model', '=', model),
+ ('name', '=', field_name),
+ ('user_id', '=', False if for_all_users else self._uid),
+ ('company_id', '=', company_id)
+ ]
+ self.search(search_criteria).unlink()
+
+ return self.create({
+ 'name': field_name,
+ 'value': pickle.dumps(value),
+ 'model': model,
+ 'key': 'default',
+ 'key2': condition and condition[:200],
+ 'user_id': False if for_all_users else self._uid,
+ 'company_id': company_id,
+ })
+
+ @api.model
+ def get_default(self, model, field_name, for_all_users=True, company_id=False, condition=False):
+ """ Return the default value defined for model, field_name, users, company and condition.
+ Return ``None`` if no such default exists.
+ """
+ search_criteria = [
+ ('key', '=', 'default'),
+ ('key2', '=', condition and condition[:200]),
+ ('model', '=', model),
+ ('name', '=', field_name),
+ ('user_id', '=', False if for_all_users else self._uid),
+ ('company_id', '=', company_id)
+ ]
+ defaults = self.search(search_criteria)
+ return pickle.loads(defaults.value.encode('utf-8')) if defaults else None
+
+ @api.model
+ def get_defaults(self, model, condition=False):
+ """Returns any default values that are defined for the current model and user,
+ (and match ``condition``, if specified), previously registered via
+ :meth:`~.set_default`.
+
+ Defaults are global to a model, not field-specific, but an optional
+ ``condition`` can be provided to restrict matching default values
+ to those that were defined for the same condition (usually based
+ on another field's value).
+
+ Default values also have priorities depending on whom they apply
+ to: only the highest priority value will be returned for any
+ field. See :meth:`~.set_default` for more details.
+
+ :param string model: model name
+ :param string condition: optional condition specification that can be used to
+ restrict the applicability of the default values
+ (e.g. based on another field's value). This is an
+ opaque string as far as the API is concerned, but client
+ stacks typically use single-field conditions in the
+ form ``'key=stringified_value'``.
+ (Currently, the condition is trimmed to 200 characters,
+ so values that share the same first 200 characters always
+ match)
+ :return: list of default values tuples of the form ``(id, field_name, value)``
+ (``id`` is the ID of the default entry, usually irrelevant)
+ """
+ # use a direct SQL query for performance reasons,
+ # this is called very often
+ query = """ SELECT v.id, v.name, v.value FROM ir_values v
+ LEFT JOIN res_users u ON (v.user_id = u.id)
+ WHERE v.key = %%s AND v.model = %%s
+ AND (v.user_id = %%s OR v.user_id IS NULL)
+ AND (v.company_id IS NULL OR
+ v.company_id = (SELECT company_id FROM res_users WHERE id = %%s)
+ )
+ %s
+ ORDER BY v.user_id, u.company_id"""
+ params = ('default', model, self._uid, self._uid)
+ if condition:
+ query = query % 'AND v.key2 = %s'
+ params += (condition[:200],)
+ else:
+ query = query % 'AND v.key2 IS NULL'
+ self._cr.execute(query, params)
+
+ # keep only the highest priority default for each field
+ defaults = {}
+ for row in self._cr.dictfetchall():
+ value = pickle.loads(row['value'].encode('utf-8'))
+ defaults.setdefault(row['name'], (row['id'], row['name'], value))
+ return defaults.values()
+
+ # use ormcache: this is called a lot by BaseModel.default_get()!
+ @api.model
+ @tools.ormcache('self._uid', 'model', 'condition')
+ def get_defaults_dict(self, model, condition=False):
+ """ Returns a dictionary mapping field names with their corresponding
+ default value. This method simply improves the returned value of
+ :meth:`~.get_defaults`.
+ """
+ return dict((f, v) for i, f, v in self.get_defaults(model, condition))
+
+ @api.model
+ @api.returns('self', lambda value: value.id)
+ def set_action(self, name, action_slot, model, action, res_id=False):
+ """Binds an the given action to the given model's action slot - for later
+ retrieval via :meth:`~.get_actions`. Any existing binding of the same action
+ to the same slot is first removed, allowing an update of the action's name.
+ See the class description for more details about the various action
+ slots: :class:`~ir_values`.
+
+ :param string name: action label, usually displayed by UI client
+ :param string action_slot: the action slot to which the action should be
+ bound to - one of ``client_action_multi``,
+ ``client_print_multi``, ``client_action_relate``,
+ ``tree_but_open``.
+ :param string model: model name
+ :param string action: action reference, in the form ``'model,id'``
+ :param int res_id: optional record id - will bind the action only to a
+ specific record of the model, not all records.
+ :return: the newly created ir.values entry
+ """
+ assert isinstance(action, basestring) and ',' in action, \
+ 'Action definition must be an action reference, e.g. "ir.actions.act_window,42"'
+ assert action_slot in ACTION_SLOTS, \
+ 'Action slot (%s) must be one of: %r' % (action_slot, ACTION_SLOTS)
+
+ # remove existing action definition of same slot and value
+ search_criteria = [
+ ('key', '=', 'action'),
+ ('key2', '=', action_slot),
+ ('model', '=', model),
+ ('res_id', '=', res_id or 0), # int field -> NULL == 0
+ ('value', '=', action),
+ ]
+ self.search(search_criteria).unlink()
+
+ return self.create({
+ 'key': 'action',
+ 'key2': action_slot,
+ 'model': model,
+ 'res_id': res_id,
+ 'name': name,
+ 'value': action,
+ })
+
+ @api.model
+ @tools.ormcache_context('self._uid', 'action_slot', 'model', 'res_id', keys=('lang',))
+ def get_actions(self, action_slot, model, res_id=False):
+ """Retrieves the list of actions bound to the given model's action slot.
+ See the class description for more details about the various action
+ slots: :class:`~.ir_values`.
+
+ :param string action_slot: the action slot to which the actions should be
+ bound to - one of ``client_action_multi``,
+ ``client_print_multi``, ``client_action_relate``,
+ ``tree_but_open``.
+ :param string model: model name
+ :param int res_id: optional record id - will bind the action only to a
+ specific record of the model, not all records.
+ :return: list of action tuples of the form ``(id, name, action_def)``,
+ where ``id`` is the ID of the default entry, ``name`` is the
+ action label, and ``action_def`` is a dict containing the
+ action definition as obtained by calling
+ :meth:`~odoo.models.Model.read` on the action record.
+ """
+ assert action_slot in ACTION_SLOTS, 'Illegal action slot value: %s' % action_slot
+ # use a direct SQL query for performance reasons,
+ # this is called very often
+ query = """ SELECT v.id, v.name, v.value FROM ir_values v
+ WHERE v.key = %s AND v.key2 = %s AND v.model = %s
+ AND (v.res_id = %s OR v.res_id IS NULL OR v.res_id = 0)
+ ORDER BY v.id """
+ self._cr.execute(query, ('action', action_slot, model, res_id or None))
+
+ # map values to their corresponding action record
+ actions = []
+ for id, name, value in self._cr.fetchall():
+ if not value:
+ continue # skip if undefined
+ action_model, action_id = value.split(',')
+ if action_model not in self.env:
+ continue # unknown model? skip it!
+ action = self.env[action_model].browse(int(action_id))
+ actions.append((id, name, action))
+
+ # process values and their action
+ results = {}
+ for id, name, action in actions:
+ fields = [field for field in action._fields if field not in EXCLUDED_FIELDS]
+ # FIXME: needs cleanup
+ try:
+ action_def = {
+ field: action._fields[field].convert_to_read(action[field], action)
+ for field in fields
+ }
+ if action._name in ('ir.actions.report.xml', 'ir.actions.act_window'):
+ if action.groups_id and not action.groups_id & self.env.user.groups_id:
+ if name == 'Menuitem':
+ raise AccessError(_('You do not have the permission to perform this operation!!!'))
+ continue
+ # keep only the last action registered for each action name
+ results[name] = (id, name, action_def)
+ except (AccessError, MissingError):
+ continue
+ return sorted(results.values())