Migration Guide to the new Connector API

During the year 2017, the connector evolved greatly. We can recognize three different aspect of the framework, they all have been rewritten:

  • The Job Queue API (Queue)
  • The Event API (Events)
  • The ConnectorUnit API, which is the core of the composability of the Connector. It has been replaced by a standalone addon called component. (Components)

The Connector has been splitted in different addons:

  • queue_job in https://github.com/OCA/queue
  • component in the same repository
  • component_event in the same repository
  • connector uses the 3 addons and the parts specifics to the connectors

This guide will show how to migrate from the old API to the new one.

The previous API will stay until the migration to Odoo 11.0.

Migrating Jobs

Jobs are now more integrated within the Odoo API. They are no longer standalone functions but are applied on methods of Models. Another change is that they have been extracted into their own addon, so obviously the Python paths change.

Declaration of a job

Before

from odoo.addons.connector.queue.job import job, related_action
from ..related_action import unwrap_binding, link

# function at module-level
@job(default_channel='root.magento')
@related_action(action=link)
def import_record(session, model_name, backend_id, magento_id, force=False):
    """ Import a record from Magento """
    # ...

@job(default_channel='root.magento')
@related_action(action=unwrap_binding)
def export_record(session, model_name, binding_id, fields=None):
    """ Import a record from Magento """
    # ...

After

from odoo.addons.queue_job.job import job, related_action
from odoo import api, models


class MagentoBinding(models.AbstractModel):
    _name = 'magento.binding'
    _inherit = 'external.binding'
    _description = 'Magento Binding (abstract)'

    @job(default_channel='root.magento')
    @related_action(action='related_action_magento_link')
    @api.model
    def import_record(self, backend, external_id, force=False):
        """ Import a Magento record """
        backend.ensure_one()
        # ...

    @job(default_channel='root.magento')
    @related_action(action='related_action_unwrap_binding')
    @api.multi
    def export_record(self, fields=None):
        """ Export a record on Magento """
        self.ensure_one()
        # ...

Observations

  • The job is declared on the generic abstract binding model from which all bindings inherit. This is not a requirement, but for this kind of job it is the perfect fit.
  • session, model_name and binding_id are no longer required as they are already known in self. Jobs can be used as well on @api.multi and @api.model.
  • Passing arguments as records is supported, in the new version of import_record, no need to browse on the backend if a record was passed
  • The action of a related action is now the name of a method on the queue.job model.
  • If you need to share a job between several models, put them in an AbstractModel and add an _inherit on the models.

Invocation of a job

Before

from odoo.addons.connector.session import ConnectorSession
from .unit.export_synchronizer import export_record


class MyBinding(models.Model):
    _name = 'my.binding'
    _inherit = 'magento.binding'

    @api.multi
    def button_trigger_export_sync(self):
        session = ConnectorSession.from_env(self.env)
        export_record(session, binding._name, self.id, fields=['name'])

    @api.multi
    def button_trigger_export_async(self):
        session = ConnectorSession.from_env(self.env)
        export_record.delay(session, self._name, self.id,
                            fields=['name'], priority=12)

After

class MyBinding(models.Model):
    _name = 'my.binding'

    @api.multi
    def button_trigger_export_sync(self):
        self.export_record(fields=['name'])

    @api.multi
    def button_trigger_export_async(self):
        self.with_delay(priority=12).export_record(fields=['name'])

Observations

  • No more imports are needed for the invocation

  • ConnectorSession is now dead

  • Arguments for the job (such as priority) are no longer mixed with the arguments passed to the method

  • When the job is called on a “browse” record, the job will be executed on an instance of this record:

    >>> binding = self.env['my.binding'].browse(1)
    >>> binding.button_trigger_export_async()
    

    In the execution of the job:

    @job
    def export_record(self, fields=None):
        print self
        print fields
    # =>
    # my.binding,1
    # ['name']
    

Migrating Events

Events are now handled by the component_event addon.

Triggering an event

Before

First you had to create an Event instance:

on_record_create = Event()

And then import and trigger it, passing a lot of arguments to it:

from odoo.addons.connector.event import on_record_create

class Base(models.AbstractModel):
    """ The base model, which is implicitly inherited by all models. """
    _inherit = 'base'

    @api.model
    def create(self, vals):
        record = super(Base, self).create(vals)
        on_record_create.fire(self.env, self._name, record.id, vals)
        return record

After

class Base(models.AbstractModel):
    _inherit = 'base'

    @api.model
    def create(self, vals):
        record = super(Base, self).create(vals)
        self._event('on_record_create').notify(record, fields=vals.keys())
        return record

Observations

  • No more imports are needed for the invocation Only the arguments you want to pass should be passed to odoo.addons.component_event.components.event.CollectedEvents.notify().
  • The name of the event must start with 'on_'

Listening to an event

Before

from odoo.addons.connector.event import on_record_create

@on_record_create
def delay_export(env, model_name, record_id, vals):
    if session.context.get('connector_no_export'):
        return
    fields = vals.keys()
    export_record.delay(session, model_name, record_id, fields=fields)

@on_something
def do_anything(env, model_name, record_id):
  # ...

After

from odoo.addons.component.core import Component
from odoo.addons.component_event import skip_if

class MagentoListener(Component):
    _name = 'magento.event.listener'
    _inherit = 'base.connector.listener'

    @skip_if(lambda self, record, **kwargs: self.no_connector_export(record))
    def on_record_create(self, record, fields=None):
        """ Called when a record is created """
        record.with_delay().export_record(fields=fields)

    def on_something(self, record):
        # ...

Observations

  • The listeners are now components
  • The name of the method is the same than the one notified in the previous section
  • A listener Component might container several listener methods
  • It must inherit from 'base.event.listener', or one of its descendants.
  • The check of the key connector_no_export in the context can be replaced by the decorator odoo.addons.component_event.skip_if()

Listening to an event only for some Models

Before

from odoo.addons.connector.event import on_record_create

@on_record_create(model_names=['magento.address', 'magento.res.partner'])
def delay_export(env, model_name, record_id, vals):
    if session.context.get('connector_no_export'):
        return
    fields = vals.keys()
    export_record.delay(session, model_name, record_id, fields=fields)

After

from odoo.addons.component.core import Component
from odoo.addons.component_event import skip_if

class MagentoListener(Component):
    _name = 'magento.event.listener'
    _inherit = 'base.event.listener'
    _apply_on = ['magento.address', 'magento.res.partner']

    @skip_if(lambda self, record, **kwargs: self.no_connector_export(record))
    def on_record_create(self, record, fields=None):
        """ Called when a record is created """
        record.with_delay().export_record(fields=fields)

Observations

  • Same than previous example but we added _apply_on on the Component.

Migrating Components

Backends

Before

You could have several versions for a backend:

magento = backend.Backend('magento')
""" Generic Magento Backend """

magento1700 = backend.Backend(parent=magento, version='1.7')
""" Magento Backend for version 1.7 """

magento1900 = backend.Backend(parent=magento, version='1.9')
""" Magento Backend for version 1.9 """

It was linked with a Backend model such as:

class MagentoBackend(models.Model):
    _name = 'magento.backend'
    _description = 'Magento Backend'
    _inherit = 'connector.backend'

    _backend_type = 'magento'

    @api.model
    def select_versions(self):
        """ Available versions in the backend.
        Can be inherited to add custom versions.  Using this method
        to add a version from an ``_inherit`` does not constrain
        to redefine the ``version`` field in the ``_inherit`` model.
        """
        return [('1.7', '1.7+')]

    version = fields.Selection(selection='select_versions', required=True)

After

All the backend.Backend instances must be deleted.

And the _backend_type must be removed from the Backend model.

class MagentoBackend(models.Model):
    _name = 'magento.backend'
    _description = 'Magento Backend'
    _inherit = 'connector.backend'

    @api.model
    def select_versions(self):
        """ Available versions in the backend.
        Can be inherited to add custom versions.  Using this method
        to add a version from an ``_inherit`` does not constrain
        to redefine the ``version`` field in the ``_inherit`` model.
        """
        return [('1.7', '1.7+')]

    version = fields.Selection(selection='select_versions', required=True)

Observations

Inheritance

Before

You could inherit a ConnectorUnit by creating a custom Backend version and decorating your class with it

magento_custom = backend.Backend(parent=magento1700, version='custom')
""" Custom Magento Backend """
# base one
@magento
class MagentoPartnerAdapter(GenericAdapter):
    # ...

# other file...

from .backend import magento_custom

# custom one
@magento_custom
class MyPartnerAdapter(MagentoPartnerAdapter):
    # ...

    def do_something(self):
        # do it this way

You could also replace an existing class, this is mentionned in Replace or unregister a component.

After

For an existing component:

from odoo.addons.component.core import Component

class MagentoPartnerAdapter(Component):
    _name = 'magento.partner.adapter'
    _inherit = 'magento.adapter'

    def do_something(self):
        # do it this way

You can extend it:

from odoo.addons.component.core import Component

class MyPartnerAdapter(Component):
    _inherit = 'magento.partner.adapter'

    def do_something(self):
        # do it this way

Or create a new different component with the existing one as base:

from odoo.addons.component.core import Component

class MyPartnerAdapter(Component):
    _name = 'my.magento.partner.adapter'
    _inherit = 'magento.partner.adapter'

    def do_something(self):
        # do it this way

Observations

  • The inheritance is similar to the Odoo’s one (without _inherits.
  • All components have a Python inheritance on AbstractComponent or Component
  • The names are global (as in Odoo), so you should prefix them with a namespace
  • The name of the classes has no effect
  • As in Odoo Models, a Component can _inherit from a list of Components
  • All components implicitly inherits from a 'base' component

Entrypoint for working with components

Before

Previously, when you had to work with ConnectorUnit from a Model or from a job, depending of the Odoo version you to:

from odoo.addons.connector.connector import ConnectorEnvironment

# ...

    backend_record = session.env['magento.backend'].browse(backend_id)
    env = ConnectorEnvironment(backend_record, 'magento.res.partner')
    importer = env.get_connector_unit(MagentoImporter)
    importer.run(magento_id, force=force)

Or:

from odoo.addons.connector.connector import ConnectorEnvironment
from odoo.addons.connector.session import ConnectorSession

#...

    backend_record = session.env['magento.backend'].browse(backend_id)
    session = ConnectorSession.from_env(self.env)
    env = ConnectorEnvironment(backend_record, session, 'magento.res.partner')
    importer = env.get_connector_unit(MagentoImporter)
    importer.run(external_id, force=force)

Which was commonly abstracted in a helper function such as:

def get_environment(session, model_name, backend_id):
    """ Create an environment to work with.  """
    backend_record = session.env['magento.backend'].browse(backend_id)
    env = ConnectorEnvironment(backend_record, session, 'magento.res.partner')
    lang = backend_record.default_lang_id
    lang_code = lang.code if lang else 'en_US'
    if lang_code == session.context.get('lang'):
        return env
    else:
        with env.session.change_context(lang=lang_code):
            return env

After

# ...
    backend_record = self.env['magento.backend'].browse(backend_id)
    with backend_record.work_on('magento.res.partner') as work:
        importer = work.component(usage='record.importer')
        importer.run(external_id, force=force)

Observations

Find a component

Before

To find a ConnectorUnit, you had to ask for given class or subclass:

# our ConnectorUnit to find
@magento
class MagentoPartnerAdapter(GenericAdapter):
    _model_name = ['magent.res.partner']

# other file...

def run(self, record):
    backend_adapter = self.unit_for(GenericAdapter)

It was searched for the current model and the current backend.

After

For an existing component:

from odoo.addons.component.core import Component

class MagentoPartnerAdapter(Component):
    _name = 'magento.partner.adapter'
    _inherit = 'magento.adapter'

    _usage = 'backend.adapter'
    _collection = 'magento.backend'
    _apply_on = ['res.partner']

# other file...

def run(self, record):
    backend_adapter = self.component(usage='backend.adapter')

Observations

  • The model is compared with the _apply_on attribute

  • The Backend is compared with the _collection attribute, it must have the same name than the Backend Model.

  • The _usage indicates what the purpose of the component is, and allow to find the correct one for our task. It allow more dynamic usages than the previous usage of a class.

  • Usually, the _usage and the _collection will be _inherit ‘ed from a component (here from 'magento.adapter), so they won’t need to be repeated in all Components.

  • A good idea is to have a base abstract Component for the Collection, then an abstract Component for every usage:

    class BaseMagentoConnectorComponent(AbstractComponent):
    
        _name = 'base.magento.connector'
        _inherit = 'base.connector'
        _collection = 'magento.backend'
    
    class MagentoBaseExporter(AbstractComponent):
        """ Base exporter for Magento """
    
        _name = 'magento.base.exporter'
        _inherit = ['base.exporter', 'base.magento.connector']
        _usage = 'record.exporter'
    
    class MagentoImportMapper(AbstractComponent):
        _name = 'magento.import.mapper'
        _inherit = ['base.magento.connector', 'base.import.mapper']
        _usage = 'import.mapper'
    
    # ...
    
  • The main usages are: * import.mapper * export.mapper * backend.adapter * importer * exporter * binder * event.listener

  • But for the importer and exporter, I recommend to use more precise ones in the connectors: record.importer, record.exporter, batch.importer, batch.exporter

  • You are allowed to be creative with the _usage, it’s the key that will allow you to find the right one component you need. (e.g. on stock.picking you need to 1. export the record, 2. export the tracking. Then use record.exporter and tracking.exporter).

  • AbstractComponent will never be returned by a lookup

Backend Versions

Before

You could have several versions for a backend:

magento = backend.Backend('magento')
""" Generic Magento Backend """

magento1700 = backend.Backend(parent=magento, version='1.7')
""" Magento Backend for version 1.7 """

magento1900 = backend.Backend(parent=magento, version='1.9')
""" Magento Backend for version 1.9 """

And use them for a class-level dynamic dispatch

from odoo.addons.magentoerpconnect.backend import magento1700, magento1900

@magento1700
class PartnerAdapter1700(GenericAdapter):
    # ...

    def do_something(self):
        # do it this way

@magento1900
class PartnerAdapter1900(GenericAdapter):
    # ...

    def do_something(self):
        # do it that way

After

This feature has been removed, it introduced a lot of complexity (notably regarding inheritance) for few gain. The version is now optional on the backends and the version dispatch, if needed, should be handled manually.

In methods:

from odoo.addons.component.core import Component

class PartnerAdapter(Component):
    # ...

    def do_something(self):
        if self.backend_record.version == '1.7':
            # do it this way
        else:
            # do it that way

Or with a factory:

from odoo.addons.component.core import Component

class PartnerAdapterFactory(Component):
    # ...

    def get_component(self, version):
        if self.backend_record.version == '1.7':
            return self.component(usage='backend.adapter.1.7')
        else:
            return self.component(usage='backend.adapter.1.9')

Observations

  • None

Links

Replace or unregister a component

Before

You could replace a ConnectorUnit with the replace argument passed to the backend decorator:

@magento(replacing=product.ProductImportMapper)
class ProductImportMapper(product.ProductImportMapper):

After

First point: this should hardly be needed now, as you can inherit a component like Odoo Models. Still, if you need to totally replace a component by another, let’s say there is this component:

from odoo.addons.component.core import Component

class ProductImportMapper(Component):
    _name = 'magento.product.import.mapper'
    _inherit = 'magento.import.mapper'

    _apply_on = ['magento.product.product']
    # normally the following attrs are inherited from the _inherit
    _usage = 'import.mapper'
    _collection = 'magento.backend'

Then you can remove the usage of the component: it will never be used:

from odoo.addons.component.core import Component

class ProductImportMapper(Component):
    _inherit = 'magento.product.import.mapper'
    _usage = None

And create your own, that will be picked up instead of the base one:

from odoo.addons.component.core import Component

class MyProductImportMapper(Component):
    _name = 'my.magento.product.import.mapper'
    _inherit = 'magento.import.mapper'

    _apply_on = ['magento.product.product']
    # normally the following attrs are inherited from the _inherit
    _usage = 'import.mapper'
    _collection = 'magento.backend'

Observations

  • None

Links

Various hints

  • The components and the jobs know how to work with Model instances, so prefer them over ids in parameters.