Working with the server

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.

Using relation fields

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

Querying models

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)

Writing on records

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.

Transactions and low-level SQL

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.

    Tip

    In a shell session, your data manipulation won't be made effective in the database until you use self.env.cr.commit().

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().

Note

Caution!

Executing SQL directly in the database can lead to inconsistent data. You should use it only if you are sure of what you are doing.

Working with time and dates

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:

  • from_string(value): This converts a string into a date or datetime object.
  • to_string(value): This converts a date or datetime object into a string in the format expected by the server.

Working with relation fields

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}).

Manipulating recordsets

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.

Other recordset operations

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)
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset