Merge lp:~camptocamp/openerp-connector/7.0-connector-closure-functions into lp:~openerp-connector-core-editors/openerp-connector/7.0

Proposed by Guewen Baconnier @ Camptocamp
Status: Merged
Approved by: Guewen Baconnier @ Camptocamp
Approved revision: 603
Merged at revision: 604
Proposed branch: lp:~camptocamp/openerp-connector/7.0-connector-closure-functions
Merge into: lp:~openerp-connector-core-editors/openerp-connector/7.0
Diff against target: 566 lines (+393/-30)
6 files modified
connector/CHANGES.rst (+3/-0)
connector/connector.py (+5/-1)
connector/doc/guides/concepts.rst (+4/-8)
connector/tests/test_backend.py (+1/-1)
connector/tests/test_mapper.py (+118/-0)
connector/unit/mapper.py (+262/-20)
To merge this branch: bzr merge lp:~camptocamp/openerp-connector/7.0-connector-closure-functions
Reviewer Review Type Date Requested Status
Nicolas Bessi - Camptocamp (community) Needs Information
Review via email: mp+193389@code.launchpad.net

Commit message

[ADD] Added the possibility to use modifiers functions in the 'direct' mappings.

It allows to reduce the code in the @mappings methods as common modifiers can be
used. The change also include 3 modifiers: one for converting the type of the source field, one for getting the binding or openerp id of a backend field, one for getting the backend id of an openerp id or binding id.

Description of the change

Implements the blueprint: https://blueprints.launchpad.net/openerp-connector/+spec/functions-in-direct-bindings

Allows to use Modifiers in the direct mappings.

In the mappings, we have the possibility to defined 'direct' mappings. Example:

    direct = [('source_field', 'target_field')]

Integers, chars, etc. are copied. IDs of m2o relations are passed to their Binder which returns the external ID from the binding ID (or conversely returns the binding ID from the external ID).
I observed that we often need to apply a similar transformation on the source values, noted examples:
 - A backend sends '0000-00-00' for empty dates, we have to replace that by None
 - A backend sends dates in ISO 8601 format, we need to convert them to UTC then to a naive timestamp
 - A backend sends float as integer (float*100)
 - We want a default value when a value is empty
 - And in fact the most common operation is to find an external ID for a local ID and conversely, it was automatic but only for IDs of bindings
We could already do the changes using @mapping methods, but it was very repetitive.
That's why I introduced the "modifiers". Example:

    direct [(iso8601('source_date'), 'target_date'),
            (ifmissing('source_field', 'default value'), 'target_field')]

They also resolve an issue with the m2o: they were implicitly trying to search a Binder for the relation of the m2o, so not working if the relation was a "normal record". Now, I recommend to explicit the operation:

        # partner_id relation's 'res.partner' (normal record)
        # mag_partner_id relation's 'magento.res.partner' (binding)
        # s_partner_id is the id on the backend
    direct = [(backend_to_m2o('s_partner_id', binding='magento.res.partner'), 'partner_id'), # we needed to give the binding model so it can use it to find the normal ID
              (backend_to_m2o('s_partner_id'), 'mag_partner_id')] # we don't give the binding model if it is the same than the relation

Example of implementation:

    def a_function(field):
        ''' ``field`` is the name of the source field '''
        def transform(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 transform

    direct = [
            (a_function('source'), 'target'),
    ]

To post a comment you must log in.
595. By Guewen Baconnier @ Camptocamp

[ADD] changelog

Revision history for this message
Nicolas Bessi - Camptocamp (nbessi-c2c-deactivatedaccount) wrote :

Hello,

Good idea, but if you apply a Higher-order function to your mapping is still a "direct"
mapping ? I find it a little bit confusing, but I get the finality of the change.

Maybe we may add a new "middleware or what else" class attribute that will allows to wrap any of the transformation (direct or not) with a wrapping function. Like in ring/wsgi etc webstack handler.

Regards

Nicolas

review: Needs Information
596. By Guewen Baconnier @ Camptocamp

[CHG] rename closures in modifier

597. By Guewen Baconnier @ Camptocamp

[FIX] previous revision, forgot the return...

598. By Guewen Baconnier @ Camptocamp

[ADD] 'none' modifier to convert False-ish values to None

599. By Guewen Baconnier @ Camptocamp

[FIX] when we use a modifier for the m2o and the relation is not a binding, we need to provide the name of the binding model

600. By Guewen Baconnier @ Camptocamp

[FIX] previous change: if we specify a binding model, it means that we have to wrap / unwrap the records

601. By Guewen Baconnier @ Camptocamp

[MRG] from master branch

602. By Guewen Baconnier @ Camptocamp

[ADD] new argument in the 'backend_to_m2o' modifier to allow binding on inactive records

Revision history for this message
Guewen Baconnier @ Camptocamp (gbaconnier-c2c) wrote :

> Hello,
>
> Good idea, but if you apply a Higher-order function to your mapping is still a
> "direct"
> mapping ? I find it a little bit confusing, but I get the finality of the
> change.

Maybe the name 'direct' was unfortunate in a first place. But that's still 'direct' in a sense: from 1 attribute to 1 attribute, we just place a modifier between them.

>
> Maybe we may add a new "middleware or what else" class attribute that will
> allows to wrap any of the transformation (direct or not) with a wrapping
> function. Like in ring/wsgi etc webstack handler.

My first opinion is that I think that will make the mappings more complex, the solution proposed here is straightforward and readable, example:

    direct = [(backend_to_m2o('deal_id'), 'offer_id'),
              (backend_to_m2o('product_id', binding='qoqa.product.template'), 'product_tmpl_id'),
              (backend_to_m2o('vat_id'), 'tax_id'),
              ('lot_size', 'lot_size'),
              ('unit_price', 'unit_price'),
              ]

Though, I don't think I get what you mean. Can you write an example of how you would use it on a Mapper?

>
> Regards
>
> Nicolas

Revision history for this message
Nicolas Bessi - Camptocamp (nbessi-c2c-deactivatedaccount) wrote :

Hello,

I was thinking about something like in Clojure transform:
(def direct {:a [inc :a]
                      :b [dec :b]
                      :c [add :a :b]
                      :d [sub :b :c]})

direct = {'offer_id': [backend_m2o, 'deal_id'],
          'product_id': [strip_id, backend_m2o, 'product_tmpl_id'],
          'cost_price': [sum, 'key_x', 'key_y']}

After discussion with Guewen I understood some use case I have missed that make uses only introspection not working.

I got your point for the variable name, maybe we should find something like one_on_one but I can leave with that.

Regards

Nicolas

603. By Guewen Baconnier @ Camptocamp

[FIX] the backward compatibility code was outdated: binding is the name of the binding to use, to provide only when the relation is not a binding

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'connector/CHANGES.rst'
--- connector/CHANGES.rst 2013-10-09 18:52:36 +0000
+++ connector/CHANGES.rst 2013-11-09 20:51:28 +0000
@@ -4,6 +4,9 @@
42.0.1.dev042.0.1.dev0
5~~~~~~~~~~5~~~~~~~~~~
66
7* Add the possibility to use 'Modifiers' functions in the 'direct
8 mappings' (details in the documentation of the Mapper class)
9
72.0.1 (2013-09-12)102.0.1 (2013-09-12)
8~~~~~~~~~~~~~~~~~~11~~~~~~~~~~~~~~~~~~
912
1013
=== modified file 'connector/connector.py'
--- connector/connector.py 2013-09-13 13:22:49 +0000
+++ connector/connector.py 2013-11-09 20:51:28 +0000
@@ -283,11 +283,15 @@
283 """283 """
284 raise NotImplementedError284 raise NotImplementedError
285285
286 def to_backend(self, binding_id):286 def to_backend(self, binding_id, wrap=False):
287 """ Give the external ID for an OpenERP binding ID287 """ Give the external ID for an OpenERP binding ID
288 (ID in a model magento.*)288 (ID in a model magento.*)
289289
290 :param binding_id: OpenERP binding ID for which we want the backend id290 :param binding_id: OpenERP binding ID for which we want the backend id
291 :param wrap: if False, binding_id is the ID of the binding,
292 if True, binding_id is the ID of the normal record, the
293 method will search the corresponding binding and returns
294 the backend id of the binding
291 :return: external ID of the record295 :return: external ID of the record
292 """296 """
293 raise NotImplementedError297 raise NotImplementedError
294298
=== modified file 'connector/doc/guides/concepts.rst'
--- connector/doc/guides/concepts.rst 2013-11-01 09:10:27 +0000
+++ connector/doc/guides/concepts.rst 2013-11-09 20:51:28 +0000
@@ -146,8 +146,7 @@
146systems.146systems.
147147
148The connector defines some base classes, which you can find below.148The connector defines some base classes, which you can find below.
149Note that you can define your own ConnectorUnits as well without149Note that you can define your own ConnectorUnits as well.
150reusing them.
151150
152Mappings151Mappings
153========152========
@@ -159,19 +158,16 @@
159158
160It supports:159It supports:
161160
162* direct mappings161direct mappings
163
164 Fields *a* is written in field *b*.162 Fields *a* is written in field *b*.
165163
166* method mappings164method mappings
167
168 A method is used to convert one or many fields to one or many165 A method is used to convert one or many fields to one or many
169 fields, with transformation.166 fields, with transformation.
170 It can be filtered, for example only applied when the record is167 It can be filtered, for example only applied when the record is
171 created or when the source fields are modified.168 created or when the source fields are modified.
172169
173* submapping170submapping
174
175 a sub-record (lines of a sale order) is converted using another171 a sub-record (lines of a sale order) is converted using another
176 Mapper172 Mapper
177173
178174
=== modified file 'connector/tests/test_backend.py'
--- connector/tests/test_backend.py 2013-08-23 20:34:05 +0000
+++ connector/tests/test_backend.py 2013-11-09 20:51:28 +0000
@@ -73,7 +73,7 @@
73 self.uid)73 self.uid)
7474
75 def tearDown(self):75 def tearDown(self):
76 super(test_backend_register, self).setUp()76 super(test_backend_register, self).tearDown()
77 BACKENDS.backends.clear()77 BACKENDS.backends.clear()
78 del self.backend._class_entries[:]78 del self.backend._class_entries[:]
7979
8080
=== modified file 'connector/tests/test_mapper.py'
--- connector/tests/test_mapper.py 2013-04-08 11:48:59 +0000
+++ connector/tests/test_mapper.py 2013-11-09 20:51:28 +0000
@@ -2,6 +2,7 @@
22
3import unittest23import unittest2
4import mock4import mock
5import openerp.tests.common as common
56
6from openerp.addons.connector.unit.mapper import (7from openerp.addons.connector.unit.mapper import (
7 Mapper,8 Mapper,
@@ -9,8 +10,16 @@
9 MappingDefinition,10 MappingDefinition,
10 changed_by,11 changed_by,
11 only_create,12 only_create,
13 convert,
14 m2o_to_backend,
15 backend_to_m2o,
16 none,
12 mapping)17 mapping)
1318
19from openerp.addons.connector.backend import Backend
20from openerp.addons.connector.connector import Environment
21from openerp.addons.connector.session import ConnectorSession
22
1423
15class test_mapper(unittest2.TestCase):24class test_mapper(unittest2.TestCase):
16 """ Test Mapper """25 """ Test Mapper """
@@ -234,3 +243,112 @@
234 'discount': .5})]243 'discount': .5})]
235 }244 }
236 self.assertEqual(mapper.data_for_create, expected)245 self.assertEqual(mapper.data_for_create, expected)
246
247 def test_mapping_modifier(self):
248 """ Map a direct record with a modifier function """
249
250 def do_nothing(field):
251 def transform(self, record, to_attr):
252 return record[field]
253 return transform
254
255 class MyMapper(ImportMapper):
256 direct = [(do_nothing('name'), 'out_name')]
257
258 env = mock.MagicMock()
259 record = {'name': 'Guewen'}
260 mapper = MyMapper(env)
261 mapper.convert(record)
262 expected = {'out_name': 'Guewen'}
263 self.assertEqual(mapper.data, expected)
264 self.assertEqual(mapper.data_for_create, expected)
265
266 def test_mapping_convert(self):
267 """ Map a direct record with the convert modifier function """
268 class MyMapper(ImportMapper):
269 direct = [(convert('name', int), 'out_name')]
270
271 env = mock.MagicMock()
272 record = {'name': '300'}
273 mapper = MyMapper(env)
274 mapper.convert(record)
275 expected = {'out_name': 300}
276 self.assertEqual(mapper.data, expected)
277 self.assertEqual(mapper.data_for_create, expected)
278
279 def test_mapping_modifier_none(self):
280 """ Pipeline of modifiers """
281 class MyMapper(ImportMapper):
282 direct = [(none('in_f'), 'out_f'),
283 (none('in_t'), 'out_t')]
284
285 env = mock.MagicMock()
286 record = {'in_f': False, 'in_t': True}
287 mapper = MyMapper(env)
288 mapper.convert(record)
289 expected = {'out_f': None, 'out_t': True}
290 self.assertEqual(mapper.data, expected)
291 self.assertEqual(mapper.data_for_create, expected)
292
293 def test_mapping_modifier_pipeline(self):
294 """ Pipeline of modifiers """
295 class MyMapper(ImportMapper):
296 direct = [(none(convert('in_f', bool)), 'out_f'),
297 (none(convert('in_t', bool)), 'out_t')]
298
299 env = mock.MagicMock()
300 record = {'in_f': 0, 'in_t': 1}
301 mapper = MyMapper(env)
302 mapper.convert(record)
303 expected = {'out_f': None, 'out_t': True}
304 self.assertEqual(mapper.data, expected)
305 self.assertEqual(mapper.data_for_create, expected)
306
307
308class test_mapper_binding(common.TransactionCase):
309 """ Test Mapper with Bindings"""
310
311 def setUp(self):
312 super(test_mapper_binding, self).setUp()
313 self.session = ConnectorSession(self.cr, self.uid)
314 self.Partner = self.registry('res.partner')
315 self.backend = mock.Mock(wraps=Backend('x', version='y'),
316 name='backend')
317 backend_record = mock.Mock()
318 backend_record.get_backend.return_value = self.backend
319 self.env = Environment(backend_record, self.session, 'res.partner')
320 self.country_binder = mock.Mock(name='country_binder')
321 self.country_binder.return_value = self.country_binder
322 self.backend.get_class.return_value = self.country_binder
323
324 def test_mapping_m2o_to_backend(self):
325 """ Map a direct record with the m2o_to_backend modifier function """
326 class MyMapper(ImportMapper):
327 _model_name = 'res.partner'
328 direct = [(m2o_to_backend('country_id'), 'country')]
329
330 partner_id = self.ref('base.main_partner')
331 self.Partner.write(self.cr, self.uid, partner_id,
332 {'country_id': self.ref('base.ch')})
333 partner = self.Partner.browse(self.cr, self.uid, partner_id)
334 self.country_binder.to_backend.return_value = 10
335
336 mapper = MyMapper(self.env)
337 mapper.convert(partner)
338 self.country_binder.to_backend.assert_called_once_with(
339 partner.country_id.id, wrap=False)
340 self.assertEqual(mapper.data, {'country': 10})
341
342 def test_mapping_backend_to_m2o(self):
343 """ Map a direct record with the backend_to_m2o modifier function """
344 class MyMapper(ImportMapper):
345 _model_name = 'res.partner'
346 direct = [(backend_to_m2o('country'), 'country_id')]
347
348 record = {'country': 10}
349 self.country_binder.to_openerp.return_value = 44
350 mapper = MyMapper(self.env)
351 mapper.convert(record)
352 self.country_binder.to_openerp.assert_called_once_with(
353 10, unwrap=False)
354 self.assertEqual(mapper.data, {'country_id': 44})
237355
=== modified file 'connector/unit/mapper.py'
--- connector/unit/mapper.py 2013-10-28 15:17:17 +0000
+++ connector/unit/mapper.py 2013-11-09 20:51:28 +0000
@@ -60,6 +60,142 @@
60 return func60 return func
6161
6262
63def none(field):
64 """ A modifier intended to be used on the ``direct`` mappings.
65
66 Replace the False-ish values by None.
67 It can be used in a pipeline of modifiers when .
68
69 Example::
70
71 direct = [(none('source'), 'target'),
72 (none(m2o_to_backend('rel_id'), 'rel_id')]
73
74 :param field: name of the source field in the record
75 :param binding: True if the relation is a binding record
76 """
77 def modifier(self, record, to_attr):
78 if callable(field):
79 result = field(self, record, to_attr)
80 else:
81 result = record[field]
82 if not result:
83 return None
84 return result
85 return modifier
86
87
88def convert(field, conv_type):
89 """ A modifier intended to be used on the ``direct`` mappings.
90
91 Convert a field's value to a given type.
92
93 Example::
94
95 direct = [(convert('source', str), 'target')]
96
97 :param field: name of the source field in the record
98 :param binding: True if the relation is a binding record
99 """
100 def modifier(self, record, to_attr):
101 value = record[field]
102 if not value:
103 return False
104 return conv_type(value)
105 return modifier
106
107
108def m2o_to_backend(field, binding=None):
109 """ A modifier intended to be used on the ``direct`` mappings.
110
111 For a many2one, get the ID on the backend and returns it.
112
113 When the field's relation is not a binding (i.e. it does not point to
114 something like ``magento.*``), the binding model needs to be provided
115 in the ``binding`` keyword argument.
116
117 Example::
118
119 direct = [(m2o_to_backend('country_id', binding='magento.res.country'),
120 'country'),
121 (m2o_to_backend('magento_country_id'), 'country')]
122
123 :param field: name of the source field in the record
124 :param binding: name of the binding model is the relation is not a binding
125 """
126 def modifier(self, record, to_attr):
127 if not record[field]:
128 return False
129 column = self.model._all_columns[field].column
130 if column._type != 'many2one':
131 raise ValueError('The column %s should be a many2one, got %s' %
132 field, column._type)
133 rel_id = record[field].id
134 if binding is None:
135 binding_model = column._obj
136 else:
137 binding_model = binding
138 binder = self.get_binder_for_model(binding_model)
139 # if a relation is not a binding, we wrap the record in the
140 # binding, we'll return the id of the binding
141 wrap = bool(binding)
142 value = binder.to_backend(rel_id, wrap=wrap)
143 if not value:
144 raise MappingError("Can not find an external id for record "
145 "%s in model %s %s wrapping" %
146 (rel_id, binding_model,
147 'with' if wrap else 'without'))
148 return value
149 return modifier
150
151
152def backend_to_m2o(field, binding=None, with_inactive=False):
153 """ A modifier intended to be used on the ``direct`` mappings.
154
155 For a field from a backend which is an ID, search the corresponding
156 binding in OpenERP and returns its ID.
157
158 When the field's relation is not a binding (i.e. it does not point to
159 something like ``magento.*``), the binding model needs to be provided
160 in the ``binding`` keyword argument.
161
162 Example::
163
164 direct = [(backend_to_m2o('country', binding='magento.res.country'),
165 'country_id'),
166 (backend_to_m2o('country'), 'magento_country_id')]
167
168 :param field: name of the source field in the record
169 :param binding: name of the binding model is the relation is not a binding
170 :param with_inactive: include the inactive records in OpenERP in the search
171 """
172 def modifier(self, record, to_attr):
173 if not record[field]:
174 return False
175 column = self.model._all_columns[to_attr].column
176 if column._type != 'many2one':
177 raise ValueError('The column %s should be a many2one, got %s' %
178 to_attr, column._type)
179 rel_id = record[field]
180 if binding is None:
181 binding_model = column._obj
182 else:
183 binding_model = binding
184 binder = self.get_binder_for_model(binding_model)
185 # if we want the ID of a normal record, not a binding,
186 # we ask the unwrapped id to the binder
187 unwrap = bool(binding)
188 with self.session.change_context({'active_test': False}):
189 value = binder.to_openerp(rel_id, unwrap=unwrap)
190 if not value:
191 raise MappingError("Can not find an existing %s for external "
192 "record %s %s unwrapping" %
193 (binding_model, rel_id,
194 'with' if unwrap else 'without'))
195 return value
196 return modifier
197
198
63MappingDefinition = namedtuple('MappingDefinition',199MappingDefinition = namedtuple('MappingDefinition',
64 ['changed_by',200 ['changed_by',
65 'only_create'])201 'only_create'])
@@ -106,7 +242,90 @@
106242
107243
108class Mapper(ConnectorUnit):244class Mapper(ConnectorUnit):
109 """ Transform a record to a defined output """245 """ A Mapper translates an external record to an OpenERP record and
246 conversely. The output of a Mapper is a ``dict``.
247
248 3 types of mappings are supported:
249
250 Direct Mappings
251 Example::
252
253 direct = [('source', 'target')]
254
255 Here, the ``source`` field will be copied in the ``target`` field.
256
257 A modifier can be used in the source item.
258 The modifier will be applied to the source field before being
259 copied in the target field.
260 It should be a closure function respecting this idiom::
261
262 def a_function(field):
263 ''' ``field`` is the name of the source field '''
264 def modifier(self, record, to_attr):
265 ''' self is the current Mapper,
266 record is the current record to map,
267 to_attr is the target field'''
268 return record[field]
269 return modifier
270
271 And used like that::
272
273 direct = [
274 (a_function('source'), 'target'),
275 ]
276
277 A more concrete example of modifier::
278
279 def convert(field, conv_type):
280 ''' Convert the source field to a defined ``conv_type``
281 (ex. str) before returning it'''
282 def modifier(self, record, to_attr):
283 value = record[field]
284 if not value:
285 return None
286 return conv_type(value)
287 return modifier
288
289 And used like that::
290
291 direct = [
292 (convert('myfield', float), 'target_field'),
293 ]
294
295 More examples of modifiers:
296
297 * :py:func:`convert`
298 * :py:func:`m2o_to_backend`
299 * :py:func:`backend_to_m2o`
300
301 Method Mappings
302 A mapping method allows to execute arbitrary code and return one
303 or many fields::
304
305 @mapping
306 def compute_state(self, record):
307 # compute some state, using the ``record`` or not
308 state = 'pending'
309 return {'state': state}
310
311 We can also specify that a mapping methods should be applied
312 only when an object is created, and never applied on further
313 updates::
314
315 @only_create
316 @mapping
317 def default_warehouse(self, record):
318 # get default warehouse
319 warehouse_id = ...
320 return {'warehouse_id': warehouse_id}
321
322 Submappings
323 When a record contains sub-items, like the lines of a sales order,
324 we can convert the children using another Mapper::
325
326 children = [('items', 'line_ids', LineMapper)]
327
328 """
110329
111 __metaclass__ = MetaMapper330 __metaclass__ = MetaMapper
112331
@@ -136,6 +355,14 @@
136 return result355 return result
137356
138 def _map_direct(self, record, from_attr, to_attr):357 def _map_direct(self, record, from_attr, to_attr):
358 """ Apply the ``direct`` mappings.
359
360 :param record: record to convert from a source to a target
361 :param from_attr: name of the source attribute or a callable
362 :type from_attr: callable | str
363 :param to_attr: name of the target attribute
364 :type to_attr: str
365 """
139 raise NotImplementedError366 raise NotImplementedError
140367
141 def _map_children(self, record, attr, model):368 def _map_children(self, record, attr, model):
@@ -157,9 +384,6 @@
157 _logger.debug('converting record %s to model %s', record, self._model_name)384 _logger.debug('converting record %s to model %s', record, self._model_name)
158 for from_attr, to_attr in self.direct:385 for from_attr, to_attr in self.direct:
159 if (not fields or from_attr in fields):386 if (not fields or from_attr in fields):
160 # XXX not compatible with all
161 # record type (wrap
162 # records in a standard class representation?)
163 value = self._map_direct(record,387 value = self._map_direct(record,
164 from_attr,388 from_attr,
165 to_attr)389 to_attr)
@@ -262,20 +486,29 @@
262 """ Transform a record from a backend to an OpenERP record """486 """ Transform a record from a backend to an OpenERP record """
263487
264 def _map_direct(self, record, from_attr, to_attr):488 def _map_direct(self, record, from_attr, to_attr):
489 """ Apply the ``direct`` mappings.
490
491 :param record: record to convert from a source to a target
492 :param from_attr: name of the source attribute or a callable
493 :type from_attr: callable | str
494 :param to_attr: name of the target attribute
495 :type to_attr: str
496 """
497 if callable(from_attr):
498 return from_attr(self, record, to_attr)
499
265 value = record.get(from_attr)500 value = record.get(from_attr)
266 if not value:501 if not value:
267 return False502 return False
268503
504 # Backward compatibility: when a field is a relation, and a modifier is
505 # not used, we assume that the relation model is a binding.
506 # Use an explicit modifier backend_to_m2o in the 'direct' mappings to
507 # change that.
269 column = self.model._all_columns[to_attr].column508 column = self.model._all_columns[to_attr].column
270 if column._type == 'many2one':509 if column._type == 'many2one':
271 rel_id = record[from_attr]510 mapping = backend_to_m2o(from_attr)
272 model_name = column._obj511 value = mapping(self, record, to_attr)
273 binder = self.get_binder_for_model(model_name)
274 value = binder.to_openerp(rel_id)
275
276 if not value:
277 raise MappingError("Can not find an existing %s for external "
278 "record %s" % (model_name, rel_id))
279 return value512 return value
280513
281 def _init_child_mapper(self, model_name):514 def _init_child_mapper(self, model_name):
@@ -292,20 +525,29 @@
292 """ Transform a record from OpenERP to a backend record """525 """ Transform a record from OpenERP to a backend record """
293526
294 def _map_direct(self, record, from_attr, to_attr):527 def _map_direct(self, record, from_attr, to_attr):
528 """ Apply the ``direct`` mappings.
529
530 :param record: record to convert from a source to a target
531 :param from_attr: name of the source attribute or a callable
532 :type from_attr: callable | str
533 :param to_attr: name of the target attribute
534 :type to_attr: str
535 """
536 if callable(from_attr):
537 return from_attr(self, record, to_attr)
538
295 value = record[from_attr]539 value = record[from_attr]
296 if not value:540 if not value:
297 return False541 return False
298542
543 # Backward compatibility: when a field is a relation, and a modifier is
544 # not used, we assume that the relation model is a binding.
545 # Use an explicit modifier m2o_to_backend in the 'direct' mappings to
546 # change that.
299 column = self.model._all_columns[from_attr].column547 column = self.model._all_columns[from_attr].column
300 if column._type == 'many2one':548 if column._type == 'many2one':
301 rel_id = record[from_attr].id549 mapping = m2o_to_backend(from_attr)
302 model_name = column._obj550 value = mapping(self, record, to_attr)
303 binder = self.get_binder_for_model(model_name)
304 value = binder.to_backend(rel_id)
305
306 if not value:
307 raise MappingError("Can not find an external id for record "
308 "%s in model %s" % (rel_id, model_name))
309 return value551 return value
310552
311 def _init_child_mapper(self, model_name):553 def _init_child_mapper(self, model_name):