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 : 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.

Paramètres
  • 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 : AbstractComponent

Pour un enregistrement d’un modèle, capable de trouver un ID externe ou interne, ou crée la liaison entre eux

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

Paramètres
  • external_id – ID externe pour lequel on veut l’ID Odoo

  • unwrap – if True, returns the normal record else return the binding record

Renvoie

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

Type renvoyé

recordset

to_external(binding, wrap=False)[source]

Give the external ID for an Odoo binding ID

Paramètres
  • 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

Renvoie

ID externe de l’enregistrement

bind(external_id, binding)[source]

Crée le lien entre un ID externe et un ID Odoo

Paramètres
  • external_id – ID externe à lier

  • binding (int) – Odoo record to bind

unwrap_binding(binding)[source]

Pour un enregistrement de liaison, donne l’enregistrement normal

Exemple : si appelé avec un ID de magento.product.product, il va renvoyer l’ID correspondant du product.product.

Paramètres

browse – si Vrai, renvoie une instance de browse_record à la place d’un ID

unwrap_model()[source]

Pour un modèle de liaison, donne le modèle normal.

Exemple : si appelé sur une binder pour magento.product.product, il va renvoyer product.product.

Mappeurs

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

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

Bases : AbstractComponent

Un Mappeur convertit un enregistrement externe en enregistrement Odoo et vice versa. La sortie d’un Traducteur est un dict.

3 types de mappings sont pris en charge :

Mappings directs

Exemples

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

Ici le champs source sera copié vers le champ target.

Un modificateur peut être utilisé dans l’item source. Le modificateur sera appliqué au champ source avant d’être copié dans le champ destination. Il doit être une fonction de type closure pour respecter cet idiome

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

Et utilisé de cette façon

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

Un exemple plus concret de modificateur

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

Et utilisé de cette façon

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

D’autres exemples de modificateurs

Mappings par méthode

Un méthode de mapping permet d’exécuter un code arbitraire et de renvoyer un ou plusieurs champs

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

Nous pouvons aussi préciser qu’une méthode de mapping doit être appliquée lorsqu’un objet est créé, mais jamais appliquée sur les mises à jour ultérieures

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

Lorsqu’un enregistrement contient des sous-items, comme les lignes d’une commande, nous pouvons convertir les enfants grâce à une autre Mappeur

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

Il permet de créer des commandes et toutes ses lignes dans le même appel à odoo.models.BaseModel.create().

En utilisant des children pour les items d’un enregistrement, nous devons créer un Mapper pour le modèle des items, et de manière optionnelle un MapChild.

Utilisation d’un Mappeur

>>> 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'])
_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

Émets toutes les méthodes décorées avec @mapping

property options

Des options peuvent être accédées dans les méthodes de mapping avec 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]

Récupère un MapRecord avec enregistrement, prêt à être converti avec le Mappeur en cours.

Paramètres
  • record – enregistrement à transformer

  • parent – enregistrement parent optionnel, pour les items

finalize(map_record, values)[source]

Appelé à la fin du mapping.

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

Paramètres
  • map_record (MapRecord) – map_record source

  • values – valeurs mappées

Renvoie

valeurs mappées

Type renvoyé

dict

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

Bases : AbstractComponent

Mapper pour les imports.

Transforme un enregistrement depuis un backend vers un enregistrement Odoo

_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 : AbstractComponent

Mapper pour les exports.

Transforme un enregistrement depuis Odoo vers un enregistrement du backend

_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 : AbstractComponent

MapChild est responsable de la conversion des items.

Les items sont des sous-enregistrements d’un enregistrement principal. Dans cet exemple, les items sont les enregistrements qui sont dans lines

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

Un MapChild est toujours appelé depuis un autre Mapper qui fournit une configuration children.

Compte tenu de l’exemple ci-dessus, le Mapper « principal » renverrait quelque chose comme ça

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

Un MapChild est responsable de :

  • Trouver le Mapper pour convertir les items

  • Éventuellement exclure certaines lignes (en surchargeant skip_item())

  • Convertir les enregistrements des items en utilisant le Mapper trouvé

  • Formater les valeurs de sortie vers le format attendu par Odoo ou par le backend (comme vu ci-dessus avec (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).

L’implémentation par défaut ne gère pas les mises à jour : si j’importe une commande deux fois, les lignes seront dupliquées. Ceci n’est pas un problème à condition qu’un import ne prenne en charge que la création (typique pour les commandes). Il peut être implémenté au cas par cas en héritant de get_item_values() et 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]

Point d’accroche à implémenter dans les sous-classes lorsque certains enregistrements enfants doivent être sautés.

L’enregistrement parent est accessible dans map_record. S’il renvoie True, l’enregistrement enfant en cours est sauté.

Paramètres

map_record (MapRecord) – enregistrement que nous sommes en train de convertir

get_items(items, parent, to_attr, options)[source]

Renvoie la sortie formatée des valeurs des items pour un enregistrement principal

Paramètres
  • items (list) – liste d’enregistrements d’items

  • parent – enregistrement parent

  • to_attr (str) – champ destination (peut être utilisé pour inspecter la relation)

  • options – dictionnaire des options, hérité du mappeur principal

Renvoie

valeurs de sortie formatées pour l’item

get_item_values(map_record, to_attr, options)[source]

Récupère la valeur brute des Mappeurs enfants depuis les items.

I peut être surchargé par exemple pour :

  • Changer des options

  • Utiliser un Binder pour savoir si un item existe déjà afin de le modifier au lieu de l’ajouter

Paramètres
  • map_record (MapRecord) – enregistrement que nous sommes en train de convertir

  • to_attr (str) – champ destination (peut être utilisé pour inspecter la relation)

  • options – dictionnaire des options, hérité du mappeur principal

format_items(items_values)[source]

Formate les valeurs des items mappés depuis les Mappeurs enfants.

Il peut être surchargé par exemple pour ajouter les commandes de relation d’Odoo (6, 0, [IDs]), …

Par exemple, il peut être modifié pour pour gérer la mise à jour d’items existants : vérifier si un “id” a bien été défini par get_item_values() puis utilise la commande (1, ID, {valeurs})

Paramètres

items_values (list) – valeurs traduites pour les items

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

Bases : AbstractComponent

MapChild pour les 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]

Formate les valeurs des items mappés depuis les Mappeurs enfants.

Il peut être surchargé par exemple pour ajouter les commandes de relation d’Odoo (6, 0, [IDs]), …

Par exemple, il peut être modifié pour pour gérer la mise à jour d’items existants : vérifier si un “id” a bien été défini par get_item_values() puis utilise la commande (1, ID, {valeurs})

Paramètres

items_values (list) – liste des valeurs pour les items à créer

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

Bases : AbstractComponent

MapChild pour les 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

Un enregistrement préparé à être converti en utilisans un Mapper.

Les instances de MapRecord sont préparées par Mapper.map_record().

Utilisation

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

Voir values() pour avoir plus d’informations sur les arguments disponibles.

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

Force des valeurs à appliquer après une conversion.

Utilisation

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

Les valeurs affectées avec update() sont appliquées dans tous les cas, elles ont une priorité supérieure que les valeurs converties.

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

Construit et renvoie les valeurs converties selon les options.

Utilisation

>>> map_record = mapper.map_record(record)
>>> output_values = map_record.values()
Création des enregistrements

En utilisant l’option for_create, seuls les mappings décorés avec @only_create seront mappés.

>>> output_values = map_record.values(for_create=True)
Filtrer sur les champs

En utilisant l’argument fields, les mappings seront filtrés en utilisant soit la clé source dans les arguments direct, soit les arguments changed_by pour les méthodes de mapping.

>>> output_values = map_record.values(
        fields=['name', 'street']
    )
Options spécifiques

Des clé et valeurs arbitraires peuvent être définies dans les arguments kwargs. Elles peuvent être ensuites utilisées dans les méthodes de mapping en utilisant self.options.

>>> output_values = map_record.values(tax_include=True)
Paramètres
  • for_create (boolean) – précise si seuls les mappings de création (@only_create) doivent être mappés.

  • fields (list) – filtre sur les champs

  • **kwargs – options spécifiques, elles peuvent être utilisées plus tard dans les méthodes de mapping

property parent

Enregistrement parent si l’enregistrement en cours est un item

property source

Enregistrement source à convertir

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

Décorateur pour les méthodes de mapping (mapping())

Lorsque des champs sont modifié dans Odoo, nous ne devrions exporter que les champs modifiés. En utilisant ce décorateur, nous pouvons préciser quelles modifications de champs doivent déclencher quelle méthode de mapping.

Si changed_by est vide, le mapping est toujours actif.

Autant que possible, ce décorateur devrait être utilisé pour les exports. De cette façon lorsqu’on fait une modification sur une petite partie des champs d’un enregistrement, la taille de l’enregistrement en sortie sera limitée aux seuls champs devant être exportés.

Utilisation

@changed_by('input_field')
@mapping
def any(self, record):
    return {'output_field': record['input_field']}
Paramètres

*args – noms de champs qui déclenchent le mapping à la modification

connector.components.mapper.convert(field, conv_type)[source]

Un modificateur destiné à être utilisé sur les mapping directs

Convertit une valeur de champ vers un type donné

Exemples

direct = [(convert('source', str), 'target')]
Paramètres
  • field – nom du champ source dans l’enregistrement

  • binding – True si la relation est un enregistrement de binding

connector.components.mapper.external_to_m2o(field, binding=None)[source]

Un modificateur destiné à être utilisé sur les mapping directs

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

Quand la relation du champ n’est pas un binding (càd ne pointe pas vers quelque chose comme magento.*), le modèle de liaison doit être fourni avec l’argument nommé binding.

Exemples

direct = [(external_to_m2o('country', binding='magento.res.country'),
           'country_id'),
          (external_to_m2o('country'), 'magento_country_id')]
Paramètres
  • field – nom du champ source dans l’enregistrement

  • binding – nom du modèle de liaison si la relation n’est pas une liaison

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.

Exemples :

Assuming model is product.product:

direct = [
    (follow_m2o_relations('product_tmpl_id.categ_id.name'), 'cat')]
Paramètres

field – field « path », using dots for relations as usual in Odoo

connector.components.mapper.m2o_to_external(field, binding=None)[source]

Un modificateur destiné à être utilisé sur les mapping directs

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

Quand la relation du champ n’est pas un binding (càd ne pointe pas vers quelque chose comme magento.*), le modèle de liaison doit être fourni avec l’argument nommé binding.

Exemples

direct = [(m2o_to_external('country_id',
                           binding='magento.res.country'), 'country'),
          (m2o_to_external('magento_country_id'), 'country')]
Paramètres
  • field – nom du champ source dans l’enregistrement

  • binding – nom du modèle de liaison si la relation n’est pas une liaison

connector.components.mapper.mapping(func)[source]

Decorator, declare that a method is a mapping method.

C’est ensuite utilisé par le Mapper pour convertir les enregistrements.

Utilisation

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

Un modificateur destiné à être utilisé sur les mapping directs

Remplace les valeurs similaires à False par None. Ceci peut être utilisé dans un enchaînement de modificateurs quand .

Exemples

direct = [(none('source'), 'target'),
          (none(m2o_to_external('rel_id'), 'rel_id')]
Paramètres
  • field – nom du champ source dans l’enregistrement

  • binding – True si la relation est un enregistrement de binding

connector.components.mapper.only_create(func)[source]

Décorateur pour les méthodes de mapping (mapping())

Un mapping décoré avec only_create indique qu’il doit être utilisé uniquement pour a création d’enregistrements.

Utilisation

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

Adaptateur de backend

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 : AbstractComponent

Adaptateur de backend de base pour les connecteurs

_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 : AbstractComponent

Adaptateur externe de base spécialisé dans la gestion des enregistrements des systèmes externes.

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]

Recherche des enregistrements correspondant à certains critères et renvoie une liste d’IDs

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

Renvoie des informations sur l’enregistrement

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

Recherche des enregistrements correspondant à certains critères et renvoie leurs données

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

Crée un enregistrement dans le système externe

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

Met à jour les enregistrements sur le système externe

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

Supprime un enregistrement sur le système externe

Synchroniseur

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 : AbstractComponent

Classe de base des synchroniseurs

_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]

Lance la synchronisation

property mapper

Renvoie une instance de mappeur Mapper pour la synchronisation

L’instanciation est retardée parce que certaines synchronisations n’ont pas besoin d’une telle unité et celle-ci peut ne pas exister.

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

Type renvoyé

odoo.addons.component.core.Component

property binder

Renvoie une instance de liaison Binder pour la synchronisation.

L’instanciation est retardée parce que certaines synchronisations n’ont pas besoin d’une telle unité et celle-ci peut ne pas exister.

Type renvoyé

odoo.addons.component.core.Component

property backend_adapter

Renvoie une instance de BackendAdapter pour la synchronisation.

L’instanciation est retardée parce que certaines synchronisations n’ont pas besoin d’une telle unité et celle-ci peut ne pas exister.

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

Type renvoyé

odoo.addons.component.core.Component

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

Bases : AbstractComponent

Synchroniseur pour exporter des donnés d’Odoo vers un 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 : 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]

Lance la synchronisation

Paramètres

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.

Avertissement

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.

Avertissement

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().

Paramètres
  • 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 : AbstractComponent

Synchroniseur pour importer des données d’un backend vers 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 : AbstractComponent

Synchroniseur pour effacer un enregistrement du 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 : AbstractComponent

Adaptateur de backend de base pour les connecteurs

_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 : 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

Paramètres
  • 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 : BaseModel

The model on which components are subscribed

It would be for instance the backend for the connectors.

Exemples

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
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 : ComponentException

No component has been found

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

Bases : 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().

Paramètres
  • 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.

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 : 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 : 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.

Paramètres
  • 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