Monday 22 September 2014

Odoo New API: Recordsets

Recordset:

A recordset is:
  • An ordered collection of records.
  • One concept to replace:
              - browse records,
              - browse lists,
              - browse nulls.
  • An instance of the model's class.

1. The recordset as a collection:

It implements a sequence and set operations:

Operation Supported:

RecordSet also support set operations you can add, union and intersect, ... recordset:

1. Addition:

recordsetA + recordsetB: Returns the concatenation of two recordsets.

2. Substration:

recordsetA - recordsetB: Return the recordset of all the records in 'recordsetA' that are not in 'recordsetB'.

3. Intersection:

recordsetA & recordsetB: Return the intersection of two recordsets. Note that recordset order is not preserved.

4. Union:

recordsetA | recordsetB: Return the union of two recordsets. Note that recordset order is not preserved.

5. Equivalent:

recordsetA == recordsetB: Test whether two recordsets are equivalent (up to reordering).

...etc

2. The recordset as a record:

It behaves just like former browse records:

    print partner.name
    print partner['name']
    print partner.parent_id.company_id.name

Except that updates are written to database:

    partner.name = 'Agrolait'
    partner.email = 'info@agrolait.com'
    partner.parent_id = ...   # another record

If len(partners) > 1, do it on the first record:

    print partners.name           # name of first partner
    print partners[0].name

    partners.name = 'Agrolait'    # assign to first partner
    partners[0].name = 'Agrolait'

If len(partners) == 0, return the null value of the field:

    print partners.name        # False
    print partners.parent_id   # Empty recordset
    partners.name = 'Foo'      # Error

3. The recordset as an instance:

Methods of the model's class can be invoked on recordsets:

# calling convention: leave out cr, uid, ids, context
@api.multi
def write(self, values):
    result = super(C, self).write(values)
 
    # search returns a recordset instead of a list of ids
    domain = [('id', 'in', self.ids), ('parent_id', '=', False)]
    roots = self.search(domain)
 
    # modify all records in roots
    roots.write({'modified': True})
 
    return result

The missing parameters are hidden inside the recordset.

Record creation in cache only:

You can create a new record with following method:

@api.model
def new(self, values={}):
    """ new([values]) -> record """

Return a new record instance attached to the current environment and initialized with the provided 'value'. The record is not created in database, it only exists in memory.

New record is used to compute default values and perform onchanges.

Example:

record = self.new(values)

This will creates a new record with values, and attach 'self' to it.

2 comments

Odoo New API: Fields

Fields:

A field is defined as class attribute on a model class.

Fields as descriptors:

In general, a descriptor is an object attribute with 'binding behavior', one whose attribute access has been overridden by methods in the descriptor protocol. Those methods are __get__(), __set__(), and __delete__(). If any of those methods are defined for an object, it is said to be a descriptor.

The field descriptor contains the field definition, and manages accesses and assignments of the corresponding field on records.

Descriptor methods:

1. def __get__(self, record, owner):

- Return the value of field 'self' on 'record'.

- Define behavior for when the descriptor's value is retrieved.

2. def __set__(self, record, value):

- Set the value of field 'self' on 'record'.

- Define behavior for when the descriptor's value is changed.

Fields Attributes:

The following attributes may be provided when instantiating a field:

string:

The label of the field seen by users (string); if not set, the ORM takes the field name in the class (capitalized).

help:

The tooltip of the field seen by users (string).

readonly:

Whether the field is readonly (boolean, by default 'False').

required:

Whether the value of the field is required (boolean, by default 'False').

index:

Whether the field is indexed in database (boolean, by default 'False').

default:

The default value for the field; this is either a static value, or a function taking a recordset and returning a value.
         
states:

A dictionary mapping state values to lists of UI attribute-value pairs; possible attributes are: 'readonly', 'required', 'invisible'.

Note: Any state-based condition requires the 'state' field value to be available on the client-side UI. This is typically done by including it in the relevant views, possibly made invisible if not relevant for the end-user.

groups:

Comma-separated list of group xml ids (string); this restricts the field access to the users of the given groups only.
         
copy: 

Whether the field value should be copied when the record is duplicated (default: 'True' for normal fields, 'False' for 'one2many' and computed fields, including property fields and related fields).

Computed fields:

One can define a field whose value is computed instead of simply being read from the database. The attributes that are specific to computed fields are given below. To define such a field, simply provide a value for the attribute 'compute':

- compute: name of a method that computes the field.
- inverse: name of a method that inverses the field (optional).
- search: name of a method that implement search on the field (optional).
- store: whether the field is stored in database (boolean, by default 'False' on computed fields).

The methods given for 'compute', 'inverse' and 'search' are model methods. Their signature is shown in the following example:

    upper = fields.Char(compute='_compute_upper',
                        inverse='_inverse_upper',
                        search='_search_upper')

    @api.depends('name')
    def _compute_upper(self):
        for rec in self:
            self.upper = self.name.upper() if self.name else False

    def _inverse_upper(self):
        for rec in self:
            self.name = self.upper.lower() if self.upper else False

    def _search_upper(self, operator, value):
        if operator == 'like':
            operator = 'ilike'
        return [('name', operator, value)]

The compute method has to assign the field on all records of the invoked recordset. The decorator :meth:'openerp.api.depends' must be applied on the compute method to specify the field dependencies; those dependencies are used to determine when to recompute the field; re-computation is automatic and guarantees cache/database consistency. Note that the same method can be used for several fields, you simply have to assign all the given fields in the method; the method will be invoked once for all those fields.

By default, a computed field is not stored to the database, and is computed on-the-fly. Adding the attribute 'store=True' will store the field's values in the database. The advantage of a stored field is that searching on that field is done by the database itself. The disadvantage is that it requires database updates when the field must be recomputed.
     
The inverse method, as its name says, does the inverse of the compute method: the invoked records have a value for the field, and you must apply the necessary changes on the field dependencies such that the computation gives the expected value. Note that a computed field without an inverse method is readonly by default.
     
The search method is invoked when processing domains before doing an actual search on the model. It must return a domain equivalent to the condition: 'field operator value'.
     
Related fields:

The value of a related field is given by following a sequence of relational fields and reading a field on the reached model. The complete sequence of fields to traverse is specified by the attribute.

- related: sequence of field names.
     
The value of some attributes from related fields are automatically taken from the source field, when it makes sense. Examples are the attributes 'string' or 'selection' on selection fields.

By default, the values of related fields are not stored to the database. Add the attribute 'store=True' to make it stored, just like computed fields. Related fields are automatically recomputed when their dependencies are modified.
     
Company-dependent(property) fields:

Formerly known as 'property' fields, the value of those fields depends on the company. In other words, users that belong to different companies may see different values for the field on a given record.

- company_dependent: whether the field is company-dependent (boolean).
     
Incremental definition (Field Inheritance):

A field is defined as class attribute on a model class. If the model is extended (see :class:'~openerp.models.Model'), one can also extend the field definition by redefining a field with the same name and same type on the subclass. In that case, the attributes of the field are taken from the parent class and overridden by the ones given in sub-classes.

For instance, the second class below only adds a tooltip on the field 'state':

    class First(models.Model):
        _name = 'foo'
        state = fields.Selection([...], required=True)

    class Second(models.Model):
        _inherit = 'foo'
        state = fields.Selection(help="Blah blah blah")
                
No comments

Odoo New API: Metaclasses and Decorators



Odoo New API Implementation Using Metaclasses and Decorators

API:

An API(Application Programming Interface) is a set of defined functions and methods for interfacing with the underlying system or program or service running on the computer.

Metaclass:

A metaclass is defined as "the class of a class". Any class whose instances are themselves classes, is a metaclass.

Things You Could Do With Metaclasses:

There are lots of things you could do with metaclasses. Here's a partial list:

  • Enforce different inheritance semantics, e.g. automatically call base class methods when a derived class overrides.
  • Implement class methods (e.g. if the first argument is not named 'self').
  • Implement that each instance is initialized with copies of all class variables.
  • Implement a different way to store instance variables (e.g. in a list kept outside the the instance but indexed by the instance's id()).
  • Automatically wrap or trap all or certain methods:
             - for tracing
             - for precondition and post-condition checking
             - for synchronized methods
             - for automatic value caching

Metaclass's __new__ and __init__:

To control the creation and initialization of the class in the metaclass, you can implement the metaclass's __new__ method and/or __init__ constructor. Most real-life metaclasses will probably override just one of them. __new__ should be implemented when you want to control the creation of a new object (class in our case), and __init__ should be implemented when you want to control the initialization of the new object after it has been created.

New API Implementation Details:


This module provides the elements for managing two different API styles, namely the "traditional" and "record" styles.

In the "traditional" style, parameters like the database cursor, user id, context dictionary and record ids (usually denoted as 'cr', 'uid', 'context', 'ids') are passed explicitly to all methods. In the "record" style, those parameters are hidden into model instances, which gives it a more object-oriented feel.

For instance, the statements:

    model = self.pool.get(MODEL)
    ids = model.search(cr, uid, DOMAIN, context=context)
    for rec in model.browse(cr, uid, ids, context=context):
        print rec.name
    model.write(cr, uid, ids, VALUES, context=context)

may also be written as:

    env = Env(cr, uid, context)         # cr, uid, context wrapped in env
    recs = env[MODEL]                   # retrieve an instance of MODEL
    recs = recs.search(DOMAIN)          # search returns a recordset
    for rec in recs:                    # iterate over the records
        print rec.name
    recs.write(VALUES)                  # update all records in recs

Methods written in the "traditional" style are automatically decorated, following some heuristics based on parameter names.

1. class Meta(type):

It is used to automatically decorates traditional-style methods by guessing their API. It also implements the inheritance of the :func:'returns' decorators.
     
2. Metaclass's __new__:

It is used to automatically decorates traditional-style methods by guessing their API.

The guess() decorator is used by the metaclass's __new__ to decorates traditional-style methods by guessing their API.

Decorator's:

A decorator is just a callable that takes a function as an argument and returns a replacement function.

The Decorator's which is used to guess the API:

1. guess():

Decorate 'method' to make it callable in both traditional and record styles. This decorator is applied automatically by the model's metaclass, and has no effect on already-decorated methods.

The API style is determined by heuristics on the parameter names: 'cr' or 'cursor' for the cursor, 'uid' or 'user' for the user id, 'id' or 'ids' for a list of record ids, and 'context' for the context dictionary. If a traditional API is recognized, one of the decorators :func:'cr', :func:'cr_context', :func:'cr_uid', :func:'cr_uid_context', :func:'cr_uid_id', :func:'cr_uid_id_context', :func:'cr_uid_ids', :func:'cr_uid_ids_context' is applied on the method.

Method calls are considered traditional style when their first parameter is a database cursor.

2. noguess():

Decorate a method to prevent any effect from :func:'guess'.
 
The Decorator's which is used to decorate the traditional-style method:
   
1. @api.cr:

Decorate a traditional-style method that takes 'cr' as a parameter. Such a method may be called in both record and traditional styles, like:

    # recs = model.browse(cr, uid, ids, context)
    recs.method(args)

    model.method(cr, args)

2. @api.cr_context:

Decorate a traditional-style method that takes 'cr', 'context' as parameters.
 
3. @api.cr_uid:

Decorate a traditional-style method that takes 'cr', 'uid' as parameters.

4. @api.cr_uid_context:

Decorate a traditional-style method that takes 'cr', 'uid', 'context' as parameters. Such a method may be called in both record and traditional
styles, like:

    # recs = model.browse(cr, uid, ids, context)
    recs.method(args)

    model.method(cr, uid, args, context=context)

5. @api.cr_uid_id:

Decorate a traditional-style method that takes 'cr', 'uid', 'id' as parameters. Such a method may be called in both record and traditional styles. In the record style, the method automatically loops on records.

6. @api.cr_uid_id_context:

Decorate a traditional-style method that takes 'cr', 'uid', 'id', 'context' as parameters. Such a method:

    @api.cr_uid_id
    def method(self, cr, uid, id, args, context=None):
        ...

may be called in both record and traditional styles, like:

    # rec = model.browse(cr, uid, id, context)
    rec.method(args)

    model.method(cr, uid, id, args, context=context)

7. @api.cr_uid_ids:

Decorate a traditional-style method that takes 'cr', 'uid', 'ids' as parameters. Such a method may be called in both record and traditional styles.

8. @api.cr_uid_ids_context:

Decorate a traditional-style method that takes 'cr', 'uid', 'ids', 'context' as parameters. Such a method:

    @api.cr_uid_ids_context
    def method(self, cr, uid, ids, args, context=None):
        ...

may be called in both record and traditional styles, like:

    # recs = model.browse(cr, uid, ids, context)
    recs.method(args)

    model.method(cr, uid, ids, args, context=context)

It is generally not necessary, see :func:'guess'.

The Decorator's which is used to decorate the record-style method:

1. @api.model:

Decorate a record-style method where 'self' is a recordset, but its contents is not relevant, only the model is. Such a method:

    @api.model
    def method(self, args):
        ...

may be called in both record and traditional styles, like:

    # recs = model.browse(cr, uid, ids, context)
    recs.method(args)

    model.method(cr, uid, args, context=context)

Notice that no 'ids' are passed to the method in the traditional style.

2. @api.one:

Decorate a record-style method where 'self' is expected to be a singleton instance. The decorated method automatically loops on records, and makes a list with the results. In case the method is decorated with @returns, it concatenates the resulting instances. Such a method:

    @api.one
    def method(self, args):
        return self.name

may be called in both record and traditional styles, like::

    # recs = model.browse(cr, uid, ids, context)
    names = recs.method(args)

    names = model.method(cr, uid, ids, args, context=context)

Each time 'self' is redefined as current record.

3. @api.multi:

Decorate a record-style method where 'self' is a recordset. The method typically defines an operation on records. Such a method:

    @api.multi
    def method(self, args):
        ...

may be called in both record and traditional styles, like::

    # recs = model.browse(cr, uid, ids, context)
    recs.method(args)

    model.method(cr, uid, ids, args, context=context)

4. @api.constrains:

Decorates a constraint checker. Each argument must be a field name used in the check:

    @api.one
    @api.constrains('name', 'description')
    def _check_description(self):
        if self.name == self.description:
            raise ValidationError("Fields name and description must be different")

Invoked on the records on which one of the named fields has been modified.

Should raise :class:'~openerp.exceptions.ValidationError' if the validation failed.

5. @api.onchange:

Return a decorator to decorate an onchange method for given fields. Each argument must be a field name:

    @api.onchange('partner_id')
    def _onchange_partner(self):
        self.message = "Dear %s" % (self.partner_id.name or "")

In the form views where the field appears, the method will be called when one of the given fields is modified. The method is invoked on a pseudo-record that contains the values present in the form. Field assignments on that record are automatically sent back to the client.

6. @api.depends:

Return a decorator that specifies the field dependencies of a "compute" method (for new-style function fields). Each argument must be a string that consists in a dot-separated sequence of field names:

    pname = fields.Char(compute='_compute_pname')

    @api.one
    @api.depends('partner_id.name', 'partner_id.is_company')
    def _compute_pname(self):
        if self.partner_id.is_company:
            self.pname = (self.partner_id.name or "").upper()
        else:
            self.pname = self.partner_id.name

One may also pass a single function as argument. In that case, the dependencies are given by calling the function with the field's model.

7.@api.returns:

Return a decorator for methods that return instances of 'model'.

    :param model: a model name, or 'self' for the current model

    :param downgrade: a function 'downgrade(value)' to convert the
        record-style 'value' to a traditional-style output

The decorator adapts the method output to the api style: 'id', 'ids' or 'False' for the traditional style, and recordset for the record style:

    @model
    @returns('res.partner')
    def find_partner(self, arg):
        ...     # return some record

    # output depends on call style: traditional vs record style
    partner_id = model.find_partner(cr, uid, arg, context=context)

    # recs = model.browse(cr, uid, ids, context)
    partner_record = recs.find_partner(arg)

Note that the decorated method must satisfy that convention.

Those decorators are automatically *inherited*: a method that overrides a decorated existing method will be decorated with the same
'@returns(model)'.

The Decorator's which is used to decorate a method that supports the old-style API only:

1. @api.v7:

Decorate a method that supports the old-style api only. A new-style api may be provided by redefining a method with the same name and decorated with :func:'~.v8':

    @api.v7
    def foo(self, cr, uid, ids, context=None):
        ...

    @api.v8
    def foo(self):
        ...

Note that the wrapper method uses the docstring of the first method.

The Decorator's which is used to decorate a method that supports the new-style API only:

1. @api.v8:

Decorate a method that supports the new-style api only. An old-style api may be provided by redefining a method with the same name and decorated with :func:'~.v7':

    @api.v8
    def foo(self):
        ...

    @api.v7
    def foo(self, cr, uid, ids, context=None):
        ...

Note that the wrapper method uses the docstring of the first method.

No comments

Sunday 21 September 2014

Odoo New API: Environment

Environment:

An environment wraps data for ORM records:

  •  'cr', the current database cursor.
  •  'uid', the current user id.
  •  'context', the current context dictionary.

It also provides access to the registry, a cache for records, and a data structure to manage re-computations.

Encapsulates cr, uid, context:

Accessing the current cursor, user and context:

#recs.env encapsulates cr, uid, context
recs.env.cr             # shortcut: recs._cr
recs.env.uid           # shortcut: recs._uid
recs.env.context    # shortcut: recs._context

# recs.env also provides helpers
recs.env.user                              # uid as a record
recs.env.ref('base.group_user')  # resolve xml id
recs.env['res.partner']                # access to new-API model

Switching Environments:

1. Switch to new user:

Returns a new version of this recordset attached to the provided user:

self.sudo(user.id)
self.sudo()               # This will use the SUPERUSER_ID by default
# or
self.env['res.partner'].sudo().create(vals)

2. Modifying the context:

Returns a new version of this recordset attached to an extended context:

Syntax: with_context([context][, **overrides]) -> records

The extended context is either the provided 'context' in which 'overrides' are merged or the *current* context in which 'overrides' are merged e.g.:
   
# current context is {'key1': True}
r2 = records.with_context({}, key2=True)
# -> r2._context is {'key2': True}
r2 = records.with_context(key2=True)
# -> r2._context is {'key1': True, 'key2': True}

3. Switch to new Environment:

Returns a new version of this recordset attached to the provided environment:

# rebrowse recs with different parameters
env2 = recs.env(cr2, uid2, context2)
recs2 = recs.with_env(env2)

Dry Run:

Environment context manager provides the helper method to perform the action only in caches:

1. do_in_draft: Context-switch to draft mode, where all field updates are done in cache only.
2. do_in_onchange: Context-switch to 'onchange' draft mode, which is a specialized draft mode used during execution of onchange methods.
   
Invalidate the Environment Caches:

An Environment maintains multiple caches that are used by the Models/Fields classes.

In some cases, if you want to invalidate the caches, then you can do:

self.env.invalidate_all()

It will clear the cache of all environments.
1 comment

Odoo New API: Active Record Pattern


Active Record:

Active Record is the M in MVC - the model - which is the layer of the system responsible for representing business data and logic. Active Record facilitates the creation and use of business objects whose data requires persistent storage to a database. It is an implementation of the Active Record pattern which itself is a description of an Object Relational Mapping system.

Active Record Pattern:

The active record pattern is an approach to accessing data in a database. A database table is wrapped into a class.

In Active Record, objects carry both persistent data and behavior which operates on that data. Active Record takes the opinion that ensuring data access logic is part of the object will educate users of that object on how to write to and read from the database.

One of the new feature introduced in new API is a support for the active record pattern.

Example:

@api.multi
def method(self):
    for record in self:
        record.name = 'blah'
   
This will simply writes the value to database.

Implementation Detail:

With the help of special methods:

1. __iter__(self):
    Return an iterator over 'self'.

2. __getitem__(self, key):

    If 'key' is an integer or a slice, return the corresponding record
    selection as an instance (attached to 'self.env').
    Otherwise read the field 'key' of the first record in 'self'.

    Examples::

        inst = model.search(dom)    # inst is a recordset
        r4 = inst[3]                           # fourth record in inst
        rs = inst[10:20]                    # subset of inst
        nm = rs['name']                   # name of first record in inst

3. __setitem__(self, key, value):

    - Assign the field 'key' to 'value' in record 'self'.
    - It also calls the field's setter, to simply write to the database, and update cache.

Be Careful:

It is very expensive to use Active Record Pattern while writing the value. As Each assignment will trigger a write action on the database.

Example:

@api.one
def method(self):
    self.a = 1
    self.b = 7
    self.c = 8

On above example each assignment will trigger a write on the database. As the function is decorated with @api.one for each record in  recordset write will be called 3 times. So if you have 'n' numbers of records in recordset, the number of writes will be n*3.

This may leads to performance bottleneck on heavy task. In that case it is better to use:

@api.multi
def method(self):
    for record in self:
        record.write({'a': 1, 'b': 7, 'c': 8})
       
or, if you want to write same value on all the records then it is more better to use:

@api.multi
def method(self):
    self.write({'a':1, 'b': 7, 'c': 8})
No comments

Odoo: New API

Definition:

An API(Application Programming Interface) is a set of defined functions and methods for interfacing with the underlying system or program or service running on the computer.

The primary motivations behind new API:
  • Odoo development must be simple.
  • Odoo development must be simpler.
  • By exposing a better API (and some new features too).
  • Without sacrificing existing flexibility including: learning, understanding, prototyping, implementing, debugging, testing, performance tuning, maintenance.
  • Backward compatibility remains a priority.

Overview:

Odoo becomes simpler, more modular, more pythonic. It's lower the learning curve, allow you
to write less code for the same result.

Key features introduced in new API:

1. Active Record Pattern
2. Environment
3. Fields
4. Decorators
5. Recordsets

In next blog post, i am going to explore each and every features of new API in details.
1 comment

Odoo v8.0 has been released

Odoo v8.0 has been released on 14th september 2014! 

You can try it online immediately on https://www.odoo.com/start and download the final release on https://www.odoo.com/page/download.

More information:
 - Announcement: http://bit.ly/1miy5K2
 - Presentation: https://www.odoo.com/openerp_website/static/odoo8/index.html
 - What is Odoo (video): https://www.youtube.com/watch?v=o0UM3Lg7K4Y
 - Release Notes: http://bit.ly/1r43m2d


Odoo becomes more viral product by introducing CMS and eCommerce in version 8.0.
1 comment