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 safe_unique_id(database_id, model, record_id):
33 """Generate a unique string to represent a (database id,model,record_id) pair
34 without being too long, without revealing the database id, and
35 with a very low probability of collisions.
37 msg = "%s-%s-%s-%s" % (time.time(), database_id, model, record_id)
38 digest = hashlib.sha1(msg).digest()
39 digest = ''.join(chr(ord(x) ^ ord(y)) for (x,y) in zip(digest[:9], digest[9:-2]))
40 # finally, use the b64-encoded folded digest as ID part of the unique ID:
41 digest = base64.urlsafe_b64encode(digest)
43 return '%s-%s' % (model.replace('.','_'), digest)
45 class ir_edi_document(osv.osv):
46 _name = 'ir.edi.document'
47 _description = 'To represent the EDI Document of any OpenERP record.'
49 'name': fields.char("EDI token", size = 128, help="EDI Token is a unique identifier for the EDI document."),
50 'document': fields.text("Document", help="hold the serialization of the EDI document.")
54 ('name_uniq', 'unique (name)', 'The EDI Token must be unique!')
58 def new_edi_token(self, cr, uid, record):
60 Return a new, random unique token to identify an edi.document
61 :param record: It's a object of browse_record of any model
63 db_uuid = self.pool.get('ir.config_parameter').get_param(cr, uid, 'database.uuid')
65 edi_token = hashlib.sha256('%s-%s-%s-%s' % (time.time(), db_uuid, record._name, record.id)).hexdigest()
68 def serialize(self, edi_documents):
69 """Serialize the list of dictionaries using json dumps method
70 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()
71 :param edi_dicts: it's list of edi_dict
73 serialized_list = json.dumps(edi_documents)
74 return serialized_list
76 def generate_edi(self, cr, uid, records, context=None):
78 Generate the list of dictionaries using edi_export method of edi class
79 :param records: it's a object of browse_record_list of any model
83 for record in records:
84 record_model_obj = self.pool.get(record._name)
85 edi_list += record_model_obj.edi_export(cr, uid, [record], context=context)
86 return self.serialize(edi_list)
88 def get_document(self, cr, uid, edi_token, context=None):
90 Get the edi document from database using given edi token
91 returns the string serialization that is in the database (column: document) for the given edi_token or raise.
94 records = self.name_search(cr, uid, edi_token, operator='=', context=context)
96 record = records[0][0]
97 edi = self.browse(cr, uid, record, context=context)
102 def load_edi(self, cr, uid, edi_documents, context=None):
104 loads the values from list of dictionaries to the corresponding OpenERP records
105 using the edi_import method of edi class
106 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
109 :param edi_dicts: list of edi_dict
111 module_obj =self.pool.get('ir.module.module')
113 for edi_document in edi_documents:
114 module = edi_document.get('__module')
115 module_ids = module_obj.search(cr, uid, [('name','=',module),('state','not in',['uninstalled', 'uninstallable', 'to remove'])])
117 raise osv.except_osv(_('Invalid action !'),
118 _('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))
119 model = edi_document.get('__model')
120 assert model, _('model should be provided in EDI Dict')
121 model_obj = self.pool.get(model)
122 record_id = model_obj.edi_import(cr, uid, edi_document, context=context)
123 res.append((model,record_id))
126 def deserialize(self, edi_document_string):
127 """ Deserialized the edi document string
128 perform JSON deserialization from an edi document string, and returns a list of dicts
130 edi_document = json.loads(edi_document_string)
134 def export_edi(self, cr, uid, records, context=None):
136 The method handles the flow of the edi document generation and store it in
137 the database and return the edi_token of the particular document
139 * call generate_edi() to get a serialization and new_edi_token() to get a unique ID
140 * serialize the list returned by generate_edi() using serialize(), and save it in database with unique ID.
141 * return the unique ID
143 : param records: list of browse_record of any model
146 for record in records:
147 document = self.generate_edi(cr, uid, [record], context)
148 token = self.new_edi_token(cr, uid, record)
149 self.create(cr, uid, {
154 exported_ids.append(token)
157 def import_edi(self, cr, uid, edi_document=None, edi_url=None, context=None):
159 The method handles the flow of importing particular edi document and
160 updates the database values on the basis of the edi document using
163 * N: a serialized edi.document or the URL to download a serialized document
164 * If a URL is provided, download it first to get the document
165 * Calls deserialize() to get the resulting list of dicts from the document
166 * Call load_edi() with the list of dicts, to create or update the corresponding OpenERP records based on the edi.document.
169 if edi_url and not edi_document:
170 edi_document = urllib2.urlopen(edi_url).read()
171 assert edi_document, _('EDI Document should be provided')
172 edi_documents = self.deserialize(edi_document)
173 return self.load_edi(cr, uid, edi_documents, context=context)
179 _description = 'edi document handler'
181 """Mixin class for OSV objects that want be exposed as EDI documents.
182 Classes that inherit from this mixin class should override the
183 ``edi_import()`` and ``edi_export()`` methods to implement their
184 specific behavior, based on the primitives provided by this superclass."""
186 def record_xml_id(self, cr, uid, record, context=None):
187 model_data_pool = self.pool.get('ir.model.data')
188 data_ids = model_data_pool.search(cr, uid, [('res_id','=',record.id),('model','=',record._name)])
191 xml_record_id = data_ids[0]
192 xml_record = model_data_pool.browse(cr, uid, xml_record_id, context=context)
193 return xml_record.module, xml_record.name
195 def edi_xml_id(self, cr, uid, record, xml_id=None, context=None):
197 Generate a unique string to represent a pair of (the database's UUID, the XML ID).
198 Each EDI record and each relationship value are represented using a unique
199 database identifier. These database identifiers include the database unique
200 ID, as a way to uniquely refer to any record within any OpenERP instance,
203 For OpenERP records that have an existing "XML ID" (i.e. an entry in
204 ir.model.data), the EDI unique identifier for this record will be made of
205 "%s:%s" % (the database's UUID, the XML ID). The database's UUID MUST
206 NOT contain a colon characters (this is guaranteed by the UUID algorithm).
208 For OpenERP records that have no existing "XML ID", a new one should be
209 created during the EDI export. It is recommended that the generated XML ID
210 contains a readable reference to the record model, plus a unique value that
211 hides the database ID.
213 model_data_pool = self.pool.get('ir.model.data')
214 db_uuid = self.pool.get('ir.config_parameter').get_param(cr, uid, 'database.uuid')
216 _record_xml = self.record_xml_id(cr, uid, record, context=context)
218 module, xml_id = _record_xml
219 xml_id = '%s:%s.%s' % (db_uuid, module, xml_id)
222 uuid = safe_unique_id(db_uuid, record._name, record.id)
223 xml_id = '%s:%s.%s' % (db_uuid, record._module, uuid)
224 assert len(xml_id.split('.'))==2, _("'%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)
225 module, xml_id2 = xml_id.split('.')
226 xml_record_id = model_data_pool.create(cr, uid, {
228 'model': record._name,
230 'res_id': record.id}, context=context)
233 def edi_metadata(self, cr, uid, records, context=None):
234 """Return a list representing the boilerplate EDI structure for
235 exporting the record in the given browse_rec_list, including
238 The metadata fields MUST always include:
239 - __model': the OpenERP model name
240 - __module': the OpenERP module containing the model
241 - __id': the unique (cross-DB) identifier for this record
242 - __last_update': last update date of record, ISO date string in UTC
243 - __version': a list of components for the version
244 - __attachments': a list (possibly empty) of dicts describing the files attached to this record.
249 attachment_object = self.pool.get('ir.attachment')
253 for ver in release.major_version.split('.'):
260 for record in records:
261 attachment_ids = attachment_object.search(cr, uid, [('res_model','=', record._name), ('res_id', '=', record.id)])
262 attachment_dict_list = []
263 for attachment in attachment_object.browse(cr, uid, attachment_ids, context=context):
264 attachment_dict_list.append({
265 'name' : attachment.name,
266 'content': base64.encodestring(attachment.datas),
267 'file_name': attachment.datas_fname,
271 uuid = self.edi_xml_id(cr, uid, record, context=context)
274 '__last_update': False, #record.write_date, #TODO: convert into UTC
276 if not context.get('o2m_export'):
278 '__model' : record._name,
279 '__module' : record._module,
280 '__version': version,
281 '__attachments': attachment_dict_list
283 edi_dict_list.append(edi_dict)
287 def edi_m2o(self, cr, uid, record, context=None):
288 """Return a list representing a M2O EDI value for
289 the given browse_record.
290 M2O are passed as pair (ID, Name)
291 Exmaple: ['db-uuid:xml-id', 'Partner Name']
293 # generic implementation!
294 db_uuid = self.edi_xml_id(cr, uid, record, context=context)
295 relation_model_pool = self.pool.get(record._name)
296 name = relation_model_pool.name_get(cr, uid, [record.id], context=context)
297 name = name and name[0][1] or False
298 return [db_uuid, name]
300 def edi_o2m(self, cr, uid, records, edi_struct=None, context=None):
301 """Return a list representing a O2M EDI value for
302 the browse_records from the given browse_record_list.
305 [ # O2M fields would be a list of dicts, with their
306 { '__id': 'db-uuid:xml-id', # own __id.
307 '__last_update': 'iso date', # The update date, just in case...
312 # generic implementation!
317 ctx.update({'o2m_export':True})
318 for record in records:
320 model_obj = self.pool.get(record._name)
321 dict_list += model_obj.edi_export(cr, uid, [record], edi_struct=edi_struct, context=ctx)
325 def edi_m2m(self, cr, uid, records, context=None):
326 """Return a list representing a M2M EDI value for
327 the browse_records from the given browse_record_list.
330 'related_tasks': [ # M2M fields would exported as a list of pairs,
331 ['db-uuid:xml-id1', # similar to a list of M2O values.
337 # generic implementation!
340 for record in records:
341 dict_list.append(self.edi_m2o(cr, uid, record, context=None))
345 def edi_export(self, cr, uid, records, edi_struct=None, context=None):
346 """Returns a list of dicts representing an edi.document containing the
347 browse_records with ``ids``, using the generic algorithm.
348 :param edi_struct: if provided, edi_struct should be a dictionary
349 with a skeleton of the OSV fields to export as edi.
350 Basic fields can have any key as value, but o2m
351 values should have a sample skeleton dict as value.
352 For example, for a res.partner record:
361 Any field not specified in the edi_struct will not
362 be included in the exported data.
364 # generic implementation!
368 if edi_struct is None:
370 _columns = self._all_columns
371 fields_to_export = edi_struct and edi_struct.keys() or _columns.keys()
376 edi_dict.update(self.edi_metadata(cr, uid, [row], context=context)[0])
377 for field in fields_to_export:
378 _column = _columns[field].column
379 _column_dict = fields.field_to_dict(self, cr, uid, context, _column)
380 record = getattr(row, field)
383 #if _fields[field].has_key('function') or _fields[field].has_key('related_columns'):
384 # # Do not Export Function Fields and related fields
386 elif _column_dict['type'] == 'many2one':
387 value = self.edi_m2o(cr, uid, record, context=context)
388 elif _column_dict['type'] == 'many2many':
389 value = self.edi_m2m(cr, uid, record, context=context)
390 elif _column_dict['type'] == 'one2many':
391 value = self.edi_o2m(cr, uid, record, edi_struct=edi_struct.get(field, {}), context=context )
394 edi_dict[field] = value
395 edi_dict_list.append(edi_dict)
399 def edi_get_object_by_name(self, cr, uid, value, model_name, context=None):
401 model = self.pool.get(model_name)
402 object_ids = model.name_search(cr, uid, value, operator='=', context=context)
403 if object_ids and len(object_ids) == 1:
404 object_id = object_ids[0][0]
405 openobject = model.browse(cr, uid, object_id, context=context)
408 def edi_get_object(self, cr, uid, xml_id, model, context=None):
409 model_data = self.pool.get('ir.model.data')
410 assert len(xml_id.split('.'))==2, _("'%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)
411 module, xml_id2 = xml_id.split('.')
413 data_ids = model_data.search(cr, uid, [('model','=', model), ('name','=', xml_id2)])
415 model = self.pool.get(model)
416 data = model_data.browse(cr, uid, data_ids[0], context=context)
417 openobject = model.browse(cr, uid, data.res_id, context=context)
420 def edi_import_relation(self, cr, uid, relation_model, relation_value, xml_id=None, context=None):
423 relation = self.edi_get_object(cr, uid, xml_id, relation_model, context=context)
425 relation = self.edi_get_object_by_name(cr, uid, relation_value, relation_model, context=context)
427 relation_model = self.pool.get(relation_model)
428 values = {} #relation_model.default_get(cr, uid, fields, context=context)
429 values[relation_model._rec_name] = relation_value
430 relation_id = relation_model.create(cr, uid, values, context=context)
431 relation = relation_model.browse(cr, uid, relation_id, context=context)
433 record_xml = self.record_xml_id(cr, uid, relation, context=context)
435 module, xml_id = record_xml
436 xml_id = '%s.%s' % (module, xml_id)
438 xml_id = self.edi_xml_id(cr, uid, relation, context=context)
440 #TODO: update record from values ?
441 #relation_id = model_data._update(cr, uid, relation_model, module, values, xml_id=xml_id, context=context)
442 return relation and relation.id or False
444 def edi_import(self, cr, uid, edi_document, context=None):
446 """Imports a list of dicts representing an edi.document, using the
449 All relationship fields are exported in a special way, and provide their own
450 unique identifier, so that we can avoid duplication of records when importing.
451 Note: for all ir.model.data entries, the "module" value to use for read/write
452 should always be "edi_import", and the "name" value should be the full
453 db_id provided in the EDI.
456 M2O fields are always exported as a pair [db_id, name], where db_id
457 is in the form "db_uuid:xml_id", both being values that come directly
458 from the original database.
459 The import should behave like this:
460 a. Look in ir.model.data for a record that matches the db_id.
461 If found, replace the m2o value with the correct database ID and stop.
462 If not found, continue to next step.
463 b. Perform name_search(name) to look for a record that matches the
464 given m2o name. If only one record is found, create the missing
465 ir.model.data record to link it to the db_id, and the replace the m2o
466 value with the correct database ID, then stop. If zero result or
467 multiple results are found, go to next step.
468 c. Create the new record using the only field value that is known: the
469 name, and create the ir.model.data entry to map to it.
470 This should work for many models, and if not, the module should
471 provide a custom EDI import logic to care for it.
474 O2M fields are always exported as a list of dicts, where each dict corresponds
475 to a full EDI record. The import should not update existing records
476 if they already exist, it should only link them to the parent object.
477 a. Look for a record that matches the db_id provided in the __id field. If
478 found, keep the corresponding database id, and connect it to the parent
479 using a write value like (4,db_id).
480 b. If not found via db_id, create a new entry using the same method that
481 imports a full EDI record (recursive call!), grab the resulting db id,
482 and use it to connect to the parent via a write value like (4, db_id).
485 M2M fields are always exported as a list of pairs similar to M2O.
486 For each pair in the M2M:
487 a. Perform the same steps as for a Many2One (see 1.2.1.1)
488 b. After finding the database ID of the final record in the database,
489 connect it to the parent record via a write value like (4, db_id).
491 # generic implementation!
492 model_data = self.pool.get('ir.model.data')
493 assert self._name == edi_document['__model'], 'EDI Document could not import. Model of EDI Document and current model are does not match'
494 def process(datas, model_name):
496 model_pool = self.pool.get(model_name)
497 xml_id = datas['__id']
498 assert len(xml_id.split('.'))==2, _("'%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)
499 module, xml_id2 = xml_id.split('.')
500 _columns = model_pool._all_columns
501 for field in datas.keys():
502 if field not in _columns:
504 if not field.startswith('__'):
505 _column = _columns[field].column
506 _column_dict = fields.field_to_dict(self, cr, uid, context, _column)
508 edi_field_value = datas[field]
509 field_type = _column_dict['type']
510 relation_model = _column_dict.get('relation')
511 #print '???????', field, edi_field_value, relation_model, field_type
512 if not edi_field_value:
514 if _column_dict.has_key('function') or _column_dict.has_key('related_columns'):
515 # DO NOT IMPORT FUNCTION FIELD AND RELATED FIELD
517 elif field_type == 'one2many':
518 # recursive call for getting children and returning [(0,0,{})] or [(1,ID,{})]
520 relation_object = self.pool.get(relation_model)
521 for edi_relation_document in edi_field_value:
522 relation = self.edi_get_object(cr, uid, edi_relation_document['__id'], relation_model, context=context)
523 if not relation and edi_relation_document.get('name'):
524 relation = self.edi_get_object_by_name(cr, uid, edi_relation_document['name'], relation_model, context=context)
526 self.edi_xml_id(cr, uid, relation, \
527 xml_id=edi_relation_document['__id'], context=context)
528 relations.append((4, relation.id))
529 res_module, res_xml_id, newrow = process(edi_relation_document, relation_model)
530 relations.append( (relation and 1 or 0, relation and relation.id or 0, newrow))
531 values[field] = relations
532 elif field_type in ('many2one', 'many2many'):
533 if field_type == 'many2one':
534 edi_parent_documents = [edi_field_value]
536 edi_parent_documents = edi_field_value
540 for edi_parent_document in edi_parent_documents:
541 relation_id = self.edi_import_relation(cr, uid, relation_model, edi_parent_document[1], edi_parent_document[0], context=context)
542 parent_lines.append(relation_id)
544 if len(parent_lines):
545 if field_type == 'many2one':
546 values[field] = parent_lines[0]
550 for m2m_id in parent_lines:
551 many2many_ids.append((4, m2m_id))
552 values[field] = many2many_ids
555 values[field] = edi_field_value
556 return module, xml_id2, values
558 module, xml_id, data_values = process(edi_document, self._name)
559 return model_data._update(cr, uid, self._name, module, data_values, xml_id=xml_id, context=context)
560 # vim: ts=4 sts=4 sw=4 si et