diff --git a/doc/glossary.rst b/doc/glossary.rst index 278098a5923..29cf6c4c7e6 100644 --- a/doc/glossary.rst +++ b/doc/glossary.rst @@ -13,6 +13,9 @@ ``account.invoice_graph``). From within a module, the :samp:`{module}.` prefix can be left out. + Sometimes referred to as "xml id" or ``xml_id`` as XML-based + :ref:`reference/data` make extensive use of them. + format string inspired by `jinja variables`_, format strings allow more easily mixing literal content and computed content (expressions): content diff --git a/doc/reference/orm.rst b/doc/reference/orm.rst index 03548d1e399..94aa18b4863 100644 --- a/doc/reference/orm.rst +++ b/doc/reference/orm.rst @@ -4,10 +4,678 @@ ORM === +Recordsets +========== + +.. versionadded:: 8.0 + +Interaction with models and records is performed through recordsets, a sorted +set of records of the same model. + +.. warning:: contrary to what the name implies, it is currently possible for + recordsets to contain duplicates. This may change in the future. + +Methods defined on a model are executed on a recordset, and their ``self`` is +a recordset:: + + class AModel(Model): + _name = 'a.model' + def a_method(self): + # self can be anywhere between 0 records and all records in the + # database + self.do_operation() + +Iterating on a recordset will yield new sets of *a single record* +("singletons"), much like iterating on a Python string yields strings of a +single characters:: + + def do_operation(self): + print self # => a.model(1, 2, 3, 4, 5) + for record in self: + print record # => a.model(1), then a.model(2), then a.model(3), ... + +Field access +------------ + +Recordsets provide an "Active Record" interface: model fields can be read and +written directly from the record, but only on singletons (single-record +recordsets). Setting a field's value triggers an update to the database:: + + >>> record.name + Example Name + >>> record.company_id.name + Company Name + >>> record.name = "Bob" + +Trying to read or write a field on multiple records will raise an error. + +Accessing a relational field (:class:`~openerp.fields.Many2one`, +:class:`~openerp.fields.One2many`, :class:`~openerp.fields.Many2many`) +*always* returns a recordset, empty if the field is not set. + +.. danger:: + + each assignment to a field triggers a database update, when setting + multiple fields at the same time or setting fields on multiple records + (to the same value), use :meth:`~openerp.models.Model.write`:: + + # 3 * len(records) database updates + for record in records: + record.a = 1 + record.b = 2 + record.c = 3 + + # len(records) database updates + for record in records: + record.write({'a': 1, 'b': 2, 'c': 3}) + + # 1 database update + records.write({'a': 1, 'b': 2, 'c': 3}) + +Set operations +-------------- + +Recordsets are immutable, but sets of the same model can be combined using +various set operations, returning new recordsets. Set operations do *not* +preserve order. + +.. addition preserves order but can introduce duplicates + +* ``record in set`` returns whether ``record`` (which must be a 1-element + recordset) is present in ``set``. ``record not in set`` is the inverse + operation +* ``set1 | set2`` returns the union of the two recordsets, a new recordset + containing all records present in either source +* ``set1 & set2`` returns the intersection of two recordsets, a new recordset + containing only records present in both sources +* ``set1 - set2`` returns a new recordset containing only records of ``set1`` + which are *not* in ``set2`` + +Other recordset operations +-------------------------- + +Recordsets are iterable so the usual Python tools are available for +transformation (:func:`python:map`, :func:`python:sorted`, +:func:`~python:itertools.ifilter`, ...) however these return either a +:class:`python:list` or an :term:`python:iterator`, removing the ability to +call methods on their result, or to use set operations. + +Recordsets therefore provide these operations returning recordsets themselves +(when possible): + +:meth:`~openerp.models.Model.filtered` + returns a recordset containing only records satisfying the provided + predicate function. The predicate can also be a string to filter by a + field being true or false:: + + # only keep records whose company is the current user's + records.filtered(lambda r: r.company_id == user.company_id) + + # only keep records whose partner is a company + records.filtered("partner_id.is_company") + +:meth:`~openerp.models.Model.sorted` + returns a recordset sorted by the provided key function. If no key + is provided, use the model's default sort order:: + + # sort records by name + records.sorted(key=lambda r: r.name) + +:meth:`~openerp.models.Model.mapped` + applies the provided function to each record in the recordset, returns + a recordset if the results are recordsets:: + + # returns a list of summing two fields for each record in the set + records.mapped(lambda r: r.field1 + r.field2) + + The provided function can be a string to get field values:: + + # returns a list of names + records.mapped('name') + + # returns a recordset of partners + record.mapped('partner_id') + + # returns the union of all partner banks, with duplicates removed + record.mapped('partner_id.bank_ids') + +Environment +=========== + +The :class:`~openerp.api.Environment` stores various contextual data used by +the ORM: the database cursor (for database queries), the current user +(for access rights checking) and the current context (storing arbitrary +metadata). The environment also stores caches. + +All recordsets have an environment, which is immutable, can be accessed +using :attr:`~openerp.models.Model.env` and gives access to the current user +(:attr:`~openerp.api.Environment.user`), the cursor +(:attr:`~openerp.api.Environment.cr`) or the context +(:attr:`~openerp.api.Environment.context`):: + + >>> records.env + + >>> records.env.user + res.user(3) + >>> records.env.cr + >> self.env['res.partner'] + res.partner + >>> self.env['res.partner'].search([['is_company', '=', True], ['customer', '=', True]]) + res.partner(7, 18, 12, 14, 17, 19, 8, 31, 26, 16, 13, 20, 30, 22, 29, 15, 23, 28, 74) + +Altering the environment +------------------------ + +The environment can be customized from a recordset. This returns a new +version of the recordset using the altered environment. + +:meth:`~openerp.models.Model.sudo` + creates a new environment with the provided user set, uses the + administrator if none is provided (to bypass access rights/rules in safe + contexts), returns a copy of the recordset it is called on using the + new environment:: + + # create partner object as administrator + env['res.partner'].sudo().create({'name': "A Partner"}) + + # list partners visible by the "public" user + public = env.ref('base.public_user') + env['res.partner'].sudo(public).search([]) + +:meth:`~openerp.models.Model.with_context` + #. can take a single positional parameter, which replaces the current + environment's context + #. can take any number of parameters by keyword, which are added to either + the current environment's context or the context set during step 1 + + :: + + # look for partner, or create one with specified timezone if none is + # found + env['res.partner'].with_context(tz=a_tz).find_or_create(email_address) + +:meth:`~openerp.models.Model.with_env` + replaces the existing environment entirely + +Common ORM methods +================== + +.. maybe these clarifications/examples should be in the APIDoc? + +:meth:`~openerp.models.Model.search` + Takes a :ref:`search domain `, returns a recordset + of matching records. Can return a subset of matching records (``offset`` + and ``limit`` parameters) and be ordered (``order`` parameter):: + + >>> # searches the current model + >>> self.search([('is_company', '=', True), ('customer', '=', True)]) + res.partner(7, 18, 12, 14, 17, 19, 8, 31, 26, 16, 13, 20, 30, 22, 29, 15, 23, 28, 74) + >>> self.search([('is_company', '=', True)], limit=1).name + 'Agrolait' + + .. tip:: to just check if any record matches a domain, or count the number + of records which do, use + :meth:`~openerp.models.Model.search_count` +:meth:`~openerp.models.Model.create` + Takes a number of field values, and returns a recordset containing the + record created:: + + >>> self.create({'name': "New Name"}) + res.partner(78) + +:meth:`~openerp.models.Model.write` + Takes a number of field values, writes them to all the records in its + recordset. Does not return anything:: + + self.write({'name': "Newer Name"}) + +:meth:`~openerp.models.Model.browse` + Takes a database id or a list of ids and returns a recordset, useful when + record ids are obtained from outside Odoo (e.g. round-trip through + external system) or :ref:`when calling methods in the old API + `:: + + >>> self.browse([7, 18, 12]) + res.partner(7, 18, 12]) + +:meth:`~openerp.models.Model.exists` + Returns a new recordset containing only the records which exist in the + database. Can be used to check whether a record (e.g. obtained externally) + still exists:: + + if not record.exists(): + raise Exception("The record has been deleted") + + or after calling a method which could have removed some records:: + + records.may_remove_some() + # only keep records which were not deleted + records = records.exists() + +:meth:`~openerp.api.Environment.ref` + Environment method returning the record matching a provided + :term:`external id`:: + + >>> env.ref('base.group_public') + res.groups(2) + +:meth:`~openerp.models.Model.ensure_one` + checks that the recordset is a singleton (only contains a single record), + raises an error otherwise:: + + records.ensure_one() + # is equivalent to but clearer than: + assert len(records) == 1, "Expected singleton" + +Creating Models +=============== + +Model fields are defined as attributes on the model itself:: + + from openerp import models, fields + class AModel(Model): + _name = 'a.model.name' + + field1 = fields.Char() + +.. warning:: this means you can not define a field and a method with the same + name, they will conflict + +By default, the field's label (user-visible name) is a capitalized version of +the field name, this can be overridden with the ``string`` parameter:: + + field2 = fields.Integer(string="an other field") + +For the various field types and parameters, see :ref:`the fields reference +`. + +Default values are defined as parameters on fields, either a value:: + + a_field = fields.Char(default="a value") + +or a function called to compute the default value, which should return that +value:: + + a_field = fields.Char(default=compute_default_value) + def compute_default_value(self): + return self.get_value() + +Computed fields +--------------- + +Fields can be computed (instead of read straight from the database) using the +``compute`` parameter. **It must assign the computed value to the field**. If +it uses the values of other *fields*, it should specify those fields using +:func:`~openerp.api.depends`:: + + from openerp import api + total = fields.Float(compute='_compute_total') + + @api.depends('value', 'tax') + def _compute_total(self): + for record in self: + record.total = record.value + record.value * record.tax + +* dependencies can be dotted paths when using sub-fields:: + + @api.depends('line_ids.value') + def _compute_total(self): + for record in self: + record.total = sum(line.value for line in record.line_ids) + +* computed fields are not stored by default, they are computed and + returned when requested. Setting ``store=True`` will store them in the + database and automatically enable searching +* searching on a computed field can also be enabled by setting the ``search`` + parameter. The value is a method name returning a + :ref:`reference/orm/domains`:: + + upper_name = field.Char(compute='_compute_upper', search='_search_upper') + + def _search_upper(self, operator, value): + if operator == 'like': + operator = 'ilike' + return [('name', operator, value)] + +* to allow *setting* values on a computed field, use the ``inverse`` + parameter. It is the name of a function reversing the computation and + setting the relevant fields:: + + document = fields.Char(compute='_get_document', inverse='_set_document') + + def _get_document(self): + for record in self: + with open(record.get_document_path) as f: + record.document = f.read() + def _set_document(self): + for record in self: + if not record.document: continue + with open(record.get_document_path()) as f: + f.write(record.document) + +* multiple fields can be computed at the same time by the same method, just + use the same method on all fields and set all of them:: + + discount_value = fields.Float(compute='_apply_discount') + total = fields.Float(compute='_apply_discount') + + @depends('value', 'discount') + def _apply_discount(self): + for record in self: + # compute actual discount from discount percentage + discount = self.value * self.discount + self.discount_value = discount + self.total = self.value - discount + +Related fields +'''''''''''''' + +A special case of computed fields are *related* (proxy) fields, which provide +the value of a sub-field on the current record. They are defined by setting +the ``related`` parameter and like regular computed fields they can be +stored:: + + nickname = fields.Char(related='user_id.partner_id.name', store=True) + +onchange: updating UI on the fly +-------------------------------- + +When a user changes a field's value in a form (but hasn't saved the form yet), +it can be useful to automatically update other fields based on that value +e.g. updating a final total when the tax is changed or a new invoice line is +added. + +* computed fields are automatically checked and recomputed, they do not need + an ``onchange`` +* for non-computed fields, the :func:`~openerp.api.onchange` decorator is used + to provide new field values:: + + @api.onchange('field1', 'field2') # if these fields are changed, call method + def check_change(self): + if self.field1 < self.field2: + self.field3 = True + + the changes performed during the method are then sent to the client program + and become visible to the user + +* Both computed fields and new-API onchanges are automatically called by the + client without having to add them in views +* It is possible to suppress the trigger from a specific field by adding + ``on_change="0"`` in a view:: + + + + will not trigger any interface update when the field is edited by the user, + even if there are function fields or explicit onchange depending on that + field. + +.. note:: + + ``onchange`` methods work on virtual records assignment on these records + is not written to the database, just used to know which value to send back + to the client + +.. _reference/orm/oldapi: + +Old API compatibility +--------------------- + +Odoo is currently transitioning from an older (less regular) API, it can be +necessary to manually bridge from one to the other manually: + +* RPC layers (both XML-RPC and JSON-RPC) are expressed in terms of the old + API, methods expressed purely in the new API are not available over RPC +* overridable methods may be called from older pieces of code still written + in the old API style + +The big differences between the old and new APIs are: + +* values of the :class:`~openerp.api.Environment` (cursor, user id and + context) are passed explicitly to methods instead +* record data (:attr:`~openerp.models.Model.ids`) are passed explicitly to + methods, and possibly not passed at all +* methods tend to work on lists of ids instead of recordsets + +By default, methods are assumed to use the new API style and are not callable +from the old API style. + +.. tip:: calls from the new API to the old API are bridged + :class: aphorism + + when using the new API style, calls to methods defined using the old API + are automatically converted on-the-fly, there should be no need to do + anything special:: + + >>> # method in the old API style + >>> def old_method(self, cr, uid, ids, context=None): + ... print ids + + >>> # method in the new API style + >>> def new_method(self): + ... # system automatically infers how to call the old-style + ... # method from the new-style method + ... self.old_method() + + >>> env[model].browse([1, 2, 3, 4]).new_method() + [1, 2, 3, 4] + +Two decorators can expose a new-style method to the old API: + +:func:`~openerp.api.model` + the method is exposed as not using ids, its recordset will generally be + empty. Its "old API" signature is ``cr, uid, *arguments, context``:: + + @api.model + def some_method(foo): + pass + # can be called as + old_style_model.some_method(cr, uid, a_value, context=context) + +:func:`~openerp.api.multi` + the method is exposed as taking a list of ids (possibly empty), its + "old API" signature is ``cr, uid, ids, *arguments, context``:: + + @api.multi + def some_method(foo): + pass + # can be called as + old_style_model.some_method(cr, uid, [id1, id2], a_value, context=context) + +Because new-style APIs tend to return recordsets and old-style APIs tend to +return lists of ids, there is also a decorator managing this: + +:func:`~openerp.api.returns` + the function is assumed to return a recordset, the first parameter should + be the name of the recordset's model or ``self`` (for the current model). + + No effect if the method is called in new API style, but transforms the + recordset into a list of ids when called from the old API style:: + + >>> @api.multi + ... @api.returns('self') + ... def some_method(): + ... return self + >>> new_style_model = env['a.model'].browse(1, 2, 3) + >>> new_style_model.some_method() + a.model(1, 2, 3) + >>> old_style_model = pool['a.model'] + >>> old_style_model.some_method(cr, uid, [1, 2, 3], context=context) + [1, 2, 3] + +Porting from the old API +------------------------ + +* methods still written in the old API should be automatically bridged by the + ORM, no need to switch to the old API, just call them as if they were a new + API method. See :ref:`reference/orm/oldapi/bridging` for more details. +* ``search`` returns a recordset, no point in e.g. browsing its result +* ``fields.related`` and ``fields.function`` are replaced by using a normal + field type with either a ``related`` or a ``compute`` parameter +* ``depends`` on field compute methods **must be complete**, it must list + **all** the fields and sub-fields which the compute method uses. It is + better to have too many dependencies (will recompute the field in cases + where that is not needed) than not enough (will forget to recompute the + field and then values will be incorrect) +* **remove** all ``onchange`` methods on computed fields. Computed fields are + automatically re-computed when one of their dependencies is changed, and + that is used to auto-generate ``onchange`` by the client +* the decorators :func:`~openerp.api.model` and :func:`~openerp.api.multi` are + for bridging *when calling from the old API context*, for internal or pure + new-api (e.g. compute) they are useless +* remove :attr:`~openerp.models.Model._default`, replace by ``default=`` + parameter on corresponding fields +* if a field's ``string`` is the titlecased version of the field name:: + + name = fields.Char(string="Name") + + it is useless and should be removed +* ``multi`` does not do anything on new API fields use the same ``compute`` + methods on all relevant fields for the same result +* provide ``compute``, ``inverse`` and ``search`` methods by name (as a + string), this makes them overridable (removes the need for an intermediate + "trampoline" function) +* double check that all fields and methods have different names, there is no + warning in case of collision (because Python handles it before Odoo sees + anything) +* the normal new-api import is ``from openerp import fields, models``. If + compatibility decorators are necessary, use ``from openerp import api, + fields, models`` +* avoid the :func:`~openerp.api.one` decorator, it probably does not do what + you expect +* remove explicit definition of :attr:`~openerp.models.Model.create_uid`, + :attr:`~openerp.models.Model.create_date`, + :attr:`~openerp.models.Model.write_uid` and + :attr:`~openerp.models.Model.write_date` fields: they are now created as + regular "legitimate" fields, and can be read and written like any other + field out-of-the-box +* when straight conversion is impossible (semantics can not be bridged) or the + "old API" version is not desirable and could be improved for the new API, it + is possible to use completely different "old API" and "new API" + implementations for the same method name using :func:`~openerp.api.v7` and + :func:`~openerp.api.v8`. The method should first be defined using the + old-API style and decorated with :func:`~openerp.api.v7`, it should then be + re-defined using the exact same name but the new-API style and decorated + with :func:`~openerp.api.v8`. Calls from an old-API context will be + dispatched to the first implementation and calls from a new-API context will + be dispatched to the second implementation. One implementation can call (and + frequently does) call the other by switching context. + + .. danger:: using these decorators makes methods extremely difficult to + override and harder to understand and document +* uses of :attr:`~openerp.models.Model._columns` or + :attr:`~openerp.models.Model._all_columns` should be replaced by + :attr:`~openerp.models.Model._fields`, which provides access to instances of + new-style :class:`openerp.fields.Field` instances (rather than old-style + :class:`openerp.osv.fields._column`). + + Non-stored computed fields created using the new API style are *not* + available in :attr:`~openerp.models.Model._columns` and can only be + inspected through :attr:`~openerp.models.Model._fields` +* reassigning ``self`` in a method is probably unnecessary and may break + translation introspection +* :class:`~openerp.api.Environment` objects rely on some threadlocal state, + which has to be set up before using them. It is necessary to do so using the + :meth:`openerp.api.Environment.manage` context manager when trying to use + the new API in contexts where it hasn't been set up yet, such as new threads + or a Python interactive environment:: + + >>> from openerp import api, modules + >>> r = modules.registry.RegistryManager.get('test') + >>> cr = r.cursor() + >>> env = api.Environment(cr, 1, {}) + Traceback (most recent call last): + ... + AttributeError: environments + >>> with api.Environment.manage(): + ... env = api.Environment(cr, 1, {}) + ... print env['res.partner'].browse(1) + ... + res.partner(1,) + +.. _reference/orm/oldapi/bridging: + +Automatic bridging of old API methods +''''''''''''''''''''''''''''''''''''' + +When models are initialized, all methods are automatically scanned and bridged +if they look like models declared in the old API style. This bridging makes +them transparently callable from new-API-style methods. + +Methods are matched as "old-API style" if their second positional parameter +(after ``self``) is called either ``cr`` or ``cursor``. The system also +recognizes the third positional parameter being called ``uid`` or ``user`` and +the fourth being called ``id`` or ``ids``. It also recognizes the presence of +any parameter called ``context``. + +When calling such methods from a new API context, the system will +automatically fill matched parameters from the current +:class:`~openerp.api.Environment` (for :attr:`~openerp.api.Environment.cr`, +:attr:`~openerp.api.Environment.user` and +:attr:`~openerp.api.Environment.context`) or the current recordset (for ``id`` +and ``ids``). + +In the rare cases where it is necessary, the bridging can be customized by +decorating the old-style method: + +* disabling it entirely, by decorating a method with + :func:`~openerp.api.noguess` there will be no bridging and methods will be + called the exact same way from the new and old API styles +* defining the bridge explicitly, this is mostly for methods which are matched + incorrectly (because parameters are named in unexpected ways): + + :func:`~openerp.api.cr` + will automatically prepend the current cursor to explicitly provided + parameters, positionally + :func:`~openerp.api.cr_uid` + will automatically prepend the current cursor and user's id to explictly + provided parameters + :func:`~openerp.api.cr_uid_ids` + will automatically prepend the current cursor, user's id and recordset's + ids to explicitly provided parameters + :func:`~openerp.api.cr_uid_id` + will loop over the current recordset and call the method once for each + record, prepending the current cursor, user's id and record's id to + explicitly provided parameters. + + .. danger:: the result of this wrapper is *always a list* when calling + from a new-API context + + All of these methods have a ``_context``-suffixed version + (e.g. :func:`~openerp.api.cr_uid_context`) which also passes the current + context *by keyword*. +* dual implementations using :func:`~openerp.api.v7` and + :func:`~openerp.api.v8` will be ignored as they provide their own "bridging" + +Low-level SQL +------------- + +The :attr:`~openerp.api.Environment.cr` attribute on environments is the +cursor for the current database transaction and allows executing SQL directly, +either for queries which are difficult to express using the ORM (e.g. complex +joins) or for performance reasons:: + + self.env.cr.execute("some_sql", param1, param2, param3) + +Because models use the same cursor and the :class:`~openerp.api.Environment` +holds various caches, these caches must be invalidated when *altering* the +database in raw SQL, or further uses of models may become incoherent. It is +necessary to clear caches when using ``CREATE``, ``UPDATE`` or ``DELETE`` in +SQL, but not ``SELECT`` (which simply reads the database). + +Clearing caches can be performed using the +:meth:`~openerp.api.Environment.invalidate_all` method of the +:class:`~openerp.api.Environment` object. + .. _reference/orm/model: -Model -===== +Model Reference +=============== .. - can't get autoattribute to import docstrings, so use regular attribute - no autoclassmethod @@ -121,7 +789,7 @@ Model .. automethod:: exists .. automethod:: filtered .. automethod:: sorted - .. automethod:: update + .. automethod:: mapped .. rubric:: Environment swapping @@ -234,7 +902,8 @@ Method decorators ================= .. automodule:: openerp.api - :members: one, multi, model, depends, constrains, onchange, returns + :members: one, multi, model, depends, constrains, onchange, returns, + v7, v8 .. _reference/orm/fields: @@ -499,4 +1168,3 @@ Domain criteria can be combined using logical operators in *prefix* form: (name is 'ABC') AND (language is NOT english) AND (country is Belgium OR Germany) -