# -*- coding: utf-8 -*- # Copyright 2013-2017 Camptocamp SA # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) import mock from odoo.addons.component.core import Component, WorkContext from odoo.addons.component.tests.common import ( ComponentRegistryCase, TransactionComponentRegistryCase, ) from odoo.addons.connector.components.mapper import ( MappingDefinition, changed_by, only_create, convert, follow_m2o_relations, m2o_to_external, external_to_m2o, none, MapOptions, mapping, ) class TestMapper(ComponentRegistryCase): def setUp(self): super(TestMapper, self).setUp() self.comp_registry.load_components('connector') def test_mapping_decorator(self): class KifKrokerMapper(Component): _name = 'kif.kroker.mapper' _inherit = 'base.mapper' @changed_by('name', 'city') @mapping @only_create def name(self): pass @changed_by('email') @mapping def email(self): pass @changed_by('street') @mapping def street(self): pass def no_decorator(self): pass # build our mapper component KifKrokerMapper._build_component(self.comp_registry) # what mappings we expect name_def = MappingDefinition(changed_by=set(('name', 'city')), only_create=True) email_def = MappingDefinition(changed_by=set(('email',)), only_create=False) street_def = MappingDefinition(changed_by=set(('street',)), only_create=False) # get our component by name in the components registry comp = self.comp_registry['kif.kroker.mapper'] # _map_methods contains the aggregated mapping methods for a Mapper self.assertEqual(comp._map_methods, {'name': name_def, 'email': email_def, 'street': street_def, }) def test_mapping_decorator_cross_classes(self): """ Mappings should not propagate to other classes""" class MomMapper(Component): _name = 'mom.mapper' _inherit = 'base.mapper' _apply_on = 'res.users' @changed_by('name', 'city') @mapping def name(self): pass class ZappMapper(Component): _name = 'zapp.mapper' _inherit = 'base.mapper' _apply_on = 'res.users' @changed_by('email') @only_create @mapping def email(self): pass self._build_components(MomMapper, ZappMapper) mom_def = MappingDefinition(changed_by=set(('name', 'city')), only_create=False) zapp_def = MappingDefinition(changed_by=set(('email',)), only_create=True) comp = self.comp_registry['mom.mapper'] self.assertEqual(comp._map_methods, {'name': mom_def}) comp = self.comp_registry['zapp.mapper'] self.assertEqual(comp._map_methods, {'email': zapp_def}) def test_mapping_decorator_cumul(self): """ Mappings should cumulate the ``super`` mappings and the local mappings.""" class FryMapper(Component): _name = 'fry.mapper' _inherit = 'base.mapper' _apply_on = 'res.users' @changed_by('name', 'city') @mapping def name(self): pass class FryMapperInherit(Component): _inherit = 'fry.mapper' @changed_by('email') @mapping def email(self): pass self._build_components(FryMapper, FryMapperInherit) name_def = MappingDefinition(changed_by=set(('name', 'city')), only_create=False) email_def = MappingDefinition(changed_by=set(('email',)), only_create=False) comp = self.comp_registry['fry.mapper'] self.assertEqual(comp._map_methods, {'name': name_def, 'email': email_def}) def test_mapping_decorator_cumul_changed_by(self): """ Mappings should cumulate the changed_by fields of the ``super`` mappings and the local mappings """ class FryMapper(Component): _name = 'fry.mapper' _inherit = 'base.mapper' _apply_on = 'res.users' @changed_by('name', 'city') @mapping def name(self): pass class FryMapperInherit(Component): _inherit = 'fry.mapper' _apply_on = 'res.users' @changed_by('email') @mapping def name(self): pass class ThirdMapper(Component): _name = 'third.mapper' _inherit = 'fry.mapper' _apply_on = 'res.users' @changed_by('email', 'street') @mapping def name(self): pass self._build_components(FryMapper, FryMapperInherit, ThirdMapper) name_def = MappingDefinition(changed_by=set(('name', 'city', 'email')), only_create=False) comp = self.comp_registry['fry.mapper'] self.assertEqual(comp._map_methods, {'name': name_def}) name_def = MappingDefinition(changed_by=set(('name', 'city', 'email', 'street')), only_create=False) comp = self.comp_registry['third.mapper'] self.assertEqual(comp._map_methods, {'name': name_def}) def test_several_bases_cumul(self): class FryMapper(Component): _name = 'fry.mapper' _inherit = 'base.mapper' _apply_on = 'res.users' @changed_by('name', 'city') @mapping def name(self): pass @only_create @mapping def street(self): pass @only_create @mapping def zip(self): pass class FarnsworthMapper(Component): _name = 'farnsworth.mapper' _inherit = 'base.mapper' _apply_on = 'res.users' @changed_by('email') @mapping def name(self): pass @changed_by('street') @mapping def city(self): pass @mapping def zip(self): pass class ThirdMapper(Component): _name = 'third.mapper' _inherit = ['fry.mapper', 'farnsworth.mapper'] _apply_on = 'res.users' @changed_by('email', 'street') @mapping def name(self): pass @mapping def email(self): pass self._build_components(FryMapper, FarnsworthMapper, ThirdMapper) name_def = MappingDefinition(changed_by=set(('name', 'city', 'email', 'street')), only_create=False) street_def = MappingDefinition(changed_by=set([]), only_create=True) city_def = MappingDefinition(changed_by=set(('street',)), only_create=False) email_def = MappingDefinition(changed_by=set([]), only_create=False) zip_def = MappingDefinition(changed_by=set([]), only_create=True) comp = self.comp_registry['third.mapper'] self.assertEqual(comp._map_methods['name'], name_def) self.assertEqual(comp._map_methods['street'], street_def) self.assertEqual(comp._map_methods['city'], city_def) self.assertEqual(comp._map_methods['email'], email_def) self.assertEqual(comp._map_methods['zip'], zip_def) def test_mapping_record(self): """ Map a record and check the result """ class MyMapper(Component): _name = 'my.mapper' _inherit = 'base.import.mapper' _apply_on = 'res.users' direct = [('name', 'out_name')] @mapping def street(self, record): return {'out_street': record['street'].upper()} self._build_components(MyMapper) record = {'name': 'Guewen', 'street': 'street'} work = mock.MagicMock(name='WorkContext()') mapper = self.comp_registry['my.mapper'](work) map_record = mapper.map_record(record) expected = {'out_name': 'Guewen', 'out_street': 'STREET'} self.assertEqual(map_record.values(), expected) self.assertEqual(map_record.values(for_create=True), expected) def test_mapping_record_on_create(self): """ Map a record and check the result for creation of record """ class MyMapper(Component): _name = 'my.mapper' _inherit = 'base.import.mapper' _apply_on = 'res.users' direct = [('name', 'out_name')] @mapping def street(self, record): return {'out_street': record['street'].upper()} @only_create @mapping def city(self, record): return {'out_city': 'city'} self._build_components(MyMapper) record = {'name': 'Guewen', 'street': 'street'} work = mock.MagicMock(name='WorkContext()') mapper = self.comp_registry['my.mapper'](work) map_record = mapper.map_record(record) expected = {'out_name': 'Guewen', 'out_street': 'STREET'} self.assertEqual(map_record.values(), expected) expected = {'out_name': 'Guewen', 'out_street': 'STREET', 'out_city': 'city'} self.assertEqual(map_record.values(for_create=True), expected) def test_mapping_update(self): """ Force values on a map record """ class MyMapper(Component): _name = 'my.mapper' _inherit = 'base.import.mapper' direct = [('name', 'out_name')] @mapping def street(self, record): return {'out_street': record['street'].upper()} @only_create @mapping def city(self, record): return {'out_city': 'city'} self._build_components(MyMapper) record = {'name': 'Guewen', 'street': 'street'} work = mock.MagicMock(name='WorkContext()') mapper = self.comp_registry['my.mapper'](work) map_record = mapper.map_record(record) map_record.update({'test': 1}, out_city='forced') expected = {'out_name': 'Guewen', 'out_street': 'STREET', 'out_city': 'forced', 'test': 1} self.assertEqual(map_record.values(), expected) expected = {'out_name': 'Guewen', 'out_street': 'STREET', 'out_city': 'forced', 'test': 1} self.assertEqual(map_record.values(for_create=True), expected) def test_finalize(self): """ Inherit finalize to modify values """ class MyMapper(Component): _name = 'my.mapper' _inherit = 'base.import.mapper' direct = [('name', 'out_name')] def finalize(self, record, values): result = super(MyMapper, self).finalize(record, values) result['test'] = 'abc' return result self._build_components(MyMapper) record = {'name': 'Guewen', 'street': 'street'} work = mock.MagicMock(name='WorkContext()') mapper = self.comp_registry['my.mapper'](work) map_record = mapper.map_record(record) expected = {'out_name': 'Guewen', 'test': 'abc'} self.assertEqual(map_record.values(), expected) expected = {'out_name': 'Guewen', 'test': 'abc'} self.assertEqual(map_record.values(for_create=True), expected) def test_some_fields(self): """ Map only a selection of fields """ class MyMapper(Component): _name = 'my.mapper' _inherit = 'base.import.mapper' direct = [('name', 'out_name'), ('street', 'out_street'), ] @changed_by('country') @mapping def country(self, record): return {'country': 'country'} self._build_components(MyMapper) record = {'name': 'Guewen', 'street': 'street', 'country': 'country'} work = mock.MagicMock(name='WorkContext()') mapper = self.comp_registry['my.mapper'](work) map_record = mapper.map_record(record) expected = {'out_name': 'Guewen', 'country': 'country'} self.assertEqual(map_record.values(fields=['name', 'country']), expected) expected = {'out_name': 'Guewen', 'country': 'country'} self.assertEqual(map_record.values(for_create=True, fields=['name', 'country']), expected) def test_mapping_modifier(self): """ Map a direct record with a modifier function """ def do_nothing(field): def transform(self, record, to_attr): return record[field] return transform class MyMapper(Component): _name = 'my.mapper' _inherit = 'base.import.mapper' direct = [(do_nothing('name'), 'out_name')] self._build_components(MyMapper) record = {'name': 'Guewen'} work = mock.MagicMock(name='WorkContext()') mapper = self.comp_registry['my.mapper'](work) map_record = mapper.map_record(record) expected = {'out_name': 'Guewen'} self.assertEqual(map_record.values(), expected) self.assertEqual(map_record.values(for_create=True), expected) def test_mapping_direct_property(self): """ Map a direct record with 'direct' being a property """ class MyMapper(Component): _name = 'my.mapper' _inherit = 'base.import.mapper' @property def direct(self): return [('name', 'out_name')] self._build_components(MyMapper) record = {'name': 'Foo'} work = mock.MagicMock(name='WorkContext()') mapper = self.comp_registry['my.mapper'](work) map_record = mapper.map_record(record) expected = {'out_name': 'Foo'} self.assertEqual(map_record.values(), expected) self.assertEqual(map_record.values(for_create=True), expected) def test_mapping_convert(self): """ Map a direct record with the convert modifier function """ class MyMapper(Component): _name = 'my.mapper' _inherit = 'base.import.mapper' direct = [(convert('name', int), 'out_name')] self._build_components(MyMapper) record = {'name': '300'} work = mock.MagicMock(name='WorkContext()') mapper = self.comp_registry['my.mapper'](work) map_record = mapper.map_record(record) expected = {'out_name': 300} self.assertEqual(map_record.values(), expected) self.assertEqual(map_record.values(for_create=True), expected) def test_mapping_modifier_none(self): """ Pipeline of modifiers """ class MyMapper(Component): _name = 'my.mapper' _inherit = 'base.import.mapper' direct = [(none('in_f'), 'out_f'), (none('in_t'), 'out_t')] self._build_components(MyMapper) record = {'in_f': False, 'in_t': True} work = mock.MagicMock(name='WorkContext()') mapper = self.comp_registry['my.mapper'](work) map_record = mapper.map_record(record) expected = {'out_f': None, 'out_t': True} self.assertEqual(map_record.values(), expected) self.assertEqual(map_record.values(for_create=True), expected) def test_mapping_modifier_pipeline(self): """ Pipeline of modifiers """ class MyMapper(Component): _name = 'my.mapper' _inherit = 'base.import.mapper' direct = [(none(convert('in_f', bool)), 'out_f'), (none(convert('in_t', bool)), 'out_t')] self._build_components(MyMapper) record = {'in_f': 0, 'in_t': 1} work = mock.MagicMock(name='WorkContext()') mapper = self.comp_registry['my.mapper'](work) map_record = mapper.map_record(record) expected = {'out_f': None, 'out_t': True} self.assertEqual(map_record.values(), expected) self.assertEqual(map_record.values(for_create=True), expected) def test_modifier_import_filter_field(self): """ A direct mapping with a modifier must still be considered from the list of fields """ class MyMapper(Component): _name = 'my.mapper' _inherit = 'base.import.mapper' direct = [('field', 'field2'), ('no_field', 'no_field2'), (convert('name', int), 'out_name')] self._build_components(MyMapper) record = {'name': '300', 'field': 'value', 'no_field': 'no_value'} work = mock.MagicMock(name='WorkContext()') mapper = self.comp_registry['my.mapper'](work) map_record = mapper.map_record(record) expected = {'out_name': 300, 'field2': 'value'} self.assertEqual(map_record.values(fields=['field', 'name']), expected) self.assertEqual(map_record.values(for_create=True, fields=['field', 'name']), expected) def test_modifier_export_filter_field(self): """ A direct mapping with a modifier on an export mapping """ class MyMapper(Component): _name = 'my.mapper' _inherit = 'base.export.mapper' direct = [('field', 'field2'), ('no_field', 'no_field2'), (convert('name', int), 'out_name')] self._build_components(MyMapper) record = {'name': '300', 'field': 'value', 'no_field': 'no_value'} work = mock.MagicMock(name='WorkContext()') mapper = self.comp_registry['my.mapper'](work) map_record = mapper.map_record(record) expected = {'out_name': 300, 'field2': 'value'} self.assertEqual(map_record.values(fields=['field', 'name']), expected) self.assertEqual(map_record.values(for_create=True, fields=['field', 'name']), expected) def test_mapping_custom_option(self): """ Usage of custom options in mappings """ class MyMapper(Component): _name = 'my.mapper' _inherit = 'base.import.mapper' @mapping def any(self, record): if self.options.custom: res = True else: res = False return {'res': res} self._build_components(MyMapper) record = {} work = mock.MagicMock(name='WorkContext()') mapper = self.comp_registry['my.mapper'](work) map_record = mapper.map_record(record) expected = {'res': True} self.assertEqual(map_record.values(custom=True), expected) def test_mapping_custom_option_not_defined(self): """ Usage of custom options not defined raise AttributeError """ class MyMapper(Component): _name = 'my.mapper' _inherit = 'base.import.mapper' @mapping def any(self, record): if self.options.custom is None: res = True else: res = False return {'res': res} self._build_components(MyMapper) record = {} work = mock.MagicMock(name='WorkContext()') mapper = self.comp_registry['my.mapper'](work) map_record = mapper.map_record(record) expected = {'res': True} self.assertEqual(map_record.values(), expected) def test_map_options(self): """ Test MapOptions """ options = MapOptions({'xyz': 'abc'}, k=1) options.l = 2 self.assertEqual(options['xyz'], 'abc') self.assertEqual(options['k'], 1) self.assertEqual(options['l'], 2) self.assertEqual(options.xyz, 'abc') self.assertEqual(options.k, 1) self.assertEqual(options.l, 2) self.assertEqual(options['undefined'], None) self.assertEqual(options.undefined, None) def test_changed_by_fields(self): """ Test attribute ``_changed_by_fields`` on Mapper.""" class MyMapper(Component): _name = 'my.mapper' _inherit = 'base.export.mapper' direct = [('street', 'out_street'), (none('in_t'), 'out_t'), (none(convert('in_f', bool)), 'out_f')] @changed_by('name', 'city') @mapping def name(self): pass @changed_by('email') @mapping def email(self): pass def no_decorator(self): pass self._build_components(MyMapper) work = mock.MagicMock(name='WorkContext()') mapper = self.comp_registry['my.mapper'](work) self.assertEqual( mapper.changed_by_fields(), set(['street', 'in_t', 'in_f', 'name', 'city', 'email'])) class TestMapperRecordsets(TransactionComponentRegistryCase): """ Test mapper with "real" records instead of mocks """ def setUp(self): super(TestMapperRecordsets, self).setUp() self.comp_registry.load_components('connector') backend_record = mock.Mock() backend_record.env = self.env self.work = WorkContext(model_name='res.partner', collection=backend_record, components_registry=self.comp_registry) def test_mapping_modifier_follow_m2o_relations(self): """ Map with the follow_m2o_relations modifier """ class MyMapper(Component): _name = 'my.mapper' _inherit = 'base.import.mapper' direct = [ (follow_m2o_relations('parent_id.name'), 'parent_name'), ] self._build_components(MyMapper) partner = self.env.ref('base.res_partner_address_4') mapper = self.comp_registry['my.mapper'](self.work) map_record = mapper.map_record(partner) expected = {'parent_name': 'Agrolait'} self.assertEqual(map_record.values(), expected) self.assertEqual(map_record.values(for_create=True), expected) class TestMapperBinding(TransactionComponentRegistryCase): """ Test Mapper with Bindings""" def setUp(self): super(TestMapperBinding, self).setUp() self.comp_registry.load_components('connector') backend_record = mock.Mock() backend_record.env = self.env backend_record._name = 'my.collection' self.work = WorkContext(model_name='res.partner', collection=backend_record, components_registry=self.comp_registry) self.country_binder = mock.MagicMock(name='country_binder') self.country_binder.return_value = self.country_binder self.country_binder._name = 'test.binder' self.country_binder._inherit = 'base.binder' self.country_binder.apply_on_models = ['res.country'] self.country_binder._usage = 'binder' self.country_binder._collection = 'my.collection' self.country_binder._abstract = False self.comp_registry['test.binder'] = self.country_binder def test_mapping_m2o_to_external(self): """ Map a direct record with the m2o_to_external modifier function """ class MyMapper(Component): _name = 'my.mapper' _inherit = 'base.import.mapper' _apply_on = 'res.partner' direct = [(m2o_to_external('country_id'), 'country')] self._build_components(MyMapper) partner = self.env.ref('base.main_partner') partner.write({'country_id': self.env.ref('base.ch').id}) self.country_binder.to_external.return_value = 10 mapper = self.comp_registry['my.mapper'](self.work) map_record = mapper.map_record(partner) self.assertEqual(map_record.values(), {'country': 10}) self.country_binder.to_external.assert_called_once_with( partner.country_id.id, wrap=False) def test_mapping_backend_to_m2o(self): """ Map a direct record with the backend_to_m2o modifier function """ class MyMapper(Component): _name = 'my.mapper' _inherit = 'base.import.mapper' _apply_on = 'res.partner' direct = [(external_to_m2o('country'), 'country_id')] self._build_components(MyMapper) record = {'country': 10} ch = self.env.ref('base.ch') self.country_binder.to_internal.return_value = ch mapper = self.comp_registry['my.mapper'](self.work) map_record = mapper.map_record(record) self.assertEqual(map_record.values(), {'country_id': ch.id}) self.country_binder.to_internal.assert_called_once_with( 10, unwrap=False) def test_mapping_record_children_no_map_child(self): """ Map a record with children, using default MapChild """ # we need these components which make the 'link' between # the main mapper and the line mapper class LineMapper(Component): _name = 'line.mapper' _inherit = 'base.import.mapper' _apply_on = 'res.currency.rate' direct = [('name', 'name')] @mapping def price(self, record): return {'rate': record['rate'] * 2} @only_create @mapping def discount(self, record): return {'test': .5} class MyMapper(Component): _name = 'my.mapper' _inherit = 'base.import.mapper' _apply_on = 'res.currency' direct = [('name', 'name')] children = [('lines', 'line_ids', 'res.currency.rate')] self._build_components(LineMapper, MyMapper) record = {'name': 'SO1', 'lines': [{'name': '2013-11-07', 'rate': 10}, {'name': '2013-11-08', 'rate': 20}]} mapper = self.comp_registry['my.mapper'](self.work) map_record = mapper.map_record(record) expected = {'name': 'SO1', 'line_ids': [(0, 0, {'name': '2013-11-07', 'rate': 20}), (0, 0, {'name': '2013-11-08', 'rate': 40})] } self.assertEqual(map_record.values(), expected) expected = {'name': 'SO1', 'line_ids': [(0, 0, {'name': '2013-11-07', 'rate': 20, 'test': .5}), (0, 0, {'name': '2013-11-08', 'rate': 40, 'test': .5})] } self.assertEqual(map_record.values(for_create=True), expected) def test_mapping_record_children(self): """ Map a record with children, using defined MapChild """ # we need these components which make the 'link' between # the main mapper and the line mapper class LineMapper(Component): _name = 'line.mapper' _inherit = 'base.import.mapper' _apply_on = 'res.currency.rate' direct = [('name', 'name')] @mapping def price(self, record): return {'rate': record['rate'] * 2} @only_create @mapping def discount(self, record): return {'test': .5} class LineImportMapChild(Component): _name = 'line.map.child.import' _inherit = 'base.map.child.import' _apply_on = 'res.currency.rate' def format_items(self, items_values): return [('ABC', values) for values in items_values] class MyMapper(Component): _name = 'my.mapper' _inherit = 'base.import.mapper' _apply_on = 'res.currency' direct = [('name', 'name')] children = [('lines', 'line_ids', 'res.currency.rate')] self._build_components(LineMapper, LineImportMapChild, MyMapper) record = {'name': 'SO1', 'lines': [{'name': '2013-11-07', 'rate': 10}, {'name': '2013-11-08', 'rate': 20}]} mapper = self.comp_registry['my.mapper'](self.work) map_record = mapper.map_record(record) expected = {'name': 'SO1', 'line_ids': [('ABC', {'name': '2013-11-07', 'rate': 20}), ('ABC', {'name': '2013-11-08', 'rate': 40})] } self.assertEqual(map_record.values(), expected) expected = {'name': 'SO1', 'line_ids': [('ABC', {'name': '2013-11-07', 'rate': 20, 'test': .5}), ('ABC', {'name': '2013-11-08', 'rate': 40, 'test': .5})] } self.assertEqual(map_record.values(for_create=True), expected) def test_mapping_record_children_void(self): """ Map a record with children, using defined MapChild """ class LineMapper(Component): _name = 'line.mapper' _inherit = 'base.import.mapper' _apply_on = 'res.currency.rate' @mapping def price(self, record): rate = record.get('rate') if rate and rate < 40: return {'rate': record['rate'] * 2} class SaleLineImportMapChild(Component): _name = 'sale.line.mapper' _inherit = 'base.map.child.import' _apply_on = 'res.currency.rate' def format_items(self, items_values): return [('ABC', values) for values in items_values] class ObjectMapper(Component): _name = 'currency.mapper' _inherit = 'base.import.mapper' _apply_on = 'res.currency' direct = [('name', 'name')] children = [('lines', 'line_ids', 'res.currency.rate')] self._build_components( ObjectMapper, SaleLineImportMapChild, LineMapper ) # Test with an excluded child record record = { 'name': 'SO1', 'lines': [ {'rate': 10}, {'rate': 20}, {'rate': 30}, {'rate': 40}, ] } mapper = self.comp_registry['currency.mapper'](self.work) map_record = mapper.map_record(record) expected = { 'name': 'SO1', 'line_ids': [ ('ABC', {'rate': 20}), ('ABC', {'rate': 40}), ('ABC', {'rate': 60}), ] } self.assertEqual(map_record.values(), expected)