[IMP] ir.edi:we must use the full __id from the EDI as the new XML-ID for objects...
[odoo/odoo.git] / openerp / addons / base / ir / ir_edi.py
1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 #    
4 #    OpenERP, Open Source Management Solution
5 #    Copyright (C) 2004-2009 Tiny SPRL (<http://tiny.be>).
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 from osv import osv,fields
23 import hashlib
24 import json
25 import time
26 import base64
27 import urllib2
28 import openerp.release as release
29
30 edi_module = 'edi_import'
31
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.
36
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,
40     without conflict.
41
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).
46
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.
51     """
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)
57         
58     return '%s-%s' % (model,digest)
59
60 class ir_edi_document(osv.osv):
61     _name = 'ir.edi.document'
62     _description = 'To represent the EDI Document of any OpenERP record.'
63     _columns = {
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.")
66                 
67     }
68     _sql_constraints = [
69         ('name_uniq', 'unique (name)', 'The EDI Token must be unique!')
70     ]
71     
72     
73     def new_edi_token(self, record):
74         """
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
77         """
78         db_uuid = self.pool.get('ir.config_parameter').get_param(cr, uid, 'database.uuid')
79
80         edi_token = hashlib.sha256('%s-%s-%s' % (time.time(), db_uuid, time.time())).hexdigest()
81         return edi_token
82     
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
87         """
88         serialized_list = json.dumps(edi_documents)
89         return serialized_list
90     
91     def generate_edi(self, cr, uid, records, context=None):
92         """
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
95         """
96         
97         edi_list = []
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)
102     
103     def get_document(self, cr, uid, edi_token, context=None):
104         """
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.
107         """
108         
109         records = self.name_search(cr, uid, edi_token, context=context)
110         if records:
111             record = records[0][0]
112             edi = self.browse(cr, uid, record, context=context)
113             return edi.document
114         else:  
115             pass
116     
117     def load_edi(self, cr, uid, edi_documents, context=None):
118         """
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 
122         osv.edi_import)
123
124         :param edi_dicts: list of edi_dict
125         """
126         res = []
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))
133         return res
134     
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
138         """
139         edi_document = json.loads(edi_document_string)
140         
141         return edi_document
142     
143     def export_edi(self, cr, uid, records, context=None):
144         """
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
147         Steps: 
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
151
152         : param records: list of browse_record of any model
153         """
154         exported_ids = []
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, {
159                          'name': token,
160                          'document': document
161                         }, context=context)
162         
163             exported_ids.append(token)
164         return exported_ids
165     
166     def import_edi(self, cr, uid, edi_document=None, edi_url=None, context=None):
167         """
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 
170         edi_loads method
171         
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.
176         """
177         
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)
183     
184 ir_edi_document()
185
186 class edi(object):
187     _name = 'edi'
188     _description = 'edi document handler'
189     
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."""
194
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')
198
199         data_ids = model_data_pool.search(cr, uid, [('res_id','=',record.id),('model','=',record._name)])
200         if len(data_ids):
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)
204         else:
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, {
208                 'name': xml_id,
209                 'model': record._name,
210                 'module': uuid,
211                 'res_id': record.id}, context=context)
212             
213         return uuid
214     
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
218            the metadata fields
219         
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.
227         """
228         if context is None:
229             context = {}
230         data_ids = []
231         attachment_object = self.pool.get('ir.attachment')
232         edi_dict_list = []
233         db_uuid = ''
234         version = []
235         for ver in release.major_version.split('.'):
236             try:
237                 ver = int(ver)
238             except:
239                 pass
240             version.append(ver)
241
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,
250                 })
251             
252             
253             db_uuid = self.edi_xml_id(cr, uid, record, context=context)
254             edi_dict = {
255                 '__id': db_uuid,
256                 '__last_update': False, #record.write_date, #TODO: convert into UTC
257             }
258             if not context.get('o2m_export'):
259                 edi_dict.update({
260                     '__model' : record._name,
261                     '__module' : record._module,
262                     '__version': version,
263                     '__attachments': attachment_dict_list
264                 })
265             edi_dict_list.append(edi_dict)
266             
267         return edi_dict_list
268
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']
274         """
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]
281         
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.
285
286         Example:
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...
290              'name': 'some name',
291              ...
292            }],
293         """
294         # generic implementation!
295         dict_list = []
296         if context is None:
297             context = {}
298         ctx = context.copy()
299         ctx.update({'o2m_export':True})
300         for record in records:
301             
302             model_obj = self.pool.get(record._name)
303             dict_list += model_obj.edi_export(cr, uid, [record], edi_struct=edi_struct, context=ctx)
304         
305         return dict_list
306         
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.
310
311         Example: 
312         'related_tasks': [                 # M2M fields would exported as a list of pairs,
313                   ['db-uuid:xml-id1',      # similar to a list of M2O values.
314                    'Task 01: bla bla'],
315                   ['db-uuid:xml-id2',
316                    'Task 02: bla bla']
317             ]
318         """
319         # generic implementation!
320         dict_list = []
321         
322         for record in records:
323             dict_list.append(self.edi_o2m(cr, uid, [record], context=None))
324       
325         return dict_list
326
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:
335                               edi_struct: {
336                                    'name': True,
337                                    'company_id': True,
338                                    'address': {
339                                        'name': True,
340                                        'street': True,
341                                    }
342                               }
343                               Any field not specified in the edi_struct will not
344                               be included in the exported data.
345         """
346         # generic implementation!
347         
348         if context is None:
349             context = {}
350         if edi_struct is None:
351             edi_struct = {}
352         _fields = self.fields_get(cr, uid, context=context)
353         fields_to_export = edi_struct and edi_struct.keys() or _fields.keys()
354         edi_dict_list = []
355         value = None
356         for row in records:
357             edi_dict = {}
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)
362                 if not record:
363                     continue
364                 #if _fields[field].has_key('function') or _fields[field].has_key('related_columns'):
365                 #    # Do not Export Function Fields and related fields
366                 #    continue
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 )
373                 else:
374                     value = record
375                 edi_dict[field] = value
376             edi_dict_list.append(edi_dict)
377         return edi_dict_list
378
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]
384         else:
385             values.update({'name': relation_value})
386             relation_id = relation_object.create(cr, uid, values, context=context)
387         return relation_id
388         
389     def edi_import(self, cr, uid, edi_document, context=None):
390     
391         """Imports a list of dicts representing an edi.document, using the
392            generic algorithm.
393
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.
399
400              1: Many2One
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.
417
418              2: One2Many
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).
428
429              3: Many2Many
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).        
435         """
436         # generic implementation!
437         
438         fields = edi_document.keys()
439         fields_to_import = []
440         data_line = []
441         model_data = self.pool.get('ir.model.data')
442         _fields = self.fields_get(cr, uid, context=context)
443         values = {}
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:
449                     continue
450                 if _fields[field].has_key('function') or _fields[field].has_key('related_columns'):
451                     # DO NOT IMPORT FUNCTION FIELD AND RELATED FIELD
452                     continue
453                 elif _fields[field]['type'] in ('many2one', 'many2many'):
454                     if _fields[field]['type'] == 'many2one':
455                         edi_parent_documents = [edi_field_value]
456                     else:
457                         edi_parent_documents = edi_field_value
458
459                     parent_lines = []
460
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)
467                         if data_ids:
468                             for data in model_data.browse(cr, uid, [data_ids[0][0]], context=context):
469                                 parent_lines.append(data.res_id)
470                         else:
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)
483                             
484                             parent_lines.append(relation_id)
485                                 
486                         
487                     if len(parent_lines):   
488                         if _fields[field]['type'] == 'many2one':
489                             values[field] = parent_lines[0]
490                             
491                         else:
492                             many2many_ids = []
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).
503             
504                     relations = []
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)
510                             if data_ids:
511                                 for data in model_data.browse(cr,uid,[data_ids[0][0]]):
512                                     relations.append(data.res_id)
513                             else:
514                                 r = relation_object.edi_import(cr, uid, edi_relation_document, context=context)
515                                 relations.append(r)
516                     one2many_ids = []
517                     for o2m_id in relations:
518                         one2many_ids.append((4,o2m_id))
519                     values[field] = one2many_ids
520                 else:
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