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.

203 lines
6.8 KiB

  1. # -*- coding: utf-8 -*-
  2. # © 2015 Antiun Ingeniería S.L. - Antonio Espinosa
  3. # © 2015 Antiun Ingeniería S.L. - Jairo Llopis
  4. # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
  5. from openerp import models, api, _
  6. from openerp.exceptions import Warning as UserError
  7. import requests
  8. import re
  9. import logging
  10. from lxml import etree
  11. from collections import OrderedDict
  12. logger = logging.getLogger(__name__)
  13. class NutsImport(models.TransientModel):
  14. _name = 'nuts.import'
  15. _description = 'Import NUTS items from European RAMON service'
  16. _parents = [False, False, False, False]
  17. _countries = {
  18. "BE": False,
  19. "BG": False,
  20. "CZ": False,
  21. "DK": False,
  22. "DE": False,
  23. "EE": False,
  24. "IE": False,
  25. "GR": False, # EL
  26. "ES": False,
  27. "FR": False,
  28. "HR": False,
  29. "IT": False,
  30. "CY": False,
  31. "LV": False,
  32. "LT": False,
  33. "LU": False,
  34. "HU": False,
  35. "MT": False,
  36. "NL": False,
  37. "AT": False,
  38. "PL": False,
  39. "PT": False,
  40. "RO": False,
  41. "SI": False,
  42. "SK": False,
  43. "FI": False,
  44. "SE": False,
  45. "GB": False, # UK
  46. }
  47. _current_country = False
  48. _map = OrderedDict([
  49. ('level', {
  50. 'xpath': '', 'attrib': 'idLevel',
  51. 'type': 'integer', 'required': True}),
  52. ('code', {
  53. 'xpath': './Label/LabelText[@language="ALL"]',
  54. 'type': 'string', 'required': True}),
  55. ('name', {
  56. 'xpath': './Label/LabelText[@language="EN"]',
  57. 'type': 'string', 'required': True}),
  58. ])
  59. def _check_node(self, node):
  60. if node.get('id') and node.get('idLevel'):
  61. return True
  62. return False
  63. def _mapping(self, node):
  64. item = {}
  65. for k, v in self._map.iteritems():
  66. field_xpath = v.get('xpath', '')
  67. field_attrib = v.get('attrib', False)
  68. field_type = v.get('type', 'string')
  69. field_required = v.get('required', False)
  70. value = ''
  71. if field_xpath:
  72. n = node.find(field_xpath)
  73. else:
  74. n = node
  75. if n is not None:
  76. if field_attrib:
  77. value = n.get(field_attrib, '')
  78. else:
  79. value = n.text
  80. if field_type == 'integer':
  81. try:
  82. value = int(value)
  83. except:
  84. value = 0
  85. else:
  86. logger.debug("xpath = '%s', not found" % field_xpath)
  87. if field_required and not value:
  88. raise UserError(
  89. _('Value not found for mandatory field %s' % k))
  90. item[k] = value
  91. return item
  92. def _download_nuts(self):
  93. url_base = 'http://ec.europa.eu'
  94. url_path = '/eurostat/ramon/nomenclatures/index.cfm'
  95. url_params = {
  96. 'TargetUrl': 'ACT_OTH_CLS_DLD',
  97. 'StrNom': 'NUTS_2013',
  98. 'StrFormat': 'XML',
  99. 'StrLanguageCode': 'EN',
  100. 'StrLayoutCode': 'HIERARCHIC'
  101. }
  102. url = url_base + url_path + '?'
  103. url += '&'.join([k + '=' + v for k, v in url_params.iteritems()])
  104. logger.info('Starting to download %s' % url)
  105. try:
  106. res_request = requests.get(url)
  107. except Exception, e:
  108. raise UserError(
  109. _('Got an error when trying to download the file: %s.') %
  110. str(e))
  111. if res_request.status_code != requests.codes.ok:
  112. raise UserError(
  113. _('Got an error %d when trying to download the file %s.')
  114. % (res_request.status_code, url))
  115. logger.info('Download successfully %d bytes' %
  116. len(res_request.content))
  117. # Workaround XML: Remove all characters before <?xml
  118. pattern = re.compile(r'^.*<\?xml', re.DOTALL)
  119. content_fixed = re.sub(pattern, '<?xml', res_request.content)
  120. if not re.match(r'<\?xml', content_fixed):
  121. raise UserError(_('Downloaded file is not a valid XML file'))
  122. return content_fixed
  123. @api.model
  124. def _load_countries(self):
  125. for k in self._countries.keys():
  126. self._countries[k] = self.env['res.country'].search(
  127. [('code', '=', k)])
  128. # Workaround to translate some country codes:
  129. # EL => GR (Greece)
  130. # UK => GB (United Kingdom)
  131. self._countries['EL'] = self._countries['GR']
  132. self._countries['UK'] = self._countries['GB']
  133. @api.model
  134. def state_mapping(self, data, node):
  135. # Method to inherit and add state_id relation depending on country
  136. level = data.get('level', 0)
  137. code = data.get('code', '')
  138. if level == 1:
  139. self._current_country = self._countries[code]
  140. return {
  141. 'country_id': self._current_country.id,
  142. }
  143. @api.model
  144. def create_or_update_nuts(self, node):
  145. if not self._check_node(node):
  146. return False
  147. nuts_model = self.env['res.partner.nuts']
  148. data = self._mapping(node)
  149. data.update(self.state_mapping(data, node))
  150. level = data.get('level', 0)
  151. if level >= 2 and level <= 5:
  152. data['parent_id'] = self._parents[level - 2]
  153. nuts = nuts_model.search([('level', '=', data['level']),
  154. ('code', '=', data['code'])])
  155. if nuts:
  156. nuts.write(data)
  157. else:
  158. nuts = nuts_model.create(data)
  159. if level >= 1 and level <= 4:
  160. self._parents[level - 1] = nuts.id
  161. return nuts
  162. @api.one
  163. def run_import(self):
  164. nuts_model = self.env['res.partner.nuts'].\
  165. with_context(defer_parent_store_computation=True)
  166. self._load_countries()
  167. # All current NUTS (for available countries),
  168. # delete if not found above
  169. nuts_to_delete = nuts_model.search(
  170. [('country_id', 'in', [x.id for x in self._countries.values()])])
  171. # Download NUTS in english, create or update
  172. logger.info('Importing NUTS 2013 English')
  173. xmlcontent = self._download_nuts()
  174. dom = etree.fromstring(xmlcontent)
  175. for node in dom.iter('Item'):
  176. logger.debug(
  177. 'Reading level=%s, id=%s',
  178. node.get('idLevel', 'N/A'),
  179. node.get('id', 'N/A'))
  180. nuts = self.create_or_update_nuts(node)
  181. if nuts and nuts in nuts_to_delete:
  182. nuts_to_delete -= nuts
  183. # Delete obsolete NUTS
  184. if nuts_to_delete:
  185. logger.info('%d NUTS entries deleted' % len(nuts_to_delete))
  186. nuts_to_delete.unlink()
  187. logger.info(
  188. 'The wizard to create NUTS entries from RAMON '
  189. 'has been successfully completed.')
  190. return True