You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

415 lines
17 KiB

  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. from openerp.tools.translate import _
  24. import datetime
  25. import logging
  26. import StringIO
  27. import traceback
  28. pp = pprint.PrettyPrinter(indent=4)
  29. class import_framework():
  30. """
  31. This class should be extends,
  32. get_data and get_mapping have to extends
  33. get_state_map and initialize can be extended
  34. for advanced purpose get_default_hook can also be extended
  35. @see dummy import for a minimal exemple
  36. """
  37. """
  38. for import_object, this domain will avoid to find an already existing object
  39. """
  40. DO_NOT_FIND_DOMAIN = [('id', '=', 0)]
  41. #TODO don't use context to pass credential parameters
  42. def __init__(self, obj, cr, uid, instance_name, module_name, context=None):
  43. self.external_id_field = 'id'
  44. self.obj = obj
  45. self.cr = cr
  46. self.uid = uid
  47. self.instance_name = instance_name
  48. self.module_name = module_name
  49. self.context = context or {}
  50. self.table_list = []
  51. self.logger = logging.getLogger(module_name)
  52. self.initialize()
  53. """
  54. Abstract Method to be implemented in
  55. the real instance
  56. """
  57. def initialize(self):
  58. """
  59. init before import
  60. usually for the login
  61. """
  62. pass
  63. def init_run(self):
  64. """
  65. call after intialize run in the thread, not in the main process
  66. TO use for long initialization operation
  67. """
  68. pass
  69. def get_data(self, table):
  70. """
  71. @return: a list of dictionaries
  72. each dictionnaries contains the list of pair external_field_name : value
  73. """
  74. return [{}]
  75. def get_link(self, from_table, ids, to_table):
  76. """
  77. @return: a dictionaries that contains the association between the id (from_table)
  78. and the list (to table) of id linked
  79. """
  80. return {}
  81. def get_external_id(self, data):
  82. """
  83. @return the external id
  84. the default implementation return self.external_id_field (that has 'id') by default
  85. if the name of id field is different, you can overwrite this method or change the value
  86. of self.external_id_field
  87. """
  88. return data[self.external_id_field]
  89. def get_mapping(self):
  90. """
  91. @return: { TABLE_NAME : {
  92. 'model' : 'openerp.model.name',
  93. #if true import the table if not just resolve dependencies, use for meta package, by default => True
  94. #Not required
  95. 'import' : True or False,
  96. #Not required
  97. 'dependencies' : [TABLE_1, TABLE_2],
  98. #Not required
  99. 'hook' : self.function_name, #get the val dict of the object, return the same val dict or False
  100. 'map' : { @see mapper
  101. 'openerp_field_name' : 'external_field_name', or val('external_field_name')
  102. 'openerp_field_id/id' : ref(TABLE_1, 'external_id_field'), #make the mapping between the external id and the xml on the right
  103. 'openerp_field2_id/id_parent' : ref(TABLE_1,'external_id_field') #indicate a self dependencies on openerp_field2_id
  104. '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
  105. 'text_field' : concat('field_1', 'field_2', .., delimiter=':'), #concat the value of the list of field in one
  106. 'description_field' : ppconcat('field_1', 'field_2', .., delimiter='\n\t'), #same as above but with a prettier formatting
  107. 'field' : call(callable, arg1, arg2, ..), #call the function with all the value, the function should send the value : self.callable
  108. 'field' : callable
  109. 'field' : call(method, val('external_field') interface of method is self, val where val is the value of the field
  110. 'field' : const(value) #always set this field to value
  111. + any custom mapper that you will define
  112. }
  113. },
  114. }
  115. """
  116. return {}
  117. def default_hook(self, val):
  118. """
  119. this hook will be apply on each table that don't have hook
  120. here we define the identity hook
  121. """
  122. return val
  123. def _import_table(self, table):
  124. self.logger.info('Import table %s' % table)
  125. data = self.get_data(table)
  126. map = self.get_mapping()[table]['map']
  127. hook = self.get_mapping()[table].get('hook', self.default_hook)
  128. model = self.get_mapping()[table]['model']
  129. final_data = []
  130. for val in data:
  131. res = hook(val)
  132. if res:
  133. final_data.append(res)
  134. return self._save_data(model, dict(map), final_data, table)
  135. def _save_data(self, model, mapping, datas, table):
  136. """
  137. @param model: the model of the object to import
  138. @param table : the external table where the data come from
  139. @param mapping : definition of the mapping
  140. @see: get_mapping
  141. @param datas : list of dictionnaries
  142. datas = [data_1, data_2, ..]
  143. data_i is a map external field_name => value
  144. and each data_i have a external id => in data_id['id']
  145. """
  146. self.logger.info(' Importing %s into %s' % (table, model))
  147. if not datas:
  148. return (0, 'No data found')
  149. mapping['id'] = 'id_new'
  150. res = []
  151. self_dependencies = []
  152. for k in mapping.keys():
  153. if '_parent' in k:
  154. self_dependencies.append((k[:-7], mapping.pop(k)))
  155. for data in datas:
  156. for k, field_name in self_dependencies:
  157. data[k] = data.get(field_name) and self._generate_xml_id(data.get(field_name), table)
  158. data['id_new'] = self._generate_xml_id(self.get_external_id(data), table)
  159. fields, values = self._fields_mapp(data, mapping, table)
  160. res.append(values)
  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. self.logger.info(_("fields imported : ") + str(fields))
  165. (p, r, warning, s) = model_obj.import_data(self.cr, self.uid, fields, res, mode='update', current_module=self.module_name, noupdate=False, context=self.context)
  166. self.logger.info('%s %s %s %s %s' % ("Done", p, r, warning, s))
  167. for (field, field_name) in self_dependencies:
  168. self.logger.info('Import parent %s' % field)
  169. self._import_self_dependencies(model_obj, field, datas)
  170. return (len(res), warning)
  171. def _import_self_dependencies(self, obj, parent_field, datas):
  172. """
  173. @param parent_field: the name of the field that generate a self_dependencies, we call the object referenced in this
  174. field the parent of the object
  175. @param datas: a list of dictionnaries
  176. Dictionnaries need to contains
  177. id_new : the xml_id of the object
  178. field_new : the xml_id of the parent
  179. """
  180. fields = ['id', parent_field]
  181. for data in datas:
  182. if data.get(parent_field):
  183. values = [data['id_new'], data[parent_field]]
  184. res = obj.import_data(self.cr, self.uid, fields, [values], mode='update', current_module=self.module_name, noupdate=False, context=self.context)
  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. use to allow syntaxical sugar like 'field': 'external_field'
  191. instead of 'field' : value('external_field')
  192. """
  193. map = dict(mapping)
  194. for key, value in map.items():
  195. if isinstance(value, basestring):
  196. map[key] = mapper.value(value)
  197. #set parent for instance of dbmapper
  198. elif isinstance(value, mapper.dbmapper):
  199. value.set_parent(self)
  200. return map
  201. def _fields_mapp(self,dict_sugar, openerp_dict, table):
  202. """
  203. call all the mapper and transform data
  204. to be compatible with import_data
  205. """
  206. fields=[]
  207. data_lst = []
  208. mapping = self._preprocess_mapping(openerp_dict)
  209. for key,val in mapping.items():
  210. if key not in fields and dict_sugar:
  211. fields.append(key)
  212. value = val(dict(dict_sugar))
  213. data_lst.append(value)
  214. return fields, data_lst
  215. def _generate_xml_id(self, name, table):
  216. """
  217. @param name: name of the object, has to be unique in for a given table
  218. @param table : table where the record we want generate come from
  219. @return: a unique xml id for record, the xml_id will be the same given the same table and same name
  220. To be used to avoid duplication of data that don't have ids
  221. """
  222. sugar_instance = self.instance_name
  223. name = name.replace('.', '_').replace(',', '_')
  224. return sugar_instance + "_" + table + "_" + name
  225. """
  226. Public interface of the framework
  227. those function can be use in the callable function defined in the mapping
  228. """
  229. def xml_id_exist(self, table, external_id):
  230. """
  231. Check if the external id exist in the openerp database
  232. in order to check if the id exist the table where it come from
  233. should be provide
  234. @return the xml_id generated if the external_id exist in the database or false
  235. """
  236. if not external_id:
  237. return False
  238. xml_id = self._generate_xml_id(external_id, table)
  239. id = self.obj.pool.get('ir.model.data').search(self.cr, self.uid, [('name', '=', xml_id), ('module', '=', self.module_name)])
  240. return id and xml_id or False
  241. def name_exist(self, table, name, model):
  242. """
  243. Check if the object with the name exist in the openerp database
  244. in order to check if the id exist the table where it come from
  245. should be provide and the model of the object
  246. """
  247. fields = ['name']
  248. data = [name]
  249. return self.import_object(fields, data, model, table, name, [('name', '=', name)])
  250. def get_mapped_id(self, table, external_id, context=None):
  251. """
  252. @return return the databse id linked with the external_id
  253. """
  254. if not external_id:
  255. return False
  256. xml_id = self._generate_xml_id(external_id, table)
  257. return self.obj.pool.get('ir.model.data').get_object_reference(self.cr, self.uid, self.module_name, xml_id)[1]
  258. def import_object_mapping(self, mapping, data, model, table, name, domain_search=False):
  259. """
  260. same as import_objects but instead of two list fields and data,
  261. this method take a dictionnaries : external_field : value
  262. and the mapping similar to the one define in 'map' key
  263. @see import_object, get_mapping
  264. """
  265. fields, datas = self._fields_mapp(data, mapping, table)
  266. return self.import_object(fields, datas, model, table, name, domain_search)
  267. def import_object(self, fields, data, model, table, name, domain_search=False):
  268. """
  269. 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
  270. use import_data that will take care to create/update or do nothing with the data
  271. this method return the xml_id
  272. To be use, when you want to create an object or link if already exist
  273. use DO_NOT_LINK_DOMAIN to create always a new object
  274. @param fields: list of fields needed to create the object without id
  275. @param data: the list of the data, in the same order as the field
  276. ex : fields = ['firstname', 'lastname'] ; data = ['John', 'Mc donalds']
  277. @param model: the openerp's model of the create/update object
  278. @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
  279. @param unique_name: the name of the object that we want to create/update get the id
  280. @param domain_search : the domain that should find the unique existing record
  281. @return: the xml_id of the ressources
  282. """
  283. domain_search = not domain_search and [('name', 'ilike', name)] or domain_search
  284. obj = self.obj.pool.get(model)
  285. if not obj: #if the model doesn't exist
  286. return False
  287. xml_id = self._generate_xml_id(name, table)
  288. xml_ref = self.mapped_id_if_exist(model, domain_search, table, name)
  289. fields.append('id')
  290. data.append(xml_id)
  291. obj.import_data(self.cr, self.uid, fields, [data], mode='update', current_module=self.module_name, noupdate=True, context=self.context)
  292. return xml_ref or xml_id
  293. def mapped_id_if_exist(self, model, domain, table, name):
  294. """
  295. To be use when we want link with and existing object, if the object don't exist
  296. just ignore.
  297. @param domain : search domain to find existing record, should return a unique record
  298. @param xml_id: xml_id give to the mapping
  299. @param name: external_id or name of the object to create if there is no id
  300. @param table: the name of the table of the object to map
  301. @return : the xml_id if the record exist in the db, False otherwise
  302. """
  303. obj = self.obj.pool.get(model)
  304. ids = obj.search(self.cr, self.uid, domain, context=self.context)
  305. if ids:
  306. xml_id = self._generate_xml_id(name, table)
  307. ir_model_data_obj = obj.pool.get('ir.model.data')
  308. id = ir_model_data_obj._update(self.cr, self.uid, model,
  309. self.module_name, {}, mode='update', xml_id=xml_id,
  310. noupdate=True, res_id=ids[0], context=self.context)
  311. return xml_id
  312. return False
  313. def set_table_list(self, table_list):
  314. """
  315. Set the list of table to import, this method should be call before run
  316. @param table_list: the list of external table to import
  317. ['Leads', 'Opportunity']
  318. """
  319. self.table_list = table_list
  320. def launch_import(self):
  321. """
  322. Import all data into openerp,
  323. this is the Entry point to launch the process of import
  324. """
  325. self.data_started = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
  326. error = False
  327. result = []
  328. try:
  329. self.init_run()
  330. imported = set() #to invoid importing 2 times the sames modules
  331. for table in self.table_list:
  332. to_import = self.get_mapping()[table].get('import', True)
  333. if not table in imported:
  334. res = self._resolve_dependencies(self.get_mapping()[table].get('dependencies', []), imported)
  335. result.extend(res)
  336. if to_import:
  337. (position, warning) = self._import_table(table)
  338. result.append((table, position, warning))
  339. imported.add(table)
  340. self.cr.commit()
  341. except Exception, err:
  342. sh = StringIO.StringIO()
  343. traceback.print_exc(file=sh)
  344. error = sh.getvalue()
  345. self.logger.error(error)
  346. self.date_ended = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
  347. def _resolve_dependencies(self, dep, imported):
  348. """
  349. import dependencies recursively
  350. and avoid to import twice the same table
  351. """
  352. result = []
  353. for dependency in dep:
  354. if not dependency in imported:
  355. to_import = self.get_mapping()[dependency].get('import', True)
  356. res = self._resolve_dependencies(self.get_mapping()[dependency].get('dependencies', []), imported)
  357. result.extend(res)
  358. if to_import:
  359. r = self._import_table(dependency)
  360. (position, warning) = r
  361. result.append((dependency, position, warning))
  362. imported.add(dependency)
  363. return result
  364. # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: