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.

262 lines
9.4 KiB

  1. # Copyright 2014-2016 Akretion (Alexis de Lattre
  2. # <alexis.delattre@akretion.com>)
  3. # Copyright 2014 Lorenzo Battistini <lorenzo.battistini@agilebg.com>
  4. # Copyright 2017 Eficent Business and IT Consulting Services, S.L.
  5. # <contact@eficent.com>
  6. # Copyright 2018 Aitor Bouzas <aitor.bouzas@adaptivecity.com>
  7. # Copyright 2016-2019 Tecnativa - Pedro M. Baeza
  8. # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
  9. import csv
  10. import io
  11. import logging
  12. import os
  13. import tempfile
  14. import zipfile
  15. import requests
  16. from odoo import _, api, fields, models
  17. from odoo.exceptions import UserError
  18. logger = logging.getLogger(__name__)
  19. class CityZipGeonamesImport(models.TransientModel):
  20. _name = "city.zip.geonames.import"
  21. _description = "Import City Zips from Geonames"
  22. _rec_name = "country_id"
  23. country_id = fields.Many2one("res.country", "Country", required=True)
  24. code_row_index = fields.Integer(
  25. related="country_id.geonames_state_code_column", readonly=True
  26. )
  27. name_row_index = fields.Integer(related="country_id.geonames_state_name_column")
  28. letter_case = fields.Selection(
  29. [("unchanged", "Unchanged"), ("title", "Title Case"), ("upper", "Upper Case")],
  30. string="Letter Case",
  31. default="unchanged",
  32. help="Converts retreived city and state names to Title Case "
  33. "(upper case on each first letter of a word) or Upper Case "
  34. "(all letters upper case).",
  35. )
  36. @api.model
  37. def transform_city_name(self, city, country):
  38. """Override it for transforming city name (if needed)
  39. :param city: Original city name
  40. :param country: Country record
  41. :return: Transformed city name
  42. """
  43. res = city
  44. if self.letter_case == "title":
  45. res = city.title()
  46. elif self.letter_case == "upper":
  47. res = city.upper()
  48. return res
  49. @api.model
  50. def _domain_search_res_city(self, row, country):
  51. return [
  52. ("name", "=", self.transform_city_name(row[2], country)),
  53. ("country_id", "=", country.id),
  54. ]
  55. @api.model
  56. def _domain_search_city_zip(self, row, res_city):
  57. domain = [("name", "=", row[1])]
  58. if res_city:
  59. domain += [("city_id", "=", res_city.id)]
  60. return domain
  61. @api.model
  62. def select_state(self, row, country):
  63. code = row[self.code_row_index or 4]
  64. return self.env["res.country.state"].search(
  65. [("country_id", "=", country.id), ("code", "=", code)], limit=1
  66. )
  67. @api.model
  68. def select_city(self, row, country):
  69. res_city_model = self.env["res.city"]
  70. return res_city_model.search(
  71. self._domain_search_res_city(row, country), limit=1
  72. )
  73. @api.model
  74. def select_zip(self, row, country):
  75. city = self.select_city(row, country)
  76. return self.env["res.city.zip"].search(self._domain_search_city_zip(row, city))
  77. @api.model
  78. def prepare_state(self, row, country):
  79. return {
  80. "name": row[self.name_row_index or 3],
  81. "code": row[self.code_row_index or 4],
  82. "country_id": country.id,
  83. }
  84. @api.model
  85. def prepare_city(self, row, country, state_id):
  86. vals = {
  87. "name": self.transform_city_name(row[2], country),
  88. "state_id": state_id,
  89. "country_id": country.id,
  90. }
  91. return vals
  92. @api.model
  93. def prepare_zip(self, row, city_id):
  94. vals = {"name": row[1], "city_id": city_id}
  95. return vals
  96. @api.model
  97. def get_and_parse_csv(self):
  98. country_code = self.country_id.code
  99. config_url = self.env["ir.config_parameter"].get_param(
  100. "geonames.url", default="http://download.geonames.org/export/zip/%s.zip"
  101. )
  102. url = config_url % country_code
  103. logger.info("Starting to download %s" % url)
  104. res_request = requests.get(url)
  105. if res_request.status_code != requests.codes.ok:
  106. raise UserError(
  107. _("Got an error %d when trying to download the file %s.")
  108. % (res_request.status_code, url)
  109. )
  110. f_geonames = zipfile.ZipFile(io.BytesIO(res_request.content))
  111. tempdir = tempfile.mkdtemp(prefix="odoo")
  112. f_geonames.extract("%s.txt" % country_code, tempdir)
  113. data_file = open(
  114. os.path.join(tempdir, "%s.txt" % country_code), "r", encoding="utf-8"
  115. )
  116. data_file.seek(0)
  117. reader = csv.reader(data_file, delimiter=" ")
  118. parsed_csv = [row for i, row in enumerate(reader)]
  119. data_file.close()
  120. logger.info("The geonames zipfile has been decompressed")
  121. return parsed_csv
  122. def _create_states(self, parsed_csv, search_states, max_import):
  123. # States
  124. state_vals_list = []
  125. state_dict = {}
  126. for i, row in enumerate(parsed_csv):
  127. if max_import and i == max_import:
  128. break
  129. state = self.select_state(row, self.country_id) if search_states else False
  130. if not state:
  131. state_vals = self.prepare_state(row, self.country_id)
  132. if state_vals not in state_vals_list:
  133. state_vals_list.append(state_vals)
  134. else:
  135. state_dict[state.code] = state.id
  136. created_states = self.env["res.country.state"].create(state_vals_list)
  137. for i, vals in enumerate(state_vals_list):
  138. state_dict[vals["code"]] = created_states[i].id
  139. return state_dict
  140. def _create_cities(self, parsed_csv, search_cities, max_import, state_dict):
  141. # Cities
  142. city_vals_list = []
  143. city_dict = {}
  144. for i, row in enumerate(parsed_csv):
  145. if max_import and i == max_import:
  146. break
  147. state_id = state_dict[row[self.code_row_index or 4]]
  148. city = self.select_city(row, self.country_id) if search_cities else False
  149. if not city:
  150. city_vals = self.prepare_city(row, self.country_id, state_id)
  151. if city_vals not in city_vals_list:
  152. city_vals_list.append(city_vals)
  153. else:
  154. city_dict[(city.name, state_id)] = city.id
  155. created_cities = self.env["res.city"].create(city_vals_list)
  156. for i, vals in enumerate(city_vals_list):
  157. city_dict[(vals["name"], vals["state_id"])] = created_cities[i].id
  158. return city_dict
  159. def run_import(self):
  160. self.ensure_one()
  161. parsed_csv = self.get_and_parse_csv()
  162. return self._process_csv(parsed_csv)
  163. def _process_csv(self, parsed_csv):
  164. state_model = self.env["res.country.state"]
  165. zip_model = self.env["res.city.zip"]
  166. res_city_model = self.env["res.city"]
  167. # Store current record list
  168. current_zips = zip_model.search(
  169. [("city_id.country_id", "=", self.country_id.id)]
  170. )
  171. search_zips = True and len(current_zips) > 0 or False
  172. current_cities = res_city_model.search(
  173. [("country_id", "=", self.country_id.id)]
  174. )
  175. search_cities = True and len(current_cities) > 0 or False
  176. current_states = state_model.search([("country_id", "=", self.country_id.id)])
  177. search_states = True and len(current_states) > 0 or False
  178. max_import = self.env.context.get("max_import", 0)
  179. logger.info("Starting to create the cities and/or city zip entries")
  180. state_dict = self._create_states(parsed_csv, search_states, max_import)
  181. city_dict = self._create_cities(
  182. parsed_csv, search_cities, max_import, state_dict
  183. )
  184. # Zips
  185. zip_vals_list = []
  186. for i, row in enumerate(parsed_csv):
  187. if max_import and i == max_import:
  188. break
  189. # Don't search if there aren't any records
  190. zip_code = False
  191. if search_zips:
  192. zip_code = self.select_zip(row, self.country_id)
  193. if not zip_code:
  194. state_id = state_dict[row[self.code_row_index or 4]]
  195. city_id = city_dict[
  196. (self.transform_city_name(row[2], self.country_id), state_id)
  197. ]
  198. zip_vals = self.prepare_zip(row, city_id)
  199. if zip_vals not in zip_vals_list:
  200. zip_vals_list.append(zip_vals)
  201. delete_zips = self.env["res.city.zip"].create(zip_vals_list)
  202. current_zips -= delete_zips
  203. if not max_import:
  204. current_zips.unlink()
  205. logger.info(
  206. "%d city zip entries deleted for country %s"
  207. % (len(current_zips), self.country_id.name)
  208. )
  209. # Since we wrapped the entire cities
  210. # creation in a function we need
  211. # to perform a search with city_dict in
  212. # order to know which are the new ones so
  213. # we can delete the old ones
  214. created_cities = res_city_model.search(
  215. [
  216. ("country_id", "=", self.country_id.id),
  217. ("id", "in", list(city_dict.values())),
  218. ]
  219. )
  220. current_cities -= created_cities
  221. current_cities.unlink()
  222. logger.info(
  223. "%d res.city entries deleted for country %s"
  224. % (len(current_cities), self.country_id.name)
  225. )
  226. logger.info(
  227. "The wizard to create cities and/or city zip entries from "
  228. "geonames has been successfully completed."
  229. )
  230. return True