Write a wizard to guide the user

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.

Getting ready

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)

How to do it…

To add a wizard for recording borrowed books to the addon module, you need to perform the following steps:

  1. Add a new transient model to the module with the following definition:
    class LibraryLoanWizard(models.TransientModel):
        _name = 'library.loan.wizard'
        member_id = fields.Many2one('library.member', 'Member')
        book_ids = fields.Many2many('library.book', 'Books')
  2. Add the callback method performing the action on the transient model. Add the following code to the 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})
  3. Create a form view for the model. Add the following view definition to the module views:
    <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>
  4. Create an action and a menu entry to display the wizard. Add the following declarations to the module menu file:
    <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"
              />

How it works…

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:

  • Records are periodically removed from the database so the tables for transient models don't grow up in size over time
  • You cannot define access rules on TransientModels. Anyone is allowed to create a record, but only the user who created a record can read it and use it.
  • You must not define 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.

There's more…

Here are a few tips to enhance your wizards.

Using the context to compute default values

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

active_model

This is the name of the model related to the action. This is generally the model being displayed on screen.

active_id

This indicates that a single record is active, and provides the ID of that record.

active_ids

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 [active_id]).

active_domain

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)

Wizards and code reuse

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

Redirecting the user

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.

..................Content has been hidden....................

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