[FIX] edi: get partner already exits or not using get_object and get_object_by_name.
[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 split_xml_id(xml_id):
33     assert len(xml_id.split('.'))==2, \
34             _("'%s' contains too many dots. XML ids should not contain dots ! These are used to refer to other modules data, as in module.reference_id") % (xml_id)
35     return xml_id.split('.')
36
37 def safe_unique_id(database_id, model, record_id):
38     """Generate a unique string to represent a (database id,model,record_id) pair
39     without being too long, without revealing the database id, and
40     with a very low probability of collisions.
41     """
42     msg = "%s-%s-%s-%s" % (time.time(), database_id, model, record_id)
43     digest = hashlib.sha1(msg).digest()
44     digest = ''.join(chr(ord(x) ^ ord(y)) for (x,y) in zip(digest[:9], digest[9:-2]))
45     # finally, use the b64-encoded folded digest as ID part of the unique ID:
46     digest = base64.urlsafe_b64encode(digest)
47         
48     return '%s-%s' % (model.replace('.','_'), digest)
49
50 class ir_edi_document(osv.osv):
51     _name = 'ir.edi.document'
52     _description = 'To represent the EDI Document of any OpenERP record.'
53     _columns = {
54                 'name': fields.char("EDI token", size = 128, help="EDI Token is a unique identifier for the EDI document."),
55                 'document': fields.text("Document", help="hold the serialization of the EDI document.")
56                 
57     }
58     _sql_constraints = [
59         ('name_uniq', 'unique (name)', 'The EDI Token must be unique!')
60     ]
61     
62     
63     def new_edi_token(self, cr, uid, record):
64         """
65         Return a new, random unique token to identify an edi.document
66         :param record: It's a object of browse_record of any model
67         """
68         db_uuid = self.pool.get('ir.config_parameter').get_param(cr, uid, 'database.uuid')
69
70         edi_token = hashlib.sha256('%s-%s-%s-%s' % (time.time(), db_uuid, record._name, record.id)).hexdigest()
71         return edi_token
72     
73     def serialize(self, edi_documents):
74         """Serialize the list of dictionaries using json dumps method
75         perform a JSON serialization of a list of dicts prepared by generate_edi() and return a UTF-8 encoded string that could be passed to deserialize()
76         :param edi_dicts: it's list of edi_dict
77         """
78         serialized_list = json.dumps(edi_documents)
79         return serialized_list
80     
81     def generate_edi(self, cr, uid, records, context=None):
82         """
83         Generate the list of dictionaries using edi_export method of edi class 
84         :param records: it's a object of browse_record_list of any model
85         """
86         
87         edi_list = []
88         for record in records:
89             record_model_obj = self.pool.get(record._name)
90             edi_list += record_model_obj.edi_export(cr, uid, [record], context=context)
91         return self.serialize(edi_list)
92     
93     def get_document(self, cr, uid, edi_token, context=None):
94         """
95         Get the edi document from database using given edi token 
96         returns the string serialization that is in the database (column: document) for the given edi_token or raise.
97         """
98         
99         records = self.name_search(cr, uid, edi_token, operator='=', context=context)
100         if records:
101             record = records[0][0]
102             edi = self.browse(cr, uid, record, context=context)
103             return edi.document
104         else:  
105             pass
106     
107     def load_edi(self, cr, uid, edi_documents, context=None):
108         """
109         loads the values from list of dictionaries to the corresponding OpenERP records
110         using the edi_import method of edi class
111         For each edi record (dict) in the list, call the corresponding osv.edi_import() method, based on the __model attribute (see section 2.4 of  for spec of 
112         osv.edi_import)
113
114         :param edi_dicts: list of edi_dict
115         """
116         module_obj =self.pool.get('ir.module.module')
117         res = []
118         for edi_document in edi_documents:
119             module = edi_document.get('__module')
120             module_ids = module_obj.search(cr, uid, [('name','=',module),('state','not in',['uninstalled', 'uninstallable', 'to remove'])])
121             if not module_ids:
122                 raise osv.except_osv(_('Invalid action !'),
123                             _('The document you are trying to import requires the OpenERP "%s" application. The OpenERP configuration assistant will help with this if you are connected as an administrator.')%(module))
124             model = edi_document.get('__model')
125             assert model, _('model should be provided in EDI Dict')
126             model_obj = self.pool.get(model)
127             record_id = model_obj.edi_import(cr, uid, edi_document, context=context)
128             res.append((model,record_id))
129         return res
130     
131     def deserialize(self, edi_document_string):
132         """ Deserialized the edi document string
133         perform JSON deserialization from an edi document string, and returns a list of dicts
134         """
135         edi_document = json.loads(edi_document_string)
136         
137         return edi_document
138     
139     def export_edi(self, cr, uid, records, context=None):
140         """
141         The method handles the flow of the edi document generation and store it in 
142             the database and return the edi_token of the particular document
143         Steps: 
144         * call generate_edi() to get a serialization and new_edi_token() to get a unique ID
145         * serialize the list returned by generate_edi() using serialize(), and save it in database with unique ID.
146         * return the unique ID
147
148         : param records: list of browse_record of any model
149         """
150         exported_ids = []
151         for record in records:
152             document = self.generate_edi(cr, uid, [record], context)
153             token = self.new_edi_token(cr, uid, record)
154             self.create(cr, uid, {
155                          'name': token,
156                          'document': document
157                         }, context=context)
158         
159             exported_ids.append(token)
160         return exported_ids
161     
162     def import_edi(self, cr, uid, edi_document=None, edi_url=None, context=None):
163         """
164         The method handles the flow of importing particular edi document and 
165         updates the database values on the basis of the edi document using 
166         edi_loads method
167         
168         * N: a serialized edi.document or the URL to download a serialized document
169         * If a URL is provided, download it first to get the document
170         * Calls deserialize() to get the resulting list of dicts from the document
171         * Call load_edi() with the list of dicts, to create or update the corresponding OpenERP records based on the edi.document.
172         """
173         
174         if edi_url and not edi_document:
175             edi_document = urllib2.urlopen(edi_url).read()
176         assert edi_document, _('EDI Document should be provided')
177         edi_documents = self.deserialize(edi_document)
178         return self.load_edi(cr, uid, edi_documents, context=context)
179     
180 ir_edi_document()
181
182 class edi(object):
183     _name = 'edi'
184     _description = 'edi document handler'
185     
186     """Mixin class for OSV objects that want be exposed as EDI documents.
187        Classes that inherit from this mixin class should override the 
188        ``edi_import()`` and ``edi_export()`` methods to implement their
189        specific behavior, based on the primitives provided by this superclass."""
190
191     def record_xml_id(self, cr, uid, record, context=None):
192         model_data_pool = self.pool.get('ir.model.data')
193         data_ids = model_data_pool.search(cr, uid, [('res_id','=',record.id),('model','=',record._name)])
194         if not data_ids:
195             return False
196         xml_record_id = data_ids[0]
197         xml_record = model_data_pool.browse(cr, uid, xml_record_id, context=context)
198         return xml_record.module, xml_record.name
199
200     def edi_xml_id(self, cr, uid, record, xml_id=None, context=None):
201         """
202         Generate a unique string to represent a pair of (the database's UUID, the XML ID).
203         Each EDI record and each relationship value are represented using a unique
204         database identifier. These database identifiers include the database unique
205         ID, as a way to uniquely refer to any record within any OpenERP instance,
206         without conflict.
207
208         For OpenERP records that have an existing "XML ID" (i.e. an entry in
209         ir.model.data), the EDI unique identifier for this record will be made of
210         "%s:%s" % (the database's UUID, the XML ID). The database's UUID MUST
211         NOT contain a colon characters (this is guaranteed by the UUID algorithm).
212
213         For OpenERP records that have no existing "XML ID", a new one should be
214         created during the EDI export. It is recommended that the generated XML ID
215         contains a readable reference to the record model, plus a unique value that
216         hides the database ID.
217         """
218         model_data_pool = self.pool.get('ir.model.data')
219         db_uuid = self.pool.get('ir.config_parameter').get_param(cr, uid, 'database.uuid')
220
221         _record_xml = self.record_xml_id(cr, uid, record, context=context)
222         if _record_xml:
223             module, xml_id = _record_xml
224             xml_id = '%s:%s.%s' % (db_uuid, module, xml_id)
225         else:
226             if not xml_id:
227                 uuid = safe_unique_id(db_uuid, record._name, record.id)
228                 xml_id = '%s:%s.%s' % (db_uuid, record._module, uuid)
229             module, xml_id2 = split_xml_id(xml_id)
230             xml_record_id = model_data_pool.create(cr, uid, {
231                 'name': xml_id2,
232                 'model': record._name,
233                 'module': module,
234                 'res_id': record.id}, context=context)
235         return xml_id
236     
237     def edi_metadata(self, cr, uid, records, context=None):
238         """Return a list representing the boilerplate EDI structure for
239            exporting the record in the given browse_rec_list, including
240            the metadata fields
241         
242         The metadata fields MUST always include:
243         - __model': the OpenERP model name
244         - __module': the OpenERP module containing the model
245         - __id': the unique (cross-DB) identifier for this record
246         - __last_update': last update date of record, ISO date string in UTC
247         - __version': a list of components for the version
248         - __attachments': a list (possibly empty) of dicts describing the files attached to this record.
249         """
250         if context is None:
251             context = {}
252         data_ids = []
253         attachment_object = self.pool.get('ir.attachment')
254         edi_dict_list = []
255         db_uuid = ''
256         version = []
257         for ver in release.major_version.split('.'):
258             try:
259                 ver = int(ver)
260             except:
261                 pass
262             version.append(ver)
263
264         for record in records:
265             attachment_ids = attachment_object.search(cr, uid, [('res_model','=', record._name), ('res_id', '=', record.id)])
266             attachment_dict_list = []
267             for attachment in attachment_object.browse(cr, uid, attachment_ids, context=context):
268                 attachment_dict_list.append({
269                         'name' : attachment.name,
270                         'content': base64.encodestring(attachment.datas),
271                         'file_name': attachment.datas_fname,
272                 })
273             
274             
275             uuid = self.edi_xml_id(cr, uid, record, context=context)
276             edi_dict = {
277                 '__id': uuid,
278                 '__last_update': False, #record.write_date, #TODO: convert into UTC
279             }
280             if not context.get('o2m_export'):
281                 edi_dict.update({
282                     '__model' : record._name,
283                     '__module' : record._module,
284                     '__version': version,
285                     '__attachments': attachment_dict_list
286                 })
287             edi_dict_list.append(edi_dict)
288             
289         return edi_dict_list
290
291     def edi_m2o(self, cr, uid, record, context=None):
292         """Return a list representing a M2O EDI value for
293            the given browse_record.
294         M2O are passed as pair (ID, Name)
295         Exmaple: ['db-uuid:xml-id',  'Partner Name']
296         """
297         # generic implementation!
298         db_uuid = self.edi_xml_id(cr, uid, record, context=context)
299         relation_model_pool = self.pool.get(record._name)  
300         name = relation_model_pool.name_get(cr, uid, [record.id], context=context)
301         name = name and name[0][1] or False
302         return [db_uuid, name]
303         
304     def edi_o2m(self, cr, uid, records, edi_struct=None, context=None):
305         """Return a list representing a O2M EDI value for
306            the browse_records from the given browse_record_list.
307
308         Example:
309          [                                # O2M fields would be a list of dicts, with their
310            { '__id': 'db-uuid:xml-id',    # own __id.
311              '__last_update': 'iso date', # The update date, just in case...
312              'name': 'some name',
313              ...
314            }],
315         """
316         # generic implementation!
317         dict_list = []
318         if context is None:
319             context = {}
320         ctx = context.copy()
321         ctx.update({'o2m_export':True})
322         for record in records:
323             
324             model_obj = self.pool.get(record._name)
325             dict_list += model_obj.edi_export(cr, uid, [record], edi_struct=edi_struct, context=ctx)
326         
327         return dict_list
328         
329     def edi_m2m(self, cr, uid, records, context=None):
330         """Return a list representing a M2M EDI value for
331            the browse_records from the given browse_record_list.
332
333         Example: 
334         'related_tasks': [                 # M2M fields would exported as a list of pairs,
335                   ['db-uuid:xml-id1',      # similar to a list of M2O values.
336                    'Task 01: bla bla'],
337                   ['db-uuid:xml-id2',
338                    'Task 02: bla bla']
339             ]
340         """
341         # generic implementation!
342         dict_list = []
343         
344         for record in records:
345             dict_list.append(self.edi_m2o(cr, uid, record, context=None))
346       
347         return dict_list
348
349     def edi_export(self, cr, uid, records, edi_struct=None, context=None):
350         """Returns a list of dicts representing an edi.document containing the
351            browse_records with ``ids``, using the generic algorithm.
352            :param edi_struct: if provided, edi_struct should be a dictionary
353                               with a skeleton of the OSV fields to export as edi.
354                               Basic fields can have any key as value, but o2m
355                               values should have a sample skeleton dict as value.
356                               For example, for a res.partner record:
357                               edi_struct: {
358                                    'name': True,
359                                    'company_id': True,
360                                    'address': {
361                                        'name': True,
362                                        'street': True,
363                                    }
364                               }
365                               Any field not specified in the edi_struct will not
366                               be included in the exported data.
367         """
368         # generic implementation!
369         
370         if context is None:
371             context = {}
372         if edi_struct is None:
373             edi_struct = {}
374         _columns = self._all_columns
375         fields_to_export = edi_struct and edi_struct.keys() or _columns.keys()
376         edi_dict_list = []
377         value = None
378         for row in records:
379             edi_dict = {}
380             edi_dict.update(self.edi_metadata(cr, uid, [row], context=context)[0])
381             for field in fields_to_export:
382                 _column = _columns[field].column
383                 _column_dict = fields.field_to_dict(self, cr, uid, context, _column)
384                 record = getattr(row, field)
385                 if not record:
386                     continue
387                 #if _fields[field].has_key('function') or _fields[field].has_key('related_columns'):
388                 #    # Do not Export Function Fields and related fields
389                 #    continue
390                 elif _column_dict['type'] == 'many2one':
391                     value = self.edi_m2o(cr, uid, record, context=context)
392                 elif _column_dict['type'] == 'many2many':
393                     value = self.edi_m2m(cr, uid, record, context=context)
394                 elif _column_dict['type'] == 'one2many':
395                     value = self.edi_o2m(cr, uid, record, edi_struct=edi_struct.get(field, {}), context=context )
396                 else:
397                     value = record
398                 edi_dict[field] = value
399             edi_dict_list.append(edi_dict)
400         return edi_dict_list
401
402
403     def edi_get_object_by_name(self, cr, uid, value, model_name, context=None):
404         openobject = False
405         model = self.pool.get(model_name)
406         object_ids = model.name_search(cr, uid, value, operator='=', context=context)
407         if object_ids and len(object_ids) == 1:
408             object_id = object_ids[0][0]
409             openobject = model.browse(cr, uid, object_id, context=context)
410         return openobject
411
412     def edi_get_object(self, cr, uid, xml_id, model, context=None):
413         model_data = self.pool.get('ir.model.data')
414         module, xml_id2 = split_xml_id(xml_id)
415         openobject = False
416         data_ids = model_data.search(cr, uid, [('model','=', model), ('name','=', xml_id2)])
417         if data_ids:
418             model = self.pool.get(model)
419             data = model_data.browse(cr, uid, data_ids[0], context=context)
420             openobject = model.browse(cr, uid, data.res_id, context=context)
421         return openobject
422
423     def edi_create_relation(self, cr, uid, relation_model, relation_value, context=None):
424         relation_model = self.pool.get(relation_model)
425         values = {} #relation_model.default_get(cr, uid, fields, context=context)
426         values[relation_model._rec_name] = relation_value            
427         return relation_model.create(cr, uid, values, context=context)
428
429     def edi_import_relation(self, cr, uid, relation_model, relation_value, xml_id=None, context=None):
430         relation = False
431         if xml_id:
432             relation = self.edi_get_object(cr, uid, xml_id, relation_model, context=context)
433         if not relation:
434             relation = self.edi_get_object_by_name(cr, uid, relation_value, relation_model, context=context)
435         if not relation:
436             relation_id = self.edi_create_relation(cr, uid, relation_model, relation_value, context=context)
437             relation = relation_model.browse(cr, uid, relation_id, context=context)
438
439         record_xml = self.record_xml_id(cr, uid, relation, context=context)
440         if record_xml:
441             module, xml_id = record_xml
442             xml_id = '%s.%s' % (module, xml_id)
443         if not xml_id:
444             xml_id = self.edi_xml_id(cr, uid, relation, context=context)
445         
446         #TODO: update record from values ?
447         #relation_id = model_data._update(cr, uid, relation_model, module, values, xml_id=xml_id, context=context)
448         return relation and relation.id or False
449         
450     def edi_import(self, cr, uid, edi_document, context=None):
451     
452         """Imports a list of dicts representing an edi.document, using the
453            generic algorithm.
454
455              All relationship fields are exported in a special way, and provide their own
456              unique identifier, so that we can avoid duplication of records when importing.
457              Note: for all ir.model.data entries, the "module" value to use for read/write
458                    should always be "edi_import", and the "name" value should be the full
459                    db_id provided in the EDI.
460
461              1: Many2One
462              M2O fields are always exported as a pair [db_id, name], where db_id
463              is in the form "db_uuid:xml_id", both being values that come directly
464              from the original database.
465              The import should behave like this:
466                  a. Look in ir.model.data for a record that matches the db_id.
467                     If found, replace the m2o value with the correct database ID and stop.
468                     If not found, continue to next step.
469                  b. Perform name_search(name) to look for a record that matches the
470                     given m2o name. If only one record is found, create the missing
471                     ir.model.data record to link it to the db_id, and the replace the m2o
472                     value with the correct database ID, then stop. If zero result or
473                     multiple results are found, go to next step.
474                  c. Create the new record using the only field value that is known: the
475                     name, and create the ir.model.data entry to map to it.
476                     This should work for many models, and if not, the module should
477                     provide a custom EDI import logic to care for it.
478
479              2: One2Many
480              O2M fields are always exported as a list of dicts, where each dict corresponds
481              to a full EDI record. The import should not update existing records
482              if they already exist, it should only link them to the parent object.
483                  a. Look for a record that matches the db_id provided in the __id field. If
484                     found, keep the corresponding database id, and connect it to the parent
485                     using a write value like (4,db_id).
486                  b. If not found via db_id, create a new entry using the same method that
487                     imports a full EDI record (recursive call!), grab the resulting db id,
488                     and use it to connect to the parent via a write value like (4, db_id).
489
490              3: Many2Many
491              M2M fields are always exported as a list of pairs similar to M2O.
492              For each pair in the M2M:
493                  a. Perform the same steps as for a Many2One (see 1.2.1.1)
494                  b. After finding the database ID of the final record in the database,
495                     connect it to the parent record via a write value like (4, db_id).        
496         """
497         # generic implementation!
498         model_data = self.pool.get('ir.model.data')
499         assert self._name == edi_document['__model'], 'EDI Document could not import. Model of EDI Document and current model are does not match'
500         def process(datas, model_name):
501             values = {}
502             model_pool = self.pool.get(model_name)
503             xml_id = datas['__id']
504             module, xml_id2 = split_xml_id(xml_id)
505             _columns = model_pool._all_columns
506             for field in datas.keys():
507                 if field not in _columns:
508                     continue
509                 if not field.startswith('__'):
510                     _column = _columns[field].column
511                     _column_dict = fields.field_to_dict(self, cr, uid, context, _column)
512                 
513                     edi_field_value = datas[field]
514                     field_type = _column_dict['type']
515                     relation_model = _column_dict.get('relation')
516                     if not edi_field_value:
517                         continue
518                     if _column_dict.has_key('function') or _column_dict.has_key('related_columns'):
519                         # DO NOT IMPORT FUNCTION FIELD AND RELATED FIELD
520                         continue
521                     elif field_type == 'one2many':
522                         # recursive call for getting children and returning [(0,0,{})] or [(1,ID,{})]
523                         relations = []
524                         relation_object = self.pool.get(relation_model)
525                         for edi_relation_document in edi_field_value:
526                             relation = self.edi_get_object(cr, uid, edi_relation_document['__id'], relation_model, context=context)
527                             if not relation and edi_relation_document.get('name'):
528                                 relation = self.edi_get_object_by_name(cr, uid, edi_relation_document['name'], relation_model, context=context)
529                             if relation:
530                                 self.edi_xml_id(cr, uid, relation, \
531                                                     xml_id=edi_relation_document['__id'], context=context)
532                                 relations.append((4, relation.id))
533                             res_module, res_xml_id, newrow = process(edi_relation_document, relation_model)
534                             relations.append( (relation and 1 or 0, relation and relation.id or 0, newrow))
535                         values[field] = relations
536                     elif field_type in ('many2one', 'many2many'):
537                         if field_type == 'many2one':
538                             edi_parent_documents = [edi_field_value]
539                         else:
540                             edi_parent_documents = edi_field_value
541
542                         parent_lines = []
543
544                         for edi_parent_document in edi_parent_documents:
545                             relation_id = self.edi_import_relation(cr, uid, relation_model, edi_parent_document[1], edi_parent_document[0], context=context)
546                             parent_lines.append(relation_id)
547                             
548                         if len(parent_lines):   
549                             if field_type == 'many2one':
550                                 values[field] = parent_lines[0]
551                                 
552                             else:
553                                 many2many_ids = []
554                                 for m2m_id in parent_lines:
555                                     many2many_ids.append((4, m2m_id))
556                                 values[field] = many2many_ids
557                     
558                     else:
559                         values[field] = edi_field_value
560             return module, xml_id2, values
561         
562         module, xml_id, data_values = process(edi_document, self._name)
563         return model_data._update(cr, uid, self._name, module, data_values, xml_id=xml_id, context=context)
564 # vim: ts=4 sts=4 sw=4 si et