[FIX] account_payment: the amount_residual field is not searchable so it cannot be...
[odoo/odoo.git] / addons / edi / models / edi.py
1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Business Applications
5 #    Copyright (c) 2011-2012 OpenERP S.A. <http://openerp.com>
6 #
7 #    This program is free software: you can redistribute it and/or modify
8 #    it under the terms of the GNU Affero General Public License as
9 #    published by the Free Software Foundation, either version 3 of the
10 #    License, or (at your option) any later version.
11 #
12 #    This program is distributed in the hope that it will be useful,
13 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
14 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 #    GNU Affero General Public License for more details.
16 #
17 #    You should have received a copy of the GNU Affero General Public License
18 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
19 #
20 ##############################################################################
21
22 import base64
23 import hashlib
24 import simplejson as json
25 import logging
26 import re
27 import time
28 import urllib2
29
30 import openerp
31 import openerp.release as release
32 from openerp.osv import osv, fields
33 from openerp.tools.translate import _
34 from openerp.tools.safe_eval import safe_eval as eval
35 _logger = logging.getLogger(__name__)
36
37 EXTERNAL_ID_PATTERN = re.compile(r'^([^.:]+)(?::([^.]+))?\.(\S+)$')
38 EDI_VIEW_WEB_URL = '%s/edi/view?db=%s&token=%s'
39 EDI_PROTOCOL_VERSION = 1 # arbitrary ever-increasing version number
40 EDI_GENERATOR = 'OpenERP ' + release.major_version
41 EDI_GENERATOR_VERSION = release.version_info
42
43 def split_external_id(ext_id):
44     match = EXTERNAL_ID_PATTERN.match(ext_id)
45     assert match, \
46             _("'%s' is an invalid external ID") % (ext_id)
47     return {'module': match.group(1),
48             'db_uuid': match.group(2),
49             'id': match.group(3),
50             'full': match.group(0)}
51
52 def safe_unique_id(database_id, model, record_id):
53     """Generate a unique string to represent a (database_uuid,model,record_id) pair
54     without being too long, and with a very low probability of collisions.
55     """
56     msg = "%s-%s-%s-%s" % (time.time(), database_id, model, record_id)
57     digest = hashlib.sha1(msg).digest()
58     # fold the sha1 20 bytes digest to 9 bytes
59     digest = ''.join(chr(ord(x) ^ ord(y)) for (x,y) in zip(digest[:9], digest[9:-2]))
60     # b64-encode the 9-bytes folded digest to a reasonable 12 chars ASCII ID
61     digest = base64.urlsafe_b64encode(digest)
62     return '%s-%s' % (model.replace('.','_'), digest)
63
64 def last_update_for(record):
65     """Returns the last update timestamp for the given record,
66        if available, otherwise False
67     """
68     if record._model._log_access:
69         record_log = record.perm_read()[0]
70         return record_log.get('write_date') or record_log.get('create_date') or False
71     return False
72
73
74 class edi(osv.AbstractModel):
75     _name = 'edi.edi'
76     _description = 'EDI Subsystem'
77
78     def new_edi_token(self, cr, uid, record):
79         """Return a new, random unique token to identify this model record,
80         and to be used as token when exporting it as an EDI document.
81
82         :param browse_record record: model record for which a token is needed
83         """
84         db_uuid = self.pool.get('ir.config_parameter').get_param(cr, uid, 'database.uuid')
85         edi_token = hashlib.sha256('%s-%s-%s-%s' % (time.time(), db_uuid, record._name, record.id)).hexdigest()
86         return edi_token
87
88     def serialize(self, edi_documents):
89         """Serialize the given EDI document structures (Python dicts holding EDI data),
90         using JSON serialization.
91
92         :param [dict] edi_documents: list of EDI document structures to serialize
93         :return: UTF-8 encoded string containing the serialized document
94         """
95         serialized_list = json.dumps(edi_documents)
96         return serialized_list
97
98     def generate_edi(self, cr, uid, records, context=None):
99         """Generates a final EDI document containing the EDI serialization
100         of the given records, which should all be instances of a Model
101         that has the :meth:`~.edi` mixin. The document is not saved in the
102         database.
103
104         :param list(browse_record) records: records to export as EDI
105         :return: UTF-8 encoded string containing the serialized records
106         """
107         edi_list = []
108         for record in records:
109             record_model = record._model
110             edi_list += record_model.edi_export(cr, uid, [record], context=context)
111         return self.serialize(edi_list)
112
113     def load_edi(self, cr, uid, edi_documents, context=None):
114         """Import the given EDI document structures into the system, using
115         :meth:`~.import_edi`.
116
117         :param edi_documents: list of Python dicts containing the deserialized
118                               version of EDI documents
119         :return: list of (model, id, action) tuple containing the model and database ID
120                  of all records that were imported in the system, plus a suggested
121                  action definition dict for displaying each document.
122         """
123         ir_module = self.pool.get('ir.module.module')
124         res = []
125         for edi_document in edi_documents:
126             module = edi_document.get('__import_module') or edi_document.get('__module')
127             assert module, 'a `__module` or `__import_module` attribute is required in each EDI document.'
128             if module != 'base' and not ir_module.search(cr, uid, [('name','=',module),('state','=','installed')]):
129                 raise osv.except_osv(_('Missing Application.'),
130                             _("The document you are trying to import requires the OpenERP `%s` application. "
131                               "You can install it by connecting as the administrator and opening the configuration assistant.")%(module,))
132             model = edi_document.get('__import_model') or edi_document.get('__model')
133             assert model, 'a `__model` or `__import_model` attribute is required in each EDI document.'
134             assert model in self.pool, 'model `%s` cannot be found, despite module `%s` being available - '\
135                               'this EDI document seems invalid or unsupported.' % (model,module)
136             model_obj = self.pool[model]
137             record_id = model_obj.edi_import(cr, uid, edi_document, context=context)
138             record_action = model_obj._edi_record_display_action(cr, uid, record_id, context=context)
139             res.append((model, record_id, record_action))
140         return res
141
142     def deserialize(self, edi_documents_string):
143         """Return deserialized version of the given EDI Document string.
144
145         :param str|unicode edi_documents_string: UTF-8 string (or unicode) containing
146                                                  JSON-serialized EDI document(s)
147         :return: Python object representing the EDI document(s) (usually a list of dicts)
148         """
149         return json.loads(edi_documents_string)
150
151     def import_edi(self, cr, uid, edi_document=None, edi_url=None, context=None):
152         """Import a JSON serialized EDI Document string into the system, first retrieving it
153         from the given ``edi_url`` if provided.
154
155         :param str|unicode edi: UTF-8 string or unicode containing JSON-serialized
156                                          EDI Document to import. Must not be provided if
157                                          ``edi_url`` is given.
158         :param str|unicode edi_url: URL where the EDI document (same format as ``edi``)
159                                     may be retrieved, without authentication.
160         """
161         if edi_url:
162             assert not edi_document, 'edi must not be provided if edi_url is given.'
163             edi_document = urllib2.urlopen(edi_url).read()
164         assert edi_document, 'EDI Document is empty!'
165         edi_documents = self.deserialize(edi_document)
166         return self.load_edi(cr, uid, edi_documents, context=context)
167
168
169 class EDIMixin(object):
170     """Mixin class for Model objects that want be exposed as EDI documents.
171        Classes that inherit from this mixin class should override the
172        ``edi_import()`` and ``edi_export()`` methods to implement their
173        specific behavior, based on the primitives provided by this mixin."""
174
175     def _edi_requires_attributes(self, attributes, edi):
176         model_name = edi.get('__imported_model') or edi.get('__model') or self._name
177         for attribute in attributes:
178             assert edi.get(attribute),\
179                  'Attribute `%s` is required in %s EDI documents.' % (attribute, model_name)
180
181     # private method, not RPC-exposed as it creates ir.model.data entries as
182     # SUPERUSER based on its parameters
183     def _edi_external_id(self, cr, uid, record, existing_id=None, existing_module=None,
184                         context=None):
185         """Generate/Retrieve unique external ID for ``record``.
186         Each EDI record and each relationship attribute in it is identified by a
187         unique external ID, which includes the database's UUID, as a way to
188         refer to any record within any OpenERP instance, without conflict.
189
190         For OpenERP records that have an existing "External ID" (i.e. an entry in
191         ir.model.data), the EDI unique identifier for this record will be made of
192         "%s:%s:%s" % (module, database UUID, ir.model.data ID). The database's
193         UUID MUST NOT contain a colon characters (this is guaranteed by the
194         UUID algorithm).
195
196         For records that have no existing ir.model.data entry, a new one will be
197         created during the EDI export. It is recommended that the generated external ID
198         contains a readable reference to the record model, plus a unique value that
199         hides the database ID. If ``existing_id`` is provided (because it came from
200         an import), it will be used instead of generating a new one.
201         If ``existing_module`` is provided (because it came from
202         an import), it will be used instead of using local values.
203
204         :param browse_record record: any browse_record needing an EDI external ID
205         :param string existing_id: optional existing external ID value, usually coming
206                                    from a just-imported EDI record, to be used instead
207                                    of generating a new one
208         :param string existing_module: optional existing module name, usually in the
209                                        format ``module:db_uuid`` and coming from a
210                                        just-imported EDI record, to be used instead
211                                        of local values
212         :return: the full unique External ID to use for record
213         """
214         ir_model_data = self.pool.get('ir.model.data')
215         db_uuid = self.pool.get('ir.config_parameter').get_param(cr, uid, 'database.uuid')
216         ext_id = record.get_external_id()[record.id]
217         if not ext_id:
218             ext_id = existing_id or safe_unique_id(db_uuid, record._name, record.id)
219             # ID is unique cross-db thanks to db_uuid (already included in existing_module)
220             module = existing_module or "%s:%s" % (record._original_module, db_uuid)
221             _logger.debug("%s: Generating new external ID `%s.%s` for %r.", self._name,
222                           module, ext_id, record)
223             ir_model_data.create(cr, openerp.SUPERUSER_ID,
224                                  {'name': ext_id,
225                                   'model': record._name,
226                                   'module': module,
227                                   'res_id': record.id})
228         else:
229             module, ext_id = ext_id.split('.')
230             if not ':' in module:
231                 # this record was not previously EDI-imported
232                 if not module == record._original_module:
233                     # this could happen for data records defined in a module that depends
234                     # on the module that owns the model, e.g. purchase defines
235                     # product.pricelist records.
236                     _logger.debug('Mismatching module: expected %s, got %s, for %s.',
237                                   module, record._original_module, record)
238                 # ID is unique cross-db thanks to db_uuid
239                 module = "%s:%s" % (module, db_uuid)
240
241         return '%s.%s' % (module, ext_id)
242
243     def _edi_record_display_action(self, cr, uid, id, context=None):
244         """Returns an appropriate action definition dict for displaying
245            the record with ID ``rec_id``.
246
247            :param int id: database ID of record to display
248            :return: action definition dict
249         """
250         return {'type': 'ir.actions.act_window',
251                 'view_mode': 'form,tree',
252                 'view_type': 'form',
253                 'res_model': self._name,
254                 'res_id': id}
255
256     def edi_metadata(self, cr, uid, records, context=None):
257         """Return a list containing the boilerplate EDI structures for
258            exporting ``records`` as EDI, including
259            the metadata fields
260
261         The metadata fields always include::
262
263             {
264                '__model': 'some.model',                # record model
265                '__module': 'module',                   # require module
266                '__id': 'module:db-uuid:model.id',      # unique global external ID for the record
267                '__last_update': '2011-01-01 10:00:00', # last update date in UTC!
268                '__version': 1,                         # EDI spec version
269                '__generator' : 'OpenERP',              # EDI generator
270                '__generator_version' : [6,1,0],        # server version, to check compatibility.
271                '__attachments_':
272            }
273
274         :param list(browse_record) records: records to export
275         :return: list of dicts containing boilerplate EDI metadata for each record,
276                  at the corresponding index from ``records``.
277         """
278         ir_attachment = self.pool.get('ir.attachment')
279         results = []
280         for record in records:
281             ext_id = self._edi_external_id(cr, uid, record, context=context)
282             edi_dict = {
283                 '__id': ext_id,
284                 '__last_update': last_update_for(record),
285                 '__model' : record._name,
286                 '__module' : record._original_module,
287                 '__version': EDI_PROTOCOL_VERSION,
288                 '__generator': EDI_GENERATOR,
289                 '__generator_version': EDI_GENERATOR_VERSION,
290             }
291             attachment_ids = ir_attachment.search(cr, uid, [('res_model','=', record._name), ('res_id', '=', record.id)])
292             if attachment_ids:
293                 attachments = []
294                 for attachment in ir_attachment.browse(cr, uid, attachment_ids, context=context):
295                     attachments.append({
296                             'name' : attachment.name,
297                             'content': attachment.datas, # already base64 encoded!
298                             'file_name': attachment.datas_fname,
299                     })
300                 edi_dict.update(__attachments=attachments)
301             results.append(edi_dict)
302         return results
303
304     def edi_m2o(self, cr, uid, record, context=None):
305         """Return a m2o EDI representation for the given record.
306
307         The EDI format for a many2one is::
308
309             ['unique_external_id', 'Document Name']
310         """
311         edi_ext_id = self._edi_external_id(cr, uid, record, context=context)
312         relation_model = record._model
313         name = relation_model.name_get(cr, uid, [record.id], context=context)
314         name = name and name[0][1] or False
315         return [edi_ext_id, name]
316
317     def edi_o2m(self, cr, uid, records, edi_struct=None, context=None):
318         """Return a list representing a O2M EDI relationship containing
319            all the given records, according to the given ``edi_struct``.
320            This is basically the same as exporting all the record using
321            :meth:`~.edi_export` with the given ``edi_struct``, and wrapping
322            the results in a list.
323
324            Example::
325
326              [                                # O2M fields would be a list of dicts, with their
327                { '__id': 'module:db-uuid.id', # own __id.
328                  '__last_update': 'iso date', # update date
329                  'name': 'some name',
330                  #...
331                },
332                # ...
333              ],
334         """
335         result = []
336         for record in records:
337             result += record._model.edi_export(cr, uid, [record], edi_struct=edi_struct, context=context)
338         return result
339
340     def edi_m2m(self, cr, uid, records, context=None):
341         """Return a list representing a M2M EDI relationship directed towards
342            all the given records.
343            This is basically the same as exporting all the record using
344            :meth:`~.edi_m2o` and wrapping the results in a list.
345
346             Example::
347
348                 # M2M fields are exported as a list of pairs, like a list of M2O values
349                 [
350                       ['module:db-uuid.id1', 'Task 01: bla bla'],
351                       ['module:db-uuid.id2', 'Task 02: bla bla']
352                 ]
353         """
354         return [self.edi_m2o(cr, uid, r, context=context) for r in records]
355
356     def edi_export(self, cr, uid, records, edi_struct=None, context=None):
357         """Returns a list of dicts representing EDI documents containing the
358            records, and matching the given ``edi_struct``, if provided.
359
360            :param edi_struct: if provided, edi_struct should be a dictionary
361                               with a skeleton of the fields to export.
362                               Basic fields can have any key as value, but o2m
363                               values should have a sample skeleton dict as value,
364                               to act like a recursive export.
365                               For example, for a res.partner record::
366
367                                   edi_struct: {
368                                        'name': True,
369                                        'company_id': True,
370                                        'address': {
371                                            'name': True,
372                                            'street': True,
373                                            }
374                                   }
375
376                               Any field not specified in the edi_struct will not
377                               be included in the exported data. Fields with no
378                               value (False) will be omitted in the EDI struct.
379                               If edi_struct is omitted, no fields will be exported
380         """
381         if edi_struct is None:
382             edi_struct = {}
383         fields_to_export = edi_struct.keys()
384         results = []
385         for record in records:
386             edi_dict = self.edi_metadata(cr, uid, [record], context=context)[0]
387             for field in fields_to_export:
388                 column = self._all_columns[field].column
389                 value = getattr(record, field)
390                 if not value and value not in ('', 0):
391                     continue
392                 elif column._type == 'many2one':
393                     value = self.edi_m2o(cr, uid, value, context=context)
394                 elif column._type == 'many2many':
395                     value = self.edi_m2m(cr, uid, value, context=context)
396                 elif column._type == 'one2many':
397                     value = self.edi_o2m(cr, uid, value, edi_struct=edi_struct.get(field, {}), context=context)
398                 edi_dict[field] = value
399             results.append(edi_dict)
400         return results
401
402     def _edi_get_object_by_name(self, cr, uid, name, model_name, context=None):
403         model = self.pool[model_name]
404         search_results = model.name_search(cr, uid, name, operator='=', context=context)
405         if len(search_results) == 1:
406             return model.browse(cr, uid, search_results[0][0], context=context)
407         return False
408
409     def _edi_generate_report_attachment(self, cr, uid, record, context=None):
410         """Utility method to generate the first PDF-type report declared for the
411            current model with ``usage`` attribute set to ``default``.
412            This must be called explicitly by models that need it, usually
413            at the beginning of ``edi_export``, before the call to ``super()``."""
414         ir_actions_report = self.pool.get('ir.actions.report.xml')
415         matching_reports = ir_actions_report.search(cr, uid, [('model','=',self._name),
416                                                               ('report_type','=','pdf'),
417                                                               ('usage','=','default')])
418         if matching_reports:
419             report = ir_actions_report.browse(cr, uid, matching_reports[0])
420             result, format = openerp.report.render_report(cr, uid, [record.id], report.report_name, {'model': self._name}, context=context)
421             eval_context = {'time': time, 'object': record}
422             if not report.attachment or not eval(report.attachment, eval_context):
423                 # no auto-saving of report as attachment, need to do it manually
424                 result = base64.b64encode(result)
425                 file_name = record.name_get()[0][1]
426                 file_name = re.sub(r'[^a-zA-Z0-9_-]', '_', file_name)
427                 file_name += ".pdf"
428                 self.pool.get('ir.attachment').create(cr, uid,
429                                                       {
430                                                        'name': file_name,
431                                                        'datas': result,
432                                                        'datas_fname': file_name,
433                                                        'res_model': self._name,
434                                                        'res_id': record.id,
435                                                        'type': 'binary'
436                                                       },
437                                                       context=context)
438
439     def _edi_import_attachments(self, cr, uid, record_id, edi, context=None):
440         ir_attachment = self.pool.get('ir.attachment')
441         for attachment in edi.get('__attachments', []):
442             # check attachment data is non-empty and valid
443             file_data = None
444             try:
445                 file_data = base64.b64decode(attachment.get('content'))
446             except TypeError:
447                 pass
448             assert file_data, 'Incorrect/Missing attachment file content.'
449             assert attachment.get('name'), 'Incorrect/Missing attachment name.'
450             assert attachment.get('file_name'), 'Incorrect/Missing attachment file name.'
451             assert attachment.get('file_name'), 'Incorrect/Missing attachment file name.'
452             ir_attachment.create(cr, uid, {'name': attachment['name'],
453                                            'datas_fname': attachment['file_name'],
454                                            'res_model': self._name,
455                                            'res_id': record_id,
456                                            # should be pure 7bit ASCII
457                                            'datas': str(attachment['content']),
458                                            }, context=context)
459
460
461     def _edi_get_object_by_external_id(self, cr, uid, external_id, model, context=None):
462         """Returns browse_record representing object identified by the model and external_id,
463            or None if no record was found with this external id.
464
465            :param external_id: fully qualified external id, in the EDI form
466                                ``module:db_uuid:identifier``.
467            :param model: model name the record belongs to.
468         """
469         ir_model_data = self.pool.get('ir.model.data')
470         # external_id is expected to have the form: ``module:db_uuid:model.random_name``
471         ext_id_members = split_external_id(external_id)
472         db_uuid = self.pool.get('ir.config_parameter').get_param(cr, uid, 'database.uuid')
473         module = ext_id_members['module']
474         ext_id = ext_id_members['id']
475         modules = []
476         ext_db_uuid = ext_id_members['db_uuid']
477         if ext_db_uuid:
478             modules.append('%s:%s' % (module, ext_id_members['db_uuid']))
479         if ext_db_uuid is None or ext_db_uuid == db_uuid:
480             # local records may also be registered without the db_uuid
481             modules.append(module)
482         data_ids = ir_model_data.search(cr, uid, [('model','=',model),
483                                                   ('name','=',ext_id),
484                                                   ('module','in',modules)])
485         if data_ids:
486             model = self.pool[model]
487             data = ir_model_data.browse(cr, uid, data_ids[0], context=context)
488             if model.exists(cr, uid, [data.res_id]):
489                 return model.browse(cr, uid, data.res_id, context=context)
490             # stale external-id, cleanup to allow re-import, as the corresponding record is gone
491             ir_model_data.unlink(cr, 1, [data_ids[0]])
492
493     def edi_import_relation(self, cr, uid, model, value, external_id, context=None):
494         """Imports a M2O/M2M relation EDI specification ``[external_id,value]`` for the
495            given model, returning the corresponding database ID:
496
497            * First, checks if the ``external_id`` is already known, in which case the corresponding
498              database ID is directly returned, without doing anything else;
499            * If the ``external_id`` is unknown, attempts to locate an existing record
500              with the same ``value`` via name_search(). If found, the given external_id will
501              be assigned to this local record (in addition to any existing one)
502            * If previous steps gave no result, create a new record with the given
503              value in the target model, assign it the given external_id, and return
504              the new database ID
505
506            :param str value: display name of the record to import
507            :param str external_id: fully-qualified external ID of the record
508            :return: database id of newly-imported or pre-existing record
509         """
510         _logger.debug("%s: Importing EDI relationship [%r,%r]", model, external_id, value)
511         target = self._edi_get_object_by_external_id(cr, uid, external_id, model, context=context)
512         need_new_ext_id = False
513         if not target:
514             _logger.debug("%s: Importing EDI relationship [%r,%r] - ID not found, trying name_get.",
515                           self._name, external_id, value)
516             target = self._edi_get_object_by_name(cr, uid, value, model, context=context)
517             need_new_ext_id = True
518         if not target:
519             _logger.debug("%s: Importing EDI relationship [%r,%r] - name not found, creating it.",
520                           self._name, external_id, value)
521             # also need_new_ext_id here, but already been set above
522             model = self.pool[model]
523             res_id, _ = model.name_create(cr, uid, value, context=context)
524             target = model.browse(cr, uid, res_id, context=context)
525         else:
526             _logger.debug("%s: Importing EDI relationship [%r,%r] - record already exists with ID %s, using it",
527                           self._name, external_id, value, target.id)
528         if need_new_ext_id:
529             ext_id_members = split_external_id(external_id)
530             # module name is never used bare when creating ir.model.data entries, in order
531             # to avoid being taken as part of the module's data, and cleanup up at next update
532             module = "%s:%s" % (ext_id_members['module'], ext_id_members['db_uuid'])
533             # create a new ir.model.data entry for this value
534             self._edi_external_id(cr, uid, target, existing_id=ext_id_members['id'], existing_module=module, context=context)
535         return target.id
536
537     def edi_import(self, cr, uid, edi, context=None):
538         """Imports a dict representing an EDI document into the system.
539
540            :param dict edi: EDI document to import
541            :return: the database ID of the imported record
542         """
543         assert self._name == edi.get('__import_model') or \
544                 ('__import_model' not in edi and self._name == edi.get('__model')), \
545                 "EDI Document Model and current model do not match: '%s' (EDI) vs '%s' (current)." % \
546                    (edi.get('__model'), self._name)
547
548         # First check the record is now already known in the database, in which case it is ignored
549         ext_id_members = split_external_id(edi['__id'])
550         existing = self._edi_get_object_by_external_id(cr, uid, ext_id_members['full'], self._name, context=context)
551         if existing:
552             _logger.info("'%s' EDI Document with ID '%s' is already known, skipping import!", self._name, ext_id_members['full'])
553             return existing.id
554
555         record_values = {}
556         o2m_todo = {} # o2m values are processed after their parent already exists
557         for field_name, field_value in edi.iteritems():
558             # skip metadata and empty fields
559             if field_name.startswith('__') or field_value is None or field_value is False:
560                 continue
561             field_info = self._all_columns.get(field_name)
562             if not field_info:
563                 _logger.warning('Ignoring unknown field `%s` when importing `%s` EDI document.', field_name, self._name)
564                 continue
565             field = field_info.column
566             # skip function/related fields
567             if isinstance(field, fields.function):
568                 _logger.warning("Unexpected function field value is found in '%s' EDI document: '%s'." % (self._name, field_name))
569                 continue
570             relation_model = field._obj
571             if field._type == 'many2one':
572                 record_values[field_name] = self.edi_import_relation(cr, uid, relation_model,
573                                                                       field_value[1], field_value[0],
574                                                                       context=context)
575             elif field._type == 'many2many':
576                 record_values[field_name] = [self.edi_import_relation(cr, uid, relation_model, m2m_value[1],
577                                                                        m2m_value[0], context=context)
578                                              for m2m_value in field_value]
579             elif field._type == 'one2many':
580                 # must wait until parent report is imported, as the parent relationship
581                 # is often required in o2m child records
582                 o2m_todo[field_name] = field_value
583             else:
584                 record_values[field_name] = field_value
585
586         module_ref = "%s:%s" % (ext_id_members['module'], ext_id_members['db_uuid'])
587         record_id = self.pool.get('ir.model.data')._update(cr, uid, self._name, module_ref, record_values,
588                                                            xml_id=ext_id_members['id'], context=context)
589
590         record_display, = self.name_get(cr, uid, [record_id], context=context)
591
592         # process o2m values, connecting them to their parent on-the-fly
593         for o2m_field, o2m_value in o2m_todo.iteritems():
594             field = self._all_columns[o2m_field].column
595             dest_model = self.pool[field._obj]
596             for o2m_line in o2m_value:
597                 # link to parent record: expects an (ext_id, name) pair
598                 o2m_line[field._fields_id] = (ext_id_members['full'], record_display[1])
599                 dest_model.edi_import(cr, uid, o2m_line, context=context)
600
601         # process the attachments, if any
602         self._edi_import_attachments(cr, uid, record_id, edi, context=context)
603
604         return record_id
605
606 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: