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