During the year 2017, the connector evolved greatly. We can recognize three different aspect of the framework, they all have been rewritten:
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/queuecomponent
in the same repositorycomponent_event
in the same repositoryconnector
uses the 3 addons and the parts specifics to the connectorsThis 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.
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.
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 """
# ...
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()
# ...
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
.import_record
, no need to browse on the backend if a record was passedqueue.job
model._inherit
on the models.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)
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'])
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']
Events are now handled by the component_event
addon.
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
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
odoo.addons.component_event.components.event.CollectedEvents.notify()
.'on_'
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):
# ...
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):
# ...
'base.event.listener'
, or one of its descendants.connector_no_export
in the context can
be replaced by the decorator odoo.addons.component_event.skip_if()
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)
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)
_apply_on
on the Component.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)
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)
odoo.addons.component.models.collection.Collection
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.
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
_inherits
.AbstractComponent
or
Component
_inherit
from a list of Components'base'
componentPreviously, 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
# ...
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)
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.
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')
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
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
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')
You could replace a ConnectorUnit
with the replace
argument passed to
the backend decorator:
@magento(replacing=product.ProductImportMapper)
class ProductImportMapper(product.ProductImportMapper):
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'