In the Use Abstract Models for reusable Model features recipe in Chapter 4, Application Models, the base class models.TransientModel
was introduced; this class shares a lot with normal Models
except that the records of transient models are periodically cleaned up in the database, hence the name transient. These are used to create wizards or dialog boxes, which are filled in the user interface by the users and generally used to perform actions on the persistent records of the database.
This recipe extends the code from Chapter 3, Creating Odoo Modules by creating a wizard to record the borrowing of books by a library member.
If you want to follow the recipe, make sure you have the my_module
addon module from Chapter 3, Creating Odoo Modules.
We will also use a simple model to record book loans:
class LibraryBookLoan(models.Model): _name = 'library.book.loan' book_id = fields.Many2one('library.book', 'Book', required=True) member_id = fields.Many2one('library.member', 'Borrower', required=True) state = fields.Selection([('ongoing', 'Ongoing'), ('done', Done')], 'State', default='ongoing', required=True)
To add a wizard for recording borrowed books to the addon module, you need to perform the following steps:
class LibraryLoanWizard(models.TransientModel):
_name = 'library.loan.wizard'
member_id = fields.Many2one('library.member', 'Member')
book_ids = fields.Many2many('library.book', 'Books')
LibraryLoanWizard
class:@api.multi def record_loans(self): for wizard in self: member = wizard.member_id books = wizard.book_ids loan = self.env['library.book.loan'] for book in wizard.book_ids: loan.create({'member_id': member.id, 'book_id': book.id})
<record id='library_loan_wizard_form' model='ir.ui.view'>
<field name='name'>library loan wizard form view</field>
<field name='model'>library.loan.wizard</field>
<field name='arch' type='xml'>
<form string="Borrow books">
<sheet>
<group>
<field name='member_id'/>
</group>
<group>
<field name='book_ids'/>
</group>
<sheet>
<footer>
<button name='record_loans'
string='OK'
class='btn-primary'
type='object'/>
or
<button string='Cancel'
class='btn-default'
special='cancel'/>
</footer>
</form>
</field>
</record>
<act_window id="action_wizard_loan_books" name="Record Loans" res_model="library.loan.wizard" view_mode="form" target="new" /> <menuitem id="menu_wizard_loan_books" parent="library_book_menu" action="action_wizard_loan_books" sequence="20" />
Step 1 defines a new model. It is no different from other models, apart from the base class, which is TransientModel
instead of Model
. Both TransientModel
and Model
share a common base class called BaseModel
, and if you check the source code of Odoo, you will see that 99 percent of the work is in BaseModel
and that both Model
and TransientModel
are almost empty.
The only things that change for TransientModel
records are as follows:
TransientModels
. Anyone is allowed to create a record, but only the user who created a record can read it and use it.One2many
fields on a TransientModel
that refer to a normal model, as this would add a column on the persistent model linking to transient data. Use Many2many
relations in this case. You can of course define Many2one
and One2many
fields for relations between transient models.We define two fields in the model, one to store the member borrowing the books and one to store the list of books being borrowed. We could add other scalar fields to record a scheduled return date, for instance.
Step 2 adds the code to the wizard class that will be called when the button defined in step 3 is clicked on. This code reads the values from the wizard and creates library.book.loan
records for each book.
Step 3 defines a view for our wizard. Please refer to the Document-style forms recipe in Chapter 8, Backend Views, for details. The important point here is the button in the footer: the type attribute is set to 'object'
, which means that when the user clicks on the button, the method with the name specified by the name attribute of the button will be called.
Step 4 makes sure we have an entry point for our wizard in the menu of the application. We use target='new'
in the action so that the form view is displayed as a dialog box over the current form. Please refer to the Add a Menu Item and Window Action recipe in Chapter 8, Backend Views, for details.
Here are a few tips to enhance your wizards.
The wizard we are presenting requires the user to fill in the name of the member in the form. There is a feature of the web client we can use to save some typing. When an action is executed, the context is updated with some values that can be used by wizards:
Key |
Value |
---|---|
|
This is the name of the model related to the action. This is generally the model being displayed on screen. |
|
This indicates that a single record is active, and provides the ID of that record. |
|
If several records were selected, this will be a list with the IDs (this happens when several items are selected in a tree view when the action is triggered. In a form view, you get |
|
An additional domain on which the wizard will operate. |
These values can be used to compute default values of the model, or even directly in the method called by the button. To improve on the recipe example, if we had a button displayed on the form view of a library.member
model to launch the wizard, then the context of the creation of the wizard would contain {'active_model': 'library.member', 'active_id': <member id>}
. In that case, you could define the member_id
field to have a default value computed by the following method:
def _default_member(self): if self.context.get('active_model') == 'library.member': return self.context.get('active_id', False)
In step 2, we could have dispensed with the for wizard in self
loop, and assumed that len(self)
is 1
, possibly adding a call to self.ensure_one()
at the beginning of the method, like this:
@api.multi def record_borrows(self): self.ensure_one() member = self.member_id books = self.book_ids member.borrow_books(books)
We recommend using the version in the recipe though, because it allows reusing the wizard from other parts of the code by creating records for the wizard, putting them in a single recordset (see the Combine recordsets recipe in Chapter 5, Basic Server Side Development, to see how to do this) and then calling record_loans()
on the recordset. Granted, here the code is trivial and you don't really need to jump through all those hoops to record that some books were borrowed by different members. However, in an Odoo instance, some operations are much more complex, and it is always nice to have a wizard available that does "the right thing." When using such wizards, be sure to check the source code for any possible use of the active_model
/ active_id
/ active_ids
keys from the context, in which case, you need to pass a custom context (see the Call a method with a modified context recipe previously for how to do this).
The method in step 2 does not return anything. This will cause the wizard dialog to be closed after the action is performed. Another possibility is to have the method return a dictionary with the fields of an ir.action
. In this case, the web client will process the action as if a menu entry had been clicked on by the user. For instance, if we wanted to display the form view of the member who has just borrowed the books, we could have written the following:
@api.multi def record_borrows(self): for wizard in self: member = wizard.member_id books = wizard.book_ids member.borrow_books(books) member_ids = self.mapped('member_id').ids action = { 'type': 'ir.action.act_window', 'name': 'Borrower', 'res_model': 'library.member', 'domain': [('id', '=', member_ids)], 'view_mode': 'form,tree', } return action
This builds a list of members who has borrowed books from this wizard (in practice, there will only be one such member, when the wizard is called from the user interface) and creates a dynamic action, which displays the members with the specified IDs.
This trick can be extended by having a wizard (with several steps to be performed one after the other), or depending on some condition from the preceding steps, by providing a Next button that calls a method defined on the wizard. This method will perform the step (maybe using a hidden field and storing the current step number), update some fields on the wizard, and return an action that will redisplay the same updated wizard and get ready for the next step.