1 # -*- coding: utf-8 -*-
2 ##############################################################################
4 # OpenERP, Open Source Management Solution
5 # Copyright (C) 2004-2009 Tiny SPRL (<http://tiny.be>).
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.
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.
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/>.
20 ##############################################################################
22 from osv import osv,fields
28 import openerp.release as release
30 edi_module = 'edi_import'
32 def safe_unique_id(model, database_id):
33 """Generate a unique string to represent a (model,database id) pair
34 without being too long, without revealing the database id, and
35 with a very low probability of collisions.
37 Each EDI record and each relationship value are represented using a unique
38 database identifier. These database identifiers include the database unique
39 ID, as a way to uniquely refer to any record within any OpenERP instance,
42 For OpenERP records that have an existing "XML ID" (i.e. an entry in
43 ir.model.data), the EDI unique identifier for this record will be made of
44 "%s:%s" % (the database's UUID, the XML ID). The database's UUID MUST
45 NOT contain a colon characters (this is guaranteed by the UUID algorithm).
47 For OpenERP records that have no existing "XML ID", a new one should be
48 created during the EDI export. It is recommended that the generated XML ID
49 contains a readable reference to the record model, plus a unique value that
50 hides the database ID.
52 msg = "%s-%s-%s" % (model, time.time(), database_id)
53 digest = hashlib.sha1(msg).digest()
54 digest = ''.join(chr(ord(x) ^ ord(y)) for (x,y) in zip(digest[:9], digest[9:-2]))
55 # finally, use the b64-encoded folded digest as ID part of the unique ID:
56 digest = base64.urlsafe_b64encode(digest)
58 return '%s-%s' % (model,digest)
60 class ir_edi_document(osv.osv):
61 _name = 'ir.edi.document'
62 _description = 'To represent the EDI Document of any OpenERP record.'
64 'name': fields.char("EDI token", size = 128, help="EDI Token is a unique identifier for the EDI document."),
65 'document': fields.text("Document", help="hold the serialization of the EDI document.")
69 ('name_uniq', 'unique (name)', 'The EDI Token must be unique!')
73 def new_edi_token(self, record):
75 Return a new, random unique token to identify an edi.document
76 :param record: It's a object of browse_record of any model
78 db_uuid = safe_unique_id(record._name, record.id)
79 edi_token = hashlib.sha256('%s-%s-%s' % (time.time(), db_uuid, time.time())).hexdigest()
82 def serialize(self, edi_documents):
83 """Serialize the list of dictionaries using json dumps method
84 perform a JSON serialization of a list of dicts prepared by generate_edi() and return a UTF-8 encoded string that could be passed to deserialize()
85 :param edi_dicts: it's list of edi_dict
87 serialized_list = json.dumps(edi_documents)
88 return serialized_list
90 def generate_edi(self, cr, uid, records, context=None):
92 Generate the list of dictionaries using edi_export method of edi class
93 :param records: it's a object of browse_record_list of any model
97 for record in records:
98 record_model_obj = self.pool.get(record._name)
99 edi_list += record_model_obj.edi_export(cr, uid, [record], context=context)
100 return self.serialize(edi_list)
102 def get_document(self, cr, uid, edi_token, context=None):
104 Get the edi document from database using given edi token
105 returns the string serialization that is in the database (column: document) for the given edi_token or raise.
108 records = self.name_search(cr, uid, edi_token, context=context)
110 record = records[0][0]
111 edi = self.browse(cr, uid, record, context=context)
116 def load_edi(self, cr, uid, edi_documents, context=None):
118 loads the values from list of dictionaries to the corresponding OpenERP records
119 using the edi_import method of edi class
120 For each edi record (dict) in the list, call the corresponding osv.edi_import() method, based on the __model attribute (see section 2.4 of for spec of
123 :param edi_dicts: list of edi_dict
126 for edi_document in edi_documents:
127 model = edi_document.get('__model')
128 assert model, _('model should be provided in EDI Dict')
129 model_obj = self.pool.get(model)
130 record_id = model_obj.edi_import(cr, uid, edi_document, context=context)
131 res.append((model,record_id))
134 def deserialize(self, edi_document_string):
135 """ Deserialized the edi document string
136 perform JSON deserialization from an edi document string, and returns a list of dicts
138 edi_document = json.loads(edi_document_string)
142 def export_edi(self, cr, uid, records, context=None):
144 The method handles the flow of the edi document generation and store it in
145 the database and return the edi_token of the particular document
147 * call generate_edi() to get a serialization and new_edi_token() to get a unique ID
148 * serialize the list returned by generate_edi() using serialize(), and save it in database with unique ID.
149 * return the unique ID
151 : param records: list of browse_record of any model
154 for record in records:
155 document = self.generate_edi(cr, uid, [record], context)
156 token = self.new_edi_token(record)
157 self.create(cr, uid, {
162 exported_ids.append(token)
165 def import_edi(self, cr, uid, edi_document=None, edi_url=None, context=None):
167 The method handles the flow of importing particular edi document and
168 updates the database values on the basis of the edi document using
171 * N: a serialized edi.document or the URL to download a serialized document
172 * If a URL is provided, download it first to get the document
173 * Calls deserialize() to get the resulting list of dicts from the document
174 * Call load_edi() with the list of dicts, to create or update the corresponding OpenERP records based on the edi.document.
177 if edi_url and not edi_document:
178 edi_document = urllib2.urlopen(edi_url).read()
179 assert edi_document, _('EDI Document should be provided')
180 edi_documents = self.deserialize(edi_document)
181 return self.load_edi(cr, uid, edi_documents, context=context)
187 _description = 'edi document handler'
189 """Mixin class for OSV objects that want be exposed as EDI documents.
190 Classes that inherit from this mixin class should override the
191 ``edi_import()`` and ``edi_export()`` methods to implement their
192 specific behavior, based on the primitives provided by this superclass."""
194 def edi_xml_id(self, cr, uid, record, context=None):
195 model_data_pool = self.pool.get('ir.model.data')
196 uuid = safe_unique_id(record._name, record.id)
197 data_ids = model_data_pool.search(cr, uid, [('res_id','=',record.id),('model','=',record._name)])
199 xml_record_id = data_ids[0]
201 xml_record_id = model_data_pool.create(cr, uid, {
203 'model': record._name,
204 'module': edi_module,
205 'res_id': record.id}, context=context)
206 xml_record = model_data_pool.browse(cr, uid, xml_record_id, context=context)
207 db_uuid = '%s:%s.%s' % (uuid, xml_record.module, xml_record.name)
210 def edi_metadata(self, cr, uid, records, context=None):
211 """Return a list representing the boilerplate EDI structure for
212 exporting the record in the given browse_rec_list, including
215 The metadata fields MUST always include:
216 - __model': the OpenERP model name
217 - __module': the OpenERP module containing the model
218 - __id': the unique (cross-DB) identifier for this record
219 - __last_update': last update date of record, ISO date string in UTC
220 - __version': a list of components for the version
221 - __attachments': a list (possibly empty) of dicts describing the files attached to this record.
226 attachment_object = self.pool.get('ir.attachment')
230 for ver in release.major_version.split('.'):
237 for record in records:
238 attachment_ids = attachment_object.search(cr, uid, [('res_model','=', record._name), ('res_id', '=', record.id)])
239 attachment_dict_list = []
240 for attachment in attachment_object.browse(cr, uid, attachment_ids, context=context):
241 attachment_dict_list.append({
242 'name' : attachment.name,
243 'content': base64.encodestring(attachment.datas),
244 'file_name': attachment.datas_fname,
248 db_uuid = self.edi_xml_id(cr, uid, record, context=context)
251 '__last_update': False, #record.write_date, #TODO: convert into UTC
253 if not context.get('o2m_export'):
255 '__model' : record._name,
256 '__module' : record._module,
257 '__version': version,
258 '__attachments': attachment_dict_list
260 edi_dict_list.append(edi_dict)
264 def edi_m2o(self, cr, uid, record, context=None):
265 """Return a list representing a M2O EDI value for
266 the given browse_record.
267 M2O are passed as pair (ID, Name)
268 Exmaple: ['db-uuid:xml-id', 'Partner Name']
270 # generic implementation!
271 db_uuid = self.edi_xml_id(cr, uid, record, context=context)
272 relation_model_pool = self.pool.get(record._name)
273 name = relation_model_pool.name_get(cr, uid, [record.id], context=context)
274 name = name and name[0][1] or False
275 return [db_uuid, name]
277 def edi_o2m(self, cr, uid, records, edi_struct=None, context=None):
278 """Return a list representing a O2M EDI value for
279 the browse_records from the given browse_record_list.
282 [ # O2M fields would be a list of dicts, with their
283 { '__id': 'db-uuid:xml-id', # own __id.
284 '__last_update': 'iso date', # The update date, just in case...
289 # generic implementation!
294 ctx.update({'o2m_export':True})
295 for record in records:
297 model_obj = self.pool.get(record._name)
298 dict_list += model_obj.edi_export(cr, uid, [record], edi_struct=edi_struct, context=ctx)
302 def edi_m2m(self, cr, uid, records, context=None):
303 """Return a list representing a M2M EDI value for
304 the browse_records from the given browse_record_list.
307 'related_tasks': [ # M2M fields would exported as a list of pairs,
308 ['db-uuid:xml-id1', # similar to a list of M2O values.
314 # generic implementation!
317 for record in records:
318 dict_list.append(self.edi_o2m(cr, uid, [record], context=None))
322 def edi_export(self, cr, uid, records, edi_struct=None, context=None):
323 """Returns a list of dicts representing an edi.document containing the
324 browse_records with ``ids``, using the generic algorithm.
325 :param edi_struct: if provided, edi_struct should be a dictionary
326 with a skeleton of the OSV fields to export as edi.
327 Basic fields can have any key as value, but o2m
328 values should have a sample skeleton dict as value.
329 For example, for a res.partner record:
338 Any field not specified in the edi_struct will not
339 be included in the exported data.
341 # generic implementation!
345 if edi_struct is None:
347 _fields = self.fields_get(cr, uid, context=context)
348 fields_to_export = edi_struct and edi_struct.keys() or _fields.keys()
353 edi_dict.update(self.edi_metadata(cr, uid, [row], context=context)[0])
354 for field in fields_to_export:
355 cols = _fields[field]
356 record = getattr(row, field)
359 if _fields[field].has_key('function') or _fields[field].has_key('related_columns'):
360 # Do not Export Function Fields and related fields
362 elif cols['type'] == 'many2one':
363 value = self.edi_m2o(cr, uid, record, context=context)
364 elif cols['type'] == 'many2many':
365 value = self.edi_m2m(cr, uid, record, context=context)
366 elif cols['type'] == 'one2many':
367 value = self.edi_o2m(cr, uid, record, edi_struct=edi_struct.get(field, {}), context=context )
370 edi_dict[field] = value
371 edi_dict_list.append(edi_dict)
374 def edi_import_relation(self, cr, uid, relation_model, relation_value, values={}, context=None):
375 relation_object = self.pool.get(relation_model)
376 relation_ids = relation_object.name_search(cr, uid, relation_value, context=context)
377 if relation_ids and len(relation_ids) == 1:
378 relation_id = relation_ids[0][0]
380 values.update({'name': relation_value})
381 relation_id = relation_object.create(cr, uid, values, context=context)
384 def edi_import(self, cr, uid, edi_document, context=None):
386 """Imports a list of dicts representing an edi.document, using the
389 All relationship fields are exported in a special way, and provide their own
390 unique identifier, so that we can avoid duplication of records when importing.
391 Note: for all ir.model.data entries, the "module" value to use for read/write
392 should always be "edi_import", and the "name" value should be the full
393 db_id provided in the EDI.
396 M2O fields are always exported as a pair [db_id, name], where db_id
397 is in the form "db_uuid:xml_id", both being values that come directly
398 from the original database.
399 The import should behave like this:
400 a. Look in ir.model.data for a record that matches the db_id.
401 If found, replace the m2o value with the correct database ID and stop.
402 If not found, continue to next step.
403 b. Perform name_search(name) to look for a record that matches the
404 given m2o name. If only one record is found, create the missing
405 ir.model.data record to link it to the db_id, and the replace the m2o
406 value with the correct database ID, then stop. If zero result or
407 multiple results are found, go to next step.
408 c. Create the new record using the only field value that is known: the
409 name, and create the ir.model.data entry to map to it.
410 This should work for many models, and if not, the module should
411 provide a custom EDI import logic to care for it.
414 O2M fields are always exported as a list of dicts, where each dict corresponds
415 to a full EDI record. The import should not update existing records
416 if they already exist, it should only link them to the parent object.
417 a. Look for a record that matches the db_id provided in the __id field. If
418 found, keep the corresponding database id, and connect it to the parent
419 using a write value like (4,db_id).
420 b. If not found via db_id, create a new entry using the same method that
421 imports a full EDI record (recursive call!), grab the resulting db id,
422 and use it to connect to the parent via a write value like (4, db_id).
425 M2M fields are always exported as a list of pairs similar to M2O.
426 For each pair in the M2M:
427 a. Perform the same steps as for a Many2One (see 1.2.1.1)
428 b. After finding the database ID of the final record in the database,
429 connect it to the parent record via a write value like (4, db_id).
431 # generic implementation!
433 fields = edi_document.keys()
434 fields_to_import = []
436 model_data = self.pool.get('ir.model.data')
437 _fields = self.fields_get(cr, uid, context=context)
439 for field in edi_document.keys():
440 if not field.startswith('__'):
441 fields_to_import.append(field)
442 edi_field_value = edi_document[field]
443 if not edi_field_value:
445 if _fields[field].has_key('function') or _fields[field].has_key('related_columns'):
446 # DO NOT IMPORT FUNCTION FIELD AND RELATED FIELD
448 elif _fields[field]['type'] in ('many2one', 'many2many'):
449 if _fields[field]['type'] == 'many2one':
450 edi_parent_documents = [edi_field_value]
452 edi_parent_documents = edi_field_value
456 for edi_parent_document in edi_parent_documents:
457 #Look in ir.model.data for a record that matches the db_id.
458 #If found, replace the m2o value with the correct database ID and stop.
459 #If not found, continue to next step
460 if edi_parent_document[0].find(':') != -1 and edi_parent_document[1] != None:
461 db_uuid, xml_id = tuple(edi_parent_document[0].split(':'))
462 data_ids = model_data.name_search(cr, uid, xml_id)
464 for data in model_data.browse(cr, uid, [data_ids[0][0]], context=context):
465 parent_lines.append(data.res_id)
467 #Perform name_search(name) to look for a record that matches the
468 #given m2o name. If only one record is found, create the missing
469 #ir.model.data record to link it to the db_id, and the replace the m2o
470 #value with the correct database ID, then stop. If zero result or
471 #multiple results are found, go to next step.
472 #Create the new record using the only field value that is known: the
473 #name, and create the ir.model.data entry to map to it.
474 relation_model = _fields[field]['relation']
475 relation_id = self.edi_import_relation(cr, uid, relation_model, edi_parent_document[1], context=context)
476 relation_object = self.pool.get(relation_model)
477 model_data.create(cr, uid, {
479 'model': relation_object._name,
480 'module':relation_object._module,
484 parent_lines.append(relation_id)
487 if len(parent_lines):
488 if _fields[field]['type'] == 'many2one':
489 values[field] = parent_lines[0]
493 for m2m_id in parent_lines:
494 many2many_ids.append((4,m2m_id))
495 values[field] = many2many_ids
496 elif _fields[field]['type'] == 'one2many':
497 #Look for a record that matches the db_id provided in the __id field. If
498 #found, keep the corresponding database id, and connect it to the parent
499 #using a write value like (4,db_id).
500 #If not found via db_id, create a new entry using the same method that
501 #imports a full EDI record (recursive call!), grab the resulting db id,
502 #and use it to connect to the parent via a write value like (4, db_id).
505 relation_object = self.pool.get(_fields[field]['relation'])
506 for edi_relation_document in edi_field_value:
507 if edi_relation_document['__id'].find(':') != -1:
508 db_uuid, xml_id = tuple(edi_relation_document['__id'].split(':'))
509 data_ids = model_data.name_search(cr, uid, xml_id)
511 for data in model_data.browse(cr,uid,[data_ids[0][0]]):
512 relations.append(data.res_id)
514 r = relation_object.edi_import(cr, uid, edi_relation_document, context=context)
517 for o2m_id in relations:
518 one2many_ids.append((4,o2m_id))
519 values[field] = one2many_ids
521 values[field] = edi_field_value
523 return model_data._update(cr, uid, self._name, edi_module, values, context=context)
524 # vim: ts=4 sts=4 sw=4 si et