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.

297 lines
12 KiB

  1. # -*- coding: utf-8 -*-
  2. # Python source code encoding : https://www.python.org/dev/peps/pep-0263/
  3. ##############################################################################
  4. #
  5. # OpenERP, Open Source Management Solution
  6. # This module copyright :
  7. # (c) 2015 Antiun Ingenieria, SL (Madrid, Spain, http://www.antiun.com)
  8. # Antonio Espinosa <antonioea@antiun.com>
  9. #
  10. # This program is free software: you can redistribute it and/or modify
  11. # it under the terms of the GNU Affero General Public License as
  12. # published by the Free Software Foundation, either version 3 of the
  13. # License, or (at your option) any later version.
  14. #
  15. # This program is distributed in the hope that it will be useful,
  16. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  17. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  18. # GNU Affero General Public License for more details.
  19. #
  20. # You should have received a copy of the GNU Affero General Public License
  21. # along with this program. If not, see <http://www.gnu.org/licenses/>.
  22. #
  23. ##############################################################################
  24. from openerp import models, api, _
  25. from openerp.exceptions import Warning
  26. import requests
  27. import re
  28. import logging
  29. from lxml import etree
  30. from collections import OrderedDict
  31. from pprint import pformat
  32. logger = logging.getLogger(__name__)
  33. class NaceImport(models.TransientModel):
  34. _name = 'nace.import'
  35. _description = 'Import NACE activities from European RAMON service'
  36. _parents = [False, False, False, False]
  37. _available_langs = {
  38. 'bg_BG': 'BG', # Bulgarian
  39. 'cs_CZ': 'CZ', # Czech
  40. 'da_DK': 'DA', # Danish
  41. 'de_DE': 'DE', # German
  42. 'et_EE': 'EE', # Estonian
  43. 'el_GR': 'EL', # Greek
  44. 'es_AR': 'ES', # Spanish (AR)
  45. 'es_BO': 'ES', # Spanish (BO)
  46. 'es_CL': 'ES', # Spanish (CL)
  47. 'es_CO': 'ES', # Spanish (CO)
  48. 'es_CR': 'ES', # Spanish (CR)
  49. 'es_DO': 'ES', # Spanish (DO)
  50. 'es_EC': 'ES', # Spanish (EC)
  51. 'es_GT': 'ES', # Spanish (GT)
  52. 'es_HN': 'ES', # Spanish (HN)
  53. 'es_MX': 'ES', # Spanish (MX)
  54. 'es_NI': 'ES', # Spanish (NI)
  55. 'es_PA': 'ES', # Spanish (PA)
  56. 'es_PE': 'ES', # Spanish (PE)
  57. 'es_PR': 'ES', # Spanish (PR)
  58. 'es_PY': 'ES', # Spanish (PY)
  59. 'es_SV': 'ES', # Spanish (SV)
  60. 'es_UY': 'ES', # Spanish (UY)
  61. 'es_VE': 'ES', # Spanish (VE)
  62. 'es_PY': 'ES', # Spanish (PY)
  63. 'es_ES': 'ES', # Spanish
  64. 'fi_FI': 'FI', # Finnish
  65. 'fr_BE': 'FR', # French (FR)
  66. 'fr_CA': 'FR', # French (CA)
  67. 'fr_CH': 'FR', # French (CH)
  68. 'fr_FR': 'FR', # French
  69. 'hr_HR': 'HR', # Croatian
  70. 'hu_HU': 'HU', # Hungarian
  71. 'it_IT': 'IT', # Italian
  72. 'lt_LT': 'LT', # Lithuanian
  73. 'lv_LV': 'LV', # Latvian
  74. # '': 'MT', # Il-Malti, has no language in Odoo
  75. 'nl_BE': 'NL', # Dutch (BE)
  76. 'nl_NL': 'NL', # Dutch
  77. 'nb_NO': 'NO', # Norwegian Bokmål
  78. 'pl_PL': 'PL', # Polish
  79. 'pt_BR': 'PT', # Portuguese (BR)
  80. 'pt_PT': 'PT', # Portuguese
  81. 'ro_RO': 'RO', # Romanian
  82. 'ru_RU': 'RU', # Russian
  83. 'sl_SI': 'SI', # Slovenian
  84. 'sk_SK': 'SK', # Slovak
  85. 'sv_SE': 'SV', # Swedish
  86. 'tr_TR': 'TR', # Turkish
  87. }
  88. _map = OrderedDict([
  89. ('level', {'xpath': '', 'attrib': 'idLevel', 'type': 'integer',
  90. 'translate': False, 'required': True}),
  91. ('code', {'xpath': '', 'attrib': 'id', 'type': 'string',
  92. 'translate': False, 'required': True}),
  93. ('name', {'xpath': './Label/LabelText', 'type': 'string',
  94. 'translate': True, 'required': True}),
  95. ('generic', {
  96. 'xpath': './Property[@name="Generic"]'
  97. '/PropertyQualifier[@name="Value"]/PropertyText',
  98. 'type': 'string', 'translate': False, 'required': False}),
  99. ('rules', {
  100. 'xpath': './Property[@name="ExplanatoryNote"]'
  101. '/PropertyQualifier[@name="Rules"]/PropertyText',
  102. 'type': 'string', 'translate': False, 'required': False}),
  103. ('central_content', {
  104. 'xpath': './Property[@name="ExplanatoryNote"]'
  105. '/PropertyQualifier[@name="CentralContent"]/PropertyText',
  106. 'type': 'string', 'translate': True, 'required': False}),
  107. ('limit_content', {
  108. 'xpath': './Property[@name="ExplanatoryNote"]'
  109. '/PropertyQualifier[@name="LimitContent"]/PropertyText',
  110. 'type': 'string', 'translate': True, 'required': False}),
  111. ('exclusions', {
  112. 'xpath': './Property[@name="ExplanatoryNote"]'
  113. '/PropertyQualifier[@name="Exclusions"]/PropertyText',
  114. 'type': 'string', 'translate': True, 'required': False}),
  115. ])
  116. def _check_node(self, node):
  117. if node.get('id') and node.get('idLevel'):
  118. return True
  119. return False
  120. def _mapping(self, node, translate):
  121. item = {}
  122. for k, v in self._map.iteritems():
  123. field_translate = v.get('translate', False)
  124. field_xpath = v.get('xpath', '')
  125. field_attrib = v.get('attrib', False)
  126. field_type = v.get('type', 'string')
  127. field_required = v.get('required', False)
  128. if field_translate == translate:
  129. # logger.info("_mapping = '%s', config = '%s', node = '%s'" %
  130. # (k, pformat(v), pformat(node)))
  131. # logger.info("_mapping:children = '%s'" %
  132. # (pformat(node.getchildren())))
  133. # for x in node.getchildren():
  134. # logger.info("_mapping:grandchildren = '%s'" %
  135. # (pformat(x.getchildren())))
  136. # logger.info("_mapping:LabelText = '%s'" %
  137. # (pformat(node.find('./Label/LabelText'))))
  138. value = ''
  139. if field_xpath:
  140. n = node.find(field_xpath)
  141. # logger.info("_mapping:Finding = '%s', result='%s'" %
  142. # (field_xpath, pformat(n)))
  143. else:
  144. n = node
  145. # logger.info("_mapping:Using same node='%s'" %
  146. # (pformat(n)))
  147. if n is not None:
  148. if field_attrib:
  149. value = n.get(field_attrib, '')
  150. else:
  151. value = n.text
  152. if field_type == 'integer':
  153. try:
  154. value = int(value)
  155. except:
  156. value = 0
  157. else:
  158. logger.debug("xpath = '%s', not found" % field_xpath)
  159. if field_required and not value:
  160. raise Warning(
  161. _('Value not found for mandatory field %s' % k))
  162. item[k] = value
  163. return item
  164. def _download_nace(self, lang_code):
  165. url_base = 'http://ec.europa.eu'
  166. url_path = '/eurostat/ramon/nomenclatures/index.cfm'
  167. url_params = {
  168. 'TargetUrl': 'ACT_OTH_CLS_DLD',
  169. 'StrNom': 'NACE_REV2',
  170. 'StrFormat': 'XML',
  171. 'StrLanguageCode': lang_code,
  172. # 'IntKey': '',
  173. # 'IntLevel': '',
  174. # 'TxtDelimiter': ';',
  175. # 'bExport': '',
  176. }
  177. url = url_base + url_path + '?'
  178. url += '&'.join([k + '=' + v for k, v in url_params.iteritems()])
  179. logger.info('Starting to download %s' % url)
  180. try:
  181. res_request = requests.get(url)
  182. except Exception, e:
  183. raise Warning(
  184. _('Got an error when trying to download the file: %s.') %
  185. str(e))
  186. if res_request.status_code != requests.codes.ok:
  187. raise Warning(
  188. _('Got an error %d when trying to download the file %s.')
  189. % (res_request.status_code, url))
  190. logger.info('Download successfully %d bytes' %
  191. len(res_request.content))
  192. # Workaround XML: Remove all characters before <?xml
  193. pattern = re.compile(r'^.*<\?xml', re.DOTALL)
  194. content_fixed = re.sub(pattern, '<?xml', res_request.content)
  195. if not re.match(r'<\?xml', content_fixed):
  196. raise Warning(_('Downloaded file is not a valid XML file'))
  197. return content_fixed
  198. @api.model
  199. def create_or_update_nace(self, node):
  200. if not self._check_node(node):
  201. return False
  202. nace_model = self.env['res.partner.nace']
  203. data = self._mapping(node, False)
  204. data.update(self._mapping(node, True))
  205. level = data.get('level', 0)
  206. if level >= 2 and level <= 5:
  207. data['parent_id'] = self._parents[level - 2]
  208. nace = nace_model.search([('level', '=', data['level']),
  209. ('code', '=', data['code'])])
  210. if nace:
  211. nace.write(data)
  212. else:
  213. nace = nace_model.create(data)
  214. if level >= 1 and level <= 4:
  215. self._parents[level - 1] = nace.id
  216. return nace
  217. @api.model
  218. def translate_nace(self, node, lang):
  219. translation_model = self.env['ir.translation']
  220. nace_model = self.env['res.partner.nace']
  221. index = self._mapping(node, False)
  222. trans = self._mapping(node, True)
  223. nace = nace_model.search([('level', '=', index['level']),
  224. ('code', '=', index['code'])])
  225. if nace:
  226. for field, value in trans.iteritems():
  227. name = 'res.partner.nace,' + field
  228. query = [('res_id', '=', nace.id),
  229. ('type', '=', 'model'),
  230. ('name', '=', name),
  231. ('lang', '=', lang.code)]
  232. translation = translation_model.search(query)
  233. data = {
  234. 'value': value,
  235. 'state': 'translated'
  236. }
  237. if translation:
  238. translation_model.write(data)
  239. else:
  240. data['res_id'] = nace.id
  241. data['type'] = 'model'
  242. data['name'] = name
  243. data['lang'] = lang.code
  244. translation_model.create(data)
  245. @api.one
  246. def run_import(self):
  247. nace_model = self.env['res.partner.nace']
  248. lang_model = self.env['res.lang']
  249. # Available lang list
  250. langs = lang_model.search(
  251. [('code', 'in', self._available_langs.keys()),
  252. ('active', '=', True)])
  253. # All current NACEs, delete if not found above
  254. naces_to_delete = nace_model.search([])
  255. # Download NACEs in english, create or update
  256. logger.info('Import NACE Rev.2 English')
  257. xmlcontent = self._download_nace('EN')
  258. dom = etree.fromstring(xmlcontent)
  259. for node in dom.iter('Item'):
  260. logger.info('Reading level=%s, code=%s' %
  261. (node.get('idLevel', 'N/A'),
  262. node.get('id', 'N/A')))
  263. nace = self.create_or_update_nace(node)
  264. if nace and nace in naces_to_delete:
  265. naces_to_delete -= nace
  266. # Download NACEs in other languages, translate them
  267. for lang in langs:
  268. logger.info('Import NACE Rev.2 %s' % lang.code)
  269. nace_lang = self._available_langs[lang.code]
  270. xmlcontent = self._download_nace(nace_lang)
  271. dom = etree.fromstring(xmlcontent)
  272. for node in dom.iter('Item'):
  273. logger.info('Reading lang=%s, level=%s, code=%s' %
  274. (nace_lang, node.get('idLevel', 'N/A'),
  275. node.get('id', 'N/A')))
  276. self.translate_nace(node, lang)
  277. # Delete obsolete NACEs
  278. if naces_to_delete:
  279. naces_to_delete.unlink()
  280. logger.info('%d NACEs entries deleted' % len(naces_to_delete))
  281. logger.info(
  282. 'The wizard to create NACEs entries from RAMON '
  283. 'has been successfully completed.')
  284. return True