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