When writing Odoo models, it is often the case that some fields are interrelated. We have seen how to specify constraints between fields in the Add constraint validations to a Model recipe in Chapter 4, Application Models. This recipe illustrates a slightly different concept—onchange methods are called when a field is modified in the user interface to update the values of other fields of the record in the web client, usually in a form view.
We will illustrate this by providing a wizard similar to the one defined in the preceding recipe, Write a wizard to guide the user, but which can be used to record loan returns. When the member is set on the wizard, the list of books is updated to the books currently borrowed by the member. While we are demonstrating onchange methods on a TransientModel
, these features are also available on normal Models
.
If you want to follow the recipe, make sure you have the my_module
addon from Chapter 3, Creating Odoo Modules, with the Write a wizard to guide the user recipe's changes applied.
You will also want to prepare your work by defining the following transient model for the wizard:
class LibraryReturnsWizard(models.TransientModel): _name = 'library.returns.wizard' member_id = fields.Many2one('library.member', 'Member') book_ids = fields.Many2many('library.book', 'Books') @api.multi def record_returns(self): loan = self.env['library.book.loan'] for rec in self: loans = loan.search( [('state', '=', 'ongoing'), ('book_id', 'in', rec.book_ids.ids), ('member_id', '=', rec.member_id.id)] ) loans.write({'state': 'done'})
Finally, you will need to define a view, an action, and a menu entry for the wizard. This is left as an exercise.
To automatically populate the list of books to return when the user is changed, you need to add an onchange method in the LibraryReturnsWizard
step with the following definition:
@api.onchange('member_id') def onchange_member(self): loan = self.env['library.book.loan'] loans = loan.search( [('state', '=', 'ongoing'), ('member_id', '=', self.member_id.id)] ) self.book_ids = loans.mapped('book_id')
An onchange method uses the @api.onchange
decorator, which is passed the names of the fields that change and thus will trigger the call to the method. In our case, we say that whenever member_id
is modified in the user interface, the method must be called.
In the body of the method, we search the books currently borrowed by the member, and we use an attribute assignment to update the book_ids
attribute of the wizard.
The basic use of onchange methods is to compute new values for fields when some other fields are changed in the user interface, as we've seen in the recipe.
Inside the body of the method, you get access to the fields displayed in the current view of the record, but not necessarily all the fields of the model. This is because onchange methods can be called while the record is being created in the user interface before it is stored in the database! Inside an onchange method, self
is in a special state, denoted by the fact that self.id
is not an integer, but an instance of openerp.models.NewId
. Therefore, you must not make any changes to the database in an onchange method, because the user may end up canceling the creation of the record, which would not roll back any changes made by the onchanges called during the edition. To check for this, you can use self.env.in_onchange()
and self.env.in_draft()
—the former returns True
if the current context of execution is an onchange method and the latter returns True
if self
is not yet committed to the database.
Additionally, onchange methods can return a Python dictionary. This dictionary can have the following keys:
warning
: The value must be another dictionary with the keys title
and message
respectively containing the title and the content of a dialog box, which will be displayed when the onchange method is run. This is useful for drawing the attention of the user to inconsistencies or to potential problems.domain
: The value must be another dictionary mapping field names to domains. This is useful when you want to change the domain of a One2many
field depending on the value of another field.For instance, suppose we have a fixed value set for expected_return_date
in our library.book.loan
model, and we want to display a warning when a member has some books that are late. We also want to restrict the choice of books to the ones currently borrowed by the user. We can rewrite the onchange method as follows:
@api.onchange('member_id') def onchange_member(self): loan = self.env['library.book.loan'] loans = loan.search( [('state', '=', 'ongoing'), ('member_id', '=', self.member_id.id)] ) self.book_ids = loans.mapped('book_id') result = { 'domain': {'book_ids': [ ('id', 'in', self.book_ids.ids)] } } late_domain = [ ('id', 'in', loans.ids), ('expected_return_date', '<', fields.Date.today()) ] late_loans = loans.search(late_domain) if late_loans: message = ('Warn the member that the following ' 'books are late: ') titles = late_loans.mapped('book_id.name') result['warning'] = { 'title': 'Late books', 'message': message + ' '.join(titles) } return result