Launchpad automatic translations update.
[odoo/odoo.git] / addons / import_base / import_framework.py
1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Management Solution
5 #    Copyright (C) 2004-2010 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 import pprint
22 import mapper
23 import pooler
24 import tools
25 from tools.translate import _
26
27 from threading import Thread
28 import datetime
29 import logging
30 import StringIO
31 import traceback
32 pp = pprint.PrettyPrinter(indent=4)
33
34
35
36
37 class import_framework(Thread):
38     """
39         This class should be extends,
40         get_data and get_mapping have to extends
41         get_state_map and initialize can be extended
42         for advanced purpose get_default_hook can also be extended
43         @see dummy import for a minimal exemple
44     """
45
46     """
47         for import_object, this domain will avoid to find an already existing object
48     """
49     DO_NOT_FIND_DOMAIN = [('id', '=', 0)]
50
51     #TODO don't use context to pass credential parameters
52     def __init__(self, obj, cr, uid, instance_name, module_name, email_to_notify=False, context=None):
53         Thread.__init__(self)
54         self.external_id_field = 'id'
55         self.obj = obj
56         self.cr = cr
57         self.uid = uid
58         self.instance_name = instance_name
59         self.module_name = module_name
60         self.context = context or {}
61         self.email = email_to_notify
62         self.table_list = []
63         self.logger = logging.getLogger(module_name)
64         self.initialize()
65
66     """
67         Abstract Method to be implemented in
68         the real instance
69     """
70     def initialize(self):
71         """
72             init before import
73             usually for the login
74         """
75         pass
76
77     def init_run(self):
78         """
79             call after intialize run in the thread, not in the main process
80             TO use for long initialization operation
81         """
82         pass
83
84     def get_data(self, table):
85         """
86             @return: a list of dictionaries
87                 each dictionnaries contains the list of pair  external_field_name : value
88         """
89         return [{}]
90
91     def get_link(self, from_table, ids, to_table):
92         """
93             @return: a dictionaries that contains the association between the id (from_table)
94                      and the list (to table) of id linked
95         """
96         return {}
97
98     def get_external_id(self, data):
99         """
100             @return the external id
101                 the default implementation return self.external_id_field (that has 'id') by default
102                 if the name of id field is different, you can overwrite this method or change the value
103                 of self.external_id_field
104         """
105         return data[self.external_id_field]
106
107     def get_mapping(self):
108         """
109             @return: { TABLE_NAME : {
110                 'model' : 'openerp.model.name',
111                 #if true import the table if not just resolve dependencies, use for meta package, by default => True
112                 #Not required
113                 'import' : True or False,
114                 #Not required
115                 'dependencies' : [TABLE_1, TABLE_2],
116                 #Not required
117                 'hook' : self.function_name, #get the val dict of the object, return the same val dict or False
118                 'map' : { @see mapper
119                     'openerp_field_name' : 'external_field_name', or val('external_field_name')
120                     'openerp_field_id/id' : ref(TABLE_1, 'external_id_field'), #make the mapping between the external id and the xml on the right
121                     'openerp_field2_id/id_parent' : ref(TABLE_1,'external_id_field') #indicate a self dependencies on openerp_field2_id
122                     'state' : map_val('state_equivalent_field', mapping), # use get_state_map to make the mapping between the value of the field and the value of the state
123                     'text_field' : concat('field_1', 'field_2', .., delimiter=':'), #concat the value of the list of field in one
124                     'description_field' : ppconcat('field_1', 'field_2', .., delimiter='\n\t'), #same as above but with a prettier formatting
125                     'field' : call(callable, arg1, arg2, ..), #call the function with all the value, the function should send the value : self.callable
126                     'field' : callable
127                     'field' : call(method, val('external_field') interface of method is self, val where val is the value of the field
128                     'field' : const(value) #always set this field to value
129                     + any custom mapper that you will define
130                 }
131             },
132
133             }
134         """
135         return {}
136
137     def default_hook(self, val):
138         """
139             this hook will be apply on each table that don't have hook
140             here we define the identity hook
141         """
142         return val
143
144     def _import_table(self, table):
145         data = self.get_data(table)
146         map = self.get_mapping()[table]['map']
147         hook = self.get_mapping()[table].get('hook', self.default_hook)
148         model = self.get_mapping()[table]['model']
149
150         final_data = []
151         for val in data:
152             res = hook(val)
153             if res:
154                 final_data.append(res)
155         return self._save_data(model, dict(map), final_data, table)
156
157     def _save_data(self, model, mapping, datas, table):
158         """
159             @param model: the model of the object to import
160             @param table : the external table where the data come from
161             @param mapping : definition of the mapping
162                              @see: get_mapping
163             @param datas : list of dictionnaries
164                 datas = [data_1, data_2, ..]
165                 data_i is a map external field_name => value
166                 and each data_i have a external id => in data_id['id']
167         """
168         self.logger.info(' Importing %s into %s' % (table, model))
169         if not datas:
170             return (0, 'No data found')
171         mapping['id'] = 'id_new'
172         res = []
173
174
175         self_dependencies = []
176         for k in mapping.keys():
177             if '_parent' in k:
178                 self_dependencies.append((k[:-7], mapping.pop(k)))
179
180         for data in datas:
181             for k, field_name in self_dependencies:
182                 data[k] = data.get(field_name) and self._generate_xml_id(data.get(field_name), table)
183
184             data['id_new'] = self._generate_xml_id(self.get_external_id(data), table)
185             fields, values = self._fields_mapp(data, mapping, table)
186             res.append(values)
187
188         model_obj = self.obj.pool.get(model)
189         if not model_obj:
190             raise ValueError(_("%s is not a valid model name") % model)
191         self.logger.debug(_(" fields imported : ") + str(fields))
192         (p, r, warning, s) = model_obj.import_data(self.cr, self.uid, fields, res, mode='update', current_module=self.module_name, noupdate=True, context=self.context)
193         for (field, field_name) in self_dependencies:
194             self._import_self_dependencies(model_obj, field, datas)
195         return (len(res), warning)
196
197     def _import_self_dependencies(self, obj, parent_field, datas):
198         """
199             @param parent_field: the name of the field that generate a self_dependencies, we call the object referenced in this
200                 field the parent of the object
201             @param datas: a list of dictionnaries
202                 Dictionnaries need to contains
203                     id_new : the xml_id of the object
204                     field_new : the xml_id of the parent
205         """
206         fields = ['id', parent_field]
207         for data in datas:
208             if data.get(parent_field):
209                 values = [data['id_new'], data[parent_field]]
210                 obj.import_data(self.cr, self.uid, fields, [values], mode='update', current_module=self.module_name, noupdate=True, context=self.context)
211
212     def _preprocess_mapping(self, mapping):
213         """
214             Preprocess the mapping :
215             after the preprocces, everything is
216             callable in the val of the dictionary
217
218             use to allow syntaxical sugar like 'field': 'external_field'
219             instead of 'field' : value('external_field')
220         """
221         map = dict(mapping)
222         for key, value in map.items():
223             if isinstance(value, basestring):
224                 map[key] = mapper.value(value)
225             #set parent for instance of dbmapper
226             elif isinstance(value, mapper.dbmapper):
227                 value.set_parent(self)
228         return map
229
230
231     def _fields_mapp(self,dict_sugar, openerp_dict, table):
232         """
233             call all the mapper and transform data
234             to be compatible with import_data
235         """
236         fields=[]
237         data_lst = []
238         mapping = self._preprocess_mapping(openerp_dict)
239         for key,val in mapping.items():
240             if key not in fields and dict_sugar:
241                 fields.append(key)
242                 value = val(dict(dict_sugar))
243                 data_lst.append(value)
244         return fields, data_lst
245
246     def _generate_xml_id(self, name, table):
247         """
248             @param name: name of the object, has to be unique in for a given table
249             @param table : table where the record we want generate come from
250             @return: a unique xml id for record, the xml_id will be the same given the same table and same name
251                      To be used to avoid duplication of data that don't have ids
252         """
253         sugar_instance = self.instance_name
254         name = name.replace('.', '_').replace(',', '_')
255         return sugar_instance + "_" + table + "_" + name
256
257
258     """
259         Public interface of the framework
260         those function can be use in the callable function defined in the mapping
261     """
262     def xml_id_exist(self, table, external_id):
263         """
264             Check if the external id exist in the openerp database
265             in order to check if the id exist the table where it come from
266             should be provide
267             @return the xml_id generated if the external_id exist in the database or false
268         """
269         if not external_id:
270             return False
271
272         xml_id = self._generate_xml_id(external_id, table)
273         id = self.obj.pool.get('ir.model.data').search(self.cr, self.uid, [('name', '=', xml_id), ('module', '=', self.module_name)])
274         return id and xml_id or False
275
276     def name_exist(self, table, name, model):
277         """
278             Check if the object with the name exist in the openerp database
279             in order to check if the id exist the table where it come from
280             should be provide and the model of the object
281         """
282         fields = ['name']
283         data = [name]
284         return self.import_object(fields, data, model, table, name, [('name', '=', name)])
285
286     def get_mapped_id(self, table, external_id, context=None):
287         """
288             @return return the databse id linked with the external_id
289         """
290         if not external_id:
291             return False
292
293         xml_id = self._generate_xml_id(external_id, table)
294         return self.obj.pool.get('ir.model.data').get_object_reference(self.cr, self.uid, self.module_name, xml_id)[1]
295
296     def import_object_mapping(self, mapping, data, model, table, name, domain_search=False):
297         """
298             same as import_objects but instead of two list fields and data,
299             this method take a dictionnaries : external_field : value
300                             and the mapping similar to the one define in 'map' key
301             @see import_object, get_mapping
302         """
303         fields, datas = self._fields_mapp(data, mapping, table)
304         return self.import_object(fields, datas, model, table, name, domain_search)
305
306     def import_object(self, fields, data, model, table, name, domain_search=False):
307         """
308             This method will import an object in the openerp, usefull for field that is only a char in sugar and is an object in openerp
309             use import_data that will take care to create/update or do nothing with the data
310             this method return the xml_id
311
312             To be use, when you want to create an object or link if already exist
313             use DO_NOT_LINK_DOMAIN to create always a new object
314             @param fields: list of fields needed to create the object without id
315             @param data: the list of the data, in the same order as the field
316                 ex : fields = ['firstname', 'lastname'] ; data = ['John', 'Mc donalds']
317             @param model: the openerp's model of the create/update object
318             @param table: the table where data come from in sugarcrm, no need to fit the real name of openerp name, just need to be unique
319             @param unique_name: the name of the object that we want to create/update get the id
320             @param domain_search : the domain that should find the unique existing record
321
322             @return: the xml_id of the ressources
323         """
324         domain_search = not domain_search and [('name', 'ilike', name)] or domain_search
325         obj = self.obj.pool.get(model)
326         if not obj: #if the model doesn't exist
327             return False
328
329         xml_id = self._generate_xml_id(name, table)
330         xml_ref = self.mapped_id_if_exist(model, domain_search, table, name)
331         fields.append('id')
332         data.append(xml_id)
333         obj.import_data(self.cr, self.uid, fields, [data], mode='update', current_module=self.module_name, noupdate=True, context=self.context)
334         return xml_ref or xml_id
335
336
337     def mapped_id_if_exist(self, model, domain, table, name):
338         """
339             To be use when we want link with and existing object, if the object don't exist
340             just ignore.
341             @param domain : search domain to find existing record, should return a unique record
342             @param xml_id: xml_id give to the mapping
343             @param name: external_id or name of the object to create if there is no id
344             @param table: the name of the table of the object to map
345             @return : the xml_id if the record exist in the db, False otherwise
346         """
347         obj = self.obj.pool.get(model)
348         ids = obj.search(self.cr, self.uid, domain, context=self.context)
349         if ids:
350             xml_id = self._generate_xml_id(name, table)
351             ir_model_data_obj = obj.pool.get('ir.model.data')
352             id = ir_model_data_obj._update(self.cr, self.uid, model,
353                              self.module_name, {}, mode='update', xml_id=xml_id,
354                              noupdate=True, res_id=ids[0], context=self.context)
355             return xml_id
356         return False
357
358
359     def set_table_list(self, table_list):
360         """
361             Set the list of table to import, this method should be call before run
362             @param table_list: the list of external table to import
363                ['Leads', 'Opportunity']
364         """
365         self.table_list = table_list
366
367     def run(self):
368         """
369             Import all data into openerp,
370             this is the Entry point to launch the process of import
371
372
373         """
374         self.data_started = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
375         self.cr = pooler.get_db(self.cr.dbname).cursor()
376         error = False
377         result = []
378         try:
379             self.init_run()
380             imported = set() #to invoid importing 2 times the sames modules
381             for table in self.table_list:
382                 to_import = self.get_mapping()[table].get('import', True)
383                 if not table in imported:
384                     res = self._resolve_dependencies(self.get_mapping()[table].get('dependencies', []), imported)
385                     result.extend(res)
386                     if to_import:
387                         (position, warning) = self._import_table(table)
388                         result.append((table, position, warning))
389                     imported.add(table)
390             self.cr.commit()
391
392         except Exception, err:
393             sh = StringIO.StringIO()
394             traceback.print_exc(file=sh)
395             error = sh.getvalue()
396             print error
397
398
399         self.date_ended = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
400         self._send_notification_email(result, error)
401
402         self.cr.close()
403
404     def _resolve_dependencies(self, dep, imported):
405         """
406             import dependencies recursively
407             and avoid to import twice the same table
408         """
409         result = []
410         for dependency in dep:
411             if not dependency in imported:
412                 to_import = self.get_mapping()[dependency].get('import', True)
413                 res = self._resolve_dependencies(self.get_mapping()[dependency].get('dependencies', []), imported)
414                 result.extend(res)
415                 if to_import:
416                     r = self._import_table(dependency)
417                     (position, warning) = r
418                     result.append((dependency, position, warning))
419                 imported.add(dependency)
420         return result
421
422     def _send_notification_email(self, result, error):
423         if not self.email:
424             return False        
425         email_obj = self.obj.pool.get('mail.message')
426         email_id = email_obj.create(self.cr, self.uid, {
427             'email_from' : 'import@module.openerp',
428             'email_to' : self.email,
429             'body_text' : self.get_email_body(result, error),
430             'subject' : self.get_email_subject(result, error),
431             'auto_delete' : True})
432         email_obj.send(self.cr, self.uid, [email_id])
433         if error:
434             self.logger.error(_("Import failed due to an unexpected error"))
435         else:
436             self.logger.info(_("Import finished, notification email sended"))
437
438     def get_email_subject(self, result, error=False):
439         """
440             This method define the subject of the email send by openerp at the end of import
441             @param result: a list of tuple
442                 (table_name, number_of_record_created/updated, warning) for each table
443             @return the subject of the mail
444
445         """
446         if error:
447             return _("Data Import failed at %s due to an unexpected error") % self.date_ended
448         return _("Import of your data finished at %s") % self.date_ended
449
450     def get_email_body(self, result, error=False):
451         """
452             This method define the body of the email send by openerp at the end of import. The body is separated in two part
453             the header (@see get_body_header), and the generate body with the list of table and number of record imported.
454             If you want to keep this it's better to overwrite get_body_header
455             @param result: a list of tuple
456                 (table_name, number_of_record_created/updated, warning) for each table
457             @return the subject of the mail
458
459         """
460
461         body = _("started at %s and finished at %s \n") % (self.data_started, self.date_ended)
462         if error:
463             body += _("but failed, in consequence no data were imported to keep database consistency \n error : \n") + error
464
465         for (table, nb, warning) in result:
466             if not warning:
467                 warning = _("with no warning")
468             else:
469                 warning = _("with warning : %s") % warning
470             body += _("%s has been successfully imported from %s %s, %s \n") % (nb, self.instance_name, table, warning)
471         return self.get_body_header(result) + "\n\n" + body
472
473     def get_body_header(self, result):
474         """
475             @return the first sentences written in the mail's body
476         """
477         return _("The import of data \n instance name : %s \n") % self.instance_name
478
479
480     #TODO documentation test
481     def run_test(self):
482         back_get_data = self.get_data
483         back_get_link = self.get_link
484         back_init = self.initialize
485         self.get_data = self.get_data_test
486         self.get_link = self.get_link_test
487         self.initialize = self.intialize_test
488         self.run()
489         self.get_data = back_get_data
490         self.get_link = back_get_link
491         self.initialize = back_init
492
493     def get_data_test(self, table):
494         return [{}]
495
496     def get_link_test(self, from_table, ids, to_table):
497         return {}
498
499     def intialize_test(self):
500         pass
501
502 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: