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.

232 lines
7.6 KiB

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