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
29 from tools.translate import _
32 def split_xml_id(xml_id):
33 assert len(xml_id.split('.'))==2, \
34 _("'%s' contains too many dots. XML ids should not contain dots ! These are used to refer to other modules data, as in module.reference_id") % (xml_id)
35 return xml_id.split('.')
37 def safe_unique_id(database_id, model, record_id):
38 """Generate a unique string to represent a (database id,model,record_id) pair
39 without being too long, without revealing the database id, and
40 with a very low probability of collisions.
42 msg = "%s-%s-%s-%s" % (time.time(), database_id, model, record_id)
43 digest = hashlib.sha1(msg).digest()
44 digest = ''.join(chr(ord(x) ^ ord(y)) for (x,y) in zip(digest[:9], digest[9:-2]))
45 # finally, use the b64-encoded folded digest as ID part of the unique ID:
46 digest = base64.urlsafe_b64encode(digest)
48 return '%s-%s' % (model.replace('.','_'), digest)
50 class ir_edi_document(osv.osv):
51 _name = 'ir.edi.document'
52 _description = 'To represent the EDI Document of any OpenERP record.'
54 'name': fields.char("EDI token", size = 128, help="EDI Token is a unique identifier for the EDI document."),
55 'document': fields.text("Document", help="hold the serialization of the EDI document.")
59 ('name_uniq', 'unique (name)', 'The EDI Token must be unique!')
63 def new_edi_token(self, cr, uid, record):
65 Return a new, random unique token to identify an edi.document
66 :param record: It's a object of browse_record of any model
68 db_uuid = self.pool.get('ir.config_parameter').get_param(cr, uid, 'database.uuid')
70 edi_token = hashlib.sha256('%s-%s-%s-%s' % (time.time(), db_uuid, record._name, record.id)).hexdigest()
73 def serialize(self, edi_documents):
74 """Serialize the list of dictionaries using json dumps method
75 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()
76 :param edi_dicts: it's list of edi_dict
78 serialized_list = json.dumps(edi_documents)
79 return serialized_list
81 def generate_edi(self, cr, uid, records, context=None):
83 Generate the list of dictionaries using edi_export method of edi class
84 :param records: it's a object of browse_record_list of any model
88 for record in records:
89 record_model_obj = self.pool.get(record._name)
90 edi_list += record_model_obj.edi_export(cr, uid, [record], context=context)
91 return self.serialize(edi_list)
93 def get_document(self, cr, uid, edi_token, context=None):
95 Get the edi document from database using given edi token
96 returns the string serialization that is in the database (column: document) for the given edi_token or raise.
99 records = self.name_search(cr, uid, edi_token, operator='=', context=context)
101 record = records[0][0]
102 edi = self.browse(cr, uid, record, context=context)
107 def load_edi(self, cr, uid, edi_documents, context=None):
109 loads the values from list of dictionaries to the corresponding OpenERP records
110 using the edi_import method of edi class
111 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
114 :param edi_dicts: list of edi_dict
116 module_obj =self.pool.get('ir.module.module')
118 for edi_document in edi_documents:
119 module = edi_document.get('__module')
120 module_ids = module_obj.search(cr, uid, [('name','=',module),('state','not in',['uninstalled', 'uninstallable', 'to remove'])])
122 raise osv.except_osv(_('Invalid action !'),
123 _('The document you are trying to import requires the OpenERP "%s" application. The OpenERP configuration assistant will help with this if you are connected as an administrator.')%(module))
124 model = edi_document.get('__model')
125 assert model, _('model should be provided in EDI Dict')
126 model_obj = self.pool.get(model)
127 record_id = model_obj.edi_import(cr, uid, edi_document, context=context)
128 res.append((model,record_id))
131 def deserialize(self, edi_document_string):
132 """ Deserialized the edi document string
133 perform JSON deserialization from an edi document string, and returns a list of dicts
135 edi_document = json.loads(edi_document_string)
139 def export_edi(self, cr, uid, records, context=None):
141 The method handles the flow of the edi document generation and store it in
142 the database and return the edi_token of the particular document
144 * call generate_edi() to get a serialization and new_edi_token() to get a unique ID
145 * serialize the list returned by generate_edi() using serialize(), and save it in database with unique ID.
146 * return the unique ID
148 : param records: list of browse_record of any model
151 for record in records:
152 document = self.generate_edi(cr, uid, [record], context)
153 token = self.new_edi_token(cr, uid, record)
154 self.create(cr, uid, {
159 exported_ids.append(token)
162 def import_edi(self, cr, uid, edi_document=None, edi_url=None, context=None):
164 The method handles the flow of importing particular edi document and
165 updates the database values on the basis of the edi document using
168 * N: a serialized edi.document or the URL to download a serialized document
169 * If a URL is provided, download it first to get the document
170 * Calls deserialize() to get the resulting list of dicts from the document
171 * Call load_edi() with the list of dicts, to create or update the corresponding OpenERP records based on the edi.document.
174 if edi_url and not edi_document:
175 edi_document = urllib2.urlopen(edi_url).read()
176 assert edi_document, _('EDI Document should be provided')
177 edi_documents = self.deserialize(edi_document)
178 return self.load_edi(cr, uid, edi_documents, context=context)
184 _description = 'edi document handler'
186 """Mixin class for OSV objects that want be exposed as EDI documents.
187 Classes that inherit from this mixin class should override the
188 ``edi_import()`` and ``edi_export()`` methods to implement their
189 specific behavior, based on the primitives provided by this superclass."""
191 def record_xml_id(self, cr, uid, record, context=None):
192 model_data_pool = self.pool.get('ir.model.data')
193 data_ids = model_data_pool.search(cr, uid, [('res_id','=',record.id),('model','=',record._name)])
196 xml_record_id = data_ids[0]
197 xml_record = model_data_pool.browse(cr, uid, xml_record_id, context=context)
198 return xml_record.module, xml_record.name
200 def edi_xml_id(self, cr, uid, record, xml_id=None, context=None):
202 Generate a unique string to represent a pair of (the database's UUID, the XML ID).
203 Each EDI record and each relationship value are represented using a unique
204 database identifier. These database identifiers include the database unique
205 ID, as a way to uniquely refer to any record within any OpenERP instance,
208 For OpenERP records that have an existing "XML ID" (i.e. an entry in
209 ir.model.data), the EDI unique identifier for this record will be made of
210 "%s:%s" % (the database's UUID, the XML ID). The database's UUID MUST
211 NOT contain a colon characters (this is guaranteed by the UUID algorithm).
213 For OpenERP records that have no existing "XML ID", a new one should be
214 created during the EDI export. It is recommended that the generated XML ID
215 contains a readable reference to the record model, plus a unique value that
216 hides the database ID.
218 model_data_pool = self.pool.get('ir.model.data')
219 db_uuid = self.pool.get('ir.config_parameter').get_param(cr, uid, 'database.uuid')
221 _record_xml = self.record_xml_id(cr, uid, record, context=context)
223 module, xml_id = _record_xml
224 xml_id = '%s:%s.%s' % (db_uuid, module, xml_id)
227 uuid = safe_unique_id(db_uuid, record._name, record.id)
228 xml_id = '%s:%s.%s' % (db_uuid, record._module, uuid)
229 module, xml_id2 = split_xml_id(xml_id)
230 xml_record_id = model_data_pool.create(cr, uid, {
232 'model': record._name,
234 'res_id': record.id}, context=context)
237 def edi_metadata(self, cr, uid, records, context=None):
238 """Return a list representing the boilerplate EDI structure for
239 exporting the record in the given browse_rec_list, including
242 The metadata fields MUST always include:
243 - __model': the OpenERP model name
244 - __module': the OpenERP module containing the model
245 - __id': the unique (cross-DB) identifier for this record
246 - __last_update': last update date of record, ISO date string in UTC
247 - __version': a list of components for the version
248 - __attachments': a list (possibly empty) of dicts describing the files attached to this record.
253 attachment_object = self.pool.get('ir.attachment')
257 for ver in release.major_version.split('.'):
264 for record in records:
265 attachment_ids = attachment_object.search(cr, uid, [('res_model','=', record._name), ('res_id', '=', record.id)])
266 attachment_dict_list = []
267 for attachment in attachment_object.browse(cr, uid, attachment_ids, context=context):
268 attachment_dict_list.append({
269 'name' : attachment.name,
270 'content': base64.encodestring(attachment.datas),
271 'file_name': attachment.datas_fname,
275 uuid = self.edi_xml_id(cr, uid, record, context=context)
278 '__last_update': False, #record.write_date, #TODO: convert into UTC
280 if not context.get('o2m_export'):
282 '__model' : record._name,
283 '__module' : record._module,
284 '__version': version,
285 '__attachments': attachment_dict_list
287 edi_dict_list.append(edi_dict)
291 def edi_m2o(self, cr, uid, record, context=None):
292 """Return a list representing a M2O EDI value for
293 the given browse_record.
294 M2O are passed as pair (ID, Name)
295 Exmaple: ['db-uuid:xml-id', 'Partner Name']
297 # generic implementation!
298 db_uuid = self.edi_xml_id(cr, uid, record, context=context)
299 relation_model_pool = self.pool.get(record._name)
300 name = relation_model_pool.name_get(cr, uid, [record.id], context=context)
301 name = name and name[0][1] or False
302 return [db_uuid, name]
304 def edi_o2m(self, cr, uid, records, edi_struct=None, context=None):
305 """Return a list representing a O2M EDI value for
306 the browse_records from the given browse_record_list.
309 [ # O2M fields would be a list of dicts, with their
310 { '__id': 'db-uuid:xml-id', # own __id.
311 '__last_update': 'iso date', # The update date, just in case...
316 # generic implementation!
321 ctx.update({'o2m_export':True})
322 for record in records:
324 model_obj = self.pool.get(record._name)
325 dict_list += model_obj.edi_export(cr, uid, [record], edi_struct=edi_struct, context=ctx)
329 def edi_m2m(self, cr, uid, records, context=None):
330 """Return a list representing a M2M EDI value for
331 the browse_records from the given browse_record_list.
334 'related_tasks': [ # M2M fields would exported as a list of pairs,
335 ['db-uuid:xml-id1', # similar to a list of M2O values.
341 # generic implementation!
344 for record in records:
345 dict_list.append(self.edi_m2o(cr, uid, record, context=None))
349 def edi_export(self, cr, uid, records, edi_struct=None, context=None):
350 """Returns a list of dicts representing an edi.document containing the
351 browse_records with ``ids``, using the generic algorithm.
352 :param edi_struct: if provided, edi_struct should be a dictionary
353 with a skeleton of the OSV fields to export as edi.
354 Basic fields can have any key as value, but o2m
355 values should have a sample skeleton dict as value.
356 For example, for a res.partner record:
365 Any field not specified in the edi_struct will not
366 be included in the exported data.
368 # generic implementation!
372 if edi_struct is None:
374 _columns = self._all_columns
375 fields_to_export = edi_struct and edi_struct.keys() or _columns.keys()
380 edi_dict.update(self.edi_metadata(cr, uid, [row], context=context)[0])
381 for field in fields_to_export:
382 _column = _columns[field].column
383 _column_dict = fields.field_to_dict(self, cr, uid, context, _column)
384 record = getattr(row, field)
387 #if _fields[field].has_key('function') or _fields[field].has_key('related_columns'):
388 # # Do not Export Function Fields and related fields
390 elif _column_dict['type'] == 'many2one':
391 value = self.edi_m2o(cr, uid, record, context=context)
392 elif _column_dict['type'] == 'many2many':
393 value = self.edi_m2m(cr, uid, record, context=context)
394 elif _column_dict['type'] == 'one2many':
395 value = self.edi_o2m(cr, uid, record, edi_struct=edi_struct.get(field, {}), context=context )
398 edi_dict[field] = value
399 edi_dict_list.append(edi_dict)
403 def edi_get_object_by_name(self, cr, uid, value, model_name, context=None):
405 model = self.pool.get(model_name)
406 object_ids = model.name_search(cr, uid, value, operator='=', context=context)
407 if object_ids and len(object_ids) == 1:
408 object_id = object_ids[0][0]
409 openobject = model.browse(cr, uid, object_id, context=context)
412 def edi_get_object(self, cr, uid, xml_id, model, context=None):
413 model_data = self.pool.get('ir.model.data')
414 module, xml_id2 = split_xml_id(xml_id)
416 data_ids = model_data.search(cr, uid, [('model','=', model), ('name','=', xml_id2)])
418 model = self.pool.get(model)
419 data = model_data.browse(cr, uid, data_ids[0], context=context)
420 openobject = model.browse(cr, uid, data.res_id, context=context)
423 def edi_create_relation(self, cr, uid, relation_model, relation_value, context=None):
424 relation_model = self.pool.get(relation_model)
425 values = {} #relation_model.default_get(cr, uid, fields, context=context)
426 values[relation_model._rec_name] = relation_value
427 return relation_model.create(cr, uid, values, context=context)
429 def edi_import_relation(self, cr, uid, relation_model, relation_value, xml_id=None, context=None):
432 relation = self.edi_get_object(cr, uid, xml_id, relation_model, context=context)
434 relation = self.edi_get_object_by_name(cr, uid, relation_value, relation_model, context=context)
436 relation_id = self.edi_create_relation(cr, uid, relation_model, relation_value, context=context)
437 relation = relation_model.browse(cr, uid, relation_id, context=context)
439 record_xml = self.record_xml_id(cr, uid, relation, context=context)
441 module, xml_id = record_xml
442 xml_id = '%s.%s' % (module, xml_id)
444 xml_id = self.edi_xml_id(cr, uid, relation, context=context)
446 #TODO: update record from values ?
447 #relation_id = model_data._update(cr, uid, relation_model, module, values, xml_id=xml_id, context=context)
448 return relation and relation.id or False
450 def edi_import(self, cr, uid, edi_document, context=None):
452 """Imports a list of dicts representing an edi.document, using the
455 All relationship fields are exported in a special way, and provide their own
456 unique identifier, so that we can avoid duplication of records when importing.
457 Note: for all ir.model.data entries, the "module" value to use for read/write
458 should always be "edi_import", and the "name" value should be the full
459 db_id provided in the EDI.
462 M2O fields are always exported as a pair [db_id, name], where db_id
463 is in the form "db_uuid:xml_id", both being values that come directly
464 from the original database.
465 The import should behave like this:
466 a. Look in ir.model.data for a record that matches the db_id.
467 If found, replace the m2o value with the correct database ID and stop.
468 If not found, continue to next step.
469 b. Perform name_search(name) to look for a record that matches the
470 given m2o name. If only one record is found, create the missing
471 ir.model.data record to link it to the db_id, and the replace the m2o
472 value with the correct database ID, then stop. If zero result or
473 multiple results are found, go to next step.
474 c. Create the new record using the only field value that is known: the
475 name, and create the ir.model.data entry to map to it.
476 This should work for many models, and if not, the module should
477 provide a custom EDI import logic to care for it.
480 O2M fields are always exported as a list of dicts, where each dict corresponds
481 to a full EDI record. The import should not update existing records
482 if they already exist, it should only link them to the parent object.
483 a. Look for a record that matches the db_id provided in the __id field. If
484 found, keep the corresponding database id, and connect it to the parent
485 using a write value like (4,db_id).
486 b. If not found via db_id, create a new entry using the same method that
487 imports a full EDI record (recursive call!), grab the resulting db id,
488 and use it to connect to the parent via a write value like (4, db_id).
491 M2M fields are always exported as a list of pairs similar to M2O.
492 For each pair in the M2M:
493 a. Perform the same steps as for a Many2One (see 1.2.1.1)
494 b. After finding the database ID of the final record in the database,
495 connect it to the parent record via a write value like (4, db_id).
497 # generic implementation!
498 model_data = self.pool.get('ir.model.data')
499 assert self._name == edi_document['__model'], 'EDI Document could not import. Model of EDI Document and current model are does not match'
500 def process(datas, model_name):
502 model_pool = self.pool.get(model_name)
503 xml_id = datas['__id']
504 module, xml_id2 = split_xml_id(xml_id)
505 _columns = model_pool._all_columns
506 for field in datas.keys():
507 if field not in _columns:
509 if not field.startswith('__'):
510 _column = _columns[field].column
511 _column_dict = fields.field_to_dict(self, cr, uid, context, _column)
513 edi_field_value = datas[field]
514 field_type = _column_dict['type']
515 relation_model = _column_dict.get('relation')
516 if not edi_field_value:
518 if _column_dict.has_key('function') or _column_dict.has_key('related_columns'):
519 # DO NOT IMPORT FUNCTION FIELD AND RELATED FIELD
521 elif field_type == 'one2many':
522 # recursive call for getting children and returning [(0,0,{})] or [(1,ID,{})]
524 relation_object = self.pool.get(relation_model)
525 for edi_relation_document in edi_field_value:
526 relation = self.edi_get_object(cr, uid, edi_relation_document['__id'], relation_model, context=context)
527 if not relation and edi_relation_document.get('name'):
528 relation = self.edi_get_object_by_name(cr, uid, edi_relation_document['name'], relation_model, context=context)
530 self.edi_xml_id(cr, uid, relation, \
531 xml_id=edi_relation_document['__id'], context=context)
532 relations.append((4, relation.id))
533 res_module, res_xml_id, newrow = process(edi_relation_document, relation_model)
534 relations.append( (relation and 1 or 0, relation and relation.id or 0, newrow))
535 values[field] = relations
536 elif field_type in ('many2one', 'many2many'):
537 if field_type == 'many2one':
538 edi_parent_documents = [edi_field_value]
540 edi_parent_documents = edi_field_value
544 for edi_parent_document in edi_parent_documents:
545 relation_id = self.edi_import_relation(cr, uid, relation_model, edi_parent_document[1], edi_parent_document[0], context=context)
546 parent_lines.append(relation_id)
548 if len(parent_lines):
549 if field_type == 'many2one':
550 values[field] = parent_lines[0]
554 for m2m_id in parent_lines:
555 many2many_ids.append((4, m2m_id))
556 values[field] = many2many_ids
559 values[field] = edi_field_value
560 return module, xml_id2, values
562 module, xml_id, data_values = process(edi_document, self._name)
563 return model_data._update(cr, uid, self._name, module, data_values, xml_id=xml_id, context=context)
564 # vim: ts=4 sts=4 sw=4 si et