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.

253 lines
9.8 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-2020 Tecnativa - Pedro M. Baeza
  8. # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
  9. from odoo import _, api, fields, models
  10. from odoo.exceptions import UserError
  11. import requests
  12. import tempfile
  13. import io
  14. import zipfile
  15. import os
  16. import logging
  17. import csv
  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',
  26. readonly=True)
  27. name_row_index = fields.Integer(
  28. related='country_id.geonames_state_name_column')
  29. letter_case = fields.Selection([
  30. ('unchanged', 'Unchanged'),
  31. ('title', 'Title Case'),
  32. ('upper', 'Upper Case'),
  33. ], string='Letter Case', default='unchanged',
  34. help="Converts retreived city and state names to Title Case "
  35. "(upper case on each first letter of a word) or Upper Case "
  36. "(all letters upper case).")
  37. @api.model
  38. def transform_city_name(self, city, country):
  39. """Override it for transforming city name (if needed)
  40. :param city: Original city name
  41. :param country: Country record
  42. :return: Transformed city name
  43. """
  44. res = city
  45. if self.letter_case == 'title':
  46. res = city.title()
  47. elif self.letter_case == 'upper':
  48. res = city.upper()
  49. return res
  50. @api.model
  51. def _domain_search_city_zip(self, row, city_id=False):
  52. domain = [('name', '=', row[1])]
  53. if city_id:
  54. domain += [('city_id', '=', city_id)]
  55. return domain
  56. @api.model
  57. def select_state(self, row, country):
  58. code = row[self.code_row_index or 4]
  59. return self.env['res.country.state'].search(
  60. [('country_id', '=', country.id),
  61. ('code', '=', code)], limit=1,
  62. )
  63. @api.model
  64. def select_city(self, row, country, state_id):
  65. # This has to be done by SQL for performance reasons avoiding
  66. # left join with ir_translation on the translatable field "name"
  67. self.env.cr.execute(
  68. "SELECT id, name FROM res_city "
  69. "WHERE name = %s AND country_id = %s AND state_id = %s LIMIT 1",
  70. (self.transform_city_name(row[2], country), country.id, state_id))
  71. row_city = self.env.cr.fetchone()
  72. return (row_city[0], row_city[1]) if row_city else (False, False)
  73. @api.model
  74. def select_zip(self, row, country, state_id):
  75. city_id, _ = self.select_city(row, country, state_id)
  76. return self.env['res.city.zip'].search(self._domain_search_city_zip(
  77. row, city_id))
  78. @api.model
  79. def prepare_state(self, row, country):
  80. return {
  81. 'name': row[self.name_row_index or 3],
  82. 'code': row[self.code_row_index or 4],
  83. 'country_id': country.id,
  84. }
  85. @api.model
  86. def prepare_city(self, row, country, state_id):
  87. vals = {
  88. 'name': self.transform_city_name(row[2], country),
  89. 'state_id': state_id,
  90. 'country_id': country.id,
  91. }
  92. return vals
  93. @api.model
  94. def prepare_zip(self, row, city_id):
  95. vals = {
  96. 'name': row[1],
  97. 'city_id': city_id,
  98. }
  99. return vals
  100. @api.model
  101. def get_and_parse_csv(self):
  102. country_code = self.country_id.code
  103. config_url = self.env['ir.config_parameter'].get_param(
  104. 'geonames.url',
  105. default='http://download.geonames.org/export/zip/%s.zip')
  106. url = config_url % country_code
  107. logger.info('Starting to download %s' % url)
  108. res_request = requests.get(url)
  109. if res_request.status_code != requests.codes.ok:
  110. raise UserError(
  111. _('Got an error %d when trying to download the file %s.')
  112. % (res_request.status_code, url))
  113. f_geonames = zipfile.ZipFile(io.BytesIO(res_request.content))
  114. tempdir = tempfile.mkdtemp(prefix='odoo')
  115. f_geonames.extract('%s.txt' % country_code, tempdir)
  116. data_file = open(os.path.join(tempdir, '%s.txt' % country_code), 'r',
  117. encoding='utf-8')
  118. data_file.seek(0)
  119. reader = csv.reader(data_file, delimiter=' ')
  120. parsed_csv = [row for i, row in enumerate(reader)]
  121. data_file.close()
  122. logger.info('The geonames zipfile has been decompressed')
  123. return parsed_csv
  124. def _create_states(self, parsed_csv, search_states, max_import):
  125. # States
  126. state_vals_list = []
  127. state_dict = {}
  128. for i, row in enumerate(parsed_csv):
  129. if max_import and i == max_import:
  130. break
  131. state = self.select_state(
  132. row, self.country_id) if search_states else False
  133. if not state:
  134. state_vals = self.prepare_state(row, self.country_id)
  135. if state_vals not in state_vals_list:
  136. state_vals_list.append(state_vals)
  137. else:
  138. state_dict[state.code] = state.id
  139. created_states = self.env['res.country.state'].create(state_vals_list)
  140. for i, vals in enumerate(state_vals_list):
  141. state_dict[vals['code']] = created_states[i].id
  142. return state_dict
  143. def _create_cities(self, parsed_csv,
  144. search_cities, max_import, state_dict):
  145. # Cities
  146. city_vals_list = []
  147. city_dict = {}
  148. for i, row in enumerate(parsed_csv):
  149. if max_import and i == max_import:
  150. break
  151. state_id = state_dict[row[self.code_row_index or 4]]
  152. city_id, city_name = (
  153. self.select_city(row, self.country_id, state_id)
  154. if search_cities else (False, False))
  155. if not city_id:
  156. city_vals = self.prepare_city(
  157. row, self.country_id, state_id)
  158. if city_vals not in city_vals_list:
  159. city_vals_list.append(city_vals)
  160. else:
  161. city_dict[(city_name, state_id)] = city_id
  162. ctx = dict(self.env.context)
  163. ctx.pop('lang', None) # make sure no translation is added
  164. created_cities = self.env['res.city'].with_context(
  165. ctx).create(city_vals_list)
  166. for i, vals in enumerate(city_vals_list):
  167. city_dict[(vals['name'], vals['state_id'])] = created_cities[i].id
  168. return city_dict
  169. @api.multi
  170. def run_import(self):
  171. self.ensure_one()
  172. parsed_csv = self.get_and_parse_csv()
  173. return self._process_csv(parsed_csv)
  174. def _process_csv(self, parsed_csv):
  175. state_model = self.env['res.country.state']
  176. zip_model = self.env['res.city.zip']
  177. res_city_model = self.env['res.city']
  178. # Store current record list
  179. old_zips = set(zip_model.search(
  180. [('city_id.country_id', '=', self.country_id.id)]).ids)
  181. search_zips = len(old_zips) > 0
  182. old_cities = set(res_city_model.search(
  183. [('country_id', '=', self.country_id.id)]).ids)
  184. search_cities = len(old_cities) > 0
  185. current_states = state_model.search(
  186. [('country_id', '=', self.country_id.id)])
  187. search_states = len(current_states) > 0
  188. max_import = self.env.context.get('max_import', 0)
  189. logger.info('Starting to create the cities and/or city zip entries')
  190. # Pre-create states and cities
  191. state_dict = self._create_states(parsed_csv,
  192. search_states, max_import)
  193. city_dict = self._create_cities(parsed_csv,
  194. search_cities, max_import, state_dict)
  195. # Zips
  196. zip_vals_list = []
  197. for i, row in enumerate(parsed_csv):
  198. if max_import and i == max_import:
  199. break
  200. # Don't search if there aren't any records
  201. zip_code = False
  202. state_id = state_dict[row[self.code_row_index or 4]]
  203. if search_zips:
  204. zip_code = self.select_zip(row, self.country_id, state_id)
  205. if not zip_code:
  206. city_id = city_dict[(
  207. self.transform_city_name(row[2], self.country_id),
  208. state_id,
  209. )]
  210. zip_vals = self.prepare_zip(row, city_id)
  211. if zip_vals not in zip_vals_list:
  212. zip_vals_list.append(zip_vals)
  213. else:
  214. old_zips.remove(zip_code.id)
  215. self.env['res.city.zip'].create(zip_vals_list)
  216. if not max_import:
  217. if old_zips:
  218. logger.info('removing city zip entries')
  219. self.env['res.city.zip'].browse(list(old_zips)).unlink()
  220. logger.info('%d city zip entries deleted for country %s' %
  221. (len(old_zips), self.country_id.name))
  222. old_cities -= set(city_dict.values())
  223. if old_cities:
  224. logger.info('removing city entries')
  225. self.env['res.city'].browse(list(old_cities)).unlink()
  226. logger.info('%d res.city entries deleted for country %s' %
  227. (len(old_cities), self.country_id.name))
  228. logger.info(
  229. 'The wizard to create cities and/or city zip entries from '
  230. 'geonames has been successfully completed.')
  231. return True