[IMP+FIX] edi_service: fixed wrong method call to get_db into import_edi_url service...
[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 = safe_unique_id(record._name, record.id)
79         edi_token = hashlib.sha256('%s-%s-%s' % (time.time(), db_uuid, time.time())).hexdigest()
80         return edi_token
81     
82     def serialize(self, edi_documents):
83         """Serialize the list of dictionaries using json dumps method
84         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()
85         :param edi_dicts: it's list of edi_dict
86         """
87         serialized_list = json.dumps(edi_documents)
88         return serialized_list
89     
90     def generate_edi(self, cr, uid, records, context=None):
91         """
92         Generate the list of dictionaries using edi_export method of edi class 
93         :param records: it's a object of browse_record_list of any model
94         """
95         
96         edi_list = []
97         for record in records:
98             record_model_obj = self.pool.get(record._name)
99             edi_list += record_model_obj.edi_export(cr, uid, [record], context=context)
100         return self.serialize(edi_list)
101     
102     def get_document(self, cr, uid, edi_token, context=None):
103         """
104         Get the edi document from database using given edi token 
105         returns the string serialization that is in the database (column: document) for the given edi_token or raise.
106         """
107         
108         records = self.name_search(cr, uid, edi_token, context=context)
109         if records:
110             record = records[0][0]
111             edi = self.browse(cr, uid, record, context=context)
112             return edi.document
113         else:  
114             pass
115     
116     def load_edi(self, cr, uid, edi_documents, context=None):
117         """
118         loads the values from list of dictionaries to the corresponding OpenERP records
119         using the edi_import method of edi class
120         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 
121         osv.edi_import)
122
123         :param edi_dicts: list of edi_dict
124         """
125         res = []
126         for edi_document in edi_documents:
127             model = edi_document.get('__model')
128             assert model, _('model should be provided in EDI Dict')
129             model_obj = self.pool.get(model)
130             record_id = model_obj.edi_import(cr, uid, edi_document, context=context)
131             res.append((model,record_id))
132         return res
133     
134     def deserialize(self, edi_document_string):
135         """ Deserialized the edi document string
136         perform JSON deserialization from an edi document string, and returns a list of dicts
137         """
138         edi_document = json.loads(edi_document_string)
139         
140         return edi_document
141     
142     def export_edi(self, cr, uid, records, context=None):
143         """
144         The method handles the flow of the edi document generation and store it in 
145             the database and return the edi_token of the particular document
146         Steps: 
147         * call generate_edi() to get a serialization and new_edi_token() to get a unique ID
148         * serialize the list returned by generate_edi() using serialize(), and save it in database with unique ID.
149         * return the unique ID
150
151         : param records: list of browse_record of any model
152         """
153         exported_ids = []
154         for record in records:
155             document = self.generate_edi(cr, uid, [record], context)
156             token = self.new_edi_token(record)
157             self.create(cr, uid, {
158                          'name': token,
159                          'document': document
160                         }, context=context)
161         
162             exported_ids.append(token)
163         return exported_ids
164     
165     def import_edi(self, cr, uid, edi_document=None, edi_url=None, context=None):
166         """
167         The method handles the flow of importing particular edi document and 
168         updates the database values on the basis of the edi document using 
169         edi_loads method
170         
171         * N: a serialized edi.document or the URL to download a serialized document
172         * If a URL is provided, download it first to get the document
173         * Calls deserialize() to get the resulting list of dicts from the document
174         * Call load_edi() with the list of dicts, to create or update the corresponding OpenERP records based on the edi.document.
175         """
176         
177         if edi_url and not edi_document:
178             edi_document = urllib2.urlopen(edi_url).read()
179         assert edi_document, _('EDI Document should be provided')
180         edi_documents = self.deserialize(edi_document)
181         return self.load_edi(cr, uid, edi_documents, context=context)
182     
183 ir_edi_document()
184
185 class edi(object):
186     _name = 'edi'
187     _description = 'edi document handler'
188     
189     """Mixin class for OSV objects that want be exposed as EDI documents.
190        Classes that inherit from this mixin class should override the 
191        ``edi_import()`` and ``edi_export()`` methods to implement their
192        specific behavior, based on the primitives provided by this superclass."""
193
194     def edi_xml_id(self, cr, uid, record, context=None):
195         model_data_pool = self.pool.get('ir.model.data')
196         uuid = safe_unique_id(record._name, record.id)
197         data_ids = model_data_pool.search(cr, uid, [('res_id','=',record.id),('model','=',record._name)])
198         if len(data_ids):
199             xml_record_id = data_ids[0]
200         else:
201             xml_record_id = model_data_pool.create(cr, uid, {
202                 'name': uuid,
203                 'model': record._name,
204                 'module': edi_module,
205                 'res_id': record.id}, context=context)
206         xml_record = model_data_pool.browse(cr, uid, xml_record_id, context=context)
207         db_uuid = '%s:%s.%s' % (uuid, xml_record.module, xml_record.name)
208         return db_uuid
209     
210     def edi_metadata(self, cr, uid, records, context=None):
211         """Return a list representing the boilerplate EDI structure for
212            exporting the record in the given browse_rec_list, including
213            the metadata fields
214         
215         The metadata fields MUST always include:
216         - __model': the OpenERP model name
217         - __module': the OpenERP module containing the model
218         - __id': the unique (cross-DB) identifier for this record
219         - __last_update': last update date of record, ISO date string in UTC
220         - __version': a list of components for the version
221         - __attachments': a list (possibly empty) of dicts describing the files attached to this record.
222         """
223         if context is None:
224             context = {}
225         data_ids = []
226         attachment_object = self.pool.get('ir.attachment')
227         edi_dict_list = []
228         db_uuid = ''
229         version = []
230         for ver in release.major_version.split('.'):
231             try:
232                 ver = int(ver)
233             except:
234                 pass
235             version.append(ver)
236
237         for record in records:
238             attachment_ids = attachment_object.search(cr, uid, [('res_model','=', record._name), ('res_id', '=', record.id)])
239             attachment_dict_list = []
240             for attachment in attachment_object.browse(cr, uid, attachment_ids, context=context):
241                 attachment_dict_list.append({
242                         'name' : attachment.name,
243                         'content': base64.encodestring(attachment.datas),
244                         'file_name': attachment.datas_fname,
245                 })
246             
247             
248             db_uuid = self.edi_xml_id(cr, uid, record, context=context)
249             edi_dict = {
250                 '__id': db_uuid,
251                 '__last_update': False, #record.write_date, #TODO: convert into UTC
252             }
253             if not context.get('o2m_export'):
254                 edi_dict.update({
255                     '__model' : record._name,
256                     '__module' : record._module,
257                     '__version': version,
258                     '__attachments': attachment_dict_list
259                 })
260             edi_dict_list.append(edi_dict)
261             
262         return edi_dict_list
263
264     def edi_m2o(self, cr, uid, record, context=None):
265         """Return a list representing a M2O EDI value for
266            the given browse_record.
267         M2O are passed as pair (ID, Name)
268         Exmaple: ['db-uuid:xml-id',  'Partner Name']
269         """
270         # generic implementation!
271         db_uuid = self.edi_xml_id(cr, uid, record, context=context)
272         relation_model_pool = self.pool.get(record._name)  
273         name = relation_model_pool.name_get(cr, uid, [record.id], context=context)
274         name = name and name[0][1] or False
275         return [db_uuid, name]
276         
277     def edi_o2m(self, cr, uid, records, edi_struct=None, context=None):
278         """Return a list representing a O2M EDI value for
279            the browse_records from the given browse_record_list.
280
281         Example:
282          [                                # O2M fields would be a list of dicts, with their
283            { '__id': 'db-uuid:xml-id',    # own __id.
284              '__last_update': 'iso date', # The update date, just in case...
285              'name': 'some name',
286              ...
287            }],
288         """
289         # generic implementation!
290         dict_list = []
291         if context is None:
292             context = {}
293         ctx = context.copy()
294         ctx.update({'o2m_export':True})
295         for record in records:
296             
297             model_obj = self.pool.get(record._name)
298             dict_list += model_obj.edi_export(cr, uid, [record], edi_struct=edi_struct, context=ctx)
299         
300         return dict_list
301         
302     def edi_m2m(self, cr, uid, records, context=None):
303         """Return a list representing a M2M EDI value for
304            the browse_records from the given browse_record_list.
305
306         Example: 
307         'related_tasks': [                 # M2M fields would exported as a list of pairs,
308                   ['db-uuid:xml-id1',      # similar to a list of M2O values.
309                    'Task 01: bla bla'],
310                   ['db-uuid:xml-id2',
311                    'Task 02: bla bla']
312             ]
313         """
314         # generic implementation!
315         dict_list = []
316         
317         for record in records:
318             dict_list.append(self.edi_o2m(cr, uid, [record], context=None))
319       
320         return dict_list
321
322     def edi_export(self, cr, uid, records, edi_struct=None, context=None):
323         """Returns a list of dicts representing an edi.document containing the
324            browse_records with ``ids``, using the generic algorithm.
325            :param edi_struct: if provided, edi_struct should be a dictionary
326                               with a skeleton of the OSV fields to export as edi.
327                               Basic fields can have any key as value, but o2m
328                               values should have a sample skeleton dict as value.
329                               For example, for a res.partner record:
330                               edi_struct: {
331                                    'name': True,
332                                    'company_id': True,
333                                    'address': {
334                                        'name': True,
335                                        'street': True,
336                                    }
337                               }
338                               Any field not specified in the edi_struct will not
339                               be included in the exported data.
340         """
341         # generic implementation!
342         
343         if context is None:
344             context = {}
345         if edi_struct is None:
346             edi_struct = {}
347         _fields = self.fields_get(cr, uid, context=context)
348         fields_to_export = edi_struct and edi_struct.keys() or _fields.keys()
349         edi_dict_list = []
350         value = None
351         for row in records:
352             edi_dict = {}
353             edi_dict.update(self.edi_metadata(cr, uid, [row], context=context)[0])
354             for field in fields_to_export:
355                 cols = _fields[field]
356                 record = getattr(row, field)
357                 if not record:
358                     continue
359                 if _fields[field].has_key('function') or _fields[field].has_key('related_columns'):
360                     # Do not Export Function Fields and related fields
361                     continue
362                 elif cols['type'] == 'many2one':
363                     value = self.edi_m2o(cr, uid, record, context=context)
364                 elif cols['type'] == 'many2many':
365                     value = self.edi_m2m(cr, uid, record, context=context)
366                 elif cols['type'] == 'one2many':
367                     value = self.edi_o2m(cr, uid, record, edi_struct=edi_struct.get(field, {}), context=context )
368                 else:
369                     value = record
370                 edi_dict[field] = value
371             edi_dict_list.append(edi_dict)
372         return edi_dict_list
373
374     def edi_import_relation(self, cr, uid, relation_model, relation_value, values={}, context=None):
375         relation_object = self.pool.get(relation_model)
376         relation_ids = relation_object.name_search(cr, uid, relation_value, context=context)
377         if relation_ids and len(relation_ids) == 1:
378             relation_id = relation_ids[0][0]
379         else:
380             values.update({'name': relation_value})
381             relation_id = relation_object.create(cr, uid, values, context=context)
382         return relation_id
383         
384     def edi_import(self, cr, uid, edi_document, context=None):
385     
386         """Imports a list of dicts representing an edi.document, using the
387            generic algorithm.
388
389              All relationship fields are exported in a special way, and provide their own
390              unique identifier, so that we can avoid duplication of records when importing.
391              Note: for all ir.model.data entries, the "module" value to use for read/write
392                    should always be "edi_import", and the "name" value should be the full
393                    db_id provided in the EDI.
394
395              1: Many2One
396              M2O fields are always exported as a pair [db_id, name], where db_id
397              is in the form "db_uuid:xml_id", both being values that come directly
398              from the original database.
399              The import should behave like this:
400                  a. Look in ir.model.data for a record that matches the db_id.
401                     If found, replace the m2o value with the correct database ID and stop.
402                     If not found, continue to next step.
403                  b. Perform name_search(name) to look for a record that matches the
404                     given m2o name. If only one record is found, create the missing
405                     ir.model.data record to link it to the db_id, and the replace the m2o
406                     value with the correct database ID, then stop. If zero result or
407                     multiple results are found, go to next step.
408                  c. Create the new record using the only field value that is known: the
409                     name, and create the ir.model.data entry to map to it.
410                     This should work for many models, and if not, the module should
411                     provide a custom EDI import logic to care for it.
412
413              2: One2Many
414              O2M fields are always exported as a list of dicts, where each dict corresponds
415              to a full EDI record. The import should not update existing records
416              if they already exist, it should only link them to the parent object.
417                  a. Look for a record that matches the db_id provided in the __id field. If
418                     found, keep the corresponding database id, and connect it to the parent
419                     using a write value like (4,db_id).
420                  b. If not found via db_id, create a new entry using the same method that
421                     imports a full EDI record (recursive call!), grab the resulting db id,
422                     and use it to connect to the parent via a write value like (4, db_id).
423
424              3: Many2Many
425              M2M fields are always exported as a list of pairs similar to M2O.
426              For each pair in the M2M:
427                  a. Perform the same steps as for a Many2One (see 1.2.1.1)
428                  b. After finding the database ID of the final record in the database,
429                     connect it to the parent record via a write value like (4, db_id).        
430         """
431         # generic implementation!
432         
433         fields = edi_document.keys()
434         fields_to_import = []
435         data_line = []
436         model_data = self.pool.get('ir.model.data')
437         _fields = self.fields_get(cr, uid, context=context)
438         values = {}
439         for field in edi_document.keys():
440             if not field.startswith('__'):
441                 fields_to_import.append(field)
442                 edi_field_value = edi_document[field]
443                 if not edi_field_value:
444                     continue
445                 if _fields[field].has_key('function') or _fields[field].has_key('related_columns'):
446                     # DO NOT IMPORT FUNCTION FIELD AND RELATED FIELD
447                     continue
448                 elif _fields[field]['type'] in ('many2one', 'many2many'):
449                     if _fields[field]['type'] == 'many2one':
450                         edi_parent_documents = [edi_field_value]
451                     else:
452                         edi_parent_documents = edi_field_value
453
454                     parent_lines = []
455
456                     for edi_parent_document in edi_parent_documents:
457                         #Look in ir.model.data for a record that matches the db_id.
458                         #If found, replace the m2o value with the correct database ID and stop.
459                         #If not found, continue to next step
460                         if edi_parent_document[0].find(':') != -1 and edi_parent_document[1] != None:
461                             db_uuid, xml_id =  tuple(edi_parent_document[0].split(':'))
462                             data_ids = model_data.name_search(cr, uid, xml_id)
463                             if data_ids:
464                                 for data in model_data.browse(cr, uid, [data_ids[0][0]], context=context):
465                                     parent_lines.append(data.res_id)
466                             else:
467                                 #Perform name_search(name) to look for a record that matches the
468                                 #given m2o name. If only one record is found, create the missing
469                                 #ir.model.data record to link it to the db_id, and the replace the m2o
470                                 #value with the correct database ID, then stop. If zero result or
471                                 #multiple results are found, go to next step.
472                                 #Create the new record using the only field value that is known: the
473                                 #name, and create the ir.model.data entry to map to it.
474                                 relation_model = _fields[field]['relation']
475                                 relation_id = self.edi_import_relation(cr, uid, relation_model, edi_parent_document[1], context=context)
476                                 relation_object = self.pool.get(relation_model)
477                                 model_data.create(cr, uid, {
478                                                     'name': xml_id,
479                                                     'model': relation_object._name,
480                                                     'module':relation_object._module,
481                                                     'res_id':relation_id 
482                                                     }, 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         
523         return model_data._update(cr, uid, self._name, edi_module, values, context=context)
524 # vim: ts=4 sts=4 sw=4 si et