With the Odoo programming API, we can write complex logic and wizards to provide a rich user interaction for our apps. In this chapter, we will see how to write code to support business logic in our models, and we will also learn how to activate it on events and user actions.

We can perform computations and validations on events, such as creating or writing on a record, or perform some logic when a button is clicked. For example, we implemented button actions for the To-do Tasks, to toggle the Is Done flag and to clear all done tasks by inactivating.

Additionally, we can also use wizards to implement more complex interactions with the user, allowing to ask for inputs and provide feedback during the interaction.

We will start by building such a wizard for our To-Do app.

Suppose our To-Do app users regularly need to set the deadlines and person responsible for a large number of tasks. They could use an assistant to help with this. It should allow them to pick the tasks to be updated and then choose the deadline date and/or the responsible user to set on them.

Wizards are forms used to get input information from users, then use it for further processing. They can be used for simple tasks, such as asking for a few parameters and running a report, or for complex data manipulations, such as the use case described earlier.

This is how our wizard will look:

Creating a wizard

We can start by creating a new addon module for the todo_wizard feature.

Our module will have a Python file and an XML file, so the todo_wizard/__manifest__.py description will be as shown in the following code:

{  'name': 'To-do Tasks Management Assistant', 
   'description': 'Mass edit your To-Do backlog.', 
   'author': 'Daniel Reis', 
   'depends': ['todo_user'], 
   'data': ['views/todo_wizard_view.xml'], } 

As in previous addons, the todo_wizard/__init__.py file is just one line:

from . import models 

Next, we need to describe the data model supporting our wizard.

A wizard displays a form view to the user, usually as a dialog window, with some fields to be filled in. These will then be used by the wizard logic.

This is implemented using the same model/view architecture as for regular views, but the supporting model is based on models.TransientModel instead of models.Model.

This type of model has also a database representation and stores state there, but this data is expected to be useful only until the wizard completes its work. A scheduled job regularly cleans up the old data from wizard database tables.

The models/todo_wizard_model.py file will define the fields we need to interact with the user: the list of tasks to be updated, the user responsible, and the deadline date to set on them.

First add the models/__init__.py file with following line of code:

from . import todo_wizard_model 

Then create the actual models/todo_wizard_model.py file:

# -*- coding: utf-8 -*- 
from odoo import models, fields, api 
 
class TodoWizard(models.TransientModel): 
    _name = 'todo.wizard' 
    _description = 'To-do Mass Assignment' 
    task_ids = fields.Many2many('todo.task', 
      string='Tasks') 
    new_deadline = fields.Date('Deadline to Set') 
    new_user_id = fields.Many2one( 
      'res.users',string='Responsible to Set') 

It's worth noting that one-to-many relations to regular models should not be used in transient models. The reason for this is that it would require the regular model to have the inverse many-to-one relation with the transient model, but this is not allowed, since there could be the need to garbage-collect the regular model records along with the transient records.  

The wizard form views are the same as for regular models, except for two specific elements:

This is the content of our views/todo_wizard_view.xml file:

<odoo> 
  <record id="To-do Task Wizard" model="ir.ui.view"> 
    <field name="name">To-do Task Wizard</field> 
    <field name="model">todo.wizard</field> 
    <field name="arch" type="xml"> 
 
      <form> 
        <div class="oe_right"> 
          <button type="object" name="do_count_tasks" 
            string="Count" /> 
          <button type="object" name="do_populate_tasks" 
            string="Get All" /> 
        </div> 
 
        <field name="task_ids"> 
          <tree> 
            <field name="name" /> 
            <field name="user_id" /> 
            <field name="date_deadline" /> 
          </tree> 
        </field> 
 
        <group> 
          <group> <field name="new_user_id" /> </group> 
          <group> <field name="new_deadline" /> </group> 
        </group> 
 
        <footer> 
          <button type="object" name="do_mass_update" 
            string="Mass Update" class="oe_highlight" 
            attrs="{'invisible': 
            [('new_deadline','=',False),
            ('new_user_id', '=',False)] 
            }" /> 
          <button special="cancel" string="Cancel"/> 
        </footer> 
      </form> 
    </field> 
  </record> 
 
  <!-- More button Action --> 
  <act_window id="todo_app.action_todo_wizard"
    name="To-Do Tasks Wizard"
    src_model="todo.task" res_model="todo.wizard" 
    view_mode="form" target="new" multi="True" /> 
</odoo> 

The <act_window> window action we see in the XML adds an option to the More button of the To-do Task form by using the src_model attribute. The target="new" attribute makes it open as a dialog window.

You might also have noticed that attrs is used in the Mass Update button, to add the nice touch of making it invisible until either a new deadline or responsible user is selected.

Next, we need to implement the actions to perform on the form buttons. Excluding the Cancel button, we have three action buttons to implement, but now we will focus on the Mass Update button.

The method called by the button is do_mass_update and it should be defined in the models/todo_wizard_model.py file, as shown in the following code:

from odoo import exceptions 
import logging 
_logger = logging.getLogger(__name__) 
 
# ... 
# class TodoWizard(models.TransientModel): 
# ... 
 
    @api.multi 
    def do_mass_update(self): 
      self.ensure_one() 
      if not (self.new_deadline or self.new_user_id):
        raise exceptions.ValidationError('No data to update!') 
      _logger.debug('Mass update on Todo Tasks %s', 
                    self.task_ids.ids) 
      vals = {} 
      if self.new_deadline: 
        vals['date_deadline'] = self.new_deadline 
      if self.new_user_id:
        vals['user_id'] = self.new_user_id 
      # Mass write values on all selected tasks 
      if vals: 
        self.task_ids.write(vals) 
      return True 

Our code should handle one wizard instance at a time, so we used self.ensure_one() to make that clear. Here self represents the browse record for the data on the wizard form. 

The method begins by validating if a new deadline date or responsible user was given, and raises an error if not. Next, we have an example of how to write a debug message to the server log.

Then the vals dictionary is built with the values to set with the mass update: the new date, new responsible, or both. And then the write method is used on a recordset to perform the mass update. This is more efficient than a loop performing individual writes on each record.

It is a good practice for methods to always return something. This is why it returns the True value at the end. The sole reason for this is that the XML-RPC protocol does not support None values, so those methods won't be usable using that protocol. In practice, you may not be aware of the issue because the web client uses JSON-RPC, not XML-RPC, but it is still a good practice to follow.

Next, we will have a closer look at logging, and then will work on the logic behind the two buttons at the top: Count and Get All.

Now suppose we want a button to automatically pick all the to-do tasks to spare the user from picking them one by one. That's the point of having the Get All button in the form. The code behind this button will get a recordset with all active tasks and assign it to the tasks in the many-to-many field.

But there is a catch here. In dialog windows, when a button is pressed, the wizard window is automatically closed. We didn't face this problem with the Count button because it uses an exception to display its message; so the action is not successful and the window is not closed.

Fortunately, we can work around this behavior by asking the client to reopen the same wizard. Model methods can return a window action to be performed by the web client, in the form of a dictionary object. This dictionary uses the same attributes used to define window actions in XML files.

We will define a helper function for the window action dictionary to reopen the wizard window, so that it can be easily reused in several buttons:

@api.multi 
def _reopen_form(self): 
    self.ensure_one() 
    return {
        'type': 'ir.actions.act_window',
        'res_model': self._name,  # this model             
        'res_id': self.id,  # the current wizard record   
        'view_type': 'form', 
        'view_mode': 'form', 
        'target': 'new'} 

It is worth noting that the window action could be something else, like jumping to a different wizard form to ask for additional user input, and that can be used to implement multi-page wizards.

Now the Get All button can do its job and still keep the user working on the same wizard:

@api.multi 
def do_populate_tasks(self): 
    self.ensure_one()         
    Task = self.env['todo.task']
    open_tasks = Task.search([('is_done', '=', False)])
    # Fill the wizard Task list with all tasks 
    self.task_ids = all_tasks 
    # reopen wizard form on same wizard record 
    return self._reopen_form() 

Here we can see how to work with any other available model: we first use self.env[] to get a reference to the model, todo.task in this case, and can then perform actions on it, such as search() to retrieve records meeting some search criteria.

The transient model stores the values in the wizard form fields, and can be read or written just like any other model. The all_tasks variable is assigned to the model task_ids one-to-many field. As you can see, this is done just like we would for any other field type.