1 # -*- coding: utf-8 -*-
2 ##############################################################################
4 # OpenERP, Open Source Management Solution
5 # Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
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.
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.
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/>.
20 ##############################################################################
25 from tools.translate import _
27 from threading import Thread
32 pp = pprint.PrettyPrinter(indent=4)
37 class import_framework(Thread):
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
47 for import_object, this domain will avoid to find an already existing object
49 DO_NOT_FIND_DOMAIN = [('id', '=', 0)]
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):
54 self.external_id_field = 'id'
58 self.instance_name = instance_name
59 self.module_name = module_name
60 self.context = context or {}
61 self.email = email_to_notify
63 self.logger = logging.getLogger(module_name)
67 Abstract Method to be implemented in
79 call after intialize run in the thread, not in the main process
80 TO use for long initialization operation
84 def get_data(self, table):
86 @return: a list of dictionaries
87 each dictionnaries contains the list of pair external_field_name : value
91 def get_link(self, from_table, ids, to_table):
93 @return: a dictionaries that contains the association between the id (from_table)
94 and the list (to table) of id linked
98 def get_external_id(self, data):
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
105 return data[self.external_id_field]
107 def get_mapping(self):
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
113 'import' : True or False,
115 'dependencies' : [TABLE_1, TABLE_2],
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
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
137 def default_hook(self, val):
139 this hook will be apply on each table that don't have hook
140 here we define the identity hook
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']
154 final_data.append(res)
155 return self._save_data(model, dict(map), final_data, table)
157 def _save_data(self, model, mapping, datas, table):
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
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']
168 self.logger.info(' Importing %s into %s' % (table, model))
170 return (0, 'No data found')
171 mapping['id'] = 'id_new'
175 self_dependencies = []
176 for k in mapping.keys():
178 self_dependencies.append((k[:-7], mapping.pop(k)))
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)
184 data['id_new'] = self._generate_xml_id(self.get_external_id(data), table)
185 fields, values = self._fields_mapp(data, mapping, table)
188 model_obj = self.obj.pool.get(model)
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)
197 def _import_self_dependencies(self, obj, parent_field, datas):
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
206 fields = ['id', parent_field]
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)
212 def _preprocess_mapping(self, mapping):
214 Preprocess the mapping :
215 after the preprocces, everything is
216 callable in the val of the dictionary
218 use to allow syntaxical sugar like 'field': 'external_field'
219 instead of 'field' : value('external_field')
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)
231 def _fields_mapp(self,dict_sugar, openerp_dict, table):
233 call all the mapper and transform data
234 to be compatible with import_data
238 mapping = self._preprocess_mapping(openerp_dict)
239 for key,val in mapping.items():
240 if key not in fields and dict_sugar:
242 value = val(dict(dict_sugar))
243 data_lst.append(value)
244 return fields, data_lst
246 def _generate_xml_id(self, name, table):
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
253 sugar_instance = self.instance_name
254 name = name.replace('.', '_').replace(',', '_')
255 return sugar_instance + "_" + table + "_" + name
259 Public interface of the framework
260 those function can be use in the callable function defined in the mapping
262 def xml_id_exist(self, table, external_id):
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
267 @return the xml_id generated if the external_id exist in the database or false
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
276 def name_exist(self, table, name, model):
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
284 return self.import_object(fields, data, model, table, name, [('name', '=', name)])
286 def get_mapped_id(self, table, external_id, context=None):
288 @return return the databse id linked with the external_id
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]
296 def import_object_mapping(self, mapping, data, model, table, name, domain_search=False):
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
303 fields, datas = self._fields_mapp(data, mapping, table)
304 return self.import_object(fields, datas, model, table, name, domain_search)
306 def import_object(self, fields, data, model, table, name, domain_search=False):
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
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
322 @return: the xml_id of the ressources
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
329 xml_id = self._generate_xml_id(name, table)
330 xml_ref = self.mapped_id_if_exist(model, domain_search, table, name)
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
337 def mapped_id_if_exist(self, model, domain, table, name):
339 To be use when we want link with and existing object, if the object don't exist
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
347 obj = self.obj.pool.get(model)
348 ids = obj.search(self.cr, self.uid, domain, context=self.context)
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)
359 def set_table_list(self, table_list):
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']
365 self.table_list = table_list
369 Import all data into openerp,
370 this is the Entry point to launch the process of import
374 self.data_started = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
375 self.cr = pooler.get_db(self.cr.dbname).cursor()
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)
387 (position, warning) = self._import_table(table)
388 result.append((table, position, warning))
392 except Exception, err:
393 sh = StringIO.StringIO()
394 traceback.print_exc(file=sh)
395 error = sh.getvalue()
399 self.date_ended = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
400 self._send_notification_email(result, error)
404 def _resolve_dependencies(self, dep, imported):
406 import dependencies recursively
407 and avoid to import twice the same table
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)
416 r = self._import_table(dependency)
417 (position, warning) = r
418 result.append((dependency, position, warning))
419 imported.add(dependency)
422 def _send_notification_email(self, result, error):
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])
434 self.logger.error(_("Import failed due to an unexpected error"))
436 self.logger.info(_("Import finished, notification email sended"))
438 def get_email_subject(self, result, error=False):
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
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
450 def get_email_body(self, result, error=False):
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
461 body = _("started at %s and finished at %s \n") % (self.data_started, self.date_ended)
463 body += _("but failed, in consequence no data were imported to keep database consistency \n error : \n") + error
465 for (table, nb, warning) in result:
467 warning = _("with no warning")
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
473 def get_body_header(self, result):
475 @return the first sentences written in the mail's body
477 return _("The import of data \n instance name : %s \n") % self.instance_name
480 #TODO documentation test
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
489 self.get_data = back_get_data
490 self.get_link = back_get_link
491 self.initialize = back_init
493 def get_data_test(self, table):
496 def get_link_test(self, from_table, ids, to_table):
499 def intialize_test(self):
502 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: