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.

213 lines
7.3 KiB

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