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.
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>
To port this module code to the new API, you need to perform the following steps:
from datetime import date, timedelta
from openerp import models, fields, api, exceptions
from openerp.tools import _
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'
_columns
definitions to attribute declaration of fields in the LibraryBook
class:_columns
keys become class attributesclass LibraryBook(models.Model): _name = 'library.book' name = fields.Char('Name', required=True) author_ids = field.Many2many('res.partner')
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)
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' )
LibraryBookLoan
class, migrate the definition of the _compute_date_due
function to the new API:self
@api.depends
decorator# 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)
LibraryBookLoan
class, migrate the definition of the _default_date
function: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()
borrow_books
and _prepare_loan
methods in the LibraryMember
class:@api.multi
decoratorcr
, uid
, ids
, context
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}
on_change_date_end
method in the LibraryMember
class:@api.onchange
decorator# 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", }, }
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>
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.
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 |
---|---|
|
|
|
|
|
|
|
|
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
.