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.

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:

  1. The first synchronizer searches the list of all the ids to import.
  2. 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 a magento.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 the res.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 in magentoerpconnect.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 just magento.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 to delay 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',
        ]

[...]