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.- Parameters
lock – The lock name. Can be anything convertible to a string. It needs to represent what should not be synchronized concurrently, usually the string will contain at least: the action, the backend name, the backend id, the model name, the external id
retry_seconds – number of seconds after which a job should be retried when the lock cannot be acquired.
Connector Components¶
Binders¶
Binders are components that know how to find the external ID for an Odoo ID, how to find the Odoo ID for an external ID and how to create the binding between them.
- class connector.components.binder.Binder(work_context)[source]¶
Bases:
AbstractComponent
For one record of a model, capable to find an external or internal id, or create the binding (link) between them
This is a default implementation that can be inherited or reimplemented in the connectors.
This implementation assumes that binding models are
_inherits
of the models they are binding.- _name = 'base.binder'¶
Name of the component
- _inherit = 'base.connector'¶
Name or list of names of the component(s) to inherit from
- _usage = 'binder'¶
Component purpose (‘import.mapper’, …).
- _external_field = 'external_id'¶
- _backend_field = 'backend_id'¶
- _odoo_field = 'odoo_id'¶
- _sync_date_field = 'sync_date'¶
- to_internal(external_id, unwrap=False)[source]¶
Give the Odoo recordset for an external ID
- Parameters
external_id – external ID for which we want the Odoo ID
unwrap – if True, returns the normal record else return the binding record
- Returns
a recordset, depending on the value of unwrap, or an empty recordset if the external_id is not mapped
- Return type
recordset
- to_external(binding, wrap=False)[source]¶
Give the external ID for an Odoo binding ID
- Parameters
binding – Odoo binding for which we want the external id
wrap – if True, binding is a normal record, the method will search the corresponding binding and return the external id of the binding
- Returns
external ID of the record
- bind(external_id, binding)[source]¶
Create the link between an external ID and an Odoo ID
- Parameters
external_id – external id to bind
binding (int) – Odoo record to bind
Mappers¶
Mappers are the components responsible to transform external records into Odoo records and conversely.
- class connector.components.mapper.Mapper(work)[source]¶
Bases:
AbstractComponent
A Mapper translates an external record to an Odoo record and conversely. The output of a Mapper is a
dict
.3 types of mappings are supported:
- Direct Mappings
Example:
direct = [('source', 'target')]
Here, the
source
field will be copied in thetarget
field.A modifier can be used in the source item. The modifier will be applied to the source field before being copied in the target field. It should be a closure function respecting this idiom:
def a_function(field): ''' ``field`` is the name of the source field. Naming the arg: ``field`` is required for the conversion''' def modifier(self, record, to_attr): ''' self is the current Mapper, record is the current record to map, to_attr is the target field''' return record[field] return modifier
And used like that:
direct = [ (a_function('source'), 'target'), ]
A more concrete example of modifier:
def convert(field, conv_type): ''' Convert the source field to a defined ``conv_type`` (ex. str) before returning it''' def modifier(self, record, to_attr): value = record[field] if not value: return None return conv_type(value) return modifier
And used like that:
direct = [ (convert('myfield', float), 'target_field'), ]
More examples of modifiers:
- Method Mappings
A mapping method allows to execute arbitrary code and return one or many fields:
@mapping def compute_state(self, record): # compute some state, using the ``record`` or not state = 'pending' return {'state': state}
We can also specify that a mapping methods should be applied only when an object is created, and never applied on further updates:
@only_create @mapping def default_warehouse(self, record): # get default warehouse warehouse_id = ... return {'warehouse_id': warehouse_id}
- Submappings
When a record contains sub-items, like the lines of a sales order, we can convert the children using another Mapper:
children = [('items', 'line_ids', 'model.name')]
It allows to create the sales order and all its lines with the same call to
odoo.models.BaseModel.create()
.When using
children
for items of a record, we need to create aMapper
for the model of the items, and optionally aMapChild
.
Usage of a Mapper:
>>> mapper = self.component(usage='mapper') >>> map_record = mapper.map_record(record) >>> values = map_record.values() >>> values = map_record.values(only_create=True) >>> values = map_record.values(fields=['name', 'street'])
- _name = 'base.mapper'¶
Name of the component
- _inherit = 'base.connector'¶
Name or list of names of the component(s) to inherit from
- _usage = 'mapper'¶
Component purpose (‘import.mapper’, …).
- property map_methods¶
Yield all the methods decorated with
@mapping
- property options¶
Options can be accessed in the mapping methods with
self.options
.
- changed_by_fields()[source]¶
Build a set of fields used by the mapper
It takes in account the
direct
fields and the fields used by the decorator:changed_by
.
- class connector.components.mapper.ImportMapper(work_context)[source]¶
Bases:
AbstractComponent
Mapper
for imports.Transform a record from a backend to an Odoo record
- _name = 'base.import.mapper'¶
Name of the component
- _inherit = 'base.mapper'¶
Name or list of names of the component(s) to inherit from
- _usage = 'import.mapper'¶
Component purpose (‘import.mapper’, …).
- class connector.components.mapper.ExportMapper(work_context)[source]¶
Bases:
AbstractComponent
Mapper
for exports.Transform a record from Odoo to a backend record
- _name = 'base.export.mapper'¶
Name of the component
- _inherit = 'base.mapper'¶
Name or list of names of the component(s) to inherit from
- _usage = 'export.mapper'¶
Component purpose (‘import.mapper’, …).
- class connector.components.mapper.MapChild(work_context)[source]¶
Bases:
AbstractComponent
MapChild is responsible to convert items.
Items are sub-records of a main record. In this example, the items are the records in
lines
:sales = {'name': 'SO10', 'lines': [{'product_id': 1, 'quantity': 2}, {'product_id': 2, 'quantity': 2}]}
A MapChild is always called from another
Mapper
which provides achildren
configuration.Considering the example above, the “main”
Mapper
would returns something as follows:{'name': 'SO10', 'lines': [(0, 0, {'product_id': 11, 'quantity': 2}), (0, 0, {'product_id': 12, 'quantity': 2})]}
A MapChild is responsible to:
Find the
Mapper
to convert the itemsPossibly filter out some lines (can be done by inheriting
skip_item()
)Convert the items’ records using the found
Mapper
Format the output values to the format expected by Odoo or the backend (as seen above with
(0, 0, {values})
A MapChild can be extended like any other
Component
. However, it is not mandatory to explicitly create a MapChild for each children mapping, the default one will be used (ImportMapChild
orExportMapChild
).The implementation by default does not take care of the updates: if I import a sales order 2 times, the lines will be duplicated. This is not a problem as long as an importation should only support the creation (typical for sales orders). It can be implemented on a case-by-case basis by inheriting
get_item_values()
andformat_items()
.- _name = 'base.map.child'¶
Name of the component
- _inherit = 'base.connector'¶
Name or list of names of the component(s) to inherit from
- _usage = None¶
Component purpose (‘import.mapper’, …).
- skip_item(map_record)[source]¶
Hook to implement in sub-classes when some child records should be skipped.
The parent record is accessible in
map_record
. If it returns True, the current child record is skipped.- Parameters
map_record (
MapRecord
) – record that we are converting
- get_items(items, parent, to_attr, options)[source]¶
Returns the formatted output values of items from a main record
- get_item_values(map_record, to_attr, options)[source]¶
Get the raw values from the child Mappers for the items.
It can be overridden for instance to:
Change options
Use a
Binder
to know if an item already exists to modify an existing item, rather than to add it
- format_items(items_values)[source]¶
Format the values of the items mapped from the child Mappers.
It can be overridden for instance to add the Odoo relationships commands
(6, 0, [IDs])
, …As instance, it can be modified to handle update of existing items: check if an ‘id’ has been defined by
get_item_values()
then use the(1, ID, {values}
) command- Parameters
items_values (list) – mapped values for the items
- class connector.components.mapper.ImportMapChild(work_context)[source]¶
Bases:
AbstractComponent
MapChild
for the Imports- _name = 'base.map.child.import'¶
Name of the component
- _inherit = 'base.map.child'¶
Name or list of names of the component(s) to inherit from
- _usage = 'import.map.child'¶
Component purpose (‘import.mapper’, …).
- format_items(items_values)[source]¶
Format the values of the items mapped from the child Mappers.
It can be overridden for instance to add the Odoo relationships commands
(6, 0, [IDs])
, …As instance, it can be modified to handle update of existing items: check if an ‘id’ has been defined by
get_item_values()
then use the(1, ID, {values}
) command- Parameters
items_values (list) – list of values for the items to create
- class connector.components.mapper.ExportMapChild(work_context)[source]¶
Bases:
AbstractComponent
MapChild
for the Exports- _name = 'base.map.child.export'¶
Name of the component
- _inherit = 'base.map.child'¶
Name or list of names of the component(s) to inherit from
- _usage = 'export.map.child'¶
Component purpose (‘import.mapper’, …).
- class connector.components.mapper.MapRecord(mapper, source, parent=None)[source]¶
Bases:
object
A record prepared to be converted using a
Mapper
.MapRecord instances are prepared by
Mapper.map_record()
.Usage:
>>> map_record = mapper.map_record(record) >>> output_values = map_record.values()
See
values()
for more information on the available arguments.- update(*args, **kwargs)[source]¶
Force values to be applied after a mapping.
Usage:
>>> map_record = mapper.map_record(record) >>> map_record.update(a=1) >>> output_values = map_record.values() # output_values will at least contain {'a': 1}
The values assigned with
update()
are in any case applied, they have a greater priority than the mapping values.
- values(for_create=None, fields=None, **kwargs)[source]¶
Build and returns the mapped values according to the options.
Usage:
>>> map_record = mapper.map_record(record) >>> output_values = map_record.values()
- Creation of records
When using the option
for_create
, only the mappings decorated with@only_create
will be mapped.>>> output_values = map_record.values(for_create=True)
- Filter on fields
When using the
fields
argument, the mappings will be filtered using either the source key indirect
arguments, either thechanged_by
arguments for the mapping methods.>>> output_values = map_record.values( fields=['name', 'street'] )
- Custom options
Arbitrary key and values can be defined in the
kwargs
arguments. They can later be used in the mapping methods usingself.options
.>>> output_values = map_record.values(tax_include=True)
- Parameters
for_create (boolean) – specify if only the mappings for creation (
@only_create
) should be mapped.fields (list) – filter on fields
**kwargs – custom options, they can later be used in the mapping methods
- property parent¶
Parent record if the current record is an item
- property source¶
Source record to be converted
- connector.components.mapper.changed_by(*args)[source]¶
Decorator for the mapping methods (
mapping()
)When fields are modified in Odoo, we want to export only the modified fields. Using this decorator, we can specify which fields updates should trigger which mapping method.
If
changed_by
is empty, the mapping is always active.As far as possible, this decorator should be used for the exports, thus, when we do an update on only a small number of fields on a record, the size of the output record will be limited to only the fields really having to be exported.
Usage:
@changed_by('input_field') @mapping def any(self, record): return {'output_field': record['input_field']}
- Parameters
*args – field names which trigger the mapping when modified
- connector.components.mapper.convert(field, conv_type)[source]¶
A modifier intended to be used on the
direct
mappings.Convert a field’s value to a given type.
Example:
direct = [(convert('source', str), 'target')]
- Parameters
field – name of the source field in the record
binding – True if the relation is a binding record
- connector.components.mapper.external_to_m2o(field, binding=None)[source]¶
A modifier intended to be used on the
direct
mappings.For a field from a backend which is an ID, search the corresponding binding in Odoo and returns it.
When the field’s relation is not a binding (i.e. it does not point to something like
magento.*
), the binding model needs to be provided in thebinding
keyword argument.Example:
direct = [(external_to_m2o('country', binding='magento.res.country'), 'country_id'), (external_to_m2o('country'), 'magento_country_id')]
- Parameters
field – name of the source field in the record
binding – name of the binding model is the relation is not a binding
- connector.components.mapper.follow_m2o_relations(field)[source]¶
A modifier intended to be used on
direct
mappings.‘Follows’ Many2one relations and return the final field value.
- Examples:
Assuming model is
product.product
:direct = [ (follow_m2o_relations('product_tmpl_id.categ_id.name'), 'cat')]
- Parameters
field – field “path”, using dots for relations as usual in Odoo
- connector.components.mapper.m2o_to_external(field, binding=None)[source]¶
A modifier intended to be used on the
direct
mappings.For a many2one, get the external ID and returns it.
When the field’s relation is not a binding (i.e. it does not point to something like
magento.*
), the binding model needs to be provided in thebinding
keyword argument.Example:
direct = [(m2o_to_external('country_id', binding='magento.res.country'), 'country'), (m2o_to_external('magento_country_id'), 'country')]
- Parameters
field – name of the source field in the record
binding – name of the binding model is the relation is not a binding
- connector.components.mapper.mapping(func)[source]¶
Decorator, declare that a method is a mapping method.
It is then used by the
Mapper
to convert the records.Usage:
@mapping def any(self, record): return {'output_field': record['input_field']}
- connector.components.mapper.none(field)[source]¶
A modifier intended to be used on the
direct
mappings.Replace the False-ish values by None. It can be used in a pipeline of modifiers when .
Example:
direct = [(none('source'), 'target'), (none(m2o_to_external('rel_id'), 'rel_id')]
- Parameters
field – name of the source field in the record
binding – True if the relation is a binding record
- connector.components.mapper.only_create(func)[source]¶
Decorator for the mapping methods (
mapping()
)A mapping decorated with
only_create
means that it has to be used only for the creation of the records.Usage:
@only_create @mapping def any(self, record): return {'output_field': record['input_field']}
Backend Adapter¶
An external adapter has a common interface to speak with the backend. It translates the basic orders (search, read, write) to the protocol used by the backend.
- class connector.components.backend_adapter.BackendAdapter(work_context)[source]¶
Bases:
AbstractComponent
Base Backend Adapter for the connectors
- _name = 'base.backend.adapter'¶
Name of the component
- _inherit = 'base.connector'¶
Name or list of names of the component(s) to inherit from
- _usage = 'backend.adapter'¶
Component purpose (‘import.mapper’, …).
- class connector.components.backend_adapter.CRUDAdapter(work_context)[source]¶
Bases:
AbstractComponent
Base External Adapter specialized in the handling of records on external systems.
This is an empty shell, Components can inherit and implement their own implementation for the methods.
- _name = 'base.backend.adapter.crud'¶
Name of the component
- _inherit = 'base.backend.adapter'¶
Name or list of names of the component(s) to inherit from
- _usage = 'backend.adapter'¶
Component purpose (‘import.mapper’, …).
- search(*args, **kwargs)[source]¶
Search records according to some criterias and returns a list of ids
Synchronizer¶
A synchronizer orchestrates a synchronization with a backend. It’s the actor who runs the flow and glues the logic of an import or export (or else). It uses other components for specialized tasks.
For instance, it will use the mappings to convert the data between both systems, the backend adapters to read or write data on the backend and the binders to create the link between them.
- class connector.components.synchronizer.Synchronizer(work_context)[source]¶
Bases:
AbstractComponent
Base class for synchronizers
- _name = 'base.synchronizer'¶
Name of the component
- _inherit = 'base.connector'¶
Name or list of names of the component(s) to inherit from
- _base_mapper_usage = 'mapper'¶
usage of the component used as mapper, can be customized in sub-classes
- _base_backend_adapter_usage = 'backend.adapter'¶
usage of the component used as backend adapter, can be customized in sub-classes
- property mapper¶
Return an instance of
Mapper
for the synchronization.The instanciation is delayed because some synchronisations do not need such an unit and the unit may not exist.
It looks for a Component with
_usage
being equal to_base_mapper_usage
.- Return type
- property binder¶
Return an instance of
Binder
for the synchronization.The instanciation is delayed because some synchronisations do not need such an unit and the unit may not exist.
- Return type
- property backend_adapter¶
Return an instance of
BackendAdapter
for the synchronization.The instanciation is delayed because some synchronisations do not need such an unit and the unit may not exist.
It looks for a Component with
_usage
being equal to_base_backend_adapter_usage
.- Return type
- class connector.components.synchronizer.Exporter(work_context)[source]¶
Bases:
AbstractComponent
Synchronizer for exporting data from Odoo to a backend
- _name = 'base.exporter'¶
Name of the component
- _inherit = 'base.synchronizer'¶
Name or list of names of the component(s) to inherit from
- _usage = 'exporter'¶
Component purpose (‘import.mapper’, …).
- _base_mapper_usage = 'export.mapper'¶
usage of the component used as mapper, can be customized in sub-classes
- class connector.components.synchronizer.GenericExporter(working_context)[source]¶
Bases:
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]¶
Run the synchronization
- Parameters
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.
Warning
The unique constraint must be created on the binding record to prevent 2 bindings to be created for the same External record.
- _export_dependency(relation, binding_model, component_usage='record.exporter', binding_field=None, binding_extra_vals=None)[source]¶
Export a dependency. The exporter class is a subclass of
GenericExporter
. If a more precise class need to be defined, it can be passed to theexporter_class
keyword argument.Warning
a commit is done at the end of the export of each dependency. The reason for that is that we pushed a record on the backend and we absolutely have to keep its ID.
So you must take care not to modify the Odoo database during an export, excepted when writing back the external ID or eventually to store external data that we have to keep on this side.
You should call this method only at the beginning of the exporter synchronization, in
_export_dependencies()
.- Parameters
relation (
odoo.models.BaseModel
) – record to export if not already 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
Synchronizer for importing data from a backend to Odoo
- _name = 'base.importer'¶
Name of the component
- _inherit = 'base.synchronizer'¶
Name or list of names of the component(s) to inherit from
- _usage = 'importer'¶
Component purpose (‘import.mapper’, …).
- _base_mapper_usage = 'import.mapper'¶
usage of the component used as mapper, can be customized in sub-classes
- class connector.components.synchronizer.Deleter(work_context)[source]¶
Bases:
AbstractComponent
Synchronizer for deleting a record on the backend
- _name = 'base.deleter'¶
Name of the component
- _inherit = 'base.synchronizer'¶
Name or list of names of the component(s) to inherit from
- _usage = 'deleter'¶
usage of the component used as mapper, can be customized in sub-classes
Listeners¶
Listeners are Components notified when events happen.
Documentation in odoo.addons.component_event.components.event
The base listener for the connectors add a method
ConnectorListener.no_connector_export()
which can be used with
odoo.addons.component_event.skip_if()
.
- class connector.components.listener.ConnectorListener(work_context)[source]¶
Bases:
AbstractComponent
Base Backend Adapter for the connectors
- _name = 'base.connector.listener'¶
Name of the component
- _inherit = ['base.connector', 'base.event.listener']¶
Name or list of names of the component(s) to inherit from
- no_connector_export(record)[source]¶
Return if the ‘connector_no_export’ has been set in context
To be used with
odoo.addons.component_event.skip_if()
on Events:from odoo.addons.component.core import Component from odoo.addons.component_event import skip_if class MyEventListener(Component): _name = 'my.event.listener' _inherit = 'base.connector.event.listener' _apply_on = ['magento.res.partner'] @skip_if(lambda: self, record, *args, **kwargs: self.no_connector_export(record)) def on_record_write(self, record, fields=None): record.with_delay().export_record()
- class connector.components.locker.RecordLocker(work_context)[source]¶
Bases:
Component
Component allowing to lock record(s) for the current transaction
Example of usage:
self.component('record.locker').lock(self.records)
See the definition of
lock()
for details.- _name = 'base.record.locker'¶
Name of the component
- _inherit = ['base.connector']¶
Name or list of names of the component(s) to inherit from
- _usage = 'record.locker'¶
Component purpose (‘import.mapper’, …).
- lock(records, seconds=None, ignore_retry=True)[source]¶
Lock the records.
Lock the record so we are sure that only one job is running for this record(s) if concurrent jobs have to run a job for the same record(s). When concurrent jobs try to work on the same record(s), the first one will lock and proceed, the others will fail to acquire it and will be retried later (
RetryableJobError
is raised).The lock is using a
FOR UPDATE NOWAIT
so any concurrent transaction trying FOR UPDATE/UPDATE will be rejected until the current transaction is committed or rollbacked.A classical use case for this is to prevent concurrent exports.
The following parameters are forwarded to the exception
RetryableJobError
- Parameters
seconds – In case of retry because the lock cannot be acquired, in how many seconds it must be retried. If not set, the queue_job configuration is used.
ignore_retry – If True, the retry counter of the job will not be increased.
Components Collection Model¶
Collection Model¶
This is the base Model shared by all the Collections. In the context of the Connector, a collection is the Backend. The _name given to the Collection Model will be the name to use in the _collection of the Components usable for the Backend.
- class odoo.addons.component.models.collection.Collection[source]¶
Bases:
BaseModel
The model on which components are subscribed
It would be for instance the
backend
for the connectors.Example:
class MagentoBackend(models.Model): _name = 'magento.backend' # name of the collection _inherit = 'collection.base' class MagentoSaleImporter(Component): _name = 'magento.sale.importer' _apply_on = 'magento.sale.order' _collection = 'magento.backend' # name of the collection def run(self, magento_id): mapper = self.component(usage='import.mapper') extra_mappers = self.many_components( usage='import.mapper.extra', ) # ...
Use it:
>>> backend = self.env['magento.backend'].browse(1) >>> with backend.work_on('magento.sale.order') as work: ... importer = work.component(usage='magento.sale.importer') ... importer.run(1)
See also:
odoo.addons.component.core.WorkContext
- work_on(model_name, **kwargs)[source]¶
Entry-point for the components, context manager
Start a work using the components on the model. Any keyword argument will be assigned to the work context. See documentation of
odoo.addons.component.core.WorkContext
.It is a context manager, so you can attach objects and clean them at the end of the work session, such as:
@contextmanager 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()
.- Parameters
collection_name – the name of the collection the component is registered into.
usage – the usage of component we are looking for
model_name – filter on components that apply on this model
- class odoo.addons.component.core.WorkContext(model_name=None, collection=None, components_registry=None, **kwargs)[source]¶
Bases:
object
Transport the context required to work with components
It is propagated through all the components, so any data or instance (like a random RPC client) that need to be propagated transversally to the components should be kept here.
Including:
- model_name¶
Name of the model we are working with. It means that any lookup for a component will be done for this model. It also provides a shortcut as a model attribute to use directly with the Odoo model from the components
- collection¶
The collection we are working with. The collection is an Odoo Model that inherit from ‘collection.base’. The collection attribute can be a record or an “empty” model.
- model¶
Odoo Model for
model_name
with the same 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.
- Parameters
module (str | unicode) – the name of the addon for which we want to load the components
registry (
ComponentRegistry
) – the registry in which we want to put the Component