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)
33 _logger = logging.getLogger(__name__)
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
66 Abstract Method to be implemented in
78 call after intialize run in the thread, not in the main process
79 TO use for long initialization operation
83 def get_data(self, table):
85 @return: a list of dictionaries
86 each dictionnaries contains the list of pair external_field_name : value
90 def get_link(self, from_table, ids, to_table):
92 @return: a dictionaries that contains the association between the id (from_table)
93 and the list (to table) of id linked
97 def get_external_id(self, data):
99 @return the external id
100 the default implementation return self.external_id_field (that has 'id') by default
101 if the name of id field is different, you can overwrite this method or change the value
102 of self.external_id_field
104 return data[self.external_id_field]
106 def get_mapping(self):
108 @return: { TABLE_NAME : {
109 'model' : 'openerp.model.name',
110 #if true import the table if not just resolve dependencies, use for meta package, by default => True
112 'import' : True or False,
114 'dependencies' : [TABLE_1, TABLE_2],
116 'hook' : self.function_name, #get the val dict of the object, return the same val dict or False
117 'map' : { @see mapper
118 'openerp_field_name' : 'external_field_name', or val('external_field_name')
119 'openerp_field_id/id' : ref(TABLE_1, 'external_id_field'), #make the mapping between the external id and the xml on the right
120 'openerp_field2_id/id_parent' : ref(TABLE_1,'external_id_field') #indicate a self dependencies on openerp_field2_id
121 '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
122 'text_field' : concat('field_1', 'field_2', .., delimiter=':'), #concat the value of the list of field in one
123 'description_field' : ppconcat('field_1', 'field_2', .., delimiter='\n\t'), #same as above but with a prettier formatting
124 'field' : call(callable, arg1, arg2, ..), #call the function with all the value, the function should send the value : self.callable
126 'field' : call(method, val('external_field') interface of method is self, val where val is the value of the field
127 'field' : const(value) #always set this field to value
128 + any custom mapper that you will define
136 def default_hook(self, val):
138 this hook will be apply on each table that don't have hook
139 here we define the identity hook
143 def _import_table(self, table):
144 data = self.get_data(table)
145 map = self.get_mapping()[table]['map']
146 hook = self.get_mapping()[table].get('hook', self.default_hook)
147 model = self.get_mapping()[table]['model']
153 final_data.append(res)
154 return self._save_data(model, dict(map), final_data, table)
156 def _save_data(self, model, mapping, datas, table):
158 @param model: the model of the object to import
159 @param table : the external table where the data come from
160 @param mapping : definition of the mapping
162 @param datas : list of dictionnaries
163 datas = [data_1, data_2, ..]
164 data_i is a map external field_name => value
165 and each data_i have a external id => in data_id['id']
167 _logger.info(' Importing %s into %s' % (table, model))
169 return (0, 'No data found')
170 mapping['id'] = 'id_new'
174 self_dependencies = []
175 for k in mapping.keys():
177 self_dependencies.append((k[:-7], mapping.pop(k)))
180 for k, field_name in self_dependencies:
181 data[k] = data.get(field_name) and self._generate_xml_id(data.get(field_name), table)
183 data['id_new'] = self._generate_xml_id(self.get_external_id(data), table)
184 fields, values = self._fields_mapp(data, mapping, table)
187 model_obj = self.obj.pool.get(model)
189 raise ValueError(_("%s is not a valid model name.") % model)
190 _logger.debug(_(" fields imported : ") + str(fields))
191 (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)
192 for (field, field_name) in self_dependencies:
193 self._import_self_dependencies(model_obj, field, datas)
194 return (len(res), warning)
196 def _import_self_dependencies(self, obj, parent_field, datas):
198 @param parent_field: the name of the field that generate a self_dependencies, we call the object referenced in this
199 field the parent of the object
200 @param datas: a list of dictionnaries
201 Dictionnaries need to contains
202 id_new : the xml_id of the object
203 field_new : the xml_id of the parent
205 fields = ['id', parent_field]
207 if data.get(parent_field):
208 values = [data['id_new'], data[parent_field]]
209 obj.import_data(self.cr, self.uid, fields, [values], mode='update', current_module=self.module_name, noupdate=True, context=self.context)
211 def _preprocess_mapping(self, mapping):
213 Preprocess the mapping :
214 after the preprocces, everything is
215 callable in the val of the dictionary
217 use to allow syntaxical sugar like 'field': 'external_field'
218 instead of 'field' : value('external_field')
221 for key, value in map.items():
222 if isinstance(value, basestring):
223 map[key] = mapper.value(value)
224 #set parent for instance of dbmapper
225 elif isinstance(value, mapper.dbmapper):
226 value.set_parent(self)
230 def _fields_mapp(self,dict_sugar, openerp_dict, table):
232 call all the mapper and transform data
233 to be compatible with import_data
237 mapping = self._preprocess_mapping(openerp_dict)
238 for key,val in mapping.items():
239 if key not in fields and dict_sugar:
241 value = val(dict(dict_sugar))
242 data_lst.append(value)
243 return fields, data_lst
245 def _generate_xml_id(self, name, table):
247 @param name: name of the object, has to be unique in for a given table
248 @param table : table where the record we want generate come from
249 @return: a unique xml id for record, the xml_id will be the same given the same table and same name
250 To be used to avoid duplication of data that don't have ids
252 sugar_instance = self.instance_name
253 name = name.replace('.', '_').replace(',', '_')
254 return sugar_instance + "_" + table + "_" + name
258 Public interface of the framework
259 those function can be use in the callable function defined in the mapping
261 def xml_id_exist(self, table, external_id):
263 Check if the external id exist in the openerp database
264 in order to check if the id exist the table where it come from
266 @return the xml_id generated if the external_id exist in the database or false
271 xml_id = self._generate_xml_id(external_id, table)
272 id = self.obj.pool.get('ir.model.data').search(self.cr, self.uid, [('name', '=', xml_id), ('module', '=', self.module_name)])
273 return id and xml_id or False
275 def name_exist(self, table, name, model):
277 Check if the object with the name exist in the openerp database
278 in order to check if the id exist the table where it come from
279 should be provide and the model of the object
283 return self.import_object(fields, data, model, table, name, [('name', '=', name)])
285 def get_mapped_id(self, table, external_id, context=None):
287 @return return the databse id linked with the external_id
292 xml_id = self._generate_xml_id(external_id, table)
293 return self.obj.pool.get('ir.model.data').get_object_reference(self.cr, self.uid, self.module_name, xml_id)[1]
295 def import_object_mapping(self, mapping, data, model, table, name, domain_search=False):
297 same as import_objects but instead of two list fields and data,
298 this method take a dictionnaries : external_field : value
299 and the mapping similar to the one define in 'map' key
300 @see import_object, get_mapping
302 fields, datas = self._fields_mapp(data, mapping, table)
303 return self.import_object(fields, datas, model, table, name, domain_search)
305 def import_object(self, fields, data, model, table, name, domain_search=False):
307 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
308 use import_data that will take care to create/update or do nothing with the data
309 this method return the xml_id
311 To be use, when you want to create an object or link if already exist
312 use DO_NOT_LINK_DOMAIN to create always a new object
313 @param fields: list of fields needed to create the object without id
314 @param data: the list of the data, in the same order as the field
315 ex : fields = ['firstname', 'lastname'] ; data = ['John', 'Mc donalds']
316 @param model: the openerp's model of the create/update object
317 @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
318 @param unique_name: the name of the object that we want to create/update get the id
319 @param domain_search : the domain that should find the unique existing record
321 @return: the xml_id of the ressources
323 domain_search = not domain_search and [('name', 'ilike', name)] or domain_search
324 obj = self.obj.pool.get(model)
325 if not obj: #if the model doesn't exist
328 xml_id = self._generate_xml_id(name, table)
329 xml_ref = self.mapped_id_if_exist(model, domain_search, table, name)
332 obj.import_data(self.cr, self.uid, fields, [data], mode='update', current_module=self.module_name, noupdate=True, context=self.context)
333 return xml_ref or xml_id
336 def mapped_id_if_exist(self, model, domain, table, name):
338 To be use when we want link with and existing object, if the object don't exist
340 @param domain : search domain to find existing record, should return a unique record
341 @param xml_id: xml_id give to the mapping
342 @param name: external_id or name of the object to create if there is no id
343 @param table: the name of the table of the object to map
344 @return : the xml_id if the record exist in the db, False otherwise
346 obj = self.obj.pool.get(model)
347 ids = obj.search(self.cr, self.uid, domain, context=self.context)
349 xml_id = self._generate_xml_id(name, table)
350 ir_model_data_obj = obj.pool.get('ir.model.data')
351 id = ir_model_data_obj._update(self.cr, self.uid, model,
352 self.module_name, {}, mode='update', xml_id=xml_id,
353 noupdate=True, res_id=ids[0], context=self.context)
358 def set_table_list(self, table_list):
360 Set the list of table to import, this method should be call before run
361 @param table_list: the list of external table to import
362 ['Leads', 'Opportunity']
364 self.table_list = table_list
368 Import all data into openerp,
369 this is the Entry point to launch the process of import
373 self.data_started = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
374 self.cr = pooler.get_db(self.cr.dbname).cursor()
379 imported = set() #to invoid importing 2 times the sames modules
380 for table in self.table_list:
381 to_import = self.get_mapping()[table].get('import', True)
382 if not table in imported:
383 res = self._resolve_dependencies(self.get_mapping()[table].get('dependencies', []), imported)
386 (position, warning) = self._import_table(table)
387 result.append((table, position, warning))
391 except Exception, err:
392 sh = StringIO.StringIO()
393 traceback.print_exc(file=sh)
394 error = sh.getvalue()
398 self.date_ended = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
399 self._send_notification_email(result, error)
403 def _resolve_dependencies(self, dep, imported):
405 import dependencies recursively
406 and avoid to import twice the same table
409 for dependency in dep:
410 if not dependency in imported:
411 to_import = self.get_mapping()[dependency].get('import', True)
412 res = self._resolve_dependencies(self.get_mapping()[dependency].get('dependencies', []), imported)
415 r = self._import_table(dependency)
416 (position, warning) = r
417 result.append((dependency, position, warning))
418 imported.add(dependency)
421 def _send_notification_email(self, result, error):
424 email_obj = self.obj.pool.get('mail.message')
425 email_id = email_obj.create(self.cr, self.uid, {
426 'email_from' : 'import@module.openerp',
427 'email_to' : self.email,
428 'body_text' : self.get_email_body(result, error),
429 'subject' : self.get_email_subject(result, error),
430 'auto_delete' : True})
431 email_obj.send(self.cr, self.uid, [email_id])
433 _logger.error(_("Import failed due to an unexpected error"))
435 _logger.info(_("Import finished, notification email sended"))
437 def get_email_subject(self, result, error=False):
439 This method define the subject of the email send by openerp at the end of import
440 @param result: a list of tuple
441 (table_name, number_of_record_created/updated, warning) for each table
442 @return the subject of the mail
446 return _("Data Import failed at %s due to an unexpected error") % self.date_ended
447 return _("Import of your data finished at %s") % self.date_ended
449 def get_email_body(self, result, error=False):
451 This method define the body of the email send by openerp at the end of import. The body is separated in two part
452 the header (@see get_body_header), and the generate body with the list of table and number of record imported.
453 If you want to keep this it's better to overwrite get_body_header
454 @param result: a list of tuple
455 (table_name, number_of_record_created/updated, warning) for each table
456 @return the subject of the mail
460 body = _("started at %s and finished at %s \n") % (self.data_started, self.date_ended)
462 body += _("but failed, in consequence no data were imported to keep database consistency \n error : \n") + error
464 for (table, nb, warning) in result:
466 warning = _("with no warning")
468 warning = _("with warning : %s") % warning
469 body += _("%s has been successfully imported from %s %s, %s \n") % (nb, self.instance_name, table, warning)
470 return self.get_body_header(result) + "\n\n" + body
472 def get_body_header(self, result):
474 @return the first sentences written in the mail's body
476 return _("The import of data \n instance name : %s \n") % self.instance_name
479 #TODO documentation test
481 back_get_data = self.get_data
482 back_get_link = self.get_link
483 back_init = self.initialize
484 self.get_data = self.get_data_test
485 self.get_link = self.get_link_test
486 self.initialize = self.intialize_test
488 self.get_data = back_get_data
489 self.get_link = back_get_link
490 self.initialize = back_init
492 def get_data_test(self, table):
495 def get_link_test(self, from_table, ids, to_table):
498 def intialize_test(self):
501 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: