Initial commit: OCA Technical packages (595 packages)

This commit is contained in:
Ernad Husremovic 2025-08-29 15:43:03 +02:00
commit 2cc02aac6e
24950 changed files with 2318079 additions and 0 deletions

View file

@ -0,0 +1,152 @@
.. _bootstrap-connector:
########################
Boostrapping a connector
########################
We'll see the steps to bootstrap a new connector.
Besides that, you may want to use the existing connectors to have some
real implementation examples:
* `Odoo Magento Connector`_
* `Odoo Prestashop Connector`_
Be aware that the connector API has changed in Odoo 10.0, so the examples
might be outdated.
Some boilerplate is necessary, so this document will guide you through
some steps. Please also take a look on the :ref:`naming-convention`.
For the sake of the example, we'll imagine we have to synchronize
Odoo with a coffee machine.
*************
Odoo Manifest
*************
As we want to synchronize Odoo with a coffee machine, we'll name
our connector connector_coffee.
First, we need to create the Odoo addons itself, editing the
``connector_coffee/__manifest__.py`` manifest.
.. code-block:: python
:emphasize-lines: 3,4
{'name': 'Coffee Connector',
'version': '1.0.0',
'category': 'Connector',
'depends': ['connector',
],
'author': 'Myself',
'license': 'LGPL-3',
'description': """
Coffee Connector
================
Connect Odoo to my coffee machine.
Features:
* Pour a coffee when Odoo is busy for too long
""",
'data': [],
'installable': True,
}
Nothing special but 2 things to note:
* It depends from ``connector``. ``connector`` itself depends from
``queue_job``, ``component`` and ``component_event``. ``queue_job`` is in the
`OCA/queue`_ repository.
* The module category should be ``Connector``.
Of course, we also need to create the ``__init__.py`` file where we will
put the imports of our python modules.
.. _OCA/queue: https://github.com/OCA/queue
*************
Backend Model
*************
Reference: :ref:`api-backend-model`
We need to create a Backend representing the external service. Every record we
synchronize will be linked with a record of ``coffee.backend``. This backend
is our *collection* of Components.
The ``coffee.backend`` model is an ``_inherit`` of ``connector.backend``. In
``connector_coffee/models/coffee_binding.py``::
from odoo import api, fields, models
class CoffeeBackend(models.Model):
_name = 'coffee.backend'
_description = 'Coffee Backend'
_inherit = 'connector.backend'
location = fields.Char(string='Location')
username = fields.Char(string='Username')
password = fields.Char(string='Password')
Notes:
* We can other fields for the configuration of the connection or the
synchronizations.
****************
Abstract Binding
****************
Reference: :ref:`api-binding-model`
In order to share common features between all the bindings (see
:ref:`binding`), create an abstract binding model.
It can be as follows (in ``connector_coffee/models/coffee_binding.py``)::
from odoo import models, fields
class CoffeeBinding(models.AbstractModel):
_name = 'coffee.binding'
_inherit = 'external.binding'
_description = 'Coffee Binding (abstract)'
# odoo_id = odoo-side id must be declared in concrete model
backend_id = fields.Many2one(
comodel_name='coffee.backend',
string='Coffee Backend',
required=True,
ondelete='restrict',
)
coffee_id = fields.Integer(string='ID in the Coffee Machine',
index=True)
Notes:
* This model inherit from ``external.binding``
* Any number of fields or methods can be added
**********
Components
**********
Reference: :ref:`api-component`
We'll probably need to create synchronizers, mappers, backend adapters,
binders and maybe our own kind of components.
Their implementation can vary from a project to another. Have a look on the
`Odoo Magento Connector`_ and `Odoo Prestashop Connector`_ projects.
.. _`Odoo Magento Connector`: https://github.com/OCA/connector-magento
.. _`Odoo Prestashop Connector`: https://github.com/OCA/connector-prestashop

View file

@ -0,0 +1,188 @@
.. _code-overview:
#############
Code Overview
#############
Some simple code examples.
***************************
Trigger and listen an event
***************************
.. code-block:: python
class AccountInvoice(models.Model):
_inherit = 'account.invoice'
@api.multi
def action_invoice_paid(self):
res = super(AccountInvoice, self).action_invoice_paid()
for record in self:
self._event('on_invoice_paid').notify(record)
return res
.. code-block:: python
from odoo.addons.component.core import Component
class MyEventListener(Component):
_name = 'my.event.listener'
_inherit = 'base.event.listener'
def on_invoice_paid(self, record):
_logger.info('invoice %s has been paid!', record.name)
Ref: :ref:`api-event`
*************************
Delay an Asynchronous Job
*************************
.. code-block:: python
from odoo.addons.queue_job.job import job
class AccountInvoice(models.Model):
_inherit = 'account.invoice'
@job
@api.multi
def export_payment(self):
self.ensure_one()
_logger.info("I'm exporting the payment for %s", self.name)
@api.multi
def action_invoice_paid(self):
res = super(AccountInvoice, self).action_invoice_paid()
for record in self:
record.with_delay(priority=5).export_payment()
return res
Ref: :ref:`api-queue`
********************
Work with components
********************
This is a highly simplified version of a micro-connector, without using
events or jobs, for the sake of the example.
.. code-block:: python
from odoo.addons.component.core import AbstractComponent
class MagentoBackend(models.Model):
_name = 'magento.backend'
_description = 'Magento Backend'
_inherit = 'connector.backend'
location = fields.Char(string='Location', required=True)
username = fields.Char(string='Username')
password = fields.Char(string='Password')
def import_partner(self, external_id):
with self.work_on(model_name='magento.res.partner') as work:
importer = work.component(usage='record.importer')
# returns an instance of PartnerImporter, which has been
# found with:the collection name (magento.backend, the model,
# and the usage).
importer.run(partner_id)
# the next 2 components are abstract and are used by inheritance
# by the others
class BaseMagentoConnectorComponent(AbstractComponent):
# same inheritance than Odoo models
_name = 'base.magento.connector'
_inherit = 'base.connector'
# subscribe to:
_collection = 'magento.backend'
# the collection will be inherited to the components below,
# because they inherit from this component
class GenericAdapter(AbstractComponent):
# same inheritance than Odoo models
_name = 'magento.adapter'
_inherit = ['base.backend.adapter', 'base.magento.connector']
# usage is used for lookups of components
_usage = 'backend.adapter'
_magento_model = None
def _call(self, *args, **kwargs):
location = self.backend_record.location
# use client API
def read(self, fields=None):
""" Search records according to some criterias
and returns a list of ids
:rtype: list
"""
return self._call('%s.info' % self._magento_model, fields)
# these are the components we need for our synchronization
class PartnerAdapter(Component):
_name = 'magento.partner.adapter'
_inherit = 'magento.adapter'
_apply_on = ['magento.res.partner']
_magento_model = 'customer'
class PartnerMapper(Component):
_name = 'magento.partner.import.mapper'
_inherit = 'magento.import.mapper' # parent component omitted for brevity
_apply_on = ['magento.res.partner']
_usage = 'import.mapper'
class PartnerBinder(Component):
_name = 'magento.partner.binder'
_inherit = 'magento.binder' # parent component omitted for brevity
_apply_on = ['magento.res.partner']
_usage = 'binder'
class PartnerImporter(Component):
_name = 'magento.partner.importer'
_inherit = 'magento.importer' # parent component omitted for brevity
_apply_on = ['magento.res.partner']
_usage = 'record.importer'
def run(self, external_id):
# get the components we need for the sync
# this one knows how to speak to magento
backend_adapter = self.component(usage='backend.adapter')
# this one knows how to convert magento data to odoo data
mapper = self.component(usage='import.mapper')
# this one knows how to link magento/odoo records
binder = self.component(usage='binder')
# read external data from magento
external_data = backend_adapter.read(external_id)
# convert to odoo data
internal_data = mapper.map_record(external_data).values()
# find if the magento id already exists in odoo
binding = binder.to_internal(external_id)
if binding:
# if yes, we update it
binding.write(internal_data)
else:
# or we create it
binding = self.model.create(internal_data)
# finally, we bind both, so the next time we import
# the record, we'll update the same record instead of
# creating a new one
binder.bind(external_id, binding)
Ref: :ref:`api-component`

View file

@ -0,0 +1,242 @@
.. _concepts:
##################
Connector Concepts
##################
The framework to develop connectors is decoupled in small pieces of
codes interacting together. Each of them can be used or not in an
implementation.
An example of implementation is the `Odoo Magento Connector`_.
This document describes them from a high-level point of view and gives
pointers to more concrete 'how-to' or small tutorials.
.. _`Odoo Magento Connector`: http://www.odoo-magento-connector.com
******
Events
******
Reference: :ref:`api-event`
Events are hooks in Odoo on which we can plug some actions. They are
based on an Observer pattern.
The same event can be shared across several connectors, easing their
implementation.
For instance, the module connector_ecommerce_ which extends the
framework with common e-commerce capabilities, adds its own events
common to e-commerce.
A connectors developer is mostly interested by:
* adding and listening to events (see :ref:`api-event`)
.. _jobs-queue:
**********
Jobs Queue
**********
Reference: :ref:`api-queue`
This feature is part of a standalone addon, but is a prerequisite for
the connector framework.
The module is ``queue_job`` in https://github.com/OCA/queue.
A connectors developer is mostly interested by:
* Delay a job (see the decorator :py:func:`~odoo.addons.queue_job.job.job`)
*******
Backend
*******
Reference: :ref:`api-backend-model`
The Backend Model is what represents the external service / system we
synchronize with. The name on the backend indicates what is the collection the
Components will be registered into. Put another way: every backend has its own
collection of Components.
It must use an ``_inherit`` on ``connector.backend``.
``connector.backend`` inherits
:class:`odoo.addons.component.models.collection.Collection` which has a
:meth:`odoo.addons.component.models.collection.Collection.work_on` that will be
used as entrypoint for the component system. This method returns a
:class:`~odoo.addons.component.core.WorkContext`
***********
WorkContext
***********
Reference: :class:`~odoo.addons.component.core.WorkContext`
A :class:`~odoo.addons.component.core.WorkContext` is the work environment or
context that will be passed transversally through all the components. This is
also the entrypoint to the component system.
A connectors developer is mostly interested by:
* Get a Component from a WorkContext (:py:meth:`~odoo.addons.component.core.WorkContext.component`)
*********
Component
*********
Reference: :ref:`api-component`
:py:class:`~odoo.addons.component.core.Component` are pluggable classes used
for the synchronizations with the external systems (or anything!)
The Components system has been extracted in a standalone addon (``component``),
which means it can really be used in a totally different way.
The connector defines some base components, which you can find below. Note
that you can and are encouraged to define your own Components as well.
Mappings
========
The base class is :py:class:`connector.components.mapper.Mapper`.
In your components, you probably want to inherit from:
* ``_inherit = 'base.import.mapper'``
* ``_inherit = 'base.export.mapper'``
And the usages for the lookups are:
* ``import.mapper``
* ``export.mapper``
A mapping translates an external record to an Odoo record and
conversely.
It supports:
direct mappings
Fields *a* is written in field *b*.
method mappings
A method is used to convert one or many fields to one or many
fields, with transformation.
It can be filtered, for example only applied when the record is
created or when the source fields are modified.
submapping
a sub-record (lines of a sale order) is converted using another
Mapper
See the documentation of the class for more details.
Synchronizers
=============
The base class is :py:class:`connector.components.synchronizer.Synchronizer`.
In your components, you probably want to inherit from:
* ``_inherit = 'base.importer'``
* ``_inherit = 'base.exporter'``
And the usages for the lookups are:
* ``importer``
* ``exporter``
However, in your implementation, it is advised to use more refined usages such
as:
* ``record.importer``
* ``record.exporter``
* ``batch.importer``
* ``batch.exporter``
* ..
A synchronizer orchestrates a synchronization with a backend. It can be a
record's import or export, a deletion of something, or anything else. For
instance, it will use the mappings to convert the data between both systems,
the backend adapters to read or write data on the backend and the binders to
create the link between them.
Backend Adapters
================
The base class is
:py:class:`connector.components.backend_adapter.BackendAdapter`.
In your components, you probably want to inherit from:
* ``_inherit = 'base.backend.adapter'``
* ``_inherit = 'base.backend.adapter.crud'``
And the usages for the lookups are:
* ``backend.adapter``
An external adapter has a common interface to speak with the backend.
It translates the basic orders (search, read, write) to the protocol
used by the backend.
Binders
=======
The base class is
:py:class:`connector.components.binder.Binder`.
In your components, you probably want to inherit from:
* ``_inherit = 'base.binder'``
And the usages for the lookups are:
* ``binder``
Binders are components that know how to find the external ID for an
Odoo ID, how to find the Odoo ID for an external ID and how to
create the binding between them. A default implementation is
available and can be inherited if needed.
Listeners
=========
The base class is
:py:class:`connector.components.listener.ConnectorListener`.
In your components, you probably want to inherit from:
* ``_inherit = 'base.connector.listener'``
This is where you will register your event listeners.
See :mod:`addons.component_event.components.event`.
.. _binding:
********
Bindings
********
Reference: :ref:`api-binding-model`
A binding represents the link of a record between Odoo and a backend.
The proposed implementation for the connectors widely use the
`_inherits` capabilities.
Say we import a customer from *Magento*.
We create a `magento.res.partner` model, which `_inherits`
`res.partner`.
This model, called a *binding* model, knows the ID of the partner in
Odoo, the ID in Magento and the relation to the backend model.
It also stores all the necessary metadata related to this customer
coming from Magento.

View file

@ -0,0 +1,39 @@
.. _jobrunner:
#######################################
Configuring channels and the job runner
#######################################
.. automodule:: odoo.addons.queue_job.jobrunner.runner
What is a channel?
------------------
.. autoclass:: odoo.addons.queue_job.jobrunner.channels.Channel
:noindex:
How to configure Channels?
--------------------------
The ``ODOO_QUEUE_JOB_CHANNELS`` environment variable must be
set before starting Odoo in order to enable the job runner
and configure the capacity of the channels.
The general syntax is ``channel(.subchannel)*(:capacity(:key(=value)?)*)?,...``.
Intermediate subchannels which are not configured explicitly are autocreated
with an unlimited capacity (except the root channel which if not configured gets
a default capacity of 1).
A delay in seconds between jobs can be set at the channel level with
the ``throttle`` key.
Example ``ODOO_QUEUE_JOB_CHANNELS``:
* ``root:4``: allow up to 4 concurrent jobs in the root channel.
* ``root:4,root.sub:2``: allow up to 4 concurrent jobs in the root channel and
up to 2 concurrent jobs in the channel named ``root.sub``.
* ``sub:2``: the same.
* ``root:4:throttle=2``: wait at least 2 seconds before starting the next job

View file

@ -0,0 +1,900 @@
.. _migration-guide:
########################################
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 (:ref:`api-queue`)
* The Event API (:ref:`api-event`)
* The ``ConnectorUnit`` API, which is the core of the composability
of the Connector. It has been replaced by a standalone addon
called ``component``. (:ref:`api-component`)
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.
.. contents:: Sections:
:local:
:backlinks: top
:depth: 2
**************
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
------
.. code-block:: python
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
-----
.. code-block:: python
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.
Links
-----
* :meth:`odoo.addons.queue_job.job.job`
* :meth:`odoo.addons.queue_job.job.related_action`
Invocation of a job
===================
Before
------
.. code-block:: python
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
-----
.. code-block:: python
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:
.. code-block:: python
>>> binding = self.env['my.binding'].browse(1)
>>> binding.button_trigger_export_async()
In the execution of the job:
.. code-block:: python
@job
def export_record(self, fields=None):
print self
print fields
# =>
# my.binding,1
# ['name']
Links
-----
* :meth:`odoo.addons.queue_job.job.job`
* :meth:`odoo.addons.queue_job.models.base.Base.with_delay`
****************
Migrating Events
****************
Events are now handled by the ``component_event`` addon.
Triggering an event
===================
Before
------
First you had to create an :class:`~odoo.addons.connector.event.Event` instance:
.. code-block:: python
on_record_create = Event()
And then import and trigger it, passing a lot of arguments to it:
.. code-block:: python
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
-----
.. code-block:: python
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
:meth:`odoo.addons.component_event.components.event.CollectedEvents.notify`.
* The name of the event must start with ``'on_'``
Links
-----
* :mod:`odoo.addons.component_event.components.event`
Listening to an event
=====================
Before
------
.. code-block:: python
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
-----
.. code-block:: python
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 :func:`odoo.addons.component_event.skip_if`
Links
-----
* :mod:`odoo.addons.component_event.components.event`
Listening to an event only for some Models
==========================================
Before
------
.. code-block:: python
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
-----
.. code-block:: python
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.
Links
-----
* :mod:`odoo.addons.component_event.components.event`
********************
Migrating Components
********************
Backends
========
Before
------
You could have several versions for a backend:
.. code-block:: python
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:
.. code-block:: python
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 :class:`backend.Backend` instances must be deleted.
And the ``_backend_type`` must be removed from the Backend model.
.. code-block:: python
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
------------
* The version is now optional in the Backend Models.
* Backend Models are based on Component's Collections:
:class:`odoo.addons.component.models.collection.Collection`
Links
-----
* :ref:`api-component`
* :class:`odoo.addons.component.models.collection.Collection`
Inheritance
===========
Before
------
You could inherit a ``ConnectorUnit`` by creating a custom Backend
version and decorating your class with it
.. code-block:: python
magento_custom = backend.Backend(parent=magento1700, version='custom')
""" Custom Magento Backend """
.. code-block:: python
# 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:
.. code-block:: python
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:
.. code-block:: python
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:
.. code-block:: python
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
:class:`~odoo.addons.component.core.AbstractComponent` or
:class:`~odoo.addons.component.core.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
Links
-----
* :ref:`api-component`
* :class:`odoo.addons.component.core.AbstractComponent`
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:
.. code-block:: python
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:
.. code-block:: python
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:
.. code-block:: python
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
-----
.. code-block:: python
# ...
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
------------
* And when you are already in a Component, refer to `Find a component`_
Links
-----
* :class:`~odoo.addons.component.core.WorkContext`
Find a component
================
Before
------
To find a ``ConnectorUnit``, you had to ask for given class or subclass:
.. code-block:: python
# 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:
.. code-block:: python
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
Links
-----
* :ref:`api-component`
* :class:`odoo.addons.component.core.AbstractComponent`
Backend Versions
================
Before
------
You could have several versions for a backend:
.. code-block:: python
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
.. code-block:: python
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:
.. code-block:: python
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:
.. code-block:: python
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
-----
* :ref:`api-component`
Replace or unregister a component
=================================
Before
------
You could replace a ``ConnectorUnit`` with the ``replace`` argument passed to
the backend decorator:
.. code-block:: python
@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:
.. code-block:: python
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:
.. code-block:: python
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:
.. code-block:: python
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
-----
* :ref:`api-component`
Various hints
=============
* The components and the jobs know how to work with Model instances,
so prefer them over ids in parameters.