Port old API code to the new API

Odoo has a long history and the so-called "traditional" or "old" API has been in use for a very long time. When designing the "new" API in Odoo 8.0, time was taken to ensure that the APIs would be able to coexist, because it was foreseen that porting the huge codebase to the new API would be a huge effort. So you will probably come across addon modules using the traditional API. When migrating them to the current version of Odoo, you may want to port them to the new API.

This recipe explains how to perform this translation. It can also serve as an aide memoire when you need to extend a module that uses the traditional API with the new API.

Getting ready

Let's port the following addon module code developed with the traditional API:

from datetime import date, timedelta
from openerp.osv import orm, fields
from openerp.tools import _, DEFAULT_SERVER_DATE_FORMAT as DATE_FMT

class library_book(orm.Model):
    _name = 'library.book'
    _columns = {
        'name': fields.char('Name', required=True),
        'author_ids': field.many2many('res.partner'),
    }

class library_member(orm.Model):
    _name = 'library.member'
    _inherits = {'res.partner': 'partner_id'}

    _columns = {
        'partner_id': fields.many2one('res.partner',
                                      'Partner',
                                      required=True),
        'loan_duration': fields.integer('Loan duration',
                                        required=True),
        'date_start': fields.date('Member since'),
        'date_end': fields.date('Expiration date'),
        'number': fields.char('Number', required=True),
    }

    _defaults = {
        'loan_duration': 10,
    }

    def on_change_date_end(self, cr, uid, date_end, context=None):
        date_end = date.strptime(date_end, DATE_FMT)
        today = date.today()
        if date_end <= today:
            return {
                'value': {'loan_duration': 0},
                'warning': {
                    'title': 'expired membership',
                    'message': "This member's membership " 
                               "has expired",
                },
            }

    def borrow_books(self, cr, uid, ids, book_ids, context=None):
        if len(ids) != 1:
            raise orm.except_orm(
                _('Error!'),
                _('It is forbidden to loan the same books '
                  'to multiple members.'))
        loan_obj = self.pool['library.book.loan']
        member = self.browse(cr, uid, ids[0], context=context)
        for book_id in book_ids:
            val = self._prepare_loan(
                cr, uid, member, book_id, context=context
            )
            loan_id = loan_obj.create(cr, uid, val, context=context)

    def _prepare_loan(self, cr, uid,
                      member, book_id,
                      context=None):
        return {'book_id': book_id,
                'member_id': member.id,
                'duration': member.loan_duration}


class library_book_loan(orm.Model):
    _name = 'library.book.loan'

    def _compute_date_due(self, cr, uid, ids, fields, arg, context=None):
        res = {}
        for loan in self.browse(cr, uid, ids, context=context):
            start_date = date.strptime(loan.date, DATE_FMT)
            due_date = start_date + timedelta(days=loan.duration)
            res[loan.id] = due_date.strftime(DATE_FMT)
        return res

    _columns = {
        'book_id': fields.many2one('library.book', required=True),
        'member_id': fields.many2one('library.member',
                                     required=True),
        'state': fields.selection([('ongoing', 'Ongoing'),
                                   ('done', 'Done')],
                                  'State', required=True),
        'date': fields.date('Loan date', required=True),
        'duration': fields.integer('Duration'),
        'date_due': fields.function(
            fnct=_compute_date_due,
            type='date',
            store=True,
            string='Due for'),
    }

    def _default_date(self, cr, uid, context=None):
        return date.today().strftime(DATE_FMT)

    _defaults = {
        'duration': 15,
        'date': _default_date,
    }

The module, of course, defined some views. Most of them will need no change, so we won't show them. The only one relevant in this recipe is the library.member form view. Here is the relevant excerpt for that view:

<record id='library.member_form_view' model='ir.ui.view'>
  <field name='model'>library.member</field>
  <field name='arch' type='xml'>
    <!-- [...] -->
    <field name='date_end'
           on_change='on_change_date_end(date_end)'/>
    <!-- [...] -->
  </field>
</record>

How to do it…

To port this module code to the new API, you need to perform the following steps:

  1. Copy the original source file to make a backup.
  2. In the new file, modify the module imports as follows:
    from datetime import date, timedelta
    from openerp import models, fields, api, exceptions
    from openerp.tools import _
  3. Modify the class definitions to the new API base classes, and rename the classes to use CamelCase:
    class LibraryBook(models.Model):
        _name = 'library.book'
    
    class LibraryMember(models.Model):
         _name = 'library.member'
        _inherits = {'res.partner': 'partner_id'}
    
    class LibraryBookLoan(models.Model):
         _name = 'library.book.loan'
  4. Migrate the _columns definitions to attribute declaration of fields in the LibraryBook class:
    • The _columns keys become class attributes
    • The names of the field types get capitalized
      class LibraryBook(models.Model):
          _name = 'library.book'
      
          name = fields.Char('Name', required=True)
          author_ids = field.Many2many('res.partner')
      

    Note

    Don't forget to remove the commas at the ends of the lines.

  5. Do the same thing for LibraryMember, taking care to move _defaults declarations to the field definition:
    class LibraryMember(models.Model):
        # [...]
    
        partner_id = fields.Many2one('res.partner',
                                      'Partner',
                                      required=True)
        loan_duration = fields.Integer('Loan duration',
                                       default=10,
                                       required=True)
        date_start = fields.Date('Member since')
        date_end = fields.Date('Expiration date')
        number = fields.Char('Number', required=True)
  6. Migrate the column's definition of the LibraryBookLoan class, changing the type of the function field to fields.Date:
    class LibraryBookLoan(models.Model):
        # [...]
    
        book_id = fields.Many2one('library.book', required=True)
        member_id = fields.Many2one('library.member',
                                    required=True)
        state = fields.Selection([('ongoing', 'Ongoing'),
                                  ('done', 'Done')],
                                 'State', required=True)
        date = fields.Date('Loan date', required=True,
                           default=_default_date)
        duration = fields.Integer('Duration', default=15)
        date_due = fields.Date(
            compute='_compute_date_due',
            store=True,
            string='Due for'
           )
  7. In the LibraryBookLoan class, migrate the definition of the _compute_date_due function to the new API:
    • Remove all the arguments except self
    • Add an @api.depends decorator
    • Change the body of the method to use the new computed field protocol (see the Add computed fields to a Model recipe in Chapter 4, Application Models, for details):
      # in class LibraryBookLoan
          @api.depends('start_date', 'due_date')
          def _compute_date_due(self):
              for loan in self:
                  start_date = fields.Date.from_string(loan.date)
                  due_date = start_date + timedelta(days=loan.duration)
                  loan.date_due = fields.Date.to_string(due_date)
  8. In the LibraryBookLoan class, migrate the definition of the _default_date function:
    • The function definition must be moved before the field declaration
    • The new prototype only has one argument, self (see the Add data fields to a Model recipe in Chapter 4, Application Models, for details):
      # in class LibraryBookLoan, before the fields definitions
          def _default_date(self):
              return fields.Date.today()
  9. Rewrite the borrow_books and _prepare_loan methods in the LibraryMember class:
    • Add an @api.multi decorator
    • Remove the arguments cr, uid, ids, context
    • Port the code to the new API (see the various recipes in this chapter for details)
    • Replace the orm.except_orm exception with UserError:
      # in class LibraryMember
          @api.multi
          def borrow_books(self, book_ids):
              if len(self) != 1:
                  raise exceptions.UserError(
                      _('It is forbidden to loan the same books '
                        'to multiple members.')
                  )
              loan_model = self.env['library.book.loan']
              for book in self.env['library.book'].browse(book_ids):
                  val = self._prepare_loan(book)
                  loan = loan_model.create(val)
          @api.multi
          def _prepare_loan(self, book)
              self.ensure_one()
              return {'book_id': book.id,
                      'member_id': self.id,
                      'duration': self.loan_duration}
  10. Port the on_change_date_end method in the LibraryMember class:
    • Add an @api.onchange decorator
    • Port the code to the new API (see the Write onchange methods recipe in this chapter for details):
      # in LibraryMember
          @api.onchange('date_end')
          def on_change_date_end(self):
              date_end = fields.Date.from_string(self.date_end)
              today = date.today()
              if date_end <= today:
                  self.loan_duration = 0
                  return {
                      'warning': {
                          'title': 'expired membership',
                          'message': "Membership has expired",
                      },
                  }
      1. Edit the library.member form's view definition and remove the on_change attribute in the date_end field:
        <record id='library.member_form_view' model='ir.ui.view'>
          <field name='model'>library.member</field>
          <field name='arch' type='xml'>
            <!-- [...] -->
            <field name='date_end'/>
            <!-- [...] -->
          </field>
        </record>

How it works

Step 1 changes the imports. The old API lives in the openerp.osv package, which we change to use openerp.models, openerp.fields, openerp.api, and openerp.exceptions. We also drop the import of DEFAULT_SERVER_DATE_FORMAT, because we will use the helper methods on fields.Date to perform datetime / string conversions.

Step 2 changes the base classes of the models. Depending on the age of the code you are migrating, you may need to replace the following:

  • osv.osv, osv.Model or orm.Model with models.Model
  • osv.osv_memory, osv.TransientModel, or orm.TransientModel with models.TransientModel

The usual class attributes such as _name, _order, _inherit, _inherits, and so on are unchanged.

Steps 3 to 8 deal with the migration of the field's definitions. The _columns dictionaries mapping field names to field definitions in the old API are migrated to class attributes.

Note

When doing so, don't forget to remove the commas following each field definition in the dictionary, otherwise the attribute value will be a 1-element tuple, and you will get errors. You also get a warning log line for these.

The field classes in openerp.fields usually have the same name as their counterparts in openerp.osv.fields, but capitalized (openerp.osv.fields.char becomes openerp.fields.Char). Default values are moved from the _defaults class attribute to a default parameter in the type declaration:

_columns = {
    'field1': fields.char('Field1'),
    'field2': fields.integer('Field2'),
}
_defaults = {
    'field2': 42,
}

Now the preceding code is modified to the following:

field1 = fields.Char('Field1')
field2 = fields.Integer('Field2', default=42)

The biggest change is for function fields. The old API uses fields.function defined with a type parameter giving the type of the column. The new API uses a field of the expected type, defined with a compute parameter, which gives the method used to compute the field:

_columns = {
    'field3': fields.function(
        _compute_field3,
        arg=None,
        fnct_inv=_store_field3,
        fnct_inv_arg=None,
        type='char',
        fnct_search=_search_field3,
        store=trigger_dict,
        multi=keyword
    ),
}

Now the preceding code is modified to the following:

field3 = fields.Char('Field1'
                     compute='_compute_field3',
                     inverse='_store_field3',
                     search='_search_field3',
                     store=boolean)

The trigger_dict in the old API is replaced with @api.depends decorators, and there is no need for the multi parameter in the new API, as the compute method can update several fields sharing the same compute parameter value. See the Add Computed Fields to a Model recipe in Chapter 4, Application Models, for more detailed information.

Step 9 migrates the business logic methods. The decorator to use on the method defines the parameter conversions, which are to be used to map the arguments. You need to choose them carefully because this can break modules using the old API extending the ported module. The same advice is valid if you need to extend a model defined using the old API with the new API. Here are the most common cases, with <args> denoting additional arbitrary arguments and keyword arguments:

Old API prototype

New API prototype

def m(self, cr, uid, ids, <args>,

context=None)

@api.multi

def m(self, <args>)

def m(self, cr, uid, <args>, context=None)

@api.model

def m(self, <args>)

def m(self, cr, <args>)

@api.cr

def m(self, <args>)

def m(self, cr, uid, <args>)

@api.cr_uid

def m(self, <args>)

Finally, steps 10 and 11 migrate the onchange method. Things have changed quite a bit between the two versions of the API; in the traditional API, onchange methods are declared in the view definitions by using an attribute on_change on the <field> element with a value describing the call to be performed when the field is edited. In the new API, this declaration is not necessary because the framework dynamically generates it in the view by analyzing the methods decorated with @api.onchange.

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

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