Components

Components are the blocks allowing to build a flexible and well decoupled code. They are based on the component addon, which can as well be used separately.

Core Components

Base Component

The connector proposes a ‘base’ Component, which can be used in the _inherit of your own components. This is not a requirement. It is already inherited by every component provided by the Connector.

Components are organized according to different usages. The connector suggests 5 main kinds of Components. Each might have a few different usages. You can be as creative as you want when it comes to creating new ones though.

One “usage” is responsible for a specific work, and alongside with the collection (the backend) and the model, the usage will be used to find the needed component for a task.

Some of the Components have an implementation in the Connector addon, but some are empty shells that need to be implemented in the different connectors.

The usual categories are:

Binder

The binders give the external ID or Odoo ID from respectively an Odoo ID or an external ID. A default implementation is available.

Most common usages:

  • binder
Mapper

The mappers transform a external record into an Odoo record or conversely.

Most common usages:

  • import.mapper
  • export.mapper
BackendAdapter

The backend.adapters implements the discussion with the backend's APIs. They usually adapt their APIs to a common interface (CRUD).

Most common usages:

  • backend.adapter
Synchronizer

A synchronizer is the main piece of a synchronization. It orchestrates the flow of a synchronization and use the other Components

Most common usages:

  • record.importer
  • record.exporter
  • batch.importer
  • batch.exporter

The possibilities for components do not stop there, look at the RecordLocker for an example of single-purpose, decoupled component.

class connector.components.core.BaseConnectorComponent(work_context)[source]

Bases: odoo.addons.component.core.AbstractComponent

Base component for the connector

Is inherited by every components of the Connector (Binder, Mapper, …) and adds a few methods which are of common usage in the connectors.

_name = 'base.connector'

Name of the component

property backend_record

Backend record we are working with

binder_for(model=None)[source]

Shortcut to get Binder for a model

Equivalent to: self.component(usage='binder', model_name='xxx')

advisory_lock_or_retry(lock, retry_seconds=1)[source]

Acquire a Postgres transactional advisory lock or retry job

When the lock cannot be acquired, it raises a odoo.addons.queue_job.exception.RetryableJobError so the job is retried after n retry_seconds.

Usage example:

lock_name = 'import_record({}, {}, {}, {})'.format(
    self.backend_record._name,
    self.backend_record.id,
    self.model._name,
    self.external_id,
)
self.advisory_lock_or_retry(lock_name, retry_seconds=2)

See odoo.addons.connector.connector.pg_try_advisory_lock() for details.

Parameters:
  • lock – The lock name. Can be anything convertible to a string. It needs to represent what should not be synchronized concurrently, usually the string will contain at least: the action, the backend name, the backend id, the model name, the external id
  • retry_seconds – number of seconds after which a job should be retried when the lock cannot be acquired.

Connector Components

Binders

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.

class connector.components.binder.Binder(work_context)[source]

Bases: odoo.addons.component.core.AbstractComponent

For one record of a model, capable to find an external or internal id, or create the binding (link) between them

This is a default implementation that can be inherited or reimplemented in the connectors.

This implementation assumes that binding models are _inherits of the models they are binding.

_name = 'base.binder'

Name of the component

_inherit = 'base.connector'

Name or list of names of the component(s) to inherit from

_usage = 'binder'

Component purpose (‘import.mapper’, …).

_external_field = 'external_id'
_backend_field = 'backend_id'
_odoo_field = 'odoo_id'
_sync_date_field = 'sync_date'
to_internal(external_id, unwrap=False)[source]

Give the Odoo recordset for an external ID

Parameters:
  • external_id – external ID for which we want the Odoo ID
  • unwrap – if True, returns the normal record else return the binding record
Returns:

a recordset, depending on the value of unwrap, or an empty recordset if the external_id is not mapped

Return type:

recordset

to_external(binding, wrap=False)[source]

Give the external ID for an Odoo binding ID

Parameters:
  • binding – Odoo binding for which we want the external id
  • wrap – if True, binding is a normal record, the method will search the corresponding binding and return the external id of the binding
Returns:

external ID of the record

bind(external_id, binding)[source]

Create the link between an external ID and an Odoo ID

Parameters:
  • external_id – external id to bind
  • binding (int) – Odoo record to bind
unwrap_binding(binding)[source]

For a binding record, gives the normal record.

Example: when called with a magento.product.product id, it will return the corresponding product.product id.

Parameters:browse – when True, returns a browse_record instance rather than an ID
unwrap_model()[source]

For a binding model, gives the normal model.

Example: when called on a binder for magento.product.product, it will return product.product.

Mappers

Mappers are the components responsible to transform external records into Odoo records and conversely.

class connector.components.mapper.Mapper(work)[source]

Bases: odoo.addons.component.core.AbstractComponent

A Mapper translates an external record to an Odoo record and conversely. The output of a Mapper is a dict.

3 types of mappings are supported:

Direct Mappings

Example:

direct = [('source', 'target')]

Here, the source field will be copied in the target field.

A modifier can be used in the source item. The modifier will be applied to the source field before being copied in the target field. It should be a closure function respecting this idiom:

def a_function(field):
    ''' ``field`` is the name of the source field.

        Naming the arg: ``field`` is required for the conversion'''
    def modifier(self, record, to_attr):
        ''' self is the current Mapper,
            record is the current record to map,
            to_attr is the target field'''
        return record[field]
    return modifier

And used like that:

direct = [
        (a_function('source'), 'target'),
]

A more concrete example of modifier:

def convert(field, conv_type):
    ''' Convert the source field to a defined ``conv_type``
    (ex. str) before returning it'''
    def modifier(self, record, to_attr):
        value = record[field]
        if not value:
            return None
        return conv_type(value)
    return modifier

And used like that:

direct = [
    (convert('myfield', float), 'target_field'),
]

More examples of modifiers:

Method Mappings

A mapping method allows to execute arbitrary code and return one or many fields:

@mapping
def compute_state(self, record):
    # compute some state, using the ``record`` or not
    state = 'pending'
    return {'state': state}

We can also specify that a mapping methods should be applied only when an object is created, and never applied on further updates:

@only_create
@mapping
def default_warehouse(self, record):
    # get default warehouse
    warehouse_id = ...
    return {'warehouse_id': warehouse_id}
Submappings

When a record contains sub-items, like the lines of a sales order, we can convert the children using another Mapper:

children = [('items', 'line_ids', 'model.name')]

It allows to create the sales order and all its lines with the same call to odoo.models.BaseModel.create().

When using children for items of a record, we need to create a Mapper for the model of the items, and optionally a MapChild.

Usage of a Mapper:

>>> mapper = self.component(usage='mapper')
>>> map_record = mapper.map_record(record)
>>> values = map_record.values()
>>> values = map_record.values(only_create=True)
>>> values = map_record.values(fields=['name', 'street'])

Example of making ‘children’ records as ‘create_only’:

class FooSaleOrderMapper(Component):
    _name = 'foo.sale.order.import.mapper'
    _inherit = 'foo.import.mapper'
    _apply_on = 'foo.sale.order'

    children = [('items', 'line_ids', 'foo.sale.order.line')]


class SaleLineMapChild(Component):
    _name = 'foo.sale.line.import.map.child'
    _inherit = ['foo.connector', 'base.map.child.import']
    _apply_on = 'foo.sale.order.line'

    def skip_item(self, map_record):
        record = map_record.source
        if not record['attributes']['quantity']:
            return True

    def get_item_values(self, map_record, to_attr, options):
        values = map_record.values(**options)
        binder = self.binder_for()
        binding = binder.to_internal(map_record.source['id'])
        if binding:
            # already exists, keeps the id
            values['id'] = binding.id
        return values

    def format_items(self, items_values):
        # if we already have an ID (found in get_item_values())
        # we change the command to update the existing record
        items = []
        for item in items_values[:]:
            if item.get('id'):
                binding_id = item.pop('id')
                # update the record
                items.append((1, binding_id, item))
            else:
                # create the record
                items.append((0, 0, item))
        return items
_name = 'base.mapper'

Name of the component

_inherit = 'base.connector'

Name or list of names of the component(s) to inherit from

_usage = 'mapper'

Component purpose (‘import.mapper’, …).

property map_methods

Yield all the methods decorated with @mapping

property options

Options can be accessed in the mapping methods with self.options.

changed_by_fields()[source]

Build a set of fields used by the mapper

It takes in account the direct fields and the fields used by the decorator: changed_by.

map_record(record, parent=None)[source]

Get a MapRecord with record, ready to be converted using the current Mapper.

Parameters:
  • record – record to transform
  • parent – optional parent record, for items
finalize(map_record, values)[source]

Called at the end of the mapping.

Can be used to modify the values generated by all the mappings before returning them.

Parameters:
  • map_record (MapRecord) – source map_record
  • values – mapped values
Returns:

mapped values

Return type:

dict

class connector.components.mapper.ImportMapper(work_context)[source]

Bases: odoo.addons.component.core.AbstractComponent

Mapper for imports.

Transform a record from a backend to an Odoo record

_name = 'base.import.mapper'

Name of the component

_inherit = 'base.mapper'

Name or list of names of the component(s) to inherit from

_usage = 'import.mapper'

Component purpose (‘import.mapper’, …).

class connector.components.mapper.ExportMapper(work_context)[source]

Bases: odoo.addons.component.core.AbstractComponent

Mapper for exports.

Transform a record from Odoo to a backend record

_name = 'base.export.mapper'

Name of the component

_inherit = 'base.mapper'

Name or list of names of the component(s) to inherit from

_usage = 'export.mapper'

Component purpose (‘import.mapper’, …).

class connector.components.mapper.MapChild(work_context)[source]

Bases: odoo.addons.component.core.AbstractComponent

MapChild is responsible to convert items.

Items are sub-records of a main record. In this example, the items are the records in lines:

sales = {'name': 'SO10',
         'lines': [{'product_id': 1, 'quantity': 2},
                   {'product_id': 2, 'quantity': 2}]}

A MapChild is always called from another Mapper which provides a children configuration.

Considering the example above, the “main” Mapper would returns something as follows:

{'name': 'SO10',
         'lines': [(0, 0, {'product_id': 11, 'quantity': 2}),
                   (0, 0, {'product_id': 12, 'quantity': 2})]}

A MapChild is responsible to:

  • Find the Mapper to convert the items
  • Possibly filter out some lines (can be done by inheriting skip_item())
  • Convert the items’ records using the found Mapper
  • Format the output values to the format expected by Odoo or the backend (as seen above with (0, 0, {values})

A MapChild can be extended like any other Component. However, it is not mandatory to explicitly create a MapChild for each children mapping, the default one will be used (ImportMapChild or ExportMapChild).

The implementation by default does not take care of the updates: if I import a sales order 2 times, the lines will be duplicated. This is not a problem as long as an importation should only support the creation (typical for sales orders). It can be implemented on a case-by-case basis by inheriting get_item_values() and format_items().

_name = 'base.map.child'

Name of the component

_inherit = 'base.connector'

Name or list of names of the component(s) to inherit from

_usage = None

Component purpose (‘import.mapper’, …).

skip_item(map_record)[source]

Hook to implement in sub-classes when some child records should be skipped.

The parent record is accessible in map_record. If it returns True, the current child record is skipped.

Parameters:map_record (MapRecord) – record that we are converting
get_items(items, parent, to_attr, options)[source]

Returns the formatted output values of items from a main record

Parameters:
  • items (list) – list of item records
  • parent – parent record
  • to_attr (str) – destination field (can be used for introspecting the relation)
  • options – dict of options, herited from the main mapper
Returns:

formatted output values for the item

get_item_values(map_record, to_attr, options)[source]

Get the raw values from the child Mappers for the items.

It can be overridden for instance to:

  • Change options
  • Use a Binder to know if an item already exists to modify an existing item, rather than to add it
Parameters:
  • map_record (MapRecord) – record that we are converting
  • to_attr (str) – destination field (can be used for introspecting the relation)
  • options – dict of options, herited from the main mapper
format_items(items_values)[source]

Format the values of the items mapped from the child Mappers.

It can be overridden for instance to add the Odoo relationships commands (6, 0, [IDs]), …

As instance, it can be modified to handle update of existing items: check if an ‘id’ has been defined by get_item_values() then use the (1, ID, {values}) command

Parameters:items_values (list) – mapped values for the items
class connector.components.mapper.ImportMapChild(work_context)[source]

Bases: odoo.addons.component.core.AbstractComponent

MapChild for the Imports

_name = 'base.map.child.import'

Name of the component

_inherit = 'base.map.child'

Name or list of names of the component(s) to inherit from

_usage = 'import.map.child'

Component purpose (‘import.mapper’, …).

format_items(items_values)[source]

Format the values of the items mapped from the child Mappers.

It can be overridden for instance to add the Odoo relationships commands (6, 0, [IDs]), …

As instance, it can be modified to handle update of existing items: check if an ‘id’ has been defined by get_item_values() then use the (1, ID, {values}) command

Parameters:items_values (list) – list of values for the items to create
class connector.components.mapper.ExportMapChild(work_context)[source]

Bases: odoo.addons.component.core.AbstractComponent

MapChild for the Exports

_name = 'base.map.child.export'

Name of the component

_inherit = 'base.map.child'

Name or list of names of the component(s) to inherit from

_usage = 'export.map.child'

Component purpose (‘import.mapper’, …).

class connector.components.mapper.MapRecord(mapper, source, parent=None)[source]

Bases: object

A record prepared to be converted using a Mapper.

MapRecord instances are prepared by Mapper.map_record().

Usage:

>>> map_record = mapper.map_record(record)
>>> output_values = map_record.values()

See values() for more information on the available arguments.

update(*args, **kwargs)[source]

Force values to be applied after a mapping.

Usage:

>>> map_record = mapper.map_record(record)
>>> map_record.update(a=1)
>>> output_values = map_record.values()
# output_values will at least contain {'a': 1}

The values assigned with update() are in any case applied, they have a greater priority than the mapping values.

values(for_create=None, fields=None, **kwargs)[source]

Build and returns the mapped values according to the options.

Usage:

>>> map_record = mapper.map_record(record)
>>> output_values = map_record.values()
Creation of records

When using the option for_create, only the mappings decorated with @only_create will be mapped.

>>> output_values = map_record.values(for_create=True)
Filter on fields

When using the fields argument, the mappings will be filtered using either the source key in direct arguments, either the changed_by arguments for the mapping methods.

>>> output_values = map_record.values(
        fields=['name', 'street']
    )
Custom options

Arbitrary key and values can be defined in the kwargs arguments. They can later be used in the mapping methods using self.options.

>>> output_values = map_record.values(tax_include=True)
Parameters:
  • for_create (boolean) – specify if only the mappings for creation (@only_create) should be mapped.
  • fields (list) – filter on fields
  • **kwargs – custom options, they can later be used in the mapping methods
property parent

Parent record if the current record is an item

property source

Source record to be converted

connector.components.mapper.changed_by(*args)[source]

Decorator for the mapping methods (mapping())

When fields are modified in Odoo, we want to export only the modified fields. Using this decorator, we can specify which fields updates should trigger which mapping method.

If changed_by is empty, the mapping is always active.

As far as possible, this decorator should be used for the exports, thus, when we do an update on only a small number of fields on a record, the size of the output record will be limited to only the fields really having to be exported.

Usage:

@changed_by('input_field')
@mapping
def any(self, record):
    return {'output_field': record['input_field']}
Parameters:*args – field names which trigger the mapping when modified
connector.components.mapper.convert(field, conv_type)[source]

A modifier intended to be used on the direct mappings.

Convert a field’s value to a given type.

Example:

direct = [(convert('source', str), 'target')]
Parameters:
  • field – name of the source field in the record
  • binding – True if the relation is a binding record
connector.components.mapper.external_to_m2o(field, binding=None)[source]

A modifier intended to be used on the direct mappings.

For a field from a backend which is an ID, search the corresponding binding in Odoo and returns it.

When the field’s relation is not a binding (i.e. it does not point to something like magento.*), the binding model needs to be provided in the binding keyword argument.

Example:

direct = [(external_to_m2o('country', binding='magento.res.country'),
           'country_id'),
          (external_to_m2o('country'), 'magento_country_id')]
Parameters:
  • field – name of the source field in the record
  • binding – name of the binding model is the relation is not a binding
connector.components.mapper.follow_m2o_relations(field)[source]

A modifier intended to be used on direct mappings.

‘Follows’ Many2one relations and return the final field value.

Examples:

Assuming model is product.product:

direct = [
    (follow_m2o_relations('product_tmpl_id.categ_id.name'), 'cat')]
Parameters:field – field “path”, using dots for relations as usual in Odoo
connector.components.mapper.m2o_to_external(field, binding=None)[source]

A modifier intended to be used on the direct mappings.

For a many2one, get the external ID and returns it.

When the field’s relation is not a binding (i.e. it does not point to something like magento.*), the binding model needs to be provided in the binding keyword argument.

Example:

direct = [(m2o_to_external('country_id',
                           binding='magento.res.country'), 'country'),
          (m2o_to_external('magento_country_id'), 'country')]
Parameters:
  • field – name of the source field in the record
  • binding – name of the binding model is the relation is not a binding
connector.components.mapper.mapping(func)[source]

Decorator, declare that a method is a mapping method.

It is then used by the Mapper to convert the records.

Usage:

@mapping
def any(self, record):
    return {'output_field': record['input_field']}
connector.components.mapper.none(field)[source]

A modifier intended to be used on the direct mappings.

Replace the False-ish values by None. It can be used in a pipeline of modifiers when .

Example:

direct = [(none('source'), 'target'),
          (none(m2o_to_external('rel_id'), 'rel_id')]
Parameters:
  • field – name of the source field in the record
  • binding – True if the relation is a binding record
connector.components.mapper.only_create(func)[source]

Decorator for the mapping methods (mapping())

A mapping decorated with only_create means that it has to be used only for the creation of the records.

Usage:

@only_create
@mapping
def any(self, record):
    return {'output_field': record['input_field']}

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.

class connector.components.backend_adapter.BackendAdapter(work_context)[source]

Bases: odoo.addons.component.core.AbstractComponent

Base Backend Adapter for the connectors

_name = 'base.backend.adapter'

Name of the component

_inherit = 'base.connector'

Name or list of names of the component(s) to inherit from

_usage = 'backend.adapter'

Component purpose (‘import.mapper’, …).

class connector.components.backend_adapter.CRUDAdapter(work_context)[source]

Bases: odoo.addons.component.core.AbstractComponent

Base External Adapter specialized in the handling of records on external systems.

This is an empty shell, Components can inherit and implement their own implementation for the methods.

_name = 'base.backend.adapter.crud'

Name of the component

_inherit = 'base.backend.adapter'

Name or list of names of the component(s) to inherit from

_usage = 'backend.adapter'

Component purpose (‘import.mapper’, …).

search(*args, **kwargs)[source]

Search records according to some criterias and returns a list of ids

read(*args, **kwargs)[source]

Returns the information of a record

search_read(*args, **kwargs)[source]

Search records according to some criterias and returns their information

create(*args, **kwargs)[source]

Create a record on the external system

write(*args, **kwargs)[source]

Update records on the external system

delete(*args, **kwargs)[source]

Delete a record on the external system

Synchronizer

A synchronizer orchestrates a synchronization with a backend. It’s the actor who runs the flow and glues the logic of an import or export (or else). It uses other components for specialized tasks.

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.

class connector.components.synchronizer.Synchronizer(work_context)[source]

Bases: odoo.addons.component.core.AbstractComponent

Base class for synchronizers

_name = 'base.synchronizer'

Name of the component

_inherit = 'base.connector'

Name or list of names of the component(s) to inherit from

_base_mapper_usage = 'mapper'

usage of the component used as mapper, can be customized in sub-classes

_base_backend_adapter_usage = 'backend.adapter'

usage of the component used as backend adapter, can be customized in sub-classes

run()[source]

Run the synchronization

property mapper

Return an instance of Mapper for the synchronization.

The instanciation is delayed because some synchronisations do not need such an unit and the unit may not exist.

It looks for a Component with _usage being equal to _base_mapper_usage.

Return type:odoo.addons.component.core.Component
property binder

Return an instance of Binder for the synchronization.

The instanciation is delayed because some synchronisations do not need such an unit and the unit may not exist.

Return type:odoo.addons.component.core.Component
property backend_adapter

Return an instance of BackendAdapter for the synchronization.

The instanciation is delayed because some synchronisations do not need such an unit and the unit may not exist.

It looks for a Component with _usage being equal to _base_backend_adapter_usage.

Return type:odoo.addons.component.core.Component
class connector.components.synchronizer.Exporter(work_context)[source]

Bases: odoo.addons.component.core.AbstractComponent

Synchronizer for exporting data from Odoo to a backend

_name = 'base.exporter'

Name of the component

_inherit = 'base.synchronizer'

Name or list of names of the component(s) to inherit from

_usage = 'exporter'

Component purpose (‘import.mapper’, …).

_base_mapper_usage = 'export.mapper'

usage of the component used as mapper, can be customized in sub-classes

class connector.components.synchronizer.GenericExporter(working_context)[source]

Bases: odoo.addons.component.core.AbstractComponent

Generic Synchronizer for exporting data from Odoo to a backend

_name = 'generic.exporter'

Name of the component

_inherit = 'base.exporter'

Name or list of names of the component(s) to inherit from

_default_binding_field = None
_should_import()[source]
_delay_import()[source]

Schedule an import of the record.

Adapt in the sub-classes when the model is not imported using import_record.

run(binding, *args, **kwargs)[source]

Run the synchronization

Parameters:binding – binding record to export
_run(fields=None)[source]

Flow of the synchronization, implemented in inherited classes

_after_export()[source]

Can do several actions after exporting a record on the backend

_lock()[source]

Lock the binding record.

Lock the binding record so we are sure that only one export job is running for this record if concurrent jobs have to export the same record.

When concurrent jobs try to export the same record, the first one will lock and proceed, the others will fail to lock and will be retried later.

This behavior works also when the export becomes multilevel with _export_dependencies(). Each level will set its own lock on the binding record it has to export.

_has_to_skip()[source]

Return True if the export can be skipped

_retry_unique_violation()[source]

Context manager: catch Unique constraint error and retry the job later.

When we execute several jobs workers concurrently, it happens that 2 jobs are creating the same record at the same time (binding record created by _export_dependency()), resulting in:

IntegrityError: duplicate key value violates unique constraint “my_backend_product_product_odoo_uniq” DETAIL: Key (backend_id, odoo_id)=(1, 4851) already exists.

In that case, we’ll retry the import just later.

Warning

The unique constraint must be created on the binding record to prevent 2 bindings to be created for the same External record.

_export_dependency(relation, binding_model, component_usage='record.exporter', binding_field=None, binding_extra_vals=None)[source]

Export a dependency. The exporter class is a subclass of GenericExporter. If a more precise class need to be defined, it can be passed to the exporter_class keyword argument.

Warning

a commit is done at the end of the export of each dependency. The reason for that is that we pushed a record on the backend and we absolutely have to keep its ID.

So you must take care not to modify the Odoo database during an export, excepted when writing back the external ID or eventually to store external data that we have to keep on this side.

You should call this method only at the beginning of the exporter synchronization, in _export_dependencies().

Parameters:
  • relation (odoo.models.BaseModel) – record to export if not already exported
  • binding_model (str | unicode) – name of the binding model for the relation
  • component_usage – ‘usage’ to look for to find the Component to for the export, by default ‘record.exporter’
  • binding_field (str | unicode) – name of the one2many field on a normal record that points to the binding record (default: my_backend_bind_ids). It is used only when the relation is not a binding but is a normal record.
Binding_extra_vals:
 

In case we want to create a new binding pass extra values for this binding

_export_dependencies()[source]

Export the dependencies for the record

_map_data()[source]

Returns an instance of MapRecord

_validate_create_data(data)[source]

Check if the values to import are correct

Pro-actively check before the Model.create if some fields are missing or invalid

Raise InvalidDataError

_validate_update_data(data)[source]

Check if the values to import are correct

Pro-actively check before the Model.update if some fields are missing or invalid

Raise InvalidDataError

_create_data(map_record, fields=None, **kwargs)[source]

Get the data to pass to _create()

_create(data)[source]

Create the External record

_update_data(map_record, fields=None, **kwargs)[source]

Get the data to pass to _update()

_update(data)[source]

Update an External record

class connector.components.synchronizer.Importer(work_context)[source]

Bases: odoo.addons.component.core.AbstractComponent

Synchronizer for importing data from a backend to Odoo

_name = 'base.importer'

Name of the component

_inherit = 'base.synchronizer'

Name or list of names of the component(s) to inherit from

_usage = 'importer'

Component purpose (‘import.mapper’, …).

_base_mapper_usage = 'import.mapper'

usage of the component used as mapper, can be customized in sub-classes

class connector.components.synchronizer.Deleter(work_context)[source]

Bases: odoo.addons.component.core.AbstractComponent

Synchronizer for deleting a record on the backend

_name = 'base.deleter'

Name of the component

_inherit = 'base.synchronizer'

Name or list of names of the component(s) to inherit from

_usage = 'deleter'

usage of the component used as mapper, can be customized in sub-classes

Listeners

Listeners are Components notified when events happen. Documentation in odoo.addons.component_event.components.event

The base listener for the connectors add a method ConnectorListener.no_connector_export() which can be used with odoo.addons.component_event.skip_if().

class connector.components.listener.ConnectorListener(work_context)[source]

Bases: odoo.addons.component.core.AbstractComponent

Base Backend Adapter for the connectors

_name = 'base.connector.listener'

Name of the component

_inherit = ['base.connector', 'base.event.listener']

Name or list of names of the component(s) to inherit from

no_connector_export(record)[source]

Return if the ‘connector_no_export’ has been set in context

To be used with odoo.addons.component_event.skip_if() on Events:

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


class MyEventListener(Component):
    _name = 'my.event.listener'
    _inherit = 'base.connector.event.listener'
    _apply_on = ['magento.res.partner']

    @skip_if(lambda: self, record, *args, **kwargs:
             self.no_connector_export(record))
    def on_record_write(self, record, fields=None):
        record.with_delay().export_record()
class connector.components.locker.RecordLocker(work_context)[source]

Bases: odoo.addons.component.core.Component

Component allowing to lock record(s) for the current transaction

Example of usage:

self.component('record.locker').lock(self.records)

See the definition of lock() for details.

_name = 'base.record.locker'

Name of the component

_inherit = ['base.connector']

Name or list of names of the component(s) to inherit from

_usage = 'record.locker'

Component purpose (‘import.mapper’, …).

lock(records, seconds=None, ignore_retry=True)[source]

Lock the records.

Lock the record so we are sure that only one job is running for this record(s) if concurrent jobs have to run a job for the same record(s). When concurrent jobs try to work on the same record(s), the first one will lock and proceed, the others will fail to acquire it and will be retried later (RetryableJobError is raised).

The lock is using a FOR UPDATE NOWAIT so any concurrent transaction trying FOR UPDATE/UPDATE will be rejected until the current transaction is committed or rollbacked.

A classical use case for this is to prevent concurrent exports.

The following parameters are forwarded to the exception RetryableJobError

Parameters:
  • seconds – In case of retry because the lock cannot be acquired, in how many seconds it must be retried. If not set, the queue_job configuration is used.
  • ignore_retry – If True, the retry counter of the job will not be increased.

Components Collection Model

Collection Model

This is the base Model shared by all the Collections. In the context of the Connector, a collection is the Backend. The _name given to the Collection Model will be the name to use in the _collection of the Components usable for the Backend.

class odoo.addons.component.models.collection.Collection[source]

Bases: odoo.models.BaseModel

The model on which components are subscribed

It would be for instance the backend for the connectors.

Example:

class MagentoBackend(models.Model):
    _name = 'magento.backend'  # name of the collection
    _inherit = 'collection.base'


class MagentoSaleImporter(Component):
    _name = 'magento.sale.importer'
    _apply_on = 'magento.sale.order'
    _collection = 'magento.backend'  # name of the collection

    def run(self, magento_id):
        mapper = self.component(usage='import.mapper')
        extra_mappers = self.many_components(
            usage='import.mapper.extra',
        )
        # ...

Use it:

>>> backend = self.env['magento.backend'].browse(1)
>>> with backend.work_on('magento.sale.order') as work:
...     importer = work.component(usage='magento.sale.importer')
...     importer.run(1)

See also: odoo.addons.component.core.WorkContext

work_on(model_name, **kwargs)[source]

Entry-point for the components, context manager

Start a work using the components on the model. Any keyword argument will be assigned to the work context. See documentation of odoo.addons.component.core.WorkContext.

It is a context manager, so you can attach objects and clean them at the end of the work session, such as:

@contextmanager
@api.multi
def work_on(self, model_name, **kwargs):
    self.ensure_one()
    magento_location = MagentoLocation(
        self.location,
        self.username,
        self.password,
    )
    # We create a Magento Client API here, so we can create the
    # client once (lazily on the first use) and propagate it
    # through all the sync session, instead of recreating a client
    # in each backend adapter usage.
    with MagentoAPI(magento_location) as magento_api:
        _super = super(MagentoBackend, self)
        # from the components we'll be able to do:
        # self.work.magento_api
        with _super.work_on(
                model_name, magento_api=magento_api, **kwargs
                ) as work:
            yield work

Components Exceptions

exception odoo.addons.component.exception.ComponentException[source]

Bases: Exception

Base Exception for the components

exception odoo.addons.component.exception.NoComponentError[source]

Bases: odoo.addons.component.exception.ComponentException

No component has been found

exception odoo.addons.component.exception.SeveralComponentError[source]

Bases: odoo.addons.component.exception.ComponentException

More than one component have been found

Components Internals

Low-level APIs of the Components.

Core

Core classes for the components. The most common classes used publicly are:

class odoo.addons.component.core.ComponentDatabases[source]

Bases: dict

Holds a registry of components for each database

class odoo.addons.component.core.ComponentRegistry(cachesize=512)[source]

Bases: object

Store all the components and allow to find them using criteria

The key is the _name of the components.

This is an OrderedDict, because we want to keep the registration order of the components, addons loaded first have their components found first.

The ready attribute must be set to True when all the components are loaded.

get(key, default=None)[source]
load_components(module)[source]
lookup(collection_name=None, usage=None, model_name=None)[source]

Find and return a list of components for a usage

If a component is not registered in a particular collection (no _collection), it will be returned in any case (as far as the usage and model_name match). This is useful to share generic components across different collections.

If no collection name is given, components from any collection will be returned.

Then, the components of a collection are filtered by usage and/or model. The _usage is mandatory on the components. When the _model_name is empty, it means it can be used for every models, and it will ignore the model_name argument.

The abstract components are never returned.

This is a rather low-level function, usually you will use the high-level AbstractComponent.component(), AbstractComponent.many_components() or even AbstractComponent.component_by_name().

Parameters:
  • collection_name – the name of the collection the component is registered into.
  • usage – the usage of component we are looking for
  • model_name – filter on components that apply on this model
class odoo.addons.component.core.WorkContext(model_name=None, collection=None, components_registry=None, **kwargs)[source]

Bases: object

Transport the context required to work with components

It is propagated through all the components, so any data or instance (like a random RPC client) that need to be propagated transversally to the components should be kept here.

Including:

model_name

Name of the model we are working with. It means that any lookup for a component will be done for this model. It also provides a shortcut as a model attribute to use directly with the Odoo model from the components

collection

The collection we are working with. The collection is an Odoo Model that inherit from ‘collection.base’. The collection attribute can be a record or an “empty” model.

model

Odoo Model for model_name with the same Odoo Environment than the collection attribute.

This is also the entrypoint to work with the components.

collection = self.env['my.collection'].browse(1)
work = WorkContext(model_name='res.partner', collection=collection)
component = work.component(usage='record.importer')

Usually you will use the context manager on the collection.base Model:

collection = self.env['my.collection'].browse(1)
with collection.work_on('res.partner') as work:
    component = work.component(usage='record.importer')

It supports any arbitrary keyword arguments that will become attributes of the instance, and be propagated throughout all the components.

collection = self.env['my.collection'].browse(1)
with collection.work_on('res.partner', hello='world') as work:
    assert work.hello == 'world'

When you need to work on a different model, a new work instance will be created for you when you are using the high-level API. This is what happens under the hood:

collection = self.env['my.collection'].browse(1)
with collection.work_on('res.partner', hello='world') as work:
    assert work.model_name == 'res.partner'
    assert work.hello == 'world'
    work2 = work.work_on('res.users')
    # => spawn a new WorkContext with a copy of the attributes
    assert work2.model_name == 'res.users'
    assert work2.hello == 'world'
property env

Return the current Odoo env

This is the environment of the current collection.

property model

Return the current Odoo model

This is the model of the current environment.

work_on(model_name=None, collection=None)[source]

Create a new work context for another model keeping attributes

Used when one need to lookup components for another model.

component_by_name(name, model_name=None)[source]

Return a component by its name

If the component exists, an instance of it will be returned, initialized with the current WorkContext.

A odoo.addons.component.exception.NoComponentError is raised if:

  • no component with this name exists
  • the _apply_on of the found component does not match with the current working model

In the latter case, it can be an indication that you need to switch to a different model, you can do so by providing the model_name argument.

component(usage=None, model_name=None, **kw)[source]

Find a component by usage and model for the current collection

It searches a component using the rules of ComponentRegistry.lookup(). When a component is found, it initialize it with the current WorkContext and returned.

A component with a _apply_on matching the asked model_name takes precedence over a generic component without _apply_on. A component with a _collection matching the current collection takes precedence over a generic component without _collection. This behavior allows to define generic components across collections and/or models and override them only for a particular collection and/or model.

A odoo.addons.component.exception.SeveralComponentError is raised if more than one component match for the provided usage/model_name.

A odoo.addons.component.exception.NoComponentError is raised if no component is found for the provided usage/model_name.

many_components(usage=None, model_name=None, **kw)[source]

Find many components by usage and model for the current collection

It searches a component using the rules of ComponentRegistry.lookup(). When components are found, they initialized with the current WorkContext and returned as a list.

If no component is found, an empty list is returned.

class odoo.addons.component.core.MetaComponent(name, bases, attrs)[source]

Bases: type

Metaclass for Components

Every new Component will be added to _modules_components, that will be used by the component builder.

property apply_on_models
class odoo.addons.component.core.AbstractComponent(work_context)[source]

Bases: object

Main Component Model

All components have a Python inheritance either on AbstractComponent or either on Component.

Abstract Components will not be returned by lookups on components, however they can be used as a base for other Components through inheritance (using _inherit).

Inheritance mechanism

The inheritance mechanism is like the Odoo’s one for Models. Each component has a _name. This is the absolute minimum in a Component class.

class MyComponent(Component):
    _name = 'my.component'

    def speak(self, message):
        print message

Every component implicitly inherit from the ‘base’ component.

There are two close but distinct inheritance types, which look familiar if you already know Odoo. The first uses _inherit with an existing name, the name of the component we want to extend. With the following example, my.component is now able to speak and to yell.

class MyComponent(Component):  # name of the class does not matter
    _inherit = 'my.component'

    def yell(self, message):
        print message.upper()

The second has a different _name, it creates a new component, including the behavior of the inherited component, but without modifying it. In the following example, my.component is still able to speak and to yell (brough by the previous inherit), but not to sing. another.component is able to speak, to yell and to sing.

class AnotherComponent(Component):
    _name = 'another.component'
    _inherit = 'my.component'

    def sing(self, message):
        print message.upper()
Registration and lookups

It is handled by 3 attributes on the class:

_collection
The name of the collection where we want to register the component. This is not strictly mandatory as a component can be shared across several collections. But usually, you want to set a collection to segregate the components for a domain. A collection can be for instance magento.backend. It is also the name of a model that inherits from collection.base. See also WorkContext and Collection.
_apply_on
List of names or name of the Odoo model(s) for which the component can be used. When not set, the component can be used on any model.
_usage
The collection and the model (_apply_on) will help to filter the candidate components according to our working context (e.g. I’m working on magento.backend with the model magento.res.partner). The usage will define what kind of task the component we are looking for serves to. For instance, it might be record.importer, export.mapper`… but you can be as creative as you want.

Now, to get a component, you’ll likely use WorkContext.component() when you start to work with components in your flow, but then from within your components, you are more likely to use one of:

Declaration of some Components can look like:

class FooBar(models.Model):
    _name = 'foo.bar.collection'
    _inherit = 'collection.base'  # this inherit is required


class FooBarBase(AbstractComponent):
    _name = 'foo.bar.base'
    _collection = 'foo.bar.collection'  # name of the model above


class Foo(Component):
    _name = 'foo'
    _inherit = 'foo.bar.base'  # we will inherit the _collection
    _apply_on = 'res.users'
    _usage = 'speak'

    def utter(self, message):
        print message


class Bar(Component):
    _name = 'bar'
    _inherit = 'foo.bar.base'  # we will inherit the _collection
    _apply_on = 'res.users'
    _usage = 'yell'

    def utter(self, message):
        print message.upper() + '!!!'


class Vocalizer(Component):
    _name = 'vocalizer'
    _inherit = 'foo.bar.base'
    _usage = 'vocalizer'
    # can be used for any model

    def vocalize(action, message):
        self.component(usage=action).utter(message)

And their usage:

>>> coll = self.env['foo.bar.collection'].browse(1)
>>> with coll.work_on('res.users') as work:
...     vocalizer = work.component(usage='vocalizer')
...     vocalizer.vocalize('speak', 'hello world')
...
hello world
...     vocalizer.vocalize('yell', 'hello world')
HELLO WORLD!!!

Hints:

  • If you want to create components without _apply_on, choose a _usage that will not conflict other existing components.
  • Unless this is what you want and in that case you use many_components() which will return all components for a usage with a matching or a not set _apply_on.
  • It is advised to namespace the names of the components (e.g. magento.xxx) to prevent conflicts between addons.
property collection

Collection we are working with

property env

Current Odoo environment, the one of the collection record

property model

The model instance we are working with

component_by_name(name, model_name=None)[source]

Return a component by its name

Shortcut to meth:~WorkContext.component_by_name

component(usage=None, model_name=None, **kw)[source]

Return a component

Shortcut to meth:~WorkContext.component

many_components(usage=None, model_name=None, **kw)[source]

Return several components

Shortcut to meth:~WorkContext.many_components

class odoo.addons.component.core.Component(work_context)[source]

Bases: odoo.addons.component.core.AbstractComponent

Concrete Component class

This is the class you inherit from when you want your component to be registered in the component collections.

Look in AbstractComponent for more details.

Components Builder

Build the components at the build of a registry.

class odoo.addons.component.builder.ComponentBuilder[source]

Bases: odoo.models.BaseModel

Build the component classes

And register them in a global registry.

Every time an Odoo registry is built, the know components are cleared and rebuilt as well. The Component classes are built using the same mechanism than Odoo’s Models: a final class is created, taking every Components with a _name and applying Components with an _inherits upon them.

The final Component classes are registered in global registry.

This class is an Odoo model, allowing us to hook the build of the components at the end of the Odoo’s registry loading, using _register_hook. This method is called after all modules are loaded, so we are sure that we have all the components Classes and in the correct order.

build_registry(components_registry, states=None, exclude_addons=None)[source]
load_components(module, components_registry=None)[source]

Build every component known by MetaComponent for an odoo module

The final component (composed by all the Component classes in this module) will be pushed into the registry.

Parameters:
  • module (str | unicode) – the name of the addon for which we want to load the components
  • registry (ComponentRegistry) – the registry in which we want to put the Component