diff --git a/doc/howtos/website.rst b/doc/howtos/website.rst index 48b0345e5a9..5d3ab9f0b84 100644 --- a/doc/howtos/website.rst +++ b/doc/howtos/website.rst @@ -367,9 +367,9 @@ Relations between models ------------------------ We have seen a pair of "basic" fields stored directly in the record. There are -:ref:`a number of basic fields `. The second +:ref:`a number of basic fields `. The second broad categories of fields are :ref:`relational -` and used to link records to one another +` and used to link records to one another (within a model or across models). For demonstration, let's create a *courses* model. Each course should have a diff --git a/doc/reference/data.rst b/doc/reference/data.rst index 416121b8255..3ab39f57484 100644 --- a/doc/reference/data.rst +++ b/doc/reference/data.rst @@ -92,7 +92,7 @@ Nothing on the field. Can be used to clear a field, or avoid using a default value for the field. ``search`` - for :ref:`relational fields `, should be + for :ref:`relational fields `, should be a :ref:`domain ` on the field's model. Will evaluate the domain, search the field's model using it and set the diff --git a/doc/reference/orm.rst b/doc/reference/orm.rst index 66f5b27f99a..d6ada51b4d4 100644 --- a/doc/reference/orm.rst +++ b/doc/reference/orm.rst @@ -6,20 +6,410 @@ ORM API ======= +.. automodule:: odoo.models + +.. _reference/orm/models: +.. _reference/orm/model: + +Models +====== + +Model fields are defined as attributes on the model itself:: + + from odoo import models, fields + class AModel(models.Model): + _name = 'a.model.name' + + field1 = fields.Char() + +.. warning:: this means you cannot define a field and a method with the same + name, the last one will silently overwrite the former ones. + +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="Field Label") + +For the list of field types and parameters, see :ref:`the fields reference +`. + +Default values are defined as parameters on fields, either as a value:: + + name = fields.Char(default="a value") + +or as a function called to compute the default value, which should return that +value:: + + def _default_name(self): + return self.get_value() + + name = fields.Char(default=lambda self: self._default_name()) + +.. rubric:: API + +.. autoclass:: odoo.models.BaseModel() + + .. autoattribute:: _auto + .. autoattribute:: _table + .. autoattribute:: _sequence + .. autoattribute:: _sql_constraints + + .. autoattribute:: _register + + .. autoattribute:: _name + .. autoattribute:: _description + + .. autoattribute:: _inherit + .. autoattribute:: _inherits + + .. autoattribute:: _rec_name + .. autoattribute:: _order + + .. autoattribute:: _check_company_auto + + .. autoattribute:: _parent_name + .. autoattribute:: _parent_store + + .. autoattribute:: _abstract + + .. seealso:: :class:`odoo.models.AbstractModel` + + .. autoattribute:: _transient + + .. seealso:: :class:`odoo.models.TransientModel` + + .. autoattribute:: _date_name + .. autoattribute:: _fold_name + +AbstractModel +------------- + +.. autoclass:: odoo.models.AbstractModel() + +Model +----- + +.. autoclass:: odoo.models.Model() + +TransientModel +-------------- + +.. autoclass:: odoo.models.TransientModel() + +.. _reference/fields: +.. _reference/orm/fields: + +Fields +====== + +.. currentmodule:: odoo.fields + +.. autoclass:: Field() + +.. .. autoattribute:: Field._slots + :annotation: + +.. _reference/fields/basic: + +Basic Fields +------------ + +.. autoclass:: Boolean() + +.. autoclass:: Char() + +.. autoclass:: Float() + +.. autoclass:: Integer() + +.. _reference/fields/advanced: + +Advanced Fields +--------------- + +.. autoclass:: Html() + +.. autoclass:: Monetary() + +.. autoclass:: Selection() + +.. autoclass:: Text() + +.. _reference/fields/date: + +Date(time) Fields +''''''''''''''''' + +Dates and Datetimes are very important fields in any kind of business +application, they are heavily used in many popular Odoo applications such as +logistics or accounting and their misuse can create invisible yet painful +bugs, this excerpt aims to provide Odoo developers with the knowledge required +to avoid misusing these fields. + +When assigning a value to a Date/Datetime field, the following options are valid: + +* A `date` or `datetime` object. +* A string in the proper server format *(YYYY-MM-DD)* for Date fields, + *(YYYY-MM-DD HH:MM:SS)* for Datetime fields. +* `False` or `None`. + +The Date and Datetime fields class have helper methods to attempt conversion +into a compatible type: :func:`~odoo.fields.Date.to_date` will convert to a `datetime.date` +object while :func:`~odoo.fields.Datetime.to_datetime` will convert to a `datetime.datetime`. + +.. admonition:: Example + + To parse date/datetimes coming from external sources:: + + fields.Date.to_date(self._context.get('date_from')) + +Date / Datetime comparison best practices: + +* Date fields can **only** be compared to date objects. +* Datetime fields can **only** be compared to datetime objects. + +.. warning:: Strings representing dates and datetimes can be compared + between each other, however the result may not be the expected + result, as a datetime string will always be greater than a + date string, therefore this practice is **heavily** + discouraged. + +Common operations with dates and datetimes such as addition, substraction or +fetching the start/end of a period are exposed through both +:class:`~odoo.fields.Date` and :class:`~odoo.fields.Datetime`. +These helpers are also available by importing `odoo.tools.date_utils`. + +.. note:: Timezones + + Datetime fields are stored as `timestamp without timezone` columns in the database and are stored + in the UTC timezone. This is by design, as it makes the Odoo database independent from the timezone + of the hosting server system. Timezone conversion is managed entirely by the client side. + +.. autoclass:: Date() + :members: today, context_today, to_date, to_string, start_of, end_of, add, subtract + +.. autoclass:: Datetime() + :members: now, today, context_timestamp, to_datetime, to_string, start_of, end_of, add, subtract + +.. _reference/fields/relational: + +Relational Fields +''''''''''''''''' + +.. autoclass:: Many2one() + +.. autoclass:: One2many() + +.. autoclass:: Many2many() + +Pseudo-relational fields +'''''''''''''''''''''''' + +.. autoclass:: Reference() + +.. autoclass:: Many2oneReference() + +.. _reference/fields/compute: + +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:`~odoo.api.depends`. :: + + from odoo 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)] + + The search method is invoked when processing domains before doing an + actual search on the model. It must return a domain equivalent to the + condition: ``field operator value``. + +.. TODO and/or by setting the store to True for search domains ? + +* Computed fields are readonly by default. 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') + + @api.depends('value', 'discount') + def _apply_discount(self): + for record in self: + # compute actual discount from discount percentage + discount = record.value * record.discount + record.discount_value = discount + record.total = record.value - discount + +.. _reference/fields/related: + +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) + +The value of a related field is given by following a sequence of +relational fields and reading a field on the reached model. The complete +sequence of fields to traverse is specified by the ``related`` attribute. + +Some field attributes are automatically copied from the source field if +they are not redefined: ``string``, ``help``, ``readonly``, ``required`` (only +if all fields in the sequence are required), ``groups``, ``digits``, ``size``, +``translate``, ``sanitize``, ``selection``, ``comodel_name``, ``domain``, +``context``. All semantic-free attributes are copied from the source +field. + +By default, the values of related fields are not stored to the database. +Add the attribute ``store=True`` to make it stored, just like computed +fields. Related fields are automatically recomputed when their +dependencies are modified. + +.. note:: The related fields are computed in sudo mode. + +.. _reference/fields/automatic: + +Automatic fields +---------------- + +.. Documented + +.. attribute:: id + + Identifier :class:`field ` + + If length of current recordset is 1, return id of unique record in it. + + Raise an Error otherwise. + +.. todo:: _log_access info + +.. attribute:: create_date + + :class:`~odoo.fields.Datetime` + +.. attribute:: create_uid + + :class:`~odoo.fields.Many2one` + +.. attribute:: write_date + + :class:`~odoo.fields.Datetime` + +.. attribute:: write_uid + + :class:`~odoo.fields.Many2one` + +.. _reference/orm/fields/reserved: + +Reserved Field names +-------------------- + +A few field names are reserved for pre-defined behaviors beyond that of +automated fields. They should be defined on a model when the related +behavior is desired: + +.. attribute:: name + + default value for :attr:`~odoo.models.BaseModel._rec_name`, used to + display records in context where a representative "naming" is + necessary. + + :class:`~odoo.fields.Char` + +.. attribute:: active + + toggles the global visibility of the record, if ``active`` is set to + ``False`` the record is invisible in most searches and listing. + + :class:`~odoo.fields.Boolean` + +.. .. attribute:: sequence +.. +.. Alterable ordering criteria, allows drag-and-drop reordering of models +.. in list views. +.. +.. :class:`~odoo.fields.Integer` + +.. attribute:: state + + lifecycle stages of the object, used by the ``states`` attribute on + :class:`fields `. + + :class:`~odoo.fields.Selection` + +.. attribute:: parent_id + + default_value of :attr:`~._parent_name`, used to organize + records in a tree structure and enables the ``child_of`` + and ``parent_of`` operators in domains. + + :class:`~odoo.fields.Many2one` + +.. attribute:: parent_path + + When :attr:`~._parent_store` is set to True, used to store a value reflecting + the tree structure of :attr:`~._parent_name`, and to optimize the operators + ``child_of`` and ``parent_of`` in search domains. + It must be declared with ``index=True`` for proper operation. + + :class:`~odoo.fields.Char` + + Recordsets ========== -.. versionadded:: 8.0 +Interactions with models and records are performed through recordsets, an ordered +collection of records of the same model. - This page documents the New API added in Odoo 8.0 which should be the - primary development API going forward. It also provides information about - porting from or bridging with the "old API" of versions 7 and earlier, but - does not explicitly document that API. See the old documentation for that. - -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 +.. 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 @@ -28,7 +418,7 @@ a recordset:: class AModel(models.Model): _name = 'a.model' def a_method(self): - # self can be anywhere between 0 records and all records in the + # self can be anything between 0 records and all records in the # database self.do_operation() @@ -37,16 +427,23 @@ Iterating on a recordset will yield new sets of *a single record* single characters:: def do_operation(self): - print self # => a.model(1, 2, 3, 4, 5) + 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), ... + 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 as attributes, but only on singletons -(single-record recordsets). +written directly from the record as attributes. + +.. note:: + + When accessing non-relational fields on a recordset of potentially multiple + records, use :meth:`~odoo.models.BaseModel.mapped`:: + + total_qty = sum(self.mapped('qty')) + Field values can also be accessed like dict items, which is more elegant and safer than ``getattr()`` for dynamic field names. Setting a field's value triggers an update to the database:: @@ -60,31 +457,15 @@ Setting a field's value triggers an update to the database:: >>> record[field] Bob -Trying to read or write a field on multiple records will raise an error. +.. warning:: + + Trying to read a field on multiple records will raise an error for non relational + fields. Accessing a relational field (:class:`~odoo.fields.Many2one`, :class:`~odoo.fields.One2many`, :class:`~odoo.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:`~odoo.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}) - Record cache and prefetching ---------------------------- @@ -124,76 +505,36 @@ for partners and one for countries:: country = partner.country_id # first pass prefetches all partners countries.add(country.name) # first pass prefetches all countries -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. +.. _reference/api/decorators: -.. addition preserves order but can introduce duplicates +Method decorators +================= -* ``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`` and ``set1 < set2`` return whether ``set1`` is a subset - of ``set2`` (resp. strict) -* ``set1 >= set2`` and ``set1 > set2`` return whether ``set1`` is a superset - of ``set2`` (resp. strict) -* ``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`` +.. automodule:: odoo.api + :members: depends, depends_context, constrains, onchange, returns, model_create_multi -Other recordset operations --------------------------- +.. .. currentmodule:: odoo.api -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. +.. .. autodata:: model +.. .. autodata:: depends +.. .. autodata:: constrains +.. .. autodata:: onchange +.. .. autodata:: returns -Recordsets therefore provide these operations returning recordsets themselves -(when possible): +.. todo:: With sphinx 2.0 : autodecorator -:meth:`~odoo.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:: +.. todo:: Add in Views reference + * It is possible to suppress the trigger from a specific field by adding + ``on_change="0"`` in a view:: - # 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") + 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. -:meth:`~odoo.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:`~odoo.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') +.. _reference/orm/environment: Environment =========== @@ -204,10 +545,14 @@ the ORM: the database cursor (for database queries), the current user metadata). The environment also stores caches. All recordsets have an environment, which is immutable, can be accessed -using :attr:`~odoo.models.Model.env` and gives access to the current user -(:attr:`~odoo.api.Environment.user`), the cursor -(:attr:`~odoo.api.Environment.cr`) or the context -(:attr:`~odoo.api.Environment.context`):: +using :attr:`~odoo.models.Model.env` and gives access to: + +* the current user (:attr:`~odoo.api.Environment.user`) +* the cursor (:attr:`~odoo.api.Environment.cr`) +* the superuser flag (:attr:`~odoo.api.Environment.su`) +* or the context (:attr:`~odoo.api.Environment.context`) + +.. code-block:: bash >>> records.env @@ -221,274 +566,40 @@ inherited. The environment can be used to get an empty recordset in an other model, and query that model:: >>> self.env['res.partner'] - 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) +.. currentmodule:: odoo.api + +.. automethod:: Environment.ref + +.. autoattribute:: Environment.lang + +.. autoattribute:: Environment.user + +.. autoattribute:: Environment.company + +.. autoattribute:: Environment.companies + +.. TODO cr, uid but not @property or methods of Environment class... + Altering the environment ------------------------ -The environment can be customized from a recordset. This returns a new -version of the recordset using the altered environment. +.. currentmodule:: odoo.models -:meth:`~odoo.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:: +.. automethod:: Model.with_context - # create partner object as administrator - env['res.partner'].sudo().create({'name': "A Partner"}) +.. automethod:: Model.with_user - # list partners visible by the "public" user - public = env.ref('base.public_user') - env['res.partner'].sudo(public).search([]) +.. automethod:: Model.with_env -:meth:`~odoo.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 +.. automethod:: Model.sudo - :: +.. _reference/orm/sql: - # 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:`~odoo.models.Model.with_env` - replaces the existing environment entirely - -Common ORM methods -================== - -.. maybe these clarifications/examples should be in the APIDoc? - -:meth:`~odoo.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:`~odoo.models.Model.search_count` - -:meth:`~odoo.models.Model.create` - Takes a dictionary of field values, or a list of such dictionaries, and - returns a recordset containing the records created:: - - >>> self.create({'name': "Joe"}) - res.partner(78) - >>> self.create([{'name': "Jack"}, {'name': "William"}, {'name': "Averell"}]) - res.partner(79, 80, 81) - - See :ref:`how to define method \`create\` with one API or the other - `. - -:meth:`~odoo.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:`~odoo.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:`~odoo.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:`~odoo.api.Environment.ref` - Environment method returning the record matching a provided - :term:`external id`:: - - >>> env.ref('base.group_public') - res.groups(2) - -:meth:`~odoo.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 odoo import models, fields - class AModel(models.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:: - - def compute_default_value(self): - return self.get_value() - a_field = fields.Char(default=compute_default_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:`~odoo.api.depends`:: - - from odoo 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 = record.value * record.discount - record.discount_value = discount - record.total = record.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:`~odoo.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 - -.. warning:: - - It is not possible for a ``one2many`` or ``many2many`` field to modify - itself via onchange. This is a webclient limitation - see `#2693 `_. - -Low-level SQL +SQL Execution ------------- The :attr:`~odoo.api.Environment.cr` attribute on environments is the @@ -504,599 +615,78 @@ 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:`~odoo.models.BaseModel.invalidate_cache` method of the -:class:`~odoo.models.BaseModel` object. +.. note:: + Clearing caches can be performed using the + :meth:`~odoo.models.Model.invalidate_cache` method. + +.. automethod:: Model.invalidate_cache + +.. warning:: + Executing raw SQL bypasses the ORM, and by consequent, Odoo security rules. + Please make sure your queries are sanitized when using user input and prefer using + ORM utilities if you don't really need to use SQL queries. -.. _reference/orm/oldapi: +.. _reference/orm/models/crud: -Compatibility between new API and old API -========================================= - -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:`~odoo.api.Environment` (cursor, user id and - context) are passed explicitly to methods instead -* record data (:attr:`~odoo.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:`~odoo.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(self, a_value): - pass - # can be called as - old_style_model.some_method(cr, uid, a_value, context=context) - -Note that a method `create` decorated with :func:`~odoo.api.model` will always -be called with a single dictionary. A method `create` decorated with the variant -:func:`~odoo.api.model_create_multi` will always be called with a list of dicts. -The decorators take care of converting the argument to one form or the other:: - - @api.model - def create(self, vals): - ... - - @api.model_create_multi - def create(self, vals_list): - ... - -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:`~odoo.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.returns('self') - ... def some_method(self): - ... 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] - -.. _reference/orm/model: - -Model Reference -=============== - -.. - can't get autoattribute to import docstrings, so use regular attribute - - no autoclassmethod +Common ORM methods +================== .. currentmodule:: odoo.models -.. autoclass:: Model +Create/update +------------- - .. rubric:: Structural attributes +.. todo:: api.model_create_multi information - .. attribute:: _name +.. automethod:: Model.create - business object name, in dot-notation (in module namespace) +.. automethod:: Model.copy - .. attribute:: _rec_name +.. automethod:: Model.default_get - Alternative field to use as name, used by osv’s name_get() - (default: ``'name'``) +.. automethod:: Model.name_create - .. attribute:: _inherit +.. automethod:: Model.write - * If :attr:`._name` is set, names of parent models to inherit from. - Can be a ``str`` if inheriting from a single parent - * If :attr:`._name` is unset, name of a single model to extend - in-place +.. automethod:: Model.flush - See :ref:`reference/orm/inheritance`. +Search/Read +----------- - .. attribute:: _order +.. automethod:: Model.browse - Ordering field when searching without an ordering specified (default: - ``'id'``) +.. automethod:: Model.search - :type: str +.. automethod:: Model.search_count - .. attribute:: _auto +.. automethod:: Model.name_search - Whether a database table should be created (default: ``True``) +.. automethod:: Model.read - If set to ``False``, override :meth:`.init` to create the database - table - - .. tip:: To create a model without any table, inherit - from ``odoo.models.AbstractModel`` +.. automethod:: Model.read_group - .. attribute:: _table +Fields/Views +'''''''''''' - Name of the table backing the model created when - :attr:`~odoo.models.Model._auto`, automatically generated by - default. +.. automethod:: Model.fields_get - .. attribute:: _inherits - - dictionary mapping the _name of the parent business objects to the - names of the corresponding foreign key fields to use:: - - _inherits = { - 'a.model': 'a_field_id', - 'b.model': 'b_field_id' - } - - implements composition-based inheritance: the new model exposes all - the fields of the :attr:`~odoo.models.Model._inherits`-ed model but - stores none of them: the values themselves remain stored on the linked - record. - - .. warning:: - - if the same field is defined on multiple - :attr:`~odoo.models.Model._inherits`-ed - - .. attribute:: _constraints - - list of ``(constraint_function, message, fields)`` defining Python - constraints. The fields list is indicative - - .. deprecated:: 8.0 - - use :func:`~odoo.api.constrains` - - .. attribute:: _sql_constraints - - list of ``(name, sql_definition, message)`` triples defining SQL - constraints to execute when generating the backing table - - .. attribute:: _parent_store - - Alongside a :attr:`~.parent_path` field, sets up an indexed storage - of the tree structure of records, to enable faster hierarchical queries - on the records of the current model using the ``child_of`` and - ``parent_of`` domain operators. - (default: ``False``) - - :type: bool - - - .. attribute:: _check_company_auto - - On write and create, call ``_check_company`` to ensure companies - consistency on the relational fields having ``check_company=True`` - as attribute. - (default: ``False``) - - .. attribute:: _parent_name - - Alternative field to use as parent, used by indexed storage of the tree structure of records - (default: ``'parent_id'``) - - :type: str - - .. attribute:: _date_name - - Alternative field to use for default calendar view (default: ``'date'``) - - :type: str - - .. attribute:: _fold_name - - Alternative field to determine folded groups in kanban views - (default: ``'fold'``) - - :type: str - - .. attribute:: _translate - - False disables translations export for this model - (default: ``True``) - - :type: bool - - .. rubric:: CRUD - - .. automethod:: create - .. automethod:: browse - .. automethod:: unlink - .. automethod:: write - - .. automethod:: read - .. automethod:: read_group - - .. rubric:: Searching - - .. automethod:: search - .. automethod:: search_count - .. automethod:: name_search - - .. rubric:: Recordset operations - - .. autoattribute:: ids - .. automethod:: ensure_one - .. automethod:: exists - .. automethod:: filtered - .. automethod:: sorted - .. automethod:: mapped - - .. rubric:: Environment swapping - - .. automethod:: sudo - .. automethod:: with_context - .. automethod:: with_env - - .. rubric:: Fields and views querying - - .. automethod:: fields_get - .. automethod:: fields_view_get - - .. rubric:: Miscellaneous methods - - .. automethod:: default_get - .. automethod:: copy - .. automethod:: name_get - .. automethod:: name_create - - .. _reference/orm/model/automatic: - - .. rubric:: Automatic fields - - .. attribute:: id - - Identifier :class:`field ` - - .. attribute:: _log_access - - Whether log access fields (``create_date``, ``write_uid``, ...) should - be generated (default: ``True``) - - .. attribute:: create_date - - Date at which the record was created - - :type: :class:`~odoo.field.Datetime` - - .. attribute:: create_uid - - Relational field to the user who created the record - - :type: ``res.users`` - - .. attribute:: write_date - - Date at which the record was last modified - - :type: :class:`~odoo.field.Datetime` - - .. attribute:: write_uid - - Relational field to the last user who modified the record - - :type: ``res.users`` - - .. rubric:: Reserved field names - - A few field names are reserved for pre-defined behaviors beyond that of - automated fields. They should be defined on a model when the related - behavior is desired: - - .. attribute:: name - - default value for :attr:`~._rec_name`, used to - display records in context where a representative "naming" is - necessary. - - :type: :class:`~odoo.fields.Char` - - .. attribute:: active - - toggles the global visibility of the record, if ``active`` is set to - ``False`` the record is invisible in most searches and listing - - :type: :class:`~odoo.fields.Boolean` - - .. attribute:: sequence - - Alterable ordering criteria, allows drag-and-drop reordering of models - in list views - - :type: :class:`~odoo.fields.Integer` - - .. attribute:: state - - lifecycle stages of the object, used by the ``states`` attribute on - :class:`fields ` - - :type: :class:`~odoo.fields.Selection` - - .. attribute:: parent_id - - used to order records in a tree structure and enables the ``child_of`` - and ``parent_of`` operators in domains - - :type: :class:`~odoo.fields.Many2one` - - .. attribute:: parent_path - - used to store an index of the tree structure when :attr:`~._parent_store` - is set to True - must be declared with ``index=True`` for proper operation. - - :type: :class:`~odoo.fields.Char` - - -.. _reference/orm/decorators: - -Method decorators -================= - -.. automodule:: odoo.api - :members: model, depends, constrains, onchange, returns - -.. _reference/orm/fields: - -Fields -====== - -.. _reference/orm/fields/basic: - -Basic fields ------------- - -.. autodoc documents descriptors as attributes, even for the *definition* of - descriptors. As a result automodule:: odoo.fields lists all the field - classes as attributes without providing inheritance info or methods (though - we don't document methods as they're not useful for "external" devs) - (because we don't support pluggable field types) (or do we?) - -.. autoclass:: odoo.fields.Field - -.. autoclass:: odoo.fields.Char - :show-inheritance: - -.. autoclass:: odoo.fields.Boolean - :show-inheritance: - -.. autoclass:: odoo.fields.Integer - :show-inheritance: - -.. autoclass:: odoo.fields.Float - :show-inheritance: - -.. autoclass:: odoo.fields.Text - :show-inheritance: - -.. autoclass:: odoo.fields.Selection - :show-inheritance: - -.. autoclass:: odoo.fields.Html - :show-inheritance: - -.. _reference/orm/fields/date_datetime: - -Date and Datetime fields ------------------------- - -Dates and Datetimes are very important fields in any kind of business -application, they are heavily used in many popular Odoo applications such as -logistics or accounting and their misuse can create invisible yet painful -bugs, this excerpt aims to provide Odoo developers with the knowledge required -to avoid misusing these fields. - -When assigning a value to a Date/Datetime field, the following options are valid: - * A string in the proper server format **(YYYY-MM-DD)** for Date fields, - **(YYYY-MM-DD HH:MM:SS)** for Datetime fields. - * A `date` or `datetime` object. - * `False` or `None`. - -If not sure of the type of the value being assigned to a Date/Datetime object, -the best course of action is to pass the value to -:func:`~odoo.fields.Date.to_date` or :func:`~odoo.fields.Datetime.to_datetime` -which will attempt to convert the value to a date or datetime object -respectively, which can then be assigned to the field in question. - -.. admonition:: Example - - To parse date/datetimes coming from external sources:: - - fields.Date.to_date(self._context.get('date_from')) - -Date / Datetime comparison best practices: - * Date fields can **only** be compared to date objects. - * Datetime fields can **only** be compared to datetime objects. - - .. warning:: Strings representing dates and datetimes can be compared - between each other, however the result may not be the expected - result, as a datetime string will always be greater than a - date string, therefore this practice is **heavily** - discouraged. - -Common operations with dates and datetimes such as addition, substraction or -fetching the start/end of a period are exposed through both -:class:`~odoo.fields.Date` and :class:`~odoo.fields.Datetime`. -These helpers are also available by importing `odoo.tools.date_utils`. - -.. autoclass:: odoo.fields.Date - :show-inheritance: - :members: today, context_today, to_date, to_string, start_of, end_of, add, subtract - -.. autoclass:: odoo.fields.Datetime - :show-inheritance: - :members: now, today, context_timestamp, to_datetime, to_string, start_of, end_of, add, subtract - -.. _reference/orm/fields/relational: - -Relational fields ------------------ - -.. autoclass:: odoo.fields.Many2one - :show-inheritance: - -.. autoclass:: odoo.fields.One2many - :show-inheritance: - -.. autoclass:: odoo.fields.Many2many - :show-inheritance: - -.. autoclass:: odoo.fields.Reference - :show-inheritance: - -.. _reference/orm/inheritance: - -Inheritance and extension -========================= - -Odoo provides three different mechanisms to extend models in a modular way: - -* creating a new model from an existing one, adding new information to the - copy but leaving the original module as-is -* extending models defined in other modules in-place, replacing the previous - version -* delegating some of the model's fields to records it contains - -.. image:: ../images/inheritance_methods.png - :align: center - -Classical inheritance ---------------------- - -When using the :attr:`~odoo.models.Model._inherit` and -:attr:`~odoo.models.Model._name` attributes together, Odoo creates a new -model using the existing one (provided via -:attr:`~odoo.models.Model._inherit`) as a base. The new model gets all the -fields, methods and meta-information (defaults & al) from its base. - -.. literalinclude:: ../../odoo/addons/test_documentation_examples/inheritance.py - :language: python - :lines: 5- - -and using them: - -.. literalinclude:: ../../odoo/addons/test_documentation_examples/tests/test_inheritance.py - :language: python - :lines: 10,11,14,19 - -will yield: - -.. literalinclude:: ../../odoo/addons/test_documentation_examples/tests/test_inheritance.py - :language: text - :lines: 16,21 - -the second model has inherited from the first model's ``check`` method and its -``name`` field, but overridden the ``call`` method, as when using standard -:ref:`Python inheritance `. - -Extension ---------- - -When using :attr:`~odoo.models.Model._inherit` but leaving out -:attr:`~odoo.models.Model._name`, the new model replaces the existing one, -essentially extending it in-place. This is useful to add new fields or methods -to existing models (created in other modules), or to customize or reconfigure -them (e.g. to change their default sort order): - -.. literalinclude:: ../../odoo/addons/test_documentation_examples/extension.py - :language: python - :lines: 7- - -.. literalinclude:: ../../odoo/addons/test_documentation_examples/tests/test_extension.py - :language: python - :lines: 10,15 - -will yield: - -.. literalinclude:: ../../odoo/addons/test_documentation_examples/tests/test_extension.py - :language: text - :lines: 13 - -.. note:: it will also yield the various :ref:`automatic fields - ` unless they've been disabled - -Delegation ----------- - -The third inheritance mechanism provides more flexibility (it can be altered -at runtime) but less power: using the :attr:`~odoo.models.Model._inherits` -a model *delegates* the lookup of any field not found on the current model -to "children" models. The delegation is performed via -:class:`~odoo.fields.Reference` fields automatically set up on the parent -model. The main difference is in the meaning. When using Delegation, the model -**has one** instead of **is one**, turning the relationship in a composition -instead of inheritance: - -.. literalinclude:: ../../odoo/addons/test_documentation_examples/delegation.py - :language: python - :lines: 5- - -.. literalinclude:: ../../odoo/addons/test_documentation_examples/tests/test_delegation.py - :language: python - :lines: 11-14,23,28 - -will result in: - -.. literalinclude:: ../../odoo/addons/test_documentation_examples/tests/test_delegation.py - :language: text - :lines: 25,30 - -and it's possible to write directly on the delegated field: - -.. literalinclude:: ../../odoo/addons/test_documentation_examples/tests/test_delegation.py - :language: python - :lines: 45 - -.. warning:: when using delegation inheritance, methods are *not* inherited, - only fields +.. automethod:: Model.fields_view_get .. _reference/orm/domains: -Domains -======= +Search domains +'''''''''''''' A domain is a list of criteria, each criterion being a triple (either a ``list`` or a ``tuple``) of ``(field_name, operator, value)`` where: -``field_name`` (``str``) +* ``field_name`` (``str``) a field name of the current model, or a relationship traversal through a :class:`~odoo.fields.Many2one` using dot-notation e.g. ``'street'`` or ``'partner_id.country'`` -``operator`` (``str``) + +* ``operator`` (``str``) an operator used to compare the ``field_name`` with the ``value``. Valid operators are: @@ -1136,21 +726,23 @@ A domain is a list of criteria, each criterion being a triple (either a ``not in`` is unequal to all of the items from ``value`` ``child_of`` - is a child (descendant) of a ``value`` record. + is a child (descendant) of a ``value`` record (value can be either + one item or a list of items). Takes the semantics of the model into account (i.e following the relationship field named by :attr:`~odoo.models.Model._parent_name`). ``parent_of`` - is a parent (ascendant) of a ``value`` record. + is a parent (ascendant) of a ``value`` record (value can be either + one item or a list of items). Takes the semantics of the model into account (i.e following the relationship field named by :attr:`~odoo.models.Model._parent_name`). -``value`` +* ``value`` variable type, must be comparable (through ``operator``) to the named - field + field. Domain criteria can be combined using logical operators in *prefix* form: @@ -1162,9 +754,7 @@ Domain criteria can be combined using logical operators in *prefix* form: ``'!'`` logical *NOT*, arity 1. - .. tip:: Mostly to negate combinations of criteria - :class: aphorism - + .. note:: Mostly to negate combinations of criteria Individual criterion generally have a negative form (e.g. ``=`` -> ``!=``, ``<`` -> ``>=``) which is simpler than negating the positive. @@ -1186,100 +776,216 @@ Domain criteria can be combined using logical operators in *prefix* form: AND (language is NOT english) AND (country is Belgium OR Germany) -Porting from the old API to the new API -======================================= +Unlink +------ -* bare lists of ids are to be avoided in the new API, use recordsets instead -* 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. -* :meth:`~odoo.models.Model.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 -* :func:`~odoo.api.depends` on ``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 decorator :func:`~odoo.api.model` is - for bridging *when calling from the old API context*, for internal or pure - new-api (e.g. compute) it is useless -* remove :attr:`~odoo.models.Model._default`, replace by ``default=`` - parameter on corresponding fields -* if a field's ``string=`` is the titlecased version of the field name:: +.. automethod:: Model.unlink - name = fields.Char(string="Name") +.. _reference/orm/records/info: - it is useless and should be removed -* the ``multi=`` parameter 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 odoo import fields, models``. If - compatibility decorators are necessary, use ``from odoo import api, - fields, models`` -* remove explicit definition of :attr:`~odoo.models.Model.create_uid`, - :attr:`~odoo.models.Model.create_date`, - :attr:`~odoo.models.Model.write_uid` and - :attr:`~odoo.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 -* uses of :attr:`~odoo.models.Model._columns` or - :attr:`~odoo.models.Model._all_columns` should be replaced by - :attr:`~odoo.models.Model._fields`, which provides access to instances of - new-style :class:`odoo.fields.Field` instances (rather than old-style - :class:`odoo.osv.fields._column`). +Record(set) information +----------------------- - Non-stored computed fields created using the new API style are *not* - available in :attr:`~odoo.models.Model._columns` and can only be - inspected through :attr:`~odoo.models.Model._fields` -* reassigning ``self`` in a method is probably unnecessary and may break - translation introspection -* :class:`~odoo.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:`odoo.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:: +.. autoattribute:: Model.ids - >>> from odoo 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,) +.. attribute:: env -.. _reference/orm/oldapi/bridging: + Returns the environment of the given recordset. -Automatic bridging of old API methods -------------------------------------- + :type: :class:`~odoo.api.Environment` -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. +.. todo:: Environment documentation -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``. +.. automethod:: Model.exists -When calling such methods from a new API context, the system will -automatically fill matched parameters from the current -:class:`~odoo.api.Environment` (for :attr:`~odoo.api.Environment.cr`, -:attr:`~odoo.api.Environment.user` and -:attr:`~odoo.api.Environment.context`) or the current recordset (for ``id`` -and ``ids``). +.. automethod:: Model.ensure_one + +.. automethod:: Model.name_get + +.. automethod:: Model.get_metadata + +.. _reference/orm/records/operations: + +Operations +---------- + +Recordsets are immutable, but sets of the same model can be combined using +various set operations, returning new recordsets. + +.. 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`` and ``set1 < set2`` return whether ``set1`` is a subset + of ``set2`` (resp. strict) +* ``set1 >= set2`` and ``set1 > set2`` return whether ``set1`` is a superset + of ``set2`` (resp. strict) +* ``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`` + +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 the following operations returning recordsets themselves +(when possible): + +Filter +'''''' + +.. automethod:: Model.filtered + +.. automethod:: Model.filtered_domain + +Map +''' + +.. automethod:: Model.mapped + +Sort +'''' + +.. automethod:: Model.sorted + +.. _reference/orm/inheritance: + +Inheritance and extension +========================= + +Odoo provides three different mechanisms to extend models in a modular way: + +* creating a new model from an existing one, adding new information to the + copy but leaving the original module as-is +* extending models defined in other modules in-place, replacing the previous + version +* delegating some of the model's fields to records it contains + +.. image:: ../images/inheritance_methods.png + :align: center + +Classical inheritance +--------------------- + +When using the :attr:`~odoo.models.Model._inherit` and +:attr:`~odoo.models.Model._name` attributes together, Odoo creates a new +model using the existing one (provided via +:attr:`~odoo.models.Model._inherit`) as a base. The new model gets all the +fields, methods and meta-information (defaults & al) from its base. + +.. literalinclude:: ../../odoo/addons/test_documentation_examples/inheritance.py + :language: python + :lines: 6- + +and using them: + +.. literalinclude:: ../../odoo/addons/test_documentation_examples/tests/test_inheritance.py + :language: python + :lines: 10,11,14,19 + +will yield: + +.. literalinclude:: ../../odoo/addons/test_documentation_examples/tests/test_inheritance.py + :language: text + :lines: 16,21 + +the second model has inherited from the first model's ``check`` method and its +``name`` field, but overridden the ``call`` method, as when using standard +:ref:`Python inheritance `. + +Extension +--------- + +When using :attr:`~odoo.models.Model._inherit` but leaving out +:attr:`~odoo.models.Model._name`, the new model replaces the existing one, +essentially extending it in-place. This is useful to add new fields or methods +to existing models (created in other modules), or to customize or reconfigure +them (e.g. to change their default sort order): + +.. literalinclude:: ../../odoo/addons/test_documentation_examples/extension.py + :language: python + :lines: 6- + +.. literalinclude:: ../../odoo/addons/test_documentation_examples/tests/test_extension.py + :language: python + :lines: 10,15 + +will yield: + +.. literalinclude:: ../../odoo/addons/test_documentation_examples/tests/test_extension.py + :language: text + :lines: 13 + +.. note:: it will also yield the various :ref:`automatic fields + ` unless they've been disabled + +Delegation +---------- + +The third inheritance mechanism provides more flexibility (it can be altered +at runtime) but less power: using the :attr:`~odoo.models.Model._inherits` +a model *delegates* the lookup of any field not found on the current model +to "children" models. The delegation is performed via +:class:`~odoo.fields.Reference` fields automatically set up on the parent +model. + +The main difference is in the meaning. When using Delegation, the model +**has one** instead of **is one**, turning the relationship in a composition +instead of inheritance: + +.. literalinclude:: ../../odoo/addons/test_documentation_examples/delegation.py + :language: python + :lines: 5- + +.. literalinclude:: ../../odoo/addons/test_documentation_examples/tests/test_delegation.py + :language: python + :lines: 11-14,23,28 + +will result in: + +.. literalinclude:: ../../odoo/addons/test_documentation_examples/tests/test_delegation.py + :language: text + :lines: 25,30 + +and it's possible to write directly on the delegated field: + +.. literalinclude:: ../../odoo/addons/test_documentation_examples/tests/test_delegation.py + :language: python + :lines: 45 + +.. warning:: when using delegation inheritance, methods are *not* inherited, + only fields + +Fields Incremental Definition +----------------------------- + +A field is defined as class attribute on a model class. If the model +is extended, one can also extend the field definition by redefining +a field with the same name and same type on the subclass. +In that case, the attributes of the field are taken from the parent class +and overridden by the ones given in subclasses. + +For instance, the second class below only adds a tooltip on the field +``state``:: + + class First(models.Model): + _name = 'foo' + state = fields.Selection([...], required=True) + + class Second(models.Model): + _inherit = 'foo' + state = fields.Selection(help="Blah blah blah") + +.. _reference/exceptions: + +Error management +================ + +.. automodule:: odoo.exceptions + :members: AccessDenied, AccessError, CacheMiss, MissingError, RedirectWarning, UserError, ValidationError diff --git a/odoo/api.py b/odoo/api.py index 931dc5ab149..f90bed82585 100644 --- a/odoo/api.py +++ b/odoo/api.py @@ -1,34 +1,9 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -""" This module provides the elements for managing two different API styles, - namely the "traditional" and "record" styles. +"""The Odoo API module defines Odoo Environments and method decorators. - In the "traditional" style, parameters like the database cursor, user id, - context dictionary and record ids (usually denoted as ``cr``, ``uid``, - ``context``, ``ids``) are passed explicitly to all methods. In the "record" - style, those parameters are hidden into model instances, which gives it a - more object-oriented feel. - - For instance, the statements:: - - model = self.pool.get(MODEL) - ids = model.search(cr, uid, DOMAIN, context=context) - for rec in model.browse(cr, uid, ids, context=context): - print rec.name - model.write(cr, uid, ids, VALUES, context=context) - - may also be written as:: - - env = Environment(cr, uid, context) # cr, uid, context wrapped in env - model = env[MODEL] # retrieve an instance of MODEL - recs = model.search(DOMAIN) # search returns a recordset - for rec in recs: # iterate over the records - print rec.name - recs.write(VALUES) # update all records in recs - - Methods written in the "traditional" style are automatically decorated, - following some heuristics based on parameter names. +.. todo:: Document this module """ __all__ = [ @@ -124,8 +99,9 @@ def propagate(method1, method2): def constrains(*args): - """ Decorates a constraint checker. Each argument must be a field name - used in the check:: + """Decorate a constraint checker. + + Each argument must be a field name used in the check:: @api.constrains('name', 'description') def _check_description(self): @@ -135,14 +111,14 @@ def constrains(*args): Invoked on the records on which one of the named fields has been modified. - Should raise :class:`~odoo.exceptions.ValidationError` if the + Should raise :exc:`~odoo.exceptions.ValidationError` if the validation failed. .. warning:: ``@constrains`` only supports simple field names, dotted names (fields of relational fields e.g. ``partner_id.customer``) are not - supported and will be ignored + supported and will be ignored. ``@constrains`` will be triggered only if the declared fields in the decorated method are included in the ``create`` or ``write`` call. @@ -156,44 +132,51 @@ def constrains(*args): def onchange(*args): - """ Return a decorator to decorate an onchange method for given fields. - Each argument must be a field name:: + """Return a decorator to decorate an onchange method for given fields. - @api.onchange('partner_id') - def _onchange_partner(self): - self.message = "Dear %s" % (self.partner_id.name or "") + In the form views where the field appears, the method will be called + when one of the given fields is modified. The method is invoked on a + pseudo-record that contains the values present in the form. Field + assignments on that record are automatically sent back to the client. - In the form views where the field appears, the method will be called - when one of the given fields is modified. The method is invoked on a - pseudo-record that contains the values present in the form. Field - assignments on that record are automatically sent back to the client. + Each argument must be a field name:: - The method may return a dictionary for changing field domains and pop up - a warning message, like in the old API:: + @api.onchange('partner_id') + def _onchange_partner(self): + self.message = "Dear %s" % (self.partner_id.name or "") - return { - 'domain': {'other_id': [('partner_id', '=', partner_id)]}, - 'warning': {'title': "Warning", 'message': "What is this?", 'type': 'notification'}, - } - If the type is set to notification, the warning will be displayed in a notification. - Otherwise it will be displayed in a dialog as default. + .. code-block:: python - .. danger:: + return { + 'domain': {'other_id': [('partner_id', '=', partner_id)]}, + 'warning': {'title': "Warning", 'message': "What is this?", 'type': 'notification'}, + } - Since ``@onchange`` returns a recordset of pseudo-records, - calling any one of the CRUD methods - (:meth:`create`, :meth:`read`, :meth:`write`, :meth:`unlink`) - on the aforementioned recordset is undefined behaviour, - as they potentially do not exist in the database yet. + If the type is set to notification, the warning will be displayed in a notification. + Otherwise it will be displayed in a dialog as default. - Instead, simply set the record's field like shown in the example - above or call the :meth:`update` method. + .. warning:: - .. warning:: + ``@onchange`` only supports simple field names, dotted names + (fields of relational fields e.g. ``partner_id.tz``) are not + supported and will be ignored + + .. danger:: + + Since ``@onchange`` returns a recordset of pseudo-records, + calling any one of the CRUD methods + (:meth:`create`, :meth:`read`, :meth:`write`, :meth:`unlink`) + on the aforementioned recordset is undefined behaviour, + as they potentially do not exist in the database yet. + + Instead, simply set the record's field like shown in the example + above or call the :meth:`update` method. + + .. warning:: + + It is not possible for a ``one2many`` or ``many2many`` field to modify + itself via onchange. This is a webclient limitation - see `#2693 `_. - ``@onchange`` only supports simple field names, dotted names - (fields of relational fields e.g. ``partner_id.tz``) are not - supported and will be ignored """ return attrsetter('_onchange', args) @@ -225,25 +208,26 @@ def depends(*args): def depends_context(*args): """ Return a decorator that specifies the context dependencies of a - non-stored "compute" method. Each argument is a key in the context's - dictionary:: + non-stored "compute" method. Each argument is a key in the context's + dictionary:: - price = fields.Float(compute='_compute_product_price') + price = fields.Float(compute='_compute_product_price') - @api.depends_context('pricelist') - def _compute_product_price(self): - for product in self: - if product.env.context.get('pricelist'): - pricelist = self.env['product.pricelist'].browse(product.env.context['pricelist']) - else: - pricelist = self.env['product.pricelist'].get_default_pricelist() - product.price = pricelist.get_products_price(product).get(product.id, 0.0) + @api.depends_context('pricelist') + def _compute_product_price(self): + for product in self: + if product.env.context.get('pricelist'): + pricelist = self.env['product.pricelist'].browse(product.env.context['pricelist']) + else: + pricelist = self.env['product.pricelist'].get_default_pricelist() + product.price = pricelist.get_products_price(product).get(product.id, 0.0) - All dependencies must be hashable. The following keys have special - support: - - 'force_company' (value in context or current company id), - - 'uid' (current user id and superuser flag), - - 'active_test' (value in env.context or value in field.context). + All dependencies must be hashable. The following keys have special + support: + + * `force_company` (value in context or current company id), + * `uid` (current user id and superuser flag), + * `active_test` (value in env.context or value in field.context). """ return attrsetter('_depends_context', args) @@ -510,6 +494,9 @@ class Environment(Mapping): :param user: optional user/user id to change the current user :param context: optional context dictionary to change the current context :param su: optional boolean to change the superuser mode + :type context: dict + :type user: int or :class:`~odoo.addons.base.models.res_users` + :type su: bool """ cr = self.cr if cr is None else cr uid = self.uid if user is None else int(user) @@ -518,7 +505,7 @@ class Environment(Mapping): return Environment(cr, uid, context, su) def ref(self, xml_id, raise_if_not_found=True): - """ return the record corresponding to the given ``xml_id`` """ + """Return the record corresponding to the given ``xml_id``.""" return self['ir.model.data'].xmlid_to_object(xml_id, raise_if_not_found=raise_if_not_found) def is_superuser(self): @@ -537,17 +524,19 @@ class Environment(Mapping): @lazy_property def user(self): - """ return the current user (as an instance) """ + """Return the current user (as an instance). + + :rtype: :class:`~odoo.addons.base.models.res_users`""" return self(su=True)['res.users'].browse(self.uid) @lazy_property def company(self): """Return the current company (as an instance). - If not specified in the context ('allowed_company_ids'), - fallback on current user main company. + If not specified in the context (`allowed_company_ids`), + fallback on current user main company. - :raise AccessError: invalid or unauthorized 'allowed_company_ids' context key content. + :raise AccessError: invalid or unauthorized `allowed_company_ids` context key content. :return: current company (default=`self.user.company_id`) :rtype: res.company @@ -574,10 +563,10 @@ class Environment(Mapping): def companies(self): """Return a recordset of the enabled companies by the user. - If not specified in the context('allowed_company_ids'), - fallback on current user companies. + If not specified in the context(`allowed_company_ids`), + fallback on current user companies. - :raise AccessError: invalid or unauthorized 'allowed_company_ids' context key content. + :raise AccessError: invalid or unauthorized `allowed_company_ids` context key content. :return: current companies (default=`self.user.company_ids`) :rtype: res.company @@ -612,7 +601,10 @@ class Environment(Mapping): @property def lang(self): - """ return the current language code """ + """Return the current language code. + + :rtype: str + """ return self.context.get('lang') def clear(self): diff --git a/odoo/exceptions.py b/odoo/exceptions.py index 75e8749b028..b3cc6b48f1e 100644 --- a/odoo/exceptions.py +++ b/odoo/exceptions.py @@ -1,13 +1,15 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -""" OpenERP core exceptions. +"""The Odoo Exceptions module defines a few core exception types. -This module defines a few exception types. Those types are understood by the -RPC layer. Any other exception type bubbling until the RPC layer will be +Those types are understood by the RPC layer. +Any other exception type bubbling until the RPC layer will be treated as a 'Server error'. -If you consider introducing new exceptions, check out the test_exceptions addon. +.. note:: + If you consider introducing new exceptions, + check out the :mod:`odoo.addons.test_exceptions` module. """ import logging @@ -29,6 +31,11 @@ class except_orm(Exception): class UserError(except_orm): + """Generic error managed by the client. + + Typically when the user tries to do something that has no sense given the current + state of a record. + """ def __init__(self, msg): super(UserError, self).__init__(msg, value='') @@ -41,10 +48,9 @@ class RedirectWarning(Exception): """ Warning with a possibility to redirect the user instead of simply displaying the warning message. - Should receive as parameters: - :param int action_id: id of the action where to perform the redirection - :param string button_text: text to put on the button that will trigger - the redirection. + :param int action_id: id of the action where to perform the redirection + :param str button_text: text to put on the button that will trigger + the redirection. """ # using this RedirectWarning won't crash if used as an except_orm @property @@ -53,8 +59,17 @@ class RedirectWarning(Exception): class AccessDenied(Exception): - """ Login/password error. no traceback. - Example: When you try to log with a wrong password.""" + """Login/password error. + + .. note:: + + No traceback. + + .. admonition:: Example + + When you try to log with a wrong password. + """ + def __init__(self, message='Access denied'): super(AccessDenied, self).__init__(message) self.with_traceback(None) @@ -63,29 +78,49 @@ class AccessDenied(Exception): class AccessError(except_orm): - """ Access rights error. - Example: When you try to read a record that you are not allowed to.""" + """Access rights error. + + .. admonition:: Example + + When you try to read a record that you are not allowed to. + """ + def __init__(self, msg): super(AccessError, self).__init__(msg) class CacheMiss(except_orm, KeyError): - """ Missing value(s) in cache. - Example: When you try to read a value in a flushed cache.""" + """Missing value(s) in cache. + + .. admonition:: Example + + When you try to read a value in a flushed cache. + """ + def __init__(self, record, field): super(CacheMiss, self).__init__("%s.%s" % (str(record), field.name)) class MissingError(except_orm): - """ Missing record(s). - Example: When you try to write on a deleted record.""" + """Missing record(s). + + .. admonition:: Example + + When you try to write on a deleted record. + """ + def __init__(self, msg): super(MissingError, self).__init__(msg) class ValidationError(except_orm): - """ Violation of python constraints - Example: When you try to create a new user with a login which already exist in the db.""" + """Violation of python constraints. + + .. admonition:: Example + + When you try to create a new user with a login which already exist in the db. + """ + def __init__(self, msg): super(ValidationError, self).__init__(msg) diff --git a/odoo/fields.py b/odoo/fields.py index 70dadd84850..3ada0e914ab 100644 --- a/odoo/fields.py +++ b/odoo/fields.py @@ -99,169 +99,80 @@ class MetaField(type): _global_seq = iter(itertools.count()) class Field(MetaField('DummyField', (object,), {})): - """ The field descriptor contains the field definition, and manages accesses - and assignments of the corresponding field on records. The following - attributes may be provided when instanciating a field: + """The field descriptor contains the field definition, and manages accesses + and assignments of the corresponding field on records. The following + attributes may be provided when instanciating a field: - :param string: the label of the field seen by users (string); if not - set, the ORM takes the field name in the class (capitalized). + :param str string: the label of the field seen by users; if not + set, the ORM takes the field name in the class (capitalized). - :param help: the tooltip of the field seen by users (string) + :param str help: the tooltip of the field seen by users - :param readonly: whether the field is readonly (boolean, by default ``False``) + :param bool readonly: whether the field is readonly (default: ``False``) - :param required: whether the value of the field is required (boolean, by - default ``False``) + This only has an impact on the UI. Any field assignation in code will work + (if the field is a stored field or an inversable one). - :param index: whether the field is indexed in database. Note: no effect - on non-stored and virtual fields. (boolean, by default ``False``) + :param bool required: whether the value of the field is required (default: ``False``) - :param default: the default value for the field; this is either a static - value, or a function taking a recordset and returning a value; use - ``default=None`` to discard default values for the field + :param bool index: whether the field is indexed in database. Note: no effect + on non-stored and virtual fields. (default: ``False``) - :param states: a dictionary mapping state values to lists of UI attribute-value - pairs; possible attributes are: 'readonly', 'required', 'invisible'. - Note: Any state-based condition requires the ``state`` field value to be + :param default: the default value for the field; this is either a static + value, or a function taking a recordset and returning a value; use + ``default=None`` to discard default values for the field + :type default: value or callable + + :param dict states: a dictionary mapping state values to lists of UI attribute-value + pairs; possible attributes are: ``readonly``, ``required``, ``invisible``. + + .. warning:: Any state-based condition requires the ``state`` field value to be available on the client-side UI. This is typically done by including it in the relevant views, possibly made invisible if not relevant for the end-user. - :param groups: comma-separated list of group xml ids (string); this - restricts the field access to the users of the given groups only + :param str groups: comma-separated list of group xml ids (string); this + restricts the field access to the users of the given groups only - :param bool copy: whether the field value should be copied when the record - is duplicated (default: ``True`` for normal fields, ``False`` for - ``one2many`` and computed fields, including property fields and - related fields) + :param bool company_dependent: whether the field value is dependent of the current company; - .. _field-computed: + The value isn't stored on the model table. It is registered as `ir.property`. + When the value of the company_dependent field is needed, an `ir.property` + is searched, linked to the current company (and current record if one property + exists). - .. rubric:: Computed fields + If the value is changed on the record, it either modifies the existing property + for the current record (if one exists), or creates a new one for the current company + and res_id. - One can define a field whose value is computed instead of simply being - read from the database. The attributes that are specific to computed - fields are given below. To define such a field, simply provide a value - for the attribute ``compute``. + If the value is changed on the company side, it will impact all records on which + the value hasn't been changed. - :param compute: name of a method that computes the field + :param bool copy: whether the field value should be copied when the record + is duplicated (default: ``True`` for normal fields, ``False`` for + ``one2many`` and computed fields, including property fields and + related fields) - :param inverse: name of a method that inverses the field (optional) + :param bool store: whether the field is stored in database + (default:``True``, ``False`` for computed fields) - :param search: name of a method that implement search on the field (optional) + .. rubric:: Computed Fields - :param store: whether the field is stored in database (boolean, by - default ``False`` on computed fields) + :param str compute: name of a method that computes the field - :param compute_sudo: whether the field should be computed in superuser - mode to bypass access rights (boolean, defaults to ``True`` for - stored fields and ``False`` for non-stored fields) + .. seealso:: :ref:`Advanced Fields/Compute fields ` - The methods given for ``compute``, ``inverse`` and ``search`` are model - methods. Their signature is shown in the following example:: + :param bool compute_sudo: whether the field should be recomputed as superuser + to bypass access rights (by default ``True`` for stored fields, ``False`` + for non stored fields) - upper = fields.Char(compute='_compute_upper', - inverse='_inverse_upper', - search='_search_upper') + :param str inverse: name of a method that inverses the field (optional) - @api.depends('name') - def _compute_upper(self): - for rec in self: - rec.upper = rec.name.upper() if rec.name else False + :param str search: name of a method that implement search on the field (optional) - def _inverse_upper(self): - for rec in self: - rec.name = rec.upper.lower() if rec.upper else False - - def _search_upper(self, operator, value): - if operator == 'like': - operator = 'ilike' - return [('name', operator, value)] - - The compute method has to assign the field on all records of the invoked - recordset. The decorator :meth:`odoo.api.depends` must be applied on - the compute method to specify the field dependencies; those dependencies - are used to determine when to recompute the field; recomputation is - automatic and guarantees cache/database consistency. Note that the same - method can be used for several fields, you simply have to assign all the - given fields in the method; the method will be invoked once for all - those fields. - - By default, a computed field is not stored to the database, and is - computed on-the-fly. Adding the attribute ``store=True`` will store the - field's values in the database. The advantage of a stored field is that - searching on that field is done by the database itself. The disadvantage - is that it requires database updates when the field must be recomputed. - - The inverse method, as its name says, does the inverse of the compute - method: the invoked records have a value for the field, and you must - apply the necessary changes on the field dependencies such that the - computation gives the expected value. Note that a computed field without - an inverse method is readonly by default. - - The search method is invoked when processing domains before doing an - actual search on the model. It must return a domain equivalent to the - condition: ``field operator value``. - - .. _field-related: - - .. rubric:: Related fields - - The value of a related field is given by following a sequence of - relational fields and reading a field on the reached model. The complete - sequence of fields to traverse is specified by the attribute - - :param related: sequence of field names - - Some field attributes are automatically copied from the source field if - they are not redefined: ``string``, ``help``, ``readonly``, ``required`` (only - if all fields in the sequence are required), ``groups``, ``digits``, ``size``, - ``translate``, ``sanitize``, ``selection``, ``comodel_name``, ``domain``, - ``context``. All semantic-free attributes are copied from the source - field. - - By default, the values of related fields are not stored to the database. - Add the attribute ``store=True`` to make it stored, just like computed - fields. Related fields are automatically recomputed when their - dependencies are modified. - - .. _field-company-dependent: - - .. rubric:: Company-dependent fields - - Formerly known as 'property' fields, the value of those fields depends - on the company. In other words, users that belong to different companies - may see different values for the field on a given record. - - .. warning:: - - Company-dependent fields aren't stored in the table of the model they're defined on, - instead, they are stored in the ``ir.property`` model's table. - - :param company_dependent: whether the field is company-dependent (boolean) - - .. _field-incremental-definition: - - .. rubric:: Incremental definition - - A field is defined as class attribute on a model class. If the model - is extended (see :class:`~odoo.models.Model`), one can also extend - the field definition by redefining a field with the same name and same - type on the subclass. In that case, the attributes of the field are - taken from the parent class and overridden by the ones given in - subclasses. - - For instance, the second class below only adds a tooltip on the field - ``state``:: - - class First(models.Model): - _name = 'foo' - state = fields.Selection([...], required=True) - - class Second(models.Model): - _inherit = 'foo' - state = fields.Selection(help="Blah blah blah") + :param str related: sequence of field names + .. seealso:: :ref:`Advanced fields/Related fields ` """ type = None # type of the field (string) @@ -1225,11 +1136,13 @@ class Integer(Field): class Float(Field): - """ The precision digits are given by the attribute + """The precision digits are given by the attribute - :param digits: a pair (total, decimal), or a function taking a database - cursor and returning a pair (total, decimal) + :param digits: a pair (total, decimal) or a + string referencing a `decimal.precision` record. + :type digits: tuple(int,int) or str """ + type = 'float' column_cast_from = ('int4', 'numeric', 'float8') _slots = { @@ -1290,8 +1203,8 @@ class Float(Field): class Monetary(Field): """ The decimal precision and currency symbol are taken from the attribute - :param currency_field: name of the field holding the currency this monetary - field is expressed in (default: `currency_id`) + :param str currency_field: name of the field holding the currency + this monetary field is expressed in (default: `\'currency_id\'`) """ type = 'monetary' column_type = ('numeric', 'numeric') @@ -1516,6 +1429,7 @@ class Char(_String): may also be a callable such that ``translate(callback, value)`` translates ``value`` by using ``callback(term)`` to retrieve the translation of terms. + :type translate: bool or callable """ type = 'char' column_cast_from = ('text',) @@ -1569,6 +1483,7 @@ class Text(_String): may also be a callable such that ``translate(callback, value)`` translates ``value`` by using ``callback(term)`` to retrieve the translation of terms. + :type translate: bool or callable """ type = 'text' column_type = ('text', 'text') @@ -1640,6 +1555,9 @@ class Html(_String): class Date(Field): + """ This field type encapsulates a python date object. + :type date: + """ type = 'date' column_type = ('date', 'date') column_cast_from = ('timestamp',) @@ -1651,17 +1569,18 @@ class Date(Field): @staticmethod def today(*args): - """ Return the current day in the format expected by the ORM. - This function may be used to compute default values. + """Return the current day in the format expected by the ORM. + + .. note:: This function may be used to compute default values. """ return date.today() @staticmethod def context_today(record, timestamp=None): - """ - Return the current date as seen in the client's timezone in a format - fit for date fields. This method may be used to compute default - values. + """Return the current date as seen in the client's timezone in a format + fit for date fields. + + .. note:: This method may be used to compute default values. :param record: recordset from which the timezone will be obtained. :param datetime timestamp: optional datetime value to use instead of @@ -1683,19 +1602,18 @@ class Date(Field): @staticmethod def to_date(value): - """ - Attempt to convert ``value`` to a :class:`date` object. + """Attempt to convert ``value`` to a :class:`date` object. - This function can take as input different kinds of types: - * A falsy object, in which case None will be returned. - * A string representing a date or datetime. - * A date object, in which case the object will be returned as-is. - * A datetime object, in which case it will be converted to a date object and all\ - datetime-specific information will be lost (HMS, TZ, ...). + .. warning:: + + If a datetime object is given as value, + it will be converted to a date object and all + datetime-specific information will be lost (HMS, TZ, ...). :param value: value to convert. + :type value: str or date or datetime :return: an object representing ``value``. - :rtype: date + :rtype: date or None """ if not value: return None @@ -1738,6 +1656,9 @@ class Date(Field): class Datetime(Field): + """ This field type encapsulates a python datetime object. + :type datetime: + """ type = 'datetime' column_type = ('timestamp', 'timestamp') column_cast_from = ('date',) @@ -1749,33 +1670,32 @@ class Datetime(Field): @staticmethod def now(*args): - """ Return the current day and time in the format expected by the ORM. - This function may be used to compute default values. + """Return the current day and time in the format expected by the ORM. + + .. note:: This function may be used to compute default values. """ # microseconds must be annihilated as they don't comply with the server datetime format return datetime.now().replace(microsecond=0) @staticmethod def today(*args): - """ - Return the current day, at midnight (00:00:00). - """ + """Return the current day, at midnight (00:00:00).""" return Datetime.now().replace(hour=0, minute=0, second=0) @staticmethod def context_timestamp(record, timestamp): - """ - Returns the given timestamp converted to the client's timezone. - This method is *not* meant for use as a default initializer, - because datetime fields are automatically converted upon - display on client side. For default values, :meth:`fields.Datetime.now` - should be used instead. + """Return the given timestamp converted to the client's timezone. + + .. note:: This method is *not* meant for use as a default initializer, + because datetime fields are automatically converted upon + display on client side. For default values, :meth:`now` + should be used instead. :param record: recordset from which the timezone will be obtained. :param datetime timestamp: naive datetime value (expressed in UTC) to be converted to the client timezone. - :rtype: datetime :return: timestamp converted to timezone-aware datetime in context timezone. + :rtype: datetime """ assert isinstance(timestamp, datetime), 'Datetime instance expected' tz_name = record._context.get('tz') or record.env.user.tz @@ -1792,18 +1712,12 @@ class Datetime(Field): @staticmethod def to_datetime(value): - """ - Convert an ORM ``value`` into a :class:`datetime` value. - - This function can take as input different kinds of types: - * A falsy object, in which case None will be returned. - * A string representing a date or datetime. - * A datetime object, in which case the object will be returned as-is. - * A date object, in which case it will be converted to a datetime object. + """Convert an ORM ``value`` into a :class:`datetime` value. :param value: value to convert. + :type value: str or date or datetime :return: an object representing ``value``. - :rtype: datetime + :rtype: datetime or None """ if not value: return None @@ -1823,12 +1737,13 @@ class Datetime(Field): @staticmethod def to_string(value): - """ - Convert a :class:`datetime` or :class:`date` object to a string. + """Convert a :class:`datetime` or :class:`date` object to a string. :param value: value to convert. - :return: a string representing ``value`` in the server's datetime format, if ``value`` is - of type :class:`date`, the time portion will be midnight (00:00:00). + :type value: datetime or date + :return: a string representing ``value`` in the server's datetime format, + if ``value`` is of type :class:`date`, + the time portion will be midnight (00:00:00). :rtype: str """ return value.strftime(DATETIME_FORMAT) if value else False @@ -2071,6 +1986,7 @@ class Selection(Field): :param selection: specifies the possible values for this field. It is given as either a list of pairs ``(value, label)``, or a model method, or a method name. + :type selection: list(tuple(str,str)) or callable or str :param selection_add: provides an extension of the selection in the case of an overridden field. It is a list of pairs ``(value, label)`` or @@ -2081,10 +1997,10 @@ class Selection(Field): selection = [('a', 'A'), ('b', 'B')] selection_add = [('c', 'C'), ('b',)] > result = [('a', 'A'), ('c', 'C'), ('b', 'B')] + :type selection_add: list(tuple(str,str)) The attribute ``selection`` is mandatory except in the case of - :ref:`related fields ` or :ref:`field extensions - `. + ``related`` or extended fields. """ type = 'selection' column_type = ('varchar', pg_varchar()) @@ -2181,7 +2097,7 @@ class Selection(Field): return selection def get_values(self, env): - """ return a list of the possible values """ + """Return a list of the possible values.""" selection = self.selection if isinstance(selection, str): selection = getattr(env[self.model_name], selection)() @@ -2315,29 +2231,27 @@ class Many2one(_Relational): """ The value of such a field is a recordset of size 0 (no record) or 1 (a single record). - :param comodel_name: name of the target model (string) + :param str comodel_name: name of the target model + ``Mandatory`` except for related or extended fields. :param domain: an optional domain to set on candidate values on the client side (domain or string) - :param context: an optional context to use on the client side when - handling that field (dictionary) + :param dict context: an optional context to use on the client side when + handling that field - :param ondelete: what to do when the referred record is deleted; + :param str ondelete: what to do when the referred record is deleted; possible values are: ``'set null'``, ``'restrict'``, ``'cascade'`` - :param auto_join: whether JOINs are generated upon search through that - field (boolean, by default ``False``) + :param bool auto_join: whether JOINs are generated upon search through that + field (default: ``False``) - :param delegate: set it to ``True`` to make fields of the target model + :param bool delegate: set it to ``True`` to make fields of the target model accessible from the current model (corresponds to ``_inherits``) :param check_company: add default domain ``['|', ('company_id', '=', False), ('company_id', '=', company_id)]``. Mark the field to be verified in ``_check_company``. - - The attribute ``comodel_name`` is mandatory except in the case of related - fields or field extensions. """ type = 'many2one' column_type = ('int4', 'int4') @@ -2802,28 +2716,28 @@ class _RelationalMulti(_Relational): class One2many(_RelationalMulti): - """ One2many field; the value of such a field is the recordset of all the - records in ``comodel_name`` such that the field ``inverse_name`` is equal to - the current record. + """One2many field; the value of such a field is the recordset of all the + records in ``comodel_name`` such that the field ``inverse_name`` is equal to + the current record. - :param comodel_name: name of the target model (string) + :param str comodel_name: name of the target model - :param inverse_name: name of the inverse ``Many2one`` field in - ``comodel_name`` (string) + :param str inverse_name: name of the inverse ``Many2one`` field in + ``comodel_name`` - :param domain: an optional domain to set on candidate values on the - client side (domain or string) + :param domain: an optional domain to set on candidate values on the + client side (domain or string) - :param context: an optional context to use on the client side when - handling that field (dictionary) + :param dict context: an optional context to use on the client side when + handling that field - :param auto_join: whether JOINs are generated upon search through that - field (boolean, by default ``False``) + :param bool auto_join: whether JOINs are generated upon search through that + field (default: ``False``) - :param limit: optional limit to use upon read (integer) + :param int limit: optional limit to use upon read - The attributes ``comodel_name`` and ``inverse_name`` are mandatory except in - the case of related fields or field extensions. + The attributes ``comodel_name`` and ``inverse_name`` are mandatory except in + the case of related fields or field extensions. """ type = 'one2many' _slots = { @@ -3065,46 +2979,43 @@ class One2many(_RelationalMulti): class Many2many(_RelationalMulti): """ Many2many field; the value of such a field is the recordset. - :param comodel_name: name of the target model (string) + :param comodel_name: name of the target model (string) + mandatory except in the case of related or extended fields - The attribute ``comodel_name`` is mandatory except in the case of related - fields or field extensions. + :param str relation: optional name of the table that stores the relation in + the database - :param relation: optional name of the table that stores the relation in - the database (string) + :param str column1: optional name of the column referring to "these" records + in the table ``relation`` - :param column1: optional name of the column referring to "these" records - in the table ``relation`` (string) + :param str column2: optional name of the column referring to "those" records + in the table ``relation`` - :param column2: optional name of the column referring to "those" records - in the table ``relation`` (string) + The attributes ``relation``, ``column1`` and ``column2`` are optional. + If not given, names are automatically generated from model names, + provided ``model_name`` and ``comodel_name`` are different! - The attributes ``relation``, ``column1`` and ``column2`` are optional. - If not given, names are automatically generated from model names, - provided ``model_name`` and ``comodel_name`` are different! + Note that having several fields with implicit relation parameters on a + given model with the same comodel is not accepted by the ORM, since + those field would use the same table. The ORM prevents two many2many + fields to use the same relation parameters, except if - Note that having several fields with implicit relation parameters on a - given model with the same comodel is not accepted by the ORM, since - those field would use the same table. The ORM prevents two many2many - fields to use the same relation parameters, except if + - both fields use the same model, comodel, and relation parameters are + explicit; or - - both fields use the same model, comodel, and relation parameters are - explicit; or + - at least one field belongs to a model with ``_auto = False``. - - at least one field belongs to a model with ``_auto = False``. + :param domain: an optional domain to set on candidate values on the + client side (domain or string) - :param domain: an optional domain to set on candidate values on the - client side (domain or string) + :param dict context: an optional context to use on the client side when + handling that field - :param context: an optional context to use on the client side when - handling that field (dictionary) - - :param limit: optional limit to use upon read (integer) - - :param check_company: add default domain ``['|', ('company_id', '=', False), - ('company_id', '=', company_id)]``. Mark the field to be verified in - ``_check_company``. + :param check_company: add default domain ``['|', ('company_id', '=', False), + ('company_id', '=', company_id)]``. Mark the field to be verified in + ``_check_company``. + :param int limit: optional limit to use upon read """ type = 'many2many' _slots = { diff --git a/odoo/models.py b/odoo/models.py index b9201de446b..be614498fe4 100644 --- a/odoo/models.py +++ b/odoo/models.py @@ -237,9 +237,9 @@ VALID_AGGREGATE_FUNCTIONS = { class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})): - """ Base class for Odoo models. + """Base class for Odoo models. - Odoo models are created by inheriting: + Odoo models are created by inheriting one of the following: * :class:`Model` for regular database-persisted models @@ -261,35 +261,80 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})): explicit representation: a record is represented as a recordset of one record. - To create a class that should not be instantiated, the _register class - attribute may be set to False. + To create a class that should not be instantiated, + the :attr:`~odoo.models.BaseModel._register` attribute may be set to False. """ - _auto = False # don't create any database backend - _register = False # not visible in ORM registry - _abstract = True # whether model is abstract - _transient = False # whether model is transient - _name = None # the model name - _description = None # the model's informal name - _custom = False # should be True for custom models only + _auto = False + """Whether a database table should be created (default: ``True``). + If set to ``False``, override :meth:`~odoo.models.BaseModel.init` + to create the database table. - _inherit = None # Python-inherited models ('model' or ['model']) - _inherits = {} # inherited models {'parent_model': 'm2o_field'} + .. tip:: To create a model without any table, inherit + from :class:`~odoo.models.AbstractModel`. + """ + _register = False #: not visible in ORM registry + _abstract = True #: whether model is abstract + _transient = False #: whether model is transient - _table = None # SQL table name used by model - _sequence = None # SQL sequence to use for ID field - _sql_constraints = [] # SQL constraints [(name, sql_def, message)] + _name = None #: the model name (in dot-notation, module namespace) + _description = None #: the model's informal name + _custom = False #: should be True for custom models only - _rec_name = None # field to use for labeling records - _order = 'id' # default order for searching results - _parent_name = 'parent_id' # the many2one field used as parent field - _parent_store = False # set to True to compute parent_path field - _date_name = 'date' # field to use for default calendar view - _fold_name = 'fold' # field to determine folded groups in kanban views + _inherit = None + """Python-inherited models: - _needaction = False # whether the model supports "need actions" (see mail) - _translate = True # False disables translations export for this model + :type: str or list(str) + + .. note:: + + * If :attr:`._name` is set, name(s) of parent models to inherit from + * If :attr:`._name` is unset, name of a single model to extend in-place + """ + _inherits = {} + """dictionary {'parent_model': 'm2o_field'} mapping the _name of the parent business + objects to the names of the corresponding foreign key fields to use:: + + _inherits = { + 'a.model': 'a_field_id', + 'b.model': 'b_field_id' + } + + implements composition-based inheritance: the new model exposes all + the fields of the inherited models but stores none of them: + the values themselves remain stored on the linked record. + + .. warning:: + + if multiple fields with the same name are defined in the + :attr:`~odoo.models.Model._inherits`-ed models, the inherited field will + correspond to the last one (in the inherits list order). + """ + _table = None #: SQL table name used by model if :attr:`_auto` + _sequence = None #: SQL sequence to use for ID field + _sql_constraints = [] #: SQL constraints [(name, sql_def, message)] + + _rec_name = None #: field to use for labeling records, default: ``name`` + _order = 'id' #: default order field for searching results + _parent_name = 'parent_id' #: the many2one field used as parent field + _parent_store = False + """set to True to compute parent_path field. + + Alongside a :attr:`~.parent_path` field, sets up an indexed storage + of the tree structure of records, to enable faster hierarchical queries + on the records of the current model using the ``child_of`` and + ``parent_of`` domain operators. + """ + _date_name = 'date' #: field to use for default calendar view + _fold_name = 'fold' #: field to determine folded groups in kanban views + + _needaction = False # whether the model supports "need actions" (Old API) + _translate = True # False disables translations export for this model (Old API) _check_company_auto = False + """On write and create, call ``_check_company`` to ensure companies + consistency on the relational fields having ``check_company=True`` + as attribute. + """ # default values for _transient_vacuum() _transient_check_count = 0 @@ -1439,14 +1484,15 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})): Get the detailed composition of the requested view like fields, model, view architecture - :param view_id: id of the view or None - :param view_type: type of the view to return if view_id is None ('form', 'tree', ...) - :param toolbar: true to include contextual actions + :param int view_id: id of the view or None + :param str view_type: type of the view to return if view_id is None ('form', 'tree', ...) + :param bool toolbar: true to include contextual actions :param submenu: deprecated - :return: dictionary describing the composition of the requested view (including inherited views and extensions) + :return: composition of the requested view (including inherited views and extensions) + :rtype: dict :raise AttributeError: - * if the inherited view has unknown position to work with other than 'before', 'after', 'inside', 'replace' - * if some tag other than 'position' is found in parent view + * if the inherited view has unknown position to work with other than 'before', 'after', 'inside', 'replace' + * if some tag other than 'position' is found in parent view :raise Invalid ArchitectureError: if there is view type other than form, tree, calendar, search etc defined on the structure """ View = self.env['ir.ui.view'] @@ -2038,10 +2084,10 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})): @api.model def read_group(self, domain, fields, groupby, offset=0, limit=None, orderby=False, lazy=True): - """ - Get the list of records in list view grouped by the given ``groupby`` fields + """Get the list of records in list view grouped by the given ``groupby`` fields. - :param domain: list specifying search criteria [['field_name', 'operator', 'value'], ...] + :param list domain: :ref:`A search domain `. Use an empty + list to match all records. :param list fields: list of fields present in the list view specified on the object. Each element is either 'field' (field name, using the default aggregation), or 'field:agg' (aggregate field with aggregation function 'agg'), @@ -2993,19 +3039,18 @@ Fields: raise self.env['ir.rule']._make_access_error('read', forbidden) def get_metadata(self): - """ - Returns some metadata about the given records. + """Return some metadata about the given records. :return: list of ownership dictionaries for each requested record :rtype: list of dictionaries with the following keys: - * id: object id - * create_uid: user who created the record - * create_date: date when the record was created - * write_uid: last user who changed the record - * write_date: date of the last change to the record - * xmlid: XML ID to use to refer to this record (if there is one), in format ``module.name`` - * noupdate: A boolean telling if the record will be updated or not + * id: object id + * create_uid: user who created the record + * create_date: date when the record was created + * write_uid: last user who changed the record + * write_date: date of the last change to the record + * xmlid: XML ID to use to refer to this record (if there is one), in format ``module.name`` + * noupdate: A boolean telling if the record will be updated or not """ IrModelData = self.env['ir.model.data'].sudo() @@ -3320,7 +3365,7 @@ Record ids: %(records)s :raise AccessError: * if user has no write rights on the requested object * if user tries to bypass access rules for write on the requested object - :raise ValidateError: if user tries to enter invalid value for a field that is not in selection + :raise ValidationError: if user tries to enter invalid value for a field that is not in selection :raise UserError: if a loop would be created in a hierarchy of objects a result of the operation (such as setting an object as its own parent) * For numeric fields (:class:`~odoo.fields.Integer`, @@ -3355,31 +3400,28 @@ Record ids: %(records)s triplet is a command to execute on the set of records. Not all commands apply in all situations. Possible commands are: - ``(0, _, values)`` + ``(0, 0, values)`` adds a new record created from the provided ``value`` dict. ``(1, id, values)`` updates an existing record of id ``id`` with the values in ``values``. Can not be used in :meth:`~.create`. - ``(2, id, _)`` + ``(2, id, 0)`` removes the record of id ``id`` from the set, then deletes it (from the database). Can not be used in :meth:`~.create`. - ``(3, id, _)`` + ``(3, id, 0)`` removes the record of id ``id`` from the set, but does not delete it. Can not be used in :meth:`~.create`. - ``(4, id, _)`` + ``(4, id, 0)`` adds an existing record of id ``id`` to the set. - ``(5, _, _)`` + ``(5, 0, 0)`` removes all records from the set, equivalent to using the command ``3`` on every record explicitly. Can not be used in :meth:`~.create`. - ``(6, _, ids)`` + ``(6, 0, ids)`` replaces all existing records in the set by the ``ids`` list, equivalent to using the command ``5`` followed by a command ``4`` for each ``id`` in ``ids``. - - .. note:: Values marked as ``_`` in the list above are ignored and - can be anything, generally ``0`` or ``False``. """ if not self: return True @@ -3583,7 +3625,7 @@ Record ids: %(records)s :return: the created records :raise AccessError: * if user has no create rights on the requested object * if user tries to bypass access rules for create on the requested object - :raise ValidateError: if user tries to enter invalid value for a field that is not in selection + :raise ValidationError: if user tries to enter invalid value for a field that is not in selection :raise UserError: if a loop would be created in a hierarchy of objects a result of the operation (such as setting an object as its own parent) """ if not vals_list: @@ -4731,17 +4773,20 @@ Record ids: %(records)s @api.model def search_read(self, domain=None, fields=None, offset=0, limit=None, order=None): - """ - Performs a ``search()`` followed by a ``read()``. + """Perform a :meth:`search` followed by a :meth:`read`. - :param domain: Search domain, see ``args`` parameter in ``search()``. Defaults to an empty domain that will match all records. - :param fields: List of fields to read, see ``fields`` parameter in ``read()``. Defaults to all fields. - :param offset: Number of records to skip, see ``offset`` parameter in ``search()``. Defaults to 0. - :param limit: Maximum number of records to return, see ``limit`` parameter in ``search()``. Defaults to no limit. - :param order: Columns to sort result, see ``order`` parameter in ``search()``. Defaults to no sort. + :param domain: Search domain, see ``args`` parameter in :meth:`search`. + Defaults to an empty domain that will match all records. + :param fields: List of fields to read, see ``fields`` parameter in :meth:`read`. + Defaults to all fields. + :param int offset: Number of records to skip, see ``offset`` parameter in :meth:`search`. + Defaults to 0. + :param int limit: Maximum number of records to return, see ``limit`` parameter in :meth:`search`. + Defaults to no limit. + :param order: Columns to sort result, see ``order`` parameter in :meth:`search`. + Defaults to no sort. :return: List of dictionaries containing the asked fields. - :rtype: List of dictionaries. - + :rtype: list(dict). """ records = self.search(domain or [], offset=offset, limit=limit, order=order) if not records: @@ -4863,7 +4908,14 @@ Record ids: %(records)s Returns a recordset for the ids provided as parameter in the current environment. - Can take no ids, a single id or an iterable of ids. + .. code-block:: python + + self.browse([7, 18, 12]) + res.partner(7, 18, 12) + + :param ids: id(s) + :type ids: int or list(int) or None + :return: recordset """ if not ids: ids = () @@ -4892,8 +4944,9 @@ Record ids: %(records)s # def ensure_one(self): - """ Verifies that the current recorset holds a single record. Raises - an exception otherwise. + """Verify that the current recorset holds a single record. + + :raise odoo.exceptions.ValueError: ``len(self) != 1`` """ try: # unpack to ensure there is only one value is faster than len when true and @@ -4904,16 +4957,16 @@ Record ids: %(records)s raise ValueError("Expected singleton: %s" % self) def with_env(self, env): - """ Returns a new version of this recordset attached to the provided - environment + """Return a new version of this recordset attached to the provided environment. + + :param env: + :type env: :class:`~odoo.api.Environment` .. warning:: The new environment will not benefit from the current environment's data cache, so later data access may incur extra delays while re-fetching from the database. The returned recordset has the same prefetch object as ``self``. - - :type env: :class:`~odoo.api.Environment` """ return self._browse(env, self._ids, self._prefetch_ids) @@ -4924,7 +4977,7 @@ Record ids: %(records)s disabled, depending on `flag`. The superuser mode does not change the current user, and simply bypasses access rights checks. - .. note:: + .. warning:: Using ``sudo`` could cause data access to cross the boundaries of record rules, possibly mixing records that @@ -5070,12 +5123,32 @@ Record ids: %(records)s return vals if isinstance(vals, BaseModel) else [] def mapped(self, func): - """ Apply ``func`` on all records in ``self``, and return the result as a - list or a recordset (if ``func`` return recordsets). In the latter - case, the order of the returned recordset is arbitrary. + """Apply ``func`` on all records in ``self``, and return the result as a + list or a recordset (if ``func`` return recordsets). In the latter + case, the order of the returned recordset is arbitrary. - :param func: a function or a dot-separated sequence of field names - (string); any falsy value simply returns the recordset ``self`` + :param func: a function or a dot-separated sequence of field names + :type func: callable or str + :return: self if func is falsy, result of func applied to all ``self`` records. + :rtype: list or recordset + + .. code-block:: python3 + + # 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: + + .. code-block:: python3 + + # 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') """ if not func: return self # support for an empty path of fields @@ -5102,10 +5175,19 @@ Record ids: %(records)s return recs def filtered(self, func): - """ Select the records in ``self`` such that ``func(rec)`` is true, and - return them as a recordset. + """Return the records in ``self`` satisfying ``func``. - :param func: a function or a dot-separated sequence of field names + :param func: a function or a dot-separated sequence of field names + :type func: callable or str + :return: recordset of records satisfying func, may be empty. + + .. code-block:: python3 + + # 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") """ if isinstance(func, str): name = func @@ -5209,13 +5291,18 @@ Record ids: %(records)s def sorted(self, key=None, reverse=False): - """ Return the recordset ``self`` ordered by ``key``. + """Return the recordset ``self`` ordered by ``key``. - :param key: either a function of one argument that returns a - comparison key for each record, or a field name, or ``None``, in - which case records are ordered according the default model's order + :param key: either a function of one argument that returns a + comparison key for each record, or a field name, or ``None``, in + which case records are ordered according the default model's order + :type key: callable or str or None + :param bool reverse: if ``True``, return the result in reverse order - :param reverse: if ``True``, return the result in reverse order + .. code-block:: python3 + + # sort records by name + records.sorted(key=lambda r: r.name) """ if key is None: recs = self.search([('id', 'in', self.ids)]) @@ -6033,11 +6120,11 @@ class Model(AbstractModel): class TransientModel(Model): """ Model super-class for transient records, meant to be temporarily - persisted, and regularly vacuum-cleaned. + persistent, and regularly vacuum-cleaned. A TransientModel has a simplified access rights management, all users can - create new records, and may only access the records they created. The super- - user has unrestricted access to all TransientModel records. + create new records, and may only access the records they created. The + superuser has unrestricted access to all TransientModel records. """ _auto = True # automatically create database backend _register = False # not visible in ORM registry, meant to be python-inherited only