Our server code will usually run inside a method of a model, as is the case for do_mass_update()
in the preceding code.
In this context, self
represents the recordset being acted upon. Instances of model classes are actually recordsets. For actions executed from views, this will be only the record currently selected on it. If it's a form view, it is usually a single record, but in tree views, there can be several records.
The self.env
object allows us to access our running environment; this includes the information on the current session, such as the current user and session context, and also access all the other models available in the server.
To better explore programming on the server side, we can use the server interactive console, where we have an environment similar to what we can find inside a model method.
This is a new feature for version 9. It has been back-ported as a module for version 8, and it can be downloaded from the link https://www.odoo.com/apps/modules/8.0/shell/. It just needs to be placed somewhere in your add-ons path, and no further installation is necessary, or you can use the following commands to get the code from GitHub and make the module available in our custom add-ons directory:
$ cd ~/odoo-dev $ git clone https://github.com/OCA/server-tools.git -b 8.0 $ ln -s server-tools/shell custom-addons/shell $ cd ~/odoo-dev/odoo
To use this, run odoo.py
with the shell
command and the database to use as shown here:
$ ./odoo.py shell -d v8dev
You will see the server start up sequence in the terminal ending in a >>>
Python prompt. Here, self
represents the record for the administrator user as shown here:
>>> self res.users(1,) >>> self.name u'Administrator' >>> self._name 'res.users' >>> self.env <openerp.api.Environment object at 0xb3f4f52c>
In the session above, we do some inspection on our environment. self
represents a res.users
recordset containing only the record with ID 1 and name Administrator. We can also confirm the recordset's model name inspecting self._name
, and confirm that self.env
is a reference for the environment.
As usual, you can exit the prompt using Ctrl + D. This will also close the server process and bring you back to the system shell prompt.
The Model
class referenced by self is in fact a recordset, an iterable collection of records. Iterating through a recordset returns individual records.
The special case of a recordset with only one record is called a singleton. Singletons behave like records, and for all practical purposes are the same thing as a record. This particularity means that a record can be used wherever a recordset is expected.
Unlike multi-element recordsets, singletons can access their fields using the dot notation, as shown here:
>>> print self.name Administrator >>> for rec in self: print rec.name Administrator
In this example, we loop through the records in the self
recordset and print out the content of their name
field. It contains only one record, so only one name is printed out. As you can see, self
is a singleton and behaves as a record, but at the same time is iterable like a recordset.
As we saw earlier, models can have relational fields: many to one, one to many, and many to many. These field types have recordsets as values.
In the case of many to one, the value can be a singleton or an empty recordset. In both cases, we can directly access their field values. As an example, the following instructions are correct and safe:
>>> self.company_id res.company(1,) >>> self.company_id.name u'YourCompany' >>> self.company_id.currency_id res.currency(1,) >>> self.company_id.currency_id.name u'EUR'
Conveniently, an empty recordset also behaves like a singleton, and accessing its fields does not return an error but just returns False
. Because of this, we can traverse records using dot notation without worrying about errors from empty values, as shown here:
>>> self.company_id.country_id res.country() >>> self.company_id.country_id.name False
With self
we can only access the method's recordset. But the self.env
environment reference allows us to access any other model.
For example, self.env['res.partner']
returns a reference to the Partners model (which is actually an empty recordset). We can then use search()
or browse()
on it to generate recordsets.
The search()
method takes a domain expression and returns a recordset with the records matching those conditions. An empty domain []
will return all records. If the model has the active
special field, by default only the records with active=True
will be considered. A few optional keyword arguments are available, as shown here:
order:
This is a string to be used as the ORDER BY clause in the database query. This is usually a comma-separated list of field names.limit
: This sets a maximum number of records to retrieve.offset
: This ignores the first n results; it can be used with limit to query blocks of records at a time.Sometimes we just need to know the number of records meeting certain conditions. For that we can use search_count()
, which returns the record count instead of a recordset.
The browse()
method takes a list of IDs or a single ID and returns a recordset with those records. This can be convenient for the cases where we already know the IDs of the records we want.
Some usage examples of this are shown here:
>>> self.env['res.partner'].search([('name', 'like', 'Ag')]) res.partner(7, 51) >>> self.env['res.partner'].browse([7, 51]) res.partner(7, 51)
Recordsets implement the active record pattern. This means that we can assign values on them, and these changes will be made persistent in the database. This is an intuitive and convenient way to manipulate data, as shown here:
>>> admin = self.env['res.users'].browse(1) >>> admin.name = 'Superuser' >>> print admin.name Superuser
Recordsets have three methods to act on their data: create()
, write()
, and unlink()
.
The create()
method takes a dictionary to map fields to values and returns the created record. Default values are automatically applied as expected, which is shown here:
>>> Partner = self.env['res.partner'] >>> new = Partner.create({'name': 'ACME', 'is_company': True}) >>> print new res.partner(72,)
The unlink()
method deletes the records in the recordset, as shown here:
>>> rec = Partner.search([('name', '=', 'ACME')]) >>> rec.unlink() True
The write()
method takes a dictionary to map fields to values. These are updated on all elements of the recordset and nothing is returned, as shown here:
>>> Partner.write({'comment': 'Hello!'})
Using the active record pattern has some limitations; it updates only one field at a time. On the other hand, the write()
method can update several fields of several records at the same time by using a single database instruction. These differences should be kept in mind for the cases where performance can be an issue.
It is also worth mentioning copy()
to duplicate an existing record; it takes that as an optional argument and a dictionary with the values to write on the new record. For example, to create a new user copying from the Demo User:
>>> demo = self.env.ref('base.user_demo') >>> new = demo.copy({'name': 'Daniel', 'login': 'dr', 'email':''}) >>> self.env.cr.commit()
Remember that fields with the copy=False
attribute won't be copied.
Database writing operations are executed in the context of a database transaction. Usually we don't have to worry about this as the server takes care of that while running model methods.
But in some cases, we may need a finer control over the transaction. This can be done through the database cursor self.env.cr
, as shown here:
self.env.cr.commit()
: This commits the transaction's buffered write operations.self.env.savepoint()
: This sets a transaction savepoint to rollback to.self.env.rollback()
: This cancels the transaction's write operations since the last savepoint or all if no savepoint was created.With the cursor execute()
method, we can run SQL directly in the database. It takes a string with the SQL statement to run and a second optional argument with a tuple or list of values to use as parameters for the SQL. These values will be used where %s
placeholders are found.
If you're using a SELECT query, records should then be fetched. The fetchall()
function retrieves all the rows as a list of tuples
and dictfetchall()
retrieves them as a list of dictionaries, as shown in the following example:
>>> self.env.cr.execute("SELECT id, login FROM res_users WHERE login=%s OR id=%s", ('demo', 1)) >>> self.env.cr.fetchall() [(4, u'demo'), (1, u'admin')]
It's also possible to run data manipulation language instructions (DML) such as UPDATE and INSERT. Since the server keeps data caches, they may become inconsistent with the actual data in the database. Because of that, while using raw DML, the caches should be cleared afterwards by using self.env.invalidate_all()
.
For historical reasons, date and datetime values are handled as strings instead of the corresponding Python types. Also datetimes are stored in the database in UTC time. The formats used in the string representation are defined by:
openerp.tools.misc.DEFAULT_SERVER_DATE_FORMAT
openerp.tools.misc.DEFAULT_SERVER_DATETIME_FORMAT
They map to %Y-%m-%d
and %Y-%m-%d %H:%M:%S
respectively.
To help handle dates, fields.Date
and fields.Datetime
provide a few functions. For example:
>>> from openerp import fields >>> fields.Datetime.now() '2014-12-08 23:36:09' >>> fields.Datetime.from_string('2014-12-08 23:36:09') datetime.datetime(2014, 12, 8, 23, 36, 9)
Given that dates and times are handled and stored by the server in a naive UTC format, which is not time zone aware and is probably different from the time zone that the user is working on, a few other functions that help to deal with this are shown here:
fields.Date.today()
: This returns a string with the current date in the format expected by the server and using UTC as a reference. This is adequate to compute default values.fields.Datetime.now()
: This returns a string with the current datetime in the format expected by the server using UTC as a reference. This is adequate to compute default values.fields.Date.context_today(record, timestamp=None)
: This returns a string with the current date in the session's context. The timezone value is taken from the record's context, and the optional parameter to use is datetime instead of the current time.fields.Datetime.context_timestamp(record, timestamp)
: That converts a naive datetime (without timezone) into a timezone aware datetime. The timezone is extracted from the record's context, hence the name of the function.To facilitate conversion between formats, both fields.Date
and fields.Datetime
objects provide these functions:
While using the active record pattern, relational fields can be assigned recordsets.
For a many to one field, the value assigned must be a single record (a singleton recordset).
For to-many fields, their value can also be assigned with a recordset, replacing the list of linked records, if any, with a new one. Here a recordset with any size is allowed.
While using the create()
or write()
methods, where values are assigned using dictionaries, relational fields can't be assigned to recordset values. The corresponding ID, or list of IDs should be used.
For example, instead of self.write({'user_id': self.env.user})
, we should rather use self.write({'user_id': self.env.user.id})
.
We will surely want to add, remove, or replace the elements in these related fields, and so this leads to the question: how can recordsets be manipulated?
Recordsets are immutable but can be used to compose new recordsets. Some set operations are supported, which are shown here:
rs1 | rs2
: This results in a recordset with all elements from both recordsets.rs1 + rs2
: This also concatenates both recordsets into one.rs1 & rs2
: This results in a recordset with only the elements present in both recordsets.rs1 - rs2
: This results in a recordset with the rs1
elements not present in rs2
.The slice notation can also be used, as shown here:
rs[0]
and rs[-1]
retrieve the first element and the last elements.rs[1:]
results in a copy of the recordset without the first element. This yields the same records as rs – rs[0]
but preserves their order.In general, while manipulating recordsets, you should assume that the record order is not preserved. However, addition and slicing are known to keep record order.
We can use these recordset operations to change the list by removing or adding elements. You can see this in the following example:
self.task_ids |= task1
: This adds task1 element if it's not in the recordset.self.task_ids -= task1
: This removes the reference to task1 if it's present in the recordset.self.task_ids = self.task_ids[:-1]
: This unlinks the last record.While using the create()
and write()
methods with values in a dictionary, a special syntax is used to modify to many fields. This was explained in Chapter 4, Data Serialization and Module Data, in the section Setting values for relation fields. Refer to the following sample operations equivalent to the preceding ones using write()
:
self.write([(4, task1.id, False)])
: This adds task1 to the member.self.write([(3, task1.id, False)])
: This unlinks task1.self.write([(3, self.task_ids[-1].id, False)])
: This unlinks the last element.Recordsets support additional operations on them.
We can check if a record is included or is not in a recordset by doing the following:
record in recordset
record not in recordset
These operations are also available:
recordset.ids
: This returns the list with the IDs of the recordset elements.recordset.ensure_one()
: This checks if it is a single record (singleton); if it's not, it raises a ValueError exception.recordset.exists()
: This returns a copy with only the records that still exist.recordset.filtered(func)
: This returns a filtered recordset.recordset.mapped(func)
: This returns a list of mapped values.recordset.sorted(func)
: This returns an ordered recordset.Here are some usage examples for these functions:
>>> rs0 = self.env['res.partner'].search([]) >>> len(rs0) # how many records? 68 >>> rs1 = rs0.filtered(lambda r: r.name.startswith('A')) >>> print rs1 res.partner(3, 7, 6, 18, 51, 58, 39) >>> rs2 = rs1.filtered('is_company') >>> print rs2 res.partner(7, 6, 18) >>> rs2.mapped('name') [u'Agrolait', u'ASUSTeK', u'Axelor'] >>> rs2.mapped(lambda r: (r.id, r.name)) [(7, u'Agrolait'), (6, u'ASUSTeK'), (18, u'Axelor')] >> rs2.sorted(key=lambda r: r.id, reverse=True) res.partner(18, 7, 6)