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 = self.pool.get('ir.config_parameter').get_param(cr, uid, 'database.uuid')
80 edi_token = hashlib.sha256('%s-%s-%s' % (time.time(), db_uuid, time.time())).hexdigest()
83 def serialize(self, edi_documents):
84 """Serialize the list of dictionaries using json dumps method
85 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()
86 :param edi_dicts: it's list of edi_dict
88 serialized_list = json.dumps(edi_documents)
89 return serialized_list
91 def generate_edi(self, cr, uid, records, context=None):
93 Generate the list of dictionaries using edi_export method of edi class
94 :param records: it's a object of browse_record_list of any model
98 for record in records:
99 record_model_obj = self.pool.get(record._name)
100 edi_list += record_model_obj.edi_export(cr, uid, [record], context=context)
101 return self.serialize(edi_list)
103 def get_document(self, cr, uid, edi_token, context=None):
105 Get the edi document from database using given edi token
106 returns the string serialization that is in the database (column: document) for the given edi_token or raise.
109 records = self.name_search(cr, uid, edi_token, context=context)
111 record = records[0][0]
112 edi = self.browse(cr, uid, record, context=context)
117 def load_edi(self, cr, uid, edi_documents, context=None):
119 loads the values from list of dictionaries to the corresponding OpenERP records
120 using the edi_import method of edi class
121 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
124 :param edi_dicts: list of edi_dict
127 for edi_document in edi_documents:
128 model = edi_document.get('__model')
129 assert model, _('model should be provided in EDI Dict')
130 model_obj = self.pool.get(model)
131 record_id = model_obj.edi_import(cr, uid, edi_document, context=context)
132 res.append((model,record_id))
135 def deserialize(self, edi_document_string):
136 """ Deserialized the edi document string
137 perform JSON deserialization from an edi document string, and returns a list of dicts
139 edi_document = json.loads(edi_document_string)
143 def export_edi(self, cr, uid, records, context=None):
145 The method handles the flow of the edi document generation and store it in
146 the database and return the edi_token of the particular document
148 * call generate_edi() to get a serialization and new_edi_token() to get a unique ID
149 * serialize the list returned by generate_edi() using serialize(), and save it in database with unique ID.
150 * return the unique ID
152 : param records: list of browse_record of any model
155 for record in records:
156 document = self.generate_edi(cr, uid, [record], context)
157 token = self.new_edi_token(record)
158 self.create(cr, uid, {
163 exported_ids.append(token)
166 def import_edi(self, cr, uid, edi_document=None, edi_url=None, context=None):
168 The method handles the flow of importing particular edi document and
169 updates the database values on the basis of the edi document using
172 * N: a serialized edi.document or the URL to download a serialized document
173 * If a URL is provided, download it first to get the document
174 * Calls deserialize() to get the resulting list of dicts from the document
175 * Call load_edi() with the list of dicts, to create or update the corresponding OpenERP records based on the edi.document.
178 if edi_url and not edi_document:
179 edi_document = urllib2.urlopen(edi_url).read()
180 assert edi_document, _('EDI Document should be provided')
181 edi_documents = self.deserialize(edi_document)
182 return self.load_edi(cr, uid, edi_documents, context=context)
188 _description = 'edi document handler'
190 """Mixin class for OSV objects that want be exposed as EDI documents.
191 Classes that inherit from this mixin class should override the
192 ``edi_import()`` and ``edi_export()`` methods to implement their
193 specific behavior, based on the primitives provided by this superclass."""
195 def edi_xml_id(self, cr, uid, record, context=None):
196 model_data_pool = self.pool.get('ir.model.data')
197 db_uuid = self.pool.get('ir.config_parameter').get_param(cr, uid, 'database.uuid')
199 data_ids = model_data_pool.search(cr, uid, [('res_id','=',record.id),('model','=',record._name)])
201 xml_record_id = data_ids[0]
202 xml_record = model_data_pool.browse(cr, uid, xml_record_id, context=context)
203 uuid = '%s:%s' % (db_uuid, xml_record.name)
205 xml_id = safe_unique_id(model, db_uuid)
206 uuid = '%s:%s' % (db_uuid, xml_id)
207 xml_record_id = model_data_pool.create(cr, uid, {
209 'model': record._name,
211 'res_id': record.id}, context=context)
215 def edi_metadata(self, cr, uid, records, context=None):
216 """Return a list representing the boilerplate EDI structure for
217 exporting the record in the given browse_rec_list, including
220 The metadata fields MUST always include:
221 - __model': the OpenERP model name
222 - __module': the OpenERP module containing the model
223 - __id': the unique (cross-DB) identifier for this record
224 - __last_update': last update date of record, ISO date string in UTC
225 - __version': a list of components for the version
226 - __attachments': a list (possibly empty) of dicts describing the files attached to this record.
231 attachment_object = self.pool.get('ir.attachment')
235 for ver in release.major_version.split('.'):
242 for record in records:
243 attachment_ids = attachment_object.search(cr, uid, [('res_model','=', record._name), ('res_id', '=', record.id)])
244 attachment_dict_list = []
245 for attachment in attachment_object.browse(cr, uid, attachment_ids, context=context):
246 attachment_dict_list.append({
247 'name' : attachment.name,
248 'content': base64.encodestring(attachment.datas),
249 'file_name': attachment.datas_fname,
253 db_uuid = self.edi_xml_id(cr, uid, record, context=context)
256 '__last_update': False, #record.write_date, #TODO: convert into UTC
258 if not context.get('o2m_export'):
260 '__model' : record._name,
261 '__module' : record._module,
262 '__version': version,
263 '__attachments': attachment_dict_list
265 edi_dict_list.append(edi_dict)
269 def edi_m2o(self, cr, uid, record, context=None):
270 """Return a list representing a M2O EDI value for
271 the given browse_record.
272 M2O are passed as pair (ID, Name)
273 Exmaple: ['db-uuid:xml-id', 'Partner Name']
275 # generic implementation!
276 db_uuid = self.edi_xml_id(cr, uid, record, context=context)
277 relation_model_pool = self.pool.get(record._name)
278 name = relation_model_pool.name_get(cr, uid, [record.id], context=context)
279 name = name and name[0][1] or False
280 return [db_uuid, name]
282 def edi_o2m(self, cr, uid, records, edi_struct=None, context=None):
283 """Return a list representing a O2M EDI value for
284 the browse_records from the given browse_record_list.
287 [ # O2M fields would be a list of dicts, with their
288 { '__id': 'db-uuid:xml-id', # own __id.
289 '__last_update': 'iso date', # The update date, just in case...
294 # generic implementation!
299 ctx.update({'o2m_export':True})
300 for record in records:
302 model_obj = self.pool.get(record._name)
303 dict_list += model_obj.edi_export(cr, uid, [record], edi_struct=edi_struct, context=ctx)
307 def edi_m2m(self, cr, uid, records, context=None):
308 """Return a list representing a M2M EDI value for
309 the browse_records from the given browse_record_list.
312 'related_tasks': [ # M2M fields would exported as a list of pairs,
313 ['db-uuid:xml-id1', # similar to a list of M2O values.
319 # generic implementation!
322 for record in records:
323 dict_list.append(self.edi_o2m(cr, uid, [record], context=None))
327 def edi_export(self, cr, uid, records, edi_struct=None, context=None):
328 """Returns a list of dicts representing an edi.document containing the
329 browse_records with ``ids``, using the generic algorithm.
330 :param edi_struct: if provided, edi_struct should be a dictionary
331 with a skeleton of the OSV fields to export as edi.
332 Basic fields can have any key as value, but o2m
333 values should have a sample skeleton dict as value.
334 For example, for a res.partner record:
343 Any field not specified in the edi_struct will not
344 be included in the exported data.
346 # generic implementation!
350 if edi_struct is None:
352 _fields = self.fields_get(cr, uid, context=context)
353 fields_to_export = edi_struct and edi_struct.keys() or _fields.keys()
358 edi_dict.update(self.edi_metadata(cr, uid, [row], context=context)[0])
359 for field in fields_to_export:
360 cols = _fields[field]
361 record = getattr(row, field)
364 #if _fields[field].has_key('function') or _fields[field].has_key('related_columns'):
365 # # Do not Export Function Fields and related fields
367 elif cols['type'] == 'many2one':
368 value = self.edi_m2o(cr, uid, record, context=context)
369 elif cols['type'] == 'many2many':
370 value = self.edi_m2m(cr, uid, record, context=context)
371 elif cols['type'] == 'one2many':
372 value = self.edi_o2m(cr, uid, record, edi_struct=edi_struct.get(field, {}), context=context )
375 edi_dict[field] = value
376 edi_dict_list.append(edi_dict)
379 def edi_import_relation(self, cr, uid, relation_model, relation_value, values={}, context=None):
380 relation_object = self.pool.get(relation_model)
381 relation_ids = relation_object.name_search(cr, uid, relation_value, context=context)
382 if relation_ids and len(relation_ids) == 1:
383 relation_id = relation_ids[0][0]
385 values.update({'name': relation_value})
386 relation_id = relation_object.create(cr, uid, values, context=context)
389 def edi_import(self, cr, uid, edi_document, context=None):
391 """Imports a list of dicts representing an edi.document, using the
394 All relationship fields are exported in a special way, and provide their own
395 unique identifier, so that we can avoid duplication of records when importing.
396 Note: for all ir.model.data entries, the "module" value to use for read/write
397 should always be "edi_import", and the "name" value should be the full
398 db_id provided in the EDI.
401 M2O fields are always exported as a pair [db_id, name], where db_id
402 is in the form "db_uuid:xml_id", both being values that come directly
403 from the original database.
404 The import should behave like this:
405 a. Look in ir.model.data for a record that matches the db_id.
406 If found, replace the m2o value with the correct database ID and stop.
407 If not found, continue to next step.
408 b. Perform name_search(name) to look for a record that matches the
409 given m2o name. If only one record is found, create the missing
410 ir.model.data record to link it to the db_id, and the replace the m2o
411 value with the correct database ID, then stop. If zero result or
412 multiple results are found, go to next step.
413 c. Create the new record using the only field value that is known: the
414 name, and create the ir.model.data entry to map to it.
415 This should work for many models, and if not, the module should
416 provide a custom EDI import logic to care for it.
419 O2M fields are always exported as a list of dicts, where each dict corresponds
420 to a full EDI record. The import should not update existing records
421 if they already exist, it should only link them to the parent object.
422 a. Look for a record that matches the db_id provided in the __id field. If
423 found, keep the corresponding database id, and connect it to the parent
424 using a write value like (4,db_id).
425 b. If not found via db_id, create a new entry using the same method that
426 imports a full EDI record (recursive call!), grab the resulting db id,
427 and use it to connect to the parent via a write value like (4, db_id).
430 M2M fields are always exported as a list of pairs similar to M2O.
431 For each pair in the M2M:
432 a. Perform the same steps as for a Many2One (see 1.2.1.1)
433 b. After finding the database ID of the final record in the database,
434 connect it to the parent record via a write value like (4, db_id).
436 # generic implementation!
438 fields = edi_document.keys()
439 fields_to_import = []
441 model_data = self.pool.get('ir.model.data')
442 _fields = self.fields_get(cr, uid, context=context)
444 for field in edi_document.keys():
445 if not field.startswith('__'):
446 fields_to_import.append(field)
447 edi_field_value = edi_document[field]
448 if not edi_field_value:
450 if _fields[field].has_key('function') or _fields[field].has_key('related_columns'):
451 # DO NOT IMPORT FUNCTION FIELD AND RELATED FIELD
453 elif _fields[field]['type'] in ('many2one', 'many2many'):
454 if _fields[field]['type'] == 'many2one':
455 edi_parent_documents = [edi_field_value]
457 edi_parent_documents = edi_field_value
461 for edi_parent_document in edi_parent_documents:
462 #Look in ir.model.data for a record that matches the db_id.
463 #If found, replace the m2o value with the correct database ID and stop.
464 #If not found, continue to next step
465 xml_id = edi_parent_document[0]
466 data_ids = model_data.name_search(cr, uid, xml_id)
468 for data in model_data.browse(cr, uid, [data_ids[0][0]], context=context):
469 parent_lines.append(data.res_id)
471 #Perform name_search(name) to look for a record that matches the
472 #given m2o name. If only one record is found, create the missing
473 #ir.model.data record to link it to the db_id, and the replace the m2o
474 #value with the correct database ID, then stop. If zero result or
475 #multiple results are found, go to next step.
476 #Create the new record using the only field value that is known: the
477 #name, and create the ir.model.data entry to map to it.
478 relation_model = _fields[field]['relation']
479 relation_id = self.edi_import_relation(cr, uid, relation_model, edi_parent_document[1], context=context)
480 relation_object = self.pool.get(relation_model)
481 relation_record = relation_object.browse(cr, uid, relation_id, context=context)
482 self.edi_xml_id(cr, uid, relation_record, context=context)
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
522 return model_data._update(cr, uid, self._name, edi_module, values, context=context)
523 # vim: ts=4 sts=4 sw=4 si et