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 thebackend'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 ComponentsMost 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 nretry_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
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 champtarget
.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 unMapper
pour le modèle des items, et de manière optionnelle unMapChild
.
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
.
- 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 configurationchildren
.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
orExportMapChild
).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()
etformat_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
- 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
- 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 argumentsdirect
, soit les argumentschanged_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 utilisantself.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
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
- property mapper¶
Renvoie une instance de mappeur
Mapper
pour la synchronisationL’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é
- 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é
- 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é
- 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¶
- _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
- _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.
- _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 theexporter_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 exportedbinding_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
- _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 invalidRaise InvalidDataError
- 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 toTrue
when all the components are loaded.- 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 theusage
andmodel_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 themodel_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 evenAbstractComponent.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 OdooEnvironment
than thecollection
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 currentWorkContext
and returned.A component with a
_apply_on
matching the askedmodel_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 providedusage
/model_name
.A
odoo.addons.component.exception.NoComponentError
is raised if no component is found for the providedusage
/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 currentWorkContext
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 onComponent
.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 fromcollection.base
. See alsoWorkContext
andCollection
.- _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 onmagento.backend
with the modelmagento.res.partner
). The usage will define what kind of task the component we are looking for serves to. For instance, it might berecord.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:component_by_name()
(more rarely though)
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
- 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.- 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