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 ##############################################################################
27 from threading import Thread
30 pp = pprint.PrettyPrinter(indent=4)
35 class import_framework(Thread):
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
45 for import_object, this domain will avoid to find an already existing object
47 DO_NOT_FIND_DOMAIN = [('id', '=', 0)]
49 def __init__(self, obj, cr, uid, instance_name, module_name, email_to_notify=False, context=None):
54 self.instance_name = instance_name
55 self.module_name = module_name
56 self.context = context or {}
57 self.email = email_to_notify
64 Abstract Method to be implemented in
74 def get_data(self, table):
76 @return: a list of dictionaries
77 each dictionnaries contains the list of pair external_field_name : value
81 def get_mapping(self):
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
87 'import' : True or False,
89 'dependencies' : [TABLE_1, TABLE_2],
91 'hook' : self.function_name, #get the val dict of the object, return the same val dict or False
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
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
111 def default_hook(self, val):
113 this hook will be apply on each table that don't have hook
114 here we define the identity hook
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']
128 final_data.append(res)
129 return self._save_data(model, dict(map), final_data, table)
131 def _save_data(self, model, mapping, datas, table):
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
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']
143 return (0, 'No data in this table')
144 mapping['id'] = 'id_new'
148 self_dependencies = []
149 for k in mapping.keys():
151 self_dependencies.append((k[:-7], mapping.pop(k)))
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)
157 data['id_new'] = self._generate_xml_id(data['id'], table)
158 fields, values = self._fields_mapp(data, mapping, table)
161 model_obj = self.obj.pool.get(model)
163 raise ValueError("%s is not a valid model name" % model)
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)
170 def _import_self_dependencies(self, obj, parent_field, datas):
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
179 fields = ['id', parent_field]
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)
185 def _preprocess_mapping(self, mapping):
187 Preprocess the mapping :
188 after the preprocces, everything is
189 callable in the val of the dictionary
191 use to allow syntaxical sugar like 'field': 'external_field'
192 instead of 'field' : value('external_field')
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)
204 def _fields_mapp(self,dict_sugar, openerp_dict, table):
206 call all the mapper and transform data
207 to be compatible with import_data
211 mapping = self._preprocess_mapping(openerp_dict)
212 for key,val in mapping.items():
213 if key not in fields and dict_sugar:
215 value = val(dict(dict_sugar))
216 data_lst.append(value)
217 return fields, data_lst
219 def _generate_xml_id(self, name, table):
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
226 sugar_instance = self.instance_name
227 name = name.replace('.', '_').replace(',', '_')
228 return sugar_instance + "_" + table + "_" + name
232 Public interface of the framework
233 those function can be use in the callable function defined in the mapping
235 def xml_id_exist(self, table, external_id):
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
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
248 def name_exist(self, table, name, model):
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
256 return self.import_object(fields, data, model, table, name, [('name', '=', name)])
258 def get_mapped_id(self, table, external_id, context=None):
260 @return return the databse id linked with the external_id
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]
268 def import_object_mapping(self, mapping, data, model, table, name, domain_search=False):
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
275 fields, datas = self._fields_mapp(data, mapping, table)
276 return self.import_object(fields, datas, model, table, name, domain_search)
278 def import_object(self, fields, data, model, table, name, domain_search=False):
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
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
294 @return: the xml_id of the ressources
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)
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
306 def mapped_id_if_exist(self, model, domain, table, name):
308 To be use when we want link with and existing object, if the object don't exist
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
316 obj = self.obj.pool.get(model)
317 ids = obj.search(self.cr, self.uid, domain, context=self.context)
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)
328 def set_table_list(self, table_list):
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']
334 self.table_list = table_list
338 Import all data into openerp,
339 this is the Entry point to launch the process of import
343 self.data_started = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
344 self.cr = pooler.get_db(self.cr.dbname).cursor()
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)
354 (position, warning) = self._import_table(table)
355 result.append((table, position, warning))
361 self.date_ended = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
362 self._send_notification_email(result)
365 def _resolve_dependencies(self, dep, imported):
367 import dependencies recursively
368 and avoid to import twice the same table
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)
377 r = self._import_table(dependency)
378 (position, warning) = r
379 result.append((dependency, position, warning))
380 imported.add(dependency)
383 def _send_notification_email(self, result):
387 'import_sugarcrm@module.openerp',
389 self.get_email_subject(result),
390 self.get_email_body(result),
392 logger = logging.getLogger('import_sugarcam')
393 logger.info("Import finished, notification email sended")
395 def get_email_subject(self, result):
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
403 return "Import of your data finished at %s" % self.date_ended
405 def get_email_body(self, result):
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
416 body = "started at %s and finished at %s \n" % (self.data_started, self.date_ended)
417 for (table, nb, warning) in result:
419 warning = "with no warning"
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
425 def get_body_header(self, result):
427 @return the first sentences written in the mail's body
429 return "The import of data \n instance name : %s \n" % self.instance_name
431 #For example of use see import_sugarcrm