[IMP] base, doc: orm page refactoring.

closes odoo/odoo#39725

X-original-commit: 7ce7c58592c7c7202a37c73767c477be5a835752
Signed-off-by: Victor Feyens (vfe) <vfe@odoo.com>
This commit is contained in:
Victor Feyens
2019-11-04 11:16:12 +00:00
committed by fw-bot
parent b2f3f092b7
commit ae4855fa1e
7 changed files with 1172 additions and 1441 deletions
+2 -2
View File
@@ -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 <reference/orm/fields/basic>`. The second
:ref:`a number of basic fields <reference/fields/basic>`. The second
broad categories of fields are :ref:`relational
<reference/orm/fields/relational>` and used to link records to one another
<reference/fields/relational>` 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
+1 -1
View File
@@ -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 <reference/orm/fields/relational>`, should be
for :ref:`relational fields <reference/fields/relational>`, should be
a :ref:`domain <reference/orm/domains>` on the field's model.
Will evaluate the domain, search the field's model using it and set the
+715 -1009
View File
File diff suppressed because it is too large Load Diff
+77 -85
View File
@@ -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 <https://github.com/odoo/odoo/issues/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):
+53 -18
View File
@@ -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)
+151 -240
View File
@@ -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 <reference/fields/compute>`
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 <reference/fields/related>`
"""
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 <field-related>` or :ref:`field extensions
<field-incremental-definition>`.
``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 = {
+173 -86
View File
@@ -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 <reference/orm/domains>`. 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