1012 lines
		
	
	
		
			34 KiB
		
	
	
	
		
			Python
		
	
	
			
		
		
	
	
			1012 lines
		
	
	
		
			34 KiB
		
	
	
	
		
			Python
		
	
	
| # -*- coding: utf-8 -*-
 | |
| # Copyright 2017 Camptocamp SA
 | |
| # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html)
 | |
| 
 | |
| """
 | |
| 
 | |
| Mappers
 | |
| =======
 | |
| 
 | |
| Mappers are the components responsible to transform
 | |
| external records into Odoo records and conversely.
 | |
| 
 | |
| """
 | |
| 
 | |
| import logging
 | |
| from collections import namedtuple
 | |
| from contextlib import contextmanager
 | |
| 
 | |
| from odoo import models
 | |
| from odoo.addons.component.core import AbstractComponent
 | |
| from odoo.addons.component.exception import NoComponentError
 | |
| from ..exception import MappingError
 | |
| import collections
 | |
| 
 | |
| 
 | |
| _logger = logging.getLogger(__name__)
 | |
| 
 | |
| 
 | |
| def mapping(func):
 | |
|     """ Decorator, declare that a method is a mapping method.
 | |
| 
 | |
|     It is then used by the :py:class:`Mapper` to convert the records.
 | |
| 
 | |
|     Usage::
 | |
| 
 | |
|         @mapping
 | |
|         def any(self, record):
 | |
|             return {'output_field': record['input_field']}
 | |
| 
 | |
|     """
 | |
|     func.is_mapping = True
 | |
|     return func
 | |
| 
 | |
| 
 | |
| def changed_by(*args):
 | |
|     """ Decorator for the mapping methods (:py:func:`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']}
 | |
| 
 | |
|     :param ``*args``: field names which trigger the mapping when modified
 | |
| 
 | |
|     """
 | |
|     def register_mapping(func):
 | |
|         func.changed_by = args
 | |
|         return func
 | |
|     return register_mapping
 | |
| 
 | |
| 
 | |
| def only_create(func):
 | |
|     """ Decorator for the mapping methods (:py:func:`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']}
 | |
| 
 | |
|     """
 | |
|     func.only_create = True
 | |
|     return func
 | |
| 
 | |
| 
 | |
| def none(field):
 | |
|     """ 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')]
 | |
| 
 | |
|     :param field: name of the source field in the record
 | |
|     :param binding: True if the relation is a binding record
 | |
|     """
 | |
|     def modifier(self, record, to_attr):
 | |
|         if isinstance(field, collections.Callable):
 | |
|             result = field(self, record, to_attr)
 | |
|         else:
 | |
|             result = record[field]
 | |
|         if not result:
 | |
|             return None
 | |
|         return result
 | |
|     return modifier
 | |
| 
 | |
| 
 | |
| def convert(field, conv_type):
 | |
|     """ 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')]
 | |
| 
 | |
|     :param field: name of the source field in the record
 | |
|     :param binding: True if the relation is a binding record
 | |
|     """
 | |
|     def modifier(self, record, to_attr):
 | |
|         value = record[field]
 | |
|         if not value:
 | |
|             return False
 | |
|         return conv_type(value)
 | |
|     return modifier
 | |
| 
 | |
| 
 | |
| def m2o_to_external(field, binding=None):
 | |
|     """ 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 the ``binding`` keyword argument.
 | |
| 
 | |
|     Example::
 | |
| 
 | |
|         direct = [(m2o_to_external('country_id',
 | |
|                                    binding='magento.res.country'), 'country'),
 | |
|                   (m2o_to_external('magento_country_id'), 'country')]
 | |
| 
 | |
|     :param field: name of the source field in the record
 | |
|     :param binding: name of the binding model is the relation is not a binding
 | |
|     """
 | |
|     def modifier(self, record, to_attr):
 | |
|         if not record[field]:
 | |
|             return False
 | |
|         column = self.model._fields[field]
 | |
|         if column.type != 'many2one':
 | |
|             raise ValueError('The column %s should be a Many2one, got %s' %
 | |
|                              (field, type(column)))
 | |
|         rel_id = record[field].id
 | |
|         if binding is None:
 | |
|             binding_model = column.comodel_name
 | |
|         else:
 | |
|             binding_model = binding
 | |
|         binder = self.binder_for(binding_model)
 | |
|         # if a relation is not a binding, we wrap the record in the
 | |
|         # binding, we'll return the id of the binding
 | |
|         wrap = bool(binding)
 | |
|         value = binder.to_external(rel_id, wrap=wrap)
 | |
|         if not value:
 | |
|             raise MappingError("Can not find an external id for record "
 | |
|                                "%s in model %s %s wrapping" %
 | |
|                                (rel_id, binding_model,
 | |
|                                 'with' if wrap else 'without'))
 | |
|         return value
 | |
|     return modifier
 | |
| 
 | |
| 
 | |
| def external_to_m2o(field, binding=None):
 | |
|     """ 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 the ``binding`` keyword argument.
 | |
| 
 | |
|     Example::
 | |
| 
 | |
|         direct = [(external_to_m2o('country', binding='magento.res.country'),
 | |
|                    'country_id'),
 | |
|                   (external_to_m2o('country'), 'magento_country_id')]
 | |
| 
 | |
|     :param field: name of the source field in the record
 | |
|     :param binding: name of the binding model is the relation is not a binding
 | |
|     """
 | |
|     def modifier(self, record, to_attr):
 | |
|         if not record[field]:
 | |
|             return False
 | |
|         column = self.model._fields[to_attr]
 | |
|         if column.type != 'many2one':
 | |
|             raise ValueError('The column %s should be a Many2one, got %s' %
 | |
|                              (to_attr, type(column)))
 | |
|         rel_id = record[field]
 | |
|         if binding is None:
 | |
|             binding_model = column.comodel_name
 | |
|         else:
 | |
|             binding_model = binding
 | |
|         binder = self.binder_for(binding_model)
 | |
|         # if we want the normal record, not a binding,
 | |
|         # we ask to the binder to unwrap the binding
 | |
|         unwrap = bool(binding)
 | |
|         record = binder.to_internal(rel_id, unwrap=unwrap)
 | |
|         if not record:
 | |
|             raise MappingError("Can not find an existing %s for external "
 | |
|                                "record %s %s unwrapping" %
 | |
|                                (binding_model, rel_id,
 | |
|                                 'with' if unwrap else 'without'))
 | |
|         if isinstance(record, models.BaseModel):
 | |
|             return record.id
 | |
|         else:
 | |
|             _logger.debug(
 | |
|                 'Binder for %s returned an id, '
 | |
|                 'returning a record should be preferred.', binding_model
 | |
|             )
 | |
|             return record
 | |
|     return modifier
 | |
| 
 | |
| 
 | |
| def follow_m2o_relations(field):
 | |
|     """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')]
 | |
| 
 | |
|     :param field: field "path", using dots for relations as usual in Odoo
 | |
|     """
 | |
|     def modifier(self, record, to_attr):
 | |
|         attrs = field.split('.')
 | |
|         value = record
 | |
|         for attr in attrs:
 | |
|             value = getattr(value, attr)
 | |
|         return value
 | |
|     return modifier
 | |
| 
 | |
| 
 | |
| MappingDefinition = namedtuple('MappingDefinition',
 | |
|                                ['changed_by',
 | |
|                                 'only_create'])
 | |
| 
 | |
| 
 | |
| class MapChild(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 :py:class:`Mapper` which
 | |
|     provides a ``children`` configuration.
 | |
| 
 | |
|     Considering the example above, the "main" :py:class:`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 :py:class:`Mapper` to convert the items
 | |
|     * Possibly filter out some lines (can be done by inheriting
 | |
|       :py:meth:`skip_item`)
 | |
|     * Convert the items' records using the found :py:class:`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
 | |
|     :py:class:`~component.core.Component`.
 | |
|     However, it is not mandatory to explicitly create a MapChild for
 | |
|     each children mapping, the default one will be used
 | |
|     (:py:class:`ImportMapChild` or :py:class:`ExportMapChild`).
 | |
| 
 | |
|     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 :py:meth:`get_item_values` and
 | |
|     :py:meth:`format_items`.
 | |
| 
 | |
|     """
 | |
| 
 | |
|     _name = 'base.map.child'
 | |
|     _inherit = 'base.connector'
 | |
| 
 | |
|     def _child_mapper(self):
 | |
|         raise NotImplementedError
 | |
| 
 | |
|     def skip_item(self, map_record):
 | |
|         """ 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.
 | |
| 
 | |
|         :param map_record: record that we are converting
 | |
|         :type map_record: :py:class:`MapRecord`
 | |
|         """
 | |
|         return False
 | |
| 
 | |
|     def get_items(self, items, parent, to_attr, options):
 | |
|         """ Returns the formatted output values of items from a main record
 | |
| 
 | |
|         :param items: list of item records
 | |
|         :type items: list
 | |
|         :param parent: parent record
 | |
|         :param to_attr: destination field (can be used for introspecting
 | |
|                         the relation)
 | |
|         :type to_attr: str
 | |
|         :param options: dict of options, herited from the main mapper
 | |
|         :return: formatted output values for the item
 | |
| 
 | |
|         """
 | |
|         mapper = self._child_mapper()
 | |
|         mapped = []
 | |
|         for item in items:
 | |
|             map_record = mapper.map_record(item, parent=parent)
 | |
|             if self.skip_item(map_record):
 | |
|                 continue
 | |
|             item_values = self.get_item_values(map_record, to_attr, options)
 | |
|             if item_values:
 | |
|                 mapped.append(item_values)
 | |
|         return self.format_items(mapped)
 | |
| 
 | |
|     def get_item_values(self, map_record, to_attr, options):
 | |
|         """ Get the raw values from the child Mappers for the items.
 | |
| 
 | |
|         It can be overridden for instance to:
 | |
| 
 | |
|         * Change options
 | |
|         * Use a :py:class:`~connector.connector.Binder` to know if an
 | |
|           item already exists to modify an existing item, rather than to
 | |
|           add it
 | |
| 
 | |
|         :param map_record: record that we are converting
 | |
|         :type map_record: :py:class:`MapRecord`
 | |
|         :param to_attr: destination field (can be used for introspecting
 | |
|                         the relation)
 | |
|         :type to_attr: str
 | |
|         :param options: dict of options, herited from the main mapper
 | |
| 
 | |
|         """
 | |
|         return map_record.values(**options)
 | |
| 
 | |
|     def format_items(self, items_values):
 | |
|         """ 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
 | |
|         :py:meth:`get_item_values` then use the ``(1, ID, {values}``)
 | |
|         command
 | |
| 
 | |
|         :param items_values: mapped values for the items
 | |
|         :type items_values: list
 | |
| 
 | |
|         """
 | |
|         return items_values
 | |
| 
 | |
| 
 | |
| class ImportMapChild(AbstractComponent):
 | |
|     """ :py:class:`MapChild` for the Imports """
 | |
| 
 | |
|     _name = 'base.map.child.import'
 | |
|     _inherit = 'base.map.child'
 | |
|     _usage = 'import.map.child'
 | |
| 
 | |
|     def _child_mapper(self):
 | |
|         return self.component(usage='import.mapper')
 | |
| 
 | |
|     def format_items(self, items_values):
 | |
|         """ 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
 | |
|         :py:meth:`get_item_values` then use the ``(1, ID, {values}``)
 | |
|         command
 | |
| 
 | |
|         :param items_values: list of values for the items to create
 | |
|         :type items_values: list
 | |
| 
 | |
|         """
 | |
|         return [(0, 0, values) for values in items_values]
 | |
| 
 | |
| 
 | |
| class ExportMapChild(AbstractComponent):
 | |
|     """ :py:class:`MapChild` for the Exports """
 | |
| 
 | |
|     _name = 'base.map.child.export'
 | |
|     _inherit = 'base.map.child'
 | |
|     _usage = 'export.map.child'
 | |
| 
 | |
|     def _child_mapper(self):
 | |
|         return self.component(usage='export.mapper')
 | |
| 
 | |
| 
 | |
| class Mapper(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 the ``target`` 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:
 | |
| 
 | |
|         * :py:func:`convert`
 | |
|         * :py:func:`m2o_to_external`
 | |
|         * :py:func:`external_to_m2o`
 | |
| 
 | |
|     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 :py:meth:`odoo.models.BaseModel.create()`.
 | |
| 
 | |
|         When using ``children`` for items of a record, we need to create
 | |
|         a :py:class:`Mapper` for the model of the items, and optionally a
 | |
|         :py:class:`MapChild`.
 | |
| 
 | |
|     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'
 | |
|     _inherit = 'base.connector'
 | |
|     _usage = 'mapper'
 | |
| 
 | |
|     direct = []  # direct conversion of a field to another (from_attr, to_attr)
 | |
|     children = []  # conversion of sub-records (from_attr, to_attr, model)
 | |
| 
 | |
|     _map_methods = None
 | |
| 
 | |
|     _map_child_usage = None
 | |
|     _map_child_fallback = None
 | |
| 
 | |
|     @classmethod
 | |
|     def _build_mapper_component(cls):
 | |
|         """ Build a Mapper component
 | |
| 
 | |
|         When a Mapper component is built, we will look into every of its bases
 | |
|         and look for methods decorated by ``@mapping`` or ``@changed_by``.  We
 | |
|         keep the definitions in a ``_map_methods`` attribute for later use by
 | |
|         the Mapper instances.
 | |
| 
 | |
|         The ``__bases__`` of a newly generated Component are of 2 kinds:
 | |
| 
 | |
|         * other dynamically generated components (below 'base' and
 | |
|           'second.mapper')
 | |
|         * "real" Python classes applied on top of existing components (here
 | |
|           ThirdMapper)
 | |
| 
 | |
|         ::
 | |
| 
 | |
|             >>> cls.__bases__
 | |
|             (<class 'odoo.addons.connector.tests.test_mapper.ThirdMapper'>,
 | |
|              <class 'odoo.addons.component.core.second.mapper'>,
 | |
|              <class 'odoo.addons.component.core.base'>)
 | |
| 
 | |
|         This method traverses these bases, from the bottom to the top, and
 | |
|         merges the mapping definitions. It reuses the computed definitions
 | |
|         for the generated components (for which this code already ran), and
 | |
|         inspect the real classes to find mapping methods.
 | |
| 
 | |
|         """
 | |
| 
 | |
|         map_methods = {}
 | |
|         for base in reversed(cls.__bases__):
 | |
|             if hasattr(base, '_map_methods'):
 | |
|                 # this is already a dynamically generated Component, so we can
 | |
|                 # use it's existing mappings
 | |
|                 base_map_methods = base._map_methods or {}
 | |
|                 for attr_name, definition in base_map_methods.items():
 | |
|                     if attr_name in map_methods:
 | |
|                         # Update the existing @changed_by with the content
 | |
|                         # of each base (it is mutated in place).
 | |
|                         mapping_changed_by = map_methods[attr_name].changed_by
 | |
|                         mapping_changed_by.update(definition.changed_by)
 | |
|                         # keep the last value for @only_create
 | |
|                         if definition.only_create:
 | |
|                             new_definition = MappingDefinition(
 | |
|                                 mapping_changed_by,
 | |
|                                 definition.only_create,
 | |
|                             )
 | |
|                             map_methods[attr_name] = new_definition
 | |
|                     else:
 | |
|                         map_methods[attr_name] = definition
 | |
|             else:
 | |
|                 # this is a real class that needs to be applied upon
 | |
|                 # the base Components
 | |
|                 for attr_name in dir(base):
 | |
|                     attr = getattr(base, attr_name, None)
 | |
|                     if not getattr(attr, 'is_mapping', None):
 | |
|                         continue
 | |
|                     has_only_create = getattr(attr, 'only_create', False)
 | |
|                     mapping_changed_by = set(getattr(attr, 'changed_by', ()))
 | |
| 
 | |
|                     # if already existing, it has been defined in an previous
 | |
|                     # base, extend the @changed_by set
 | |
|                     if map_methods.get(attr_name) is not None:
 | |
|                         definition = map_methods[attr_name]
 | |
|                         mapping_changed_by.update(definition.changed_by)
 | |
| 
 | |
|                     # keep the last choice for only_create
 | |
|                     definition = MappingDefinition(mapping_changed_by,
 | |
|                                                    has_only_create)
 | |
|                     map_methods[attr_name] = definition
 | |
| 
 | |
|         cls._map_methods = map_methods
 | |
| 
 | |
|     @classmethod
 | |
|     def _complete_component_build(cls):
 | |
|         super(Mapper, cls)._complete_component_build()
 | |
|         cls._build_mapper_component()
 | |
| 
 | |
|     def __init__(self, work):
 | |
|         super(Mapper, self).__init__(work)
 | |
|         self._options = None
 | |
| 
 | |
|     def _map_direct(self, record, from_attr, to_attr):
 | |
|         """ Apply the ``direct`` mappings.
 | |
| 
 | |
|         :param record: record to convert from a source to a target
 | |
|         :param from_attr: name of the source attribute or a callable
 | |
|         :type from_attr: callable | str
 | |
|         :param to_attr: name of the target attribute
 | |
|         :type to_attr: str
 | |
|         """
 | |
|         raise NotImplementedError
 | |
| 
 | |
|     def _map_children(self, record, attr, model):
 | |
|         raise NotImplementedError
 | |
| 
 | |
|     @property
 | |
|     def map_methods(self):
 | |
|         """ Yield all the methods decorated with ``@mapping`` """
 | |
|         for meth, definition in self._map_methods.items():
 | |
|             yield getattr(self, meth), definition
 | |
| 
 | |
|     def _get_map_child_component(self, model_name):
 | |
|         try:
 | |
|             mapper_child = self.component(usage=self._map_child_usage,
 | |
|                                           model_name=model_name)
 | |
|         except NoComponentError:
 | |
|             assert self._map_child_fallback is not None, (
 | |
|                 "_map_child_fallback required")
 | |
|             # does not force developers to use a MapChild ->
 | |
|             # will use the default one if not explicitely defined
 | |
|             mapper_child = self.component_by_name(
 | |
|                 self._map_child_fallback,
 | |
|                 model_name=model_name
 | |
|             )
 | |
|         return mapper_child
 | |
| 
 | |
|     def _map_child(self, map_record, from_attr, to_attr, model_name):
 | |
|         """ Convert items of the record as defined by children """
 | |
|         assert self._map_child_usage is not None, "_map_child_usage required"
 | |
|         child_records = map_record.source[from_attr]
 | |
|         mapper_child = self._get_map_child_component(model_name)
 | |
|         items = mapper_child.get_items(child_records, map_record,
 | |
|                                        to_attr, options=self.options)
 | |
|         return items
 | |
| 
 | |
|     @contextmanager
 | |
|     def _mapping_options(self, options):
 | |
|         """ Change the mapping options for the Mapper.
 | |
| 
 | |
|         Context Manager to use in order to alter the behavior
 | |
|         of the mapping, when using ``_apply`` or ``finalize``.
 | |
| 
 | |
|         """
 | |
|         current = self._options
 | |
|         self._options = options
 | |
|         yield
 | |
|         self._options = current
 | |
| 
 | |
|     @property
 | |
|     def options(self):
 | |
|         """ Options can be accessed in the mapping methods with
 | |
|         ``self.options``. """
 | |
|         return self._options
 | |
| 
 | |
|     def changed_by_fields(self):
 | |
|         """ 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``.
 | |
|         """
 | |
|         changed_by = set()
 | |
|         if getattr(self, 'direct', None):
 | |
|             for from_attr, __ in self.direct:
 | |
|                 fieldname = self._direct_source_field_name(from_attr)
 | |
|                 changed_by.add(fieldname)
 | |
| 
 | |
|         for method_name, method_def in self._map_methods.items():
 | |
|             changed_by |= method_def.changed_by
 | |
|         return changed_by
 | |
| 
 | |
|     def _direct_source_field_name(self, direct_entry):
 | |
|         """ Get the mapping field name. Goes through the function modifiers.
 | |
| 
 | |
|         Ex::
 | |
| 
 | |
|             [(none(convert(field_name, str)), out_field_name)]
 | |
| 
 | |
|         It assumes that the modifier has ``field`` as first argument like::
 | |
| 
 | |
|             def modifier(field, args):
 | |
| 
 | |
|         """
 | |
|         fieldname = direct_entry
 | |
|         if isinstance(direct_entry, collections.Callable):
 | |
|             # Map the closure entries with variable names
 | |
|             cells = dict(list(zip(
 | |
|                 direct_entry.__code__.co_freevars,
 | |
|                 (c.cell_contents for c in direct_entry.__closure__))))
 | |
|             assert 'field' in cells, "Modifier without 'field' argument."
 | |
|             if isinstance(cells['field'], collections.Callable):
 | |
|                 fieldname = self._direct_source_field_name(cells['field'])
 | |
|             else:
 | |
|                 fieldname = cells['field']
 | |
|         return fieldname
 | |
| 
 | |
|     def map_record(self, record, parent=None):
 | |
|         """ Get a :py:class:`MapRecord` with record, ready to be
 | |
|         converted using the current Mapper.
 | |
| 
 | |
|         :param record: record to transform
 | |
|         :param parent: optional parent record, for items
 | |
| 
 | |
|         """
 | |
|         return MapRecord(self, record, parent=parent)
 | |
| 
 | |
|     def _apply(self, map_record, options=None):
 | |
|         """ Apply the mappings on a :py:class:`MapRecord`
 | |
| 
 | |
|         :param map_record: source record to convert
 | |
|         :type map_record: :py:class:`MapRecord`
 | |
| 
 | |
|         """
 | |
|         if options is None:
 | |
|             options = {}
 | |
|         with self._mapping_options(options):
 | |
|             return self._apply_with_options(map_record)
 | |
| 
 | |
|     def _apply_with_options(self, map_record):
 | |
|         """ Apply the mappings on a :py:class:`MapRecord` with
 | |
|         contextual options (the ``options`` given in
 | |
|         :py:meth:`MapRecord.values()` are accessible in
 | |
|         ``self.options``)
 | |
| 
 | |
|         :param map_record: source record to convert
 | |
|         :type map_record: :py:class:`MapRecord`
 | |
| 
 | |
|         """
 | |
|         assert self.options is not None, (
 | |
|             "options should be defined with '_mapping_options'")
 | |
|         _logger.debug('converting record %s to model %s',
 | |
|                       map_record.source, self.model)
 | |
| 
 | |
|         fields = self.options.fields
 | |
|         for_create = self.options.for_create
 | |
|         result = {}
 | |
|         for from_attr, to_attr in self.direct:
 | |
|             if isinstance(from_attr, collections.Callable):
 | |
|                 attr_name = self._direct_source_field_name(from_attr)
 | |
|             else:
 | |
|                 attr_name = from_attr
 | |
| 
 | |
|             if (not fields or attr_name in fields):
 | |
|                 value = self._map_direct(map_record.source,
 | |
|                                          from_attr,
 | |
|                                          to_attr)
 | |
|                 result[to_attr] = value
 | |
| 
 | |
|         for meth, definition in self.map_methods:
 | |
|             mapping_changed_by = definition.changed_by
 | |
|             if (not fields or not mapping_changed_by or
 | |
|                     mapping_changed_by.intersection(fields)):
 | |
|                 if definition.only_create and not for_create:
 | |
|                     continue
 | |
|                 values = meth(map_record.source)
 | |
|                 if not values:
 | |
|                     continue
 | |
|                 if not isinstance(values, dict):
 | |
|                     raise ValueError('%s: invalid return value for the '
 | |
|                                      'mapping method %s' % (values, meth))
 | |
|                 result.update(values)
 | |
| 
 | |
|         for from_attr, to_attr, model_name in self.children:
 | |
|             if (not fields or from_attr in fields):
 | |
|                 result[to_attr] = self._map_child(map_record, from_attr,
 | |
|                                                   to_attr, model_name)
 | |
| 
 | |
|         return self.finalize(map_record, result)
 | |
| 
 | |
|     def finalize(self, map_record, values):
 | |
|         """ Called at the end of the mapping.
 | |
| 
 | |
|         Can be used to modify the values generated by all the mappings before
 | |
|         returning them.
 | |
| 
 | |
|         :param map_record: source map_record
 | |
|         :type map_record: :py:class:`MapRecord`
 | |
|         :param values: mapped values
 | |
|         :returns: mapped values
 | |
|         :rtype: dict
 | |
|         """
 | |
|         return values
 | |
| 
 | |
| 
 | |
| class ImportMapper(AbstractComponent):
 | |
|     """ :py:class:`Mapper` for imports.
 | |
| 
 | |
|     Transform a record from a backend to an Odoo record
 | |
| 
 | |
|     """
 | |
| 
 | |
|     _name = 'base.import.mapper'
 | |
|     _inherit = 'base.mapper'
 | |
|     _usage = 'import.mapper'
 | |
| 
 | |
|     _map_child_usage = 'import.map.child'
 | |
|     _map_child_fallback = 'base.map.child.import'
 | |
| 
 | |
|     def _map_direct(self, record, from_attr, to_attr):
 | |
|         """ Apply the ``direct`` mappings.
 | |
| 
 | |
|         :param record: record to convert from a source to a target
 | |
|         :param from_attr: name of the source attribute or a callable
 | |
|         :type from_attr: callable | str
 | |
|         :param to_attr: name of the target attribute
 | |
|         :type to_attr: str
 | |
|         """
 | |
|         if isinstance(from_attr, collections.Callable):
 | |
|             return from_attr(self, record, to_attr)
 | |
| 
 | |
|         value = record.get(from_attr)
 | |
|         if not value:
 | |
|             return False
 | |
| 
 | |
|         # Backward compatibility: when a field is a relation, and a modifier is
 | |
|         # not used, we assume that the relation model is a binding.
 | |
|         # Use an explicit modifier external_to_m2o in the 'direct' mappings to
 | |
|         # change that.
 | |
|         field = self.model._fields[to_attr]
 | |
|         if field.type == 'many2one':
 | |
|             mapping_func = external_to_m2o(from_attr)
 | |
|             value = mapping_func(self, record, to_attr)
 | |
|         return value
 | |
| 
 | |
| 
 | |
| class ExportMapper(AbstractComponent):
 | |
|     """ :py:class:`Mapper` for exports.
 | |
| 
 | |
|     Transform a record from Odoo to a backend record
 | |
| 
 | |
|     """
 | |
| 
 | |
|     _name = 'base.export.mapper'
 | |
|     _inherit = 'base.mapper'
 | |
|     _usage = 'export.mapper'
 | |
| 
 | |
|     _map_child_usage = 'export.map.child'
 | |
|     _map_child_fallback = 'base.map.child.export'
 | |
| 
 | |
|     def _map_direct(self, record, from_attr, to_attr):
 | |
|         """ Apply the ``direct`` mappings.
 | |
| 
 | |
|         :param record: record to convert from a source to a target
 | |
|         :param from_attr: name of the source attribute or a callable
 | |
|         :type from_attr: callable | str
 | |
|         :param to_attr: name of the target attribute
 | |
|         :type to_attr: str
 | |
|         """
 | |
|         if isinstance(from_attr, collections.Callable):
 | |
|             return from_attr(self, record, to_attr)
 | |
| 
 | |
|         value = record[from_attr]
 | |
|         if not value:
 | |
|             return False
 | |
| 
 | |
|         # Backward compatibility: when a field is a relation, and a modifier is
 | |
|         # not used, we assume that the relation model is a binding.
 | |
|         # Use an explicit modifier m2o_to_external  in the 'direct' mappings to
 | |
|         # change that.
 | |
|         field = self.model._fields[from_attr]
 | |
|         if field.type == 'many2one':
 | |
|             mapping_func = m2o_to_external(from_attr)
 | |
|             value = mapping_func(self, record, to_attr)
 | |
|         return value
 | |
| 
 | |
| 
 | |
| class MapRecord(object):
 | |
|     """ A record prepared to be converted using a :py:class:`Mapper`.
 | |
| 
 | |
|     MapRecord instances are prepared by :py:meth:`Mapper.map_record`.
 | |
| 
 | |
|     Usage::
 | |
| 
 | |
|         >>> map_record = mapper.map_record(record)
 | |
|         >>> output_values = map_record.values()
 | |
| 
 | |
|     See :py:meth:`values` for more information on the available arguments.
 | |
| 
 | |
|     """
 | |
| 
 | |
|     def __init__(self, mapper, source, parent=None):
 | |
|         self._source = source
 | |
|         self._mapper = mapper
 | |
|         self._parent = parent
 | |
|         self._forced_values = {}
 | |
| 
 | |
|     @property
 | |
|     def source(self):
 | |
|         """ Source record to be converted """
 | |
|         return self._source
 | |
| 
 | |
|     @property
 | |
|     def parent(self):
 | |
|         """ Parent record if the current record is an item """
 | |
|         return self._parent
 | |
| 
 | |
|     def values(self, for_create=None, fields=None, **kwargs):
 | |
|         """ 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 in ``direct`` arguments,
 | |
|             either the ``changed_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
 | |
|             using ``self.options``.
 | |
| 
 | |
|             ::
 | |
| 
 | |
|                 >>> output_values = map_record.values(tax_include=True)
 | |
| 
 | |
|         :param for_create: specify if only the mappings for creation
 | |
|                            (``@only_create``) should be mapped.
 | |
|         :type for_create: boolean
 | |
|         :param fields: filter on fields
 | |
|         :type fields: list
 | |
|         :param ``**kwargs``: custom options, they can later be used in the
 | |
|                              mapping methods
 | |
| 
 | |
|         """
 | |
|         options = MapOptions(for_create=for_create, fields=fields, **kwargs)
 | |
|         values = self._mapper._apply(self, options=options)
 | |
|         values.update(self._forced_values)
 | |
|         return values
 | |
| 
 | |
|     def update(self, *args, **kwargs):
 | |
|         """ 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.
 | |
| 
 | |
|         """
 | |
|         self._forced_values.update(*args, **kwargs)
 | |
| 
 | |
| 
 | |
| class MapOptions(dict):
 | |
|     """ Container for the options of mappings.
 | |
| 
 | |
|     Options can be accessed using attributes of the instance.  When an
 | |
|     option is accessed and does not exist, it returns None.
 | |
| 
 | |
|     """
 | |
| 
 | |
|     def __getitem__(self, key):
 | |
|         try:
 | |
|             return super(MapOptions, self).__getitem__(key)
 | |
|         except KeyError:
 | |
|             return None
 | |
| 
 | |
|     def __getattr__(self, key):
 | |
|         return self[key]
 | |
| 
 | |
|     def __setattr__(self, key, value):
 | |
|         self[key] = value
 |