[IMP] base.ir.edi: improve edi import process, split in different methods
[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 from tools.translate import _
30 import netsvc
31
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.
36     """
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)
42         
43     return '%s-%s' % (model.replace('.','_'), digest)
44
45 class ir_edi_document(osv.osv):
46     _name = 'ir.edi.document'
47     _description = 'To represent the EDI Document of any OpenERP record.'
48     _columns = {
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.")
51                 
52     }
53     _sql_constraints = [
54         ('name_uniq', 'unique (name)', 'The EDI Token must be unique!')
55     ]
56     
57     
58     def new_edi_token(self, cr, uid, record):
59         """
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
62         """
63         db_uuid = self.pool.get('ir.config_parameter').get_param(cr, uid, 'database.uuid')
64
65         edi_token = hashlib.sha256('%s-%s-%s-%s' % (time.time(), db_uuid, record._name, record.id)).hexdigest()
66         return edi_token
67     
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
72         """
73         serialized_list = json.dumps(edi_documents)
74         return serialized_list
75     
76     def generate_edi(self, cr, uid, records, context=None):
77         """
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
80         """
81         
82         edi_list = []
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)
87     
88     def get_document(self, cr, uid, edi_token, context=None):
89         """
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.
92         """
93         
94         records = self.name_search(cr, uid, edi_token, operator='=', context=context)
95         if records:
96             record = records[0][0]
97             edi = self.browse(cr, uid, record, context=context)
98             return edi.document
99         else:  
100             pass
101     
102     def load_edi(self, cr, uid, edi_documents, context=None):
103         """
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 
107         osv.edi_import)
108
109         :param edi_dicts: list of edi_dict
110         """
111         module_obj =self.pool.get('ir.module.module')
112         res = []
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'])])
116             if not module_ids:
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))
124         return res
125     
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
129         """
130         edi_document = json.loads(edi_document_string)
131         
132         return edi_document
133     
134     def export_edi(self, cr, uid, records, context=None):
135         """
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
138         Steps: 
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
142
143         : param records: list of browse_record of any model
144         """
145         exported_ids = []
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, {
150                          'name': token,
151                          'document': document
152                         }, context=context)
153         
154             exported_ids.append(token)
155         return exported_ids
156     
157     def import_edi(self, cr, uid, edi_document=None, edi_url=None, context=None):
158         """
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 
161         edi_loads method
162         
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.
167         """
168         
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)
174     
175 ir_edi_document()
176
177 class edi(object):
178     _name = 'edi'
179     _description = 'edi document handler'
180     
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."""
185
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)])
189         if not data_ids:
190             return False
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
194
195     def edi_xml_id(self, cr, uid, record, xml_id=None, context=None):
196         """
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,
201         without conflict.
202
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).
207
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.
212         """
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')
215
216         _record_xml = self.record_xml_id(cr, uid, record, context=context)
217         if _record_xml:
218             module, xml_id = _record_xml
219             xml_id = '%s:%s.%s' % (db_uuid, module, xml_id)
220         else:
221             if not 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, {
227                 'name': xml_id2,
228                 'model': record._name,
229                 'module': module,
230                 'res_id': record.id}, context=context)
231         return xml_id
232     
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
236            the metadata fields
237         
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.
245         """
246         if context is None:
247             context = {}
248         data_ids = []
249         attachment_object = self.pool.get('ir.attachment')
250         edi_dict_list = []
251         db_uuid = ''
252         version = []
253         for ver in release.major_version.split('.'):
254             try:
255                 ver = int(ver)
256             except:
257                 pass
258             version.append(ver)
259
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,
268                 })
269             
270             
271             uuid = self.edi_xml_id(cr, uid, record, context=context)
272             edi_dict = {
273                 '__id': uuid,
274                 '__last_update': False, #record.write_date, #TODO: convert into UTC
275             }
276             if not context.get('o2m_export'):
277                 edi_dict.update({
278                     '__model' : record._name,
279                     '__module' : record._module,
280                     '__version': version,
281                     '__attachments': attachment_dict_list
282                 })
283             edi_dict_list.append(edi_dict)
284             
285         return edi_dict_list
286
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']
292         """
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]
299         
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.
303
304         Example:
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...
308              'name': 'some name',
309              ...
310            }],
311         """
312         # generic implementation!
313         dict_list = []
314         if context is None:
315             context = {}
316         ctx = context.copy()
317         ctx.update({'o2m_export':True})
318         for record in records:
319             
320             model_obj = self.pool.get(record._name)
321             dict_list += model_obj.edi_export(cr, uid, [record], edi_struct=edi_struct, context=ctx)
322         
323         return dict_list
324         
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.
328
329         Example: 
330         'related_tasks': [                 # M2M fields would exported as a list of pairs,
331                   ['db-uuid:xml-id1',      # similar to a list of M2O values.
332                    'Task 01: bla bla'],
333                   ['db-uuid:xml-id2',
334                    'Task 02: bla bla']
335             ]
336         """
337         # generic implementation!
338         dict_list = []
339         
340         for record in records:
341             dict_list.append(self.edi_m2o(cr, uid, record, context=None))
342       
343         return dict_list
344
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:
353                               edi_struct: {
354                                    'name': True,
355                                    'company_id': True,
356                                    'address': {
357                                        'name': True,
358                                        'street': True,
359                                    }
360                               }
361                               Any field not specified in the edi_struct will not
362                               be included in the exported data.
363         """
364         # generic implementation!
365         
366         if context is None:
367             context = {}
368         if edi_struct is None:
369             edi_struct = {}
370         _columns = self._all_columns
371         fields_to_export = edi_struct and edi_struct.keys() or _columns.keys()
372         edi_dict_list = []
373         value = None
374         for row in records:
375             edi_dict = {}
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)
381                 if not record:
382                     continue
383                 #if _fields[field].has_key('function') or _fields[field].has_key('related_columns'):
384                 #    # Do not Export Function Fields and related fields
385                 #    continue
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 )
392                 else:
393                     value = record
394                 edi_dict[field] = value
395             edi_dict_list.append(edi_dict)
396         return edi_dict_list
397
398
399     def edi_get_object_by_name(self, cr, uid, value, model_name, context=None):
400         openobject = False
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)
406         return openobject
407
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('.')
412         openobject = False
413         data_ids = model_data.search(cr, uid, [('model','=', model), ('name','=', xml_id2)])
414         if data_ids:
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)
418         return openobject
419
420     def edi_import_relation(self, cr, uid, relation_model, relation_value, xml_id=None, context=None):
421         relation = False
422         if xml_id:
423             relation = self.edi_get_object(cr, uid, xml_id, relation_model, context=context)
424         if not relation:
425             relation = self.edi_get_object_by_name(cr, uid, relation_value, relation_model, context=context)
426         if not relation:
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)
432
433         record_xml = self.record_xml_id(cr, uid, relation, context=context)
434         if record_xml:
435             module, xml_id = record_xml
436             xml_id = '%s.%s' % (module, xml_id)
437         if not xml_id:
438             xml_id = self.edi_xml_id(cr, uid, relation, context=context)
439         
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
443         
444     def edi_import(self, cr, uid, edi_document, context=None):
445     
446         """Imports a list of dicts representing an edi.document, using the
447            generic algorithm.
448
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.
454
455              1: Many2One
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.
472
473              2: One2Many
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).
483
484              3: Many2Many
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).        
490         """
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):
495             values = {}
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:
503                     continue
504                 if not field.startswith('__'):
505                     _column = _columns[field].column
506                     _column_dict = fields.field_to_dict(self, cr, uid, context, _column)
507                 
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:
513                         continue
514                     if _column_dict.has_key('function') or _column_dict.has_key('related_columns'):
515                         # DO NOT IMPORT FUNCTION FIELD AND RELATED FIELD
516                         continue
517                     elif field_type == 'one2many':
518                         # recursive call for getting children and returning [(0,0,{})] or [(1,ID,{})]
519                         relations = []
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)
525                             if relation:
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]
535                         else:
536                             edi_parent_documents = edi_field_value
537
538                         parent_lines = []
539
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)
543                             
544                         if len(parent_lines):   
545                             if field_type == 'many2one':
546                                 values[field] = parent_lines[0]
547                                 
548                             else:
549                                 many2many_ids = []
550                                 for m2m_id in parent_lines:
551                                     many2many_ids.append((4, m2m_id))
552                                 values[field] = many2many_ids
553                     
554                     else:
555                         values[field] = edi_field_value
556             return module, xml_id2, values
557         
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