Chapter 7. ORM Application Logic – Supporting Business Processes

In this chapter, you will learn to write code to support business logic in your models and you will also learn how it can be activated on events and user actions. Using the Odoo programming API, we can write complex logic and wizards allow us to provide a rich user interaction with these programs.

To-do wizard

With the wizards, we can ask users to input information to be used in some processes. Suppose our to-do app users regularly need to set deadlines and the responsible persons for a large number of tasks. We could use an assistant to help them 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.

We will start by creating a new module for this feature: todo_wizard. Our module will have a Python file and an XML file, so the todo_wizard/__openerp__.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': ['todo_wizard_view.xml'], }

The todo_wizard/__init__.py file to load our code is just one line, as follows:

from . import todo_wizard_model

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

Wizard model

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

This is implemented using the model/view architecture used for regular views, with a difference: the supporting model is based on models.TransientModel instead of models.Model.

This type of model is also stored in the database, but the data is expected to be useful only until the wizard is completed or canceled. Server vacuum processes regularly clean up old wizard data from the corresponding database tables.

The todo_wizard/todo_wizard_model.py file will define the three fields we need: the lists of tasks to update, the user responsible for them, and the deadline to set on them, as shown here:

# -*- coding: utf-8 -*-
from openerp import models, fields, api
from openerp import exceptions  # will be used in the code

import logging
_logger = logging.getLogger(__name__)

class TodoWizard(models.TransientModel):
  _name = 'todo.wizard'
  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 if we used a one to many relation, we would have to add the inverse many to one field on to-do tasks. We should avoid many to one relations between transient and regular models, and so we used a many to many relation that fulfills the same purpose without the need to modify the to-do task model.

We are also adding support to message logging. The logger is initialized with the two lines just before the TodoWizard, using the Python logging standard library. To write messages to the log we can use:

_logger.debug('A DEBUG message')
_logger.info('An INFO message')
_logger.warning('A WARNING message')
_logger.error('An ERROR message')

We will see some usage examples in this chapter.

Wizard form

The wizard form view looks exactly the same as regular forms, except for two specific elements:

  • A <footer> section can be used to place the action buttons.
  • A special cancel button type is available to interrupt the wizard without performing any action.

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

<openerp>
  <data>
    <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" />
          <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" />
  </data>
 </openerp>

The 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. target="new" makes it open as a dialog window.

You might also have noticed attrs in the Mass Update button used to make it invisible until either a new deadline or responsible user is selected.

This is how our wizard will look:

Wizard form

Wizard business logic

Next we need to implement the actions performed while clicking on the Mass Update button. The method called by the button is do_mass_update and it should be defined in the todo_wizard/todo_wizard_model.py file, as shown in the following code:

    @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!')
        # else:
        _logger.debug('Mass update on Todo Tasks %s',self.task_ids.ids)
        if self.new_deadline:self.task_ids.write({'date_deadline': self.new_deadline})
        if self.new_user_id:self.task_ids.write({'user_id': self.new_user_id.id})
        return True

Our code can handle only one wizard instance at a time. We could have used @api.one, but it is not advised to do so in wizards. In some cases, we want the wizard to return a window action telling the client what to do next. That is not possible with @api.one, since it would return a list of actions instead of a single one.

Because of this, we prefer to use @api.multi but then we use ensure_one() to check that self represents a single record. It should be noted that self is a record representing 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 demonstrate writing a message to the server log.

If the validation passes, we write the new values given to the selected tasks. We are using the write method on a record set, such as the task_ids to many field to perform a mass update. This is more efficient than repeating a write on each record in a loop.

Now we will work on the logic behind the two buttons at the top: Count and Get All.

Raising exceptions

When something is not right, we will want to interrupt the program with an error message. This is done by raising an exception. Odoo provides a few additional exception classes to the ones available in Python. These are examples for the most useful ones:

from openerp import exceptions
raise exceptions.Warning('Warning message')
raise exceptions.ValidationError('Not valid message')

The Warning message also interrupts execution but can sound less severe that a ValidationError. While it's not the best user interface, we take advantage of that on the Count button to display a message to the user:

    @api.multi
    def do_count_tasks(self):
        Task = self.env['todo.task']
        count = Task.search_count([])
        raise exceptions.Warning('There are %d active tasks.' % count)

Auto-reloading code changes

When you're working on Python code, the server needs to be restarted every time the code is changed to reload it. To make life easier for developers an --auto-reload option is available. It monitors the source code and automatically reloads it if changes are detected. Here is an example of it's usage:

$ ./odoo.py -d v8dev --auto-reload

But this is a Linux-only feature. If you are using Debian/Ubuntu box to run the server, as recommended in Chapter 1, Getting Started with Odoo Development, it should work for you. The pyinotify Python package is required, and it should be installed either through apt-get or pip, as shown here:

$ sudo apt-get install python-pyinotify  # using OS packages
$ pip install pyinotify  # using pip, possibly in a virtualenv

Actions on the wizard dialog

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 record set 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 on the Count button because it uses an exception to display it's message; so the action fails and the window is not closed.

Fortunately we can work around this behavior by returning an action to the client that reopens the same wizard. The model methods are allowed to return an action for the web client to perform, in the form of a dictionary describing the window action to execute. This dictionary uses the same attributes used to define window actions in module XML.

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

    @api.multi
    def do_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 anything else, like jumping to a specific form and record, or opening another wizard form to ask for additional user input.

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

    @api.multi
    def do_populate_tasks(self):
        self.ensure_one()        
        Task = self.env['todo.task']
        all_tasks = Task.search([])
        self.task_ids = all_tasks
        # reopen wizard form on same wizard record
        return self.do_reopen_form()

Here we can see how to get a reference to a different model, which is todo.task in this case, to perform actions on it. The wizard form values are stored in the transient model and can be read and written as in regular models. We can also see that the method sets the task_ids value with the list of all active tasks.

Note that since self is not guaranteed to be a single record, we validate that using self.ensure_one(). We shouldn't use the @api.one decorator because it would wrap the returned value in a list. Since the web client expects to receive a dictionary and not a list, it wouldn't work as intended.

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

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