Tutorial: development¶
This tutorial explains how to take part in the development of
Magentoerpconnect. It will explain how to use the different pieces of
the Connector
addon to synchronize records with Magento.
Sections:
Run a function on an Event¶
3 events are registered in the Connector
addon:
on_record_create
on_record_write
on_record_unlink
If you need to create a new connector.event.Event
, please
refer to the Connector
documentation.
When a function has to be run when an event is fired, it must be
registered on this event. Those functions are called Consumers
.
In magentoerpconnect/consumer.py
, some consumers are already
defined. You can add your one, it should be decorated by
openerp.addons.magentoerpconnect.consumer.magento_consumer()
and by the event
which has to fire it:
@on_record_write(model_names=['my.model'])
@magento_consumer
def my_consumer(session, model_name, record_id, vals=None):
print 'Yeah'
Note
The consumers always start with the arguments session
and
model_name
. The next arguments vary, but they are defined
by the connector.event.Event
Find the ‘connector unit’ for a model¶
Assume that you already have a ConnectorEnvironment
.
Note
A ConnectorEnvironment
is the scope where the synchronizations
are done. It contains the browse record of the backend
(magento.backend
), a ConnectorSession
(container for cr
,
uid
, context
) and the name of the model we are working with).
You can get an instance of the ConnectorUnit
to use from the
environment. You’ll need to ask a connector unit with the base class
which interests you. Say you want a Synchronizer which import records
from Magento:
importer = environment.get_connector_unit(MagentoImporter)
importer
is an instance of the importer to use for the model of the
environment.
Say you want a binder for your model:
binder = environment.get_connector_unit(connector.Binder)
binder
is an instance of the binder for your model.
And so on…
Note
Every ConnectorUnit
instance keeps the environment as
attribute. It means that you can access to the environment
from a synchronizer with self.connector_env
.
When you are already inside a ConnectorUnit
though you can use the shortcuts:
# for the current model
importer = self.unit_for(MagentoImporter)
# for another model
importer = self.unit_for(MagentoImporter, model='another.model')
As the binders are the most used ConnectorUnit
classes, they have a
dedicated shortcut:
# for the current model
binder = self.binder_for()
# for another model
binder = self.binder_for(model='another.model')
Create an import¶
You’ll probably need to work with 4 connector units:
- a Synchronizer (presumably 2, we’ll see why soon)
- a Mapper
- a Backend Adapter
- a Binder
You will also need to create / change the Odoo models.
Note
Keep in mind: try to modify at least as possible the Odoo models and classes.
The synchronizer will handle the flow of the synchronization. It will get the data from Magento using the Backend Adapter, transform it using the Mapper, and use the Binder(s) to search the relation(s) with other imported records.
Why do we need 2 synchronizers? Because an import is generally done in 2 phases:
- The first synchronizer searches the list of all the ids to import.
- The second synchronizer imports all the ids atomically (in separate jobs).
We’ll see in details a simple import: customer groups.
Customer groups are importer as categories of partners
(res.partner.category
).
Models¶
First, we create the model:
class MagentoResPartnerCategory(models.Model):
_name = 'magento.res.partner.category'
_inherit = 'magento.binding'
_inherits = {'res.partner.category': 'openerp_id'}
openerp_id = fields.Many2one(comodel_name='res.partner.category',
string='Partner Category',
required=True,
ondelete='cascade')
tax_class_id = fields.Integer(string='Tax Class ID')
Observations:
- We do not change
res.partner.category
but create amagento.res.partner.category
model instead. - It _inherit from magento.binding
- It contains the links to the Magento backend, the category and the
ID on Magento (inherited from
magento.binding
). - This model stores the data related to one category and one Magento backend as well, so this data does not pollute the category and does not criss-cross when several backends are connected.
- It
_inherits
theres.partner.category
so we can directly use this model for the imports and the exports without complications.
We need to add the field magento_bind_ids
in
res.partner.category
to relate to the Magento Bindings:
class ResPartnerCategory(models.Model):
_inherit = 'res.partner.category'
magento_bind_ids = fields.One2many(
comodel_name='magento.res.partner.category',
inverse_name='openerp_id',
string='Magento Bindings',
readonly=True,
)
That’s the only thing we need to change (besides the view) in the Odoo’s models!
Note
The name of the field magento_bind_ids
is a convention.
Ok, we’re done with the models. Now the synchronizations!
Batch Importer¶
The first Synchronizer, which get the full list of ids to import is
usually a subclass of
magentoerpconnect.unit.import_synchronizer.BatchImporter
.
The customer groups are simple enough to use a generic class:
@magento
class DelayedBatchImporter(BatchImporter):
""" Delay import of the records """
_model_name = [
'magento.res.partner.category',
]
def _import_record(self, record):
""" Delay the import of the records"""
job.import_record.delay(self.session,
self.model._name,
self.backend_record.id,
record)
Observations:
- Decorated by
@magento
: this synchronizer will be available for all versions of Magento. Decorated with@magento1700
it would be only available for Magento 1.7. _model_name
: the list of models allowed to use this synchronizer- We just override the
_import_record
hook, the search has already be done inmagentoerpconnect.unit.import_synchronizer.BatchImporter
. import_record
is a job to import a record from its ID.- Delay the import of each record, a job will be created for each record id.
- This synchronization does not need any Binder nor Mapper, but does need a Backend Adapter to be able to speak with Magento.
So, let’s implement the Backend Adapter.
Backend Adapter¶
Most of the Magento objects can use the generic class
:py:class`magentoerpconnect.unit.backend_adapter.GenericAdapter`.
However, the search
entry point is not implemented in the API for
customer groups.
We’ll replace it using list
and select only the ids:
@magento
class PartnerCategoryAdapter(GenericAdapter):
_model_name = 'magento.res.partner.category'
_magento_model = 'ol_customer_groups'
def search(self, filters=None):
""" Search records according to some criterias
and returns a list of ids
:rtype: list
"""
return [int(row['customer_group_id']) for row
in self._call('%s.list' % self._magento_model,
[filters] if filters else [{}])]
Observations:
_model_name
is justmagento.res.partner.category
, this adapter is available only for this model._magento_model
is the first part of the entry points in the API (ie.ol_customer_groups.list
)- Only the
search
method is overriden.
We have all the pieces for the first part of the synchronization, just need to…
Delay execution of our Batch Import¶
This import will be called from the Magento Backend, we inherit magento.backend
and add a method (and add in the view as well, I won’t write the view’s xml here):
class MagentoBackend(models.Model):
_inherit = 'magento.backend'
@api.multi
def import_customer_groups(self):
session = ConnectorSession.from_env(self.env)
for backend_id in self.ids:
job.import_batch.delay(session, 'magento.res.partner.category',
backend_id)
return True
Observations:
- Encapsulate Odoo environment in a
openerp.addons.connector.session.ConnectorSession
. - Delay the job
import_batch
when we click on the button. - if the arguments were given to
import_batch
directly (without the.delay()
, the import would be done synchronously.
Overview on the jobs¶
We use 2 jobs: import_record
and import_batch
. These jobs are
already there so you don’t need to write them, but we can have a look
on them to understand what they do:
def _get_environment(session, model_name, backend_id):
model = session.env['magento.backend']
backend_record = model.browse(backend_id)
return connector.Environment(backend_record, session, model_name)
@connector.job
def import_batch(session, model_name, backend_id, filters=None):
""" Prepare a batch import of records from Magento """
env = _get_environment(session, model_name, backend_id)
importer = env.get_connector_unit(BatchImporter)
importer.run(filters)
@connector.job
def import_record(session, model_name, backend_id, magento_id):
""" Import a record from Magento """
env = _get_environment(session, model_name, backend_id)
importer = env.get_connector_unit(MagentoImporter)
importer.run(magento_id)
Observations:
- Decorated by
connector.queue.job.job
, allow todelay
the function. - We create a new environment and ask for the good importer, respectively for batch imports and record imports. The environment returns an instance of the importer to use.
- The docstring of the job is its description for the user.
At this point, if one click on the button to import the categories, the batch import would run, generate one job for each category to import, and then all these jobs would fail. We need to create the second synchronizer, the mapper and the binder.
Record Importer¶
The import of customer groups is so simple that it can use a generic
class
openerp.addons.magentoerpconnect.unit.import_synchronizer.SimpleRecordImporter
.
We just need to add the model in the _model_name
attribute:
@magento
class SimpleRecordImporter(MagentoImporter):
""" Import one Magento Website """
_model_name = [
'magento.website',
'magento.store',
'magento.storeview',
'magento.res.partner.category',
]
However, most of the imports will be more complicated than that. You
will often need to create a new class for a model, where you will need
to use some of the hooks to change the behavior
(_import_dependencies
, _after_import
for example).
Refers to the importers already created in the module and to the base
class
openerp.addons.magentoerpconnect.unit.import_synchronizer.MagentoImporter
.
The synchronizer asks to the appropriate Mapper
to transform the data
(in _map_data
). Here is how we’ll create the Mapper
.
Mapper¶
The connector.unit.mapper.Mapper
takes the record from Magento, and generates the Odoo
record. (or the reverse for the export Mappers)
The mapper for the customer groups is as follows:
@magento
class PartnerCategoryImportMapper(connector.ImportMapper):
_model_name = 'magento.res.partner.category'
direct = [('customer_group_code', 'name'),
('tax_class_id', 'tax_class_id'),
]
@mapping
def magento_id(self, record):
return {'magento_id': record['customer_group_id']}
@mapping
def backend_id(self, record):
return {'backend_id': self.backend_record.id}
Observations:
- Some mappings are in
direct
and some use a method with a@mapping
decorator. - Methods allow to have more complex mappings. (see documentation on
Mapper
)
Binder¶
For the last piece of the construct, it will be an easy one, because
normally all the Magento Models will use the same Binder, the so called
MagentoModelBinder
.
We just need to add our model in the _model_name
attribute:
@magento
class MagentoModelBinder(MagentoBinder):
"""
Bindings are done directly on the model
"""
_model_name = [
'magento.website',
'magento.store',
'magento.storeview',
'magento.res.partner.category',
]
[...]