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
240 @return the xml_id generated if the external_id exist in the database or false
245 xml_id = self._generate_xml_id(external_id, table)
246 id = self.obj.pool.get('ir.model.data').search(self.cr, self.uid, [('name', '=', xml_id), ('module', '=', self.module_name)])
247 return id and xml_id or False
249 def name_exist(self, table, name, model):
251 Check if the object with the name exist in the openerp database
252 in order to check if the id exist the table where it come from
253 should be provide and the model of the object
257 return self.import_object(fields, data, model, table, name, [('name', '=', name)])
259 def get_mapped_id(self, table, external_id, context=None):
261 @return return the databse id linked with the external_id
266 xml_id = self._generate_xml_id(external_id, table)
267 return self.obj.pool.get('ir.model.data').get_object_reference(self.cr, self.uid, self.module_name, xml_id)[1]
269 def import_object_mapping(self, mapping, data, model, table, name, domain_search=False):
271 same as import_objects but instead of two list fields and data,
272 this method take a dictionnaries : external_field : value
273 and the mapping similar to the one define in 'map' key
274 @see import_object, get_mapping
276 fields, datas = self._fields_mapp(data, mapping, table)
277 return self.import_object(fields, datas, model, table, name, domain_search)
279 def import_object(self, fields, data, model, table, name, domain_search=False):
281 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
282 use import_data that will take care to create/update or do nothing with the data
283 this method return the xml_id
285 To be use, when you want to create an object or link if already exist
286 use DO_NOT_LINK_DOMAIN to create always a new object
287 @param fields: list of fields needed to create the object without id
288 @param data: the list of the data, in the same order as the field
289 ex : fields = ['firstname', 'lastname'] ; data = ['John', 'Mc donalds']
290 @param model: the openerp's model of the create/update object
291 @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
292 @param unique_name: the name of the object that we want to create/update get the id
293 @param domain_search : the domain that should find the unique existing record
295 @return: the xml_id of the ressources
297 domain_search = not domain_search and [('name', 'ilike', name)] or domain_search
298 obj = self.obj.pool.get(model)
299 xml_id = self._generate_xml_id(name, table)
300 xml_ref = self.mapped_id_if_exist(model, domain_search, table, name)
303 obj.import_data(self.cr, self.uid, fields, [data], mode='update', current_module=self.module_name, noupdate=True, context=self.context)
304 return xml_ref or xml_id
307 def mapped_id_if_exist(self, model, domain, table, name):
309 To be use when we want link with and existing object, if the object don't exist
311 @param domain : search domain to find existing record, should return a unique record
312 @param xml_id: xml_id give to the mapping
313 @param name: external_id or name of the object to create if there is no id
314 @param table: the name of the table of the object to map
315 @return : the xml_id if the record exist in the db, False otherwise
317 obj = self.obj.pool.get(model)
318 ids = obj.search(self.cr, self.uid, domain, context=self.context)
320 xml_id = self._generate_xml_id(name, table)
321 ir_model_data_obj = obj.pool.get('ir.model.data')
322 id = ir_model_data_obj._update(self.cr, self.uid, model,
323 self.module_name, {}, mode='update', xml_id=xml_id,
324 noupdate=True, res_id=ids[0], context=self.context)
329 def set_table_list(self, table_list):
331 Set the list of table to import, this method should be call before run
332 @param table_list: the list of external table to import
333 ['Leads', 'Opportunity']
335 self.table_list = table_list
339 Import all data into openerp,
340 this is the Entry point to launch the process of import
344 self.data_started = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
345 self.cr = pooler.get_db(self.cr.dbname).cursor()
348 imported = set() #to invoid importing 2 times the sames modules
349 for table in self.table_list:
350 to_import = self.get_mapping()[table].get('import', True)
351 if not table in imported:
352 res = self._resolve_dependencies(self.get_mapping()[table].get('dependencies', []), imported)
355 (position, warning) = self._import_table(table)
356 result.append((table, position, warning))
362 self.date_ended = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
363 self._send_notification_email(result)
366 def _resolve_dependencies(self, dep, imported):
368 import dependencies recursively
369 and avoid to import twice the same table
372 for dependency in dep:
373 if not dependency in imported:
374 to_import = self.get_mapping()[dependency].get('import', True)
375 res = self._resolve_dependencies(self.get_mapping()[dependency].get('dependencies', []), imported)
378 r = self._import_table(dependency)
379 (position, warning) = r
380 result.append((dependency, position, warning))
381 imported.add(dependency)
384 def _send_notification_email(self, result):
388 'import_sugarcrm@module.openerp',
390 self.get_email_subject(result),
391 self.get_email_body(result),
393 logger = logging.getLogger('import_sugarcam')
394 logger.info("Import finished, notification email sended")
396 def get_email_subject(self, result):
398 This method define the subject of the email send by openerp at the end of import
399 @param result: a list of tuple
400 (table_name, number_of_record_created/updated, warning) for each table
401 @return the subject of the mail
404 return "Import of your data finished at %s" % self.date_ended
406 def get_email_body(self, result):
408 This method define the body of the email send by openerp at the end of import. The body is separated in two part
409 the header (@see get_body_header), and the generate body with the list of table and number of record imported.
410 If you want to keep this it's better to overwrite get_body_header
411 @param result: a list of tuple
412 (table_name, number_of_record_created/updated, warning) for each table
413 @return the subject of the mail
417 body = "started at %s and finished at %s \n" % (self.data_started, self.date_ended)
418 for (table, nb, warning) in result:
420 warning = "with no warning"
422 warning = "with warning : %s" % warning
423 body += "%s records were imported from table %s, %s \n" % (nb, table, warning)
424 return self.get_body_header(result) + "\n\n" + body
426 def get_body_header(self, result):
428 @return the first sentences written in the mail's body
430 return "The import of data \n instance name : %s \n" % self.instance_name
432 #For example of use see import_sugarcrm