|
|
@ -7,39 +7,40 @@ |
|
|
|
# Copyright 2016-2019 Tecnativa - Pedro M. Baeza |
|
|
|
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). |
|
|
|
|
|
|
|
from odoo import _, api, fields, models |
|
|
|
from odoo.exceptions import UserError |
|
|
|
import requests |
|
|
|
import tempfile |
|
|
|
import csv |
|
|
|
import io |
|
|
|
import zipfile |
|
|
|
import os |
|
|
|
import logging |
|
|
|
import csv |
|
|
|
import os |
|
|
|
import tempfile |
|
|
|
import zipfile |
|
|
|
|
|
|
|
import requests |
|
|
|
|
|
|
|
from odoo import _, api, fields, models |
|
|
|
from odoo.exceptions import UserError |
|
|
|
|
|
|
|
logger = logging.getLogger(__name__) |
|
|
|
|
|
|
|
|
|
|
|
class CityZipGeonamesImport(models.TransientModel): |
|
|
|
_name = 'city.zip.geonames.import' |
|
|
|
_description = 'Import City Zips from Geonames' |
|
|
|
_rec_name = 'country_id' |
|
|
|
_name = "city.zip.geonames.import" |
|
|
|
_description = "Import City Zips from Geonames" |
|
|
|
_rec_name = "country_id" |
|
|
|
|
|
|
|
country_id = fields.Many2one('res.country', 'Country', required=True) |
|
|
|
country_id = fields.Many2one("res.country", "Country", required=True) |
|
|
|
code_row_index = fields.Integer( |
|
|
|
related='country_id.geonames_state_code_column', |
|
|
|
readonly=True) |
|
|
|
name_row_index = fields.Integer( |
|
|
|
related='country_id.geonames_state_name_column') |
|
|
|
|
|
|
|
letter_case = fields.Selection([ |
|
|
|
('unchanged', 'Unchanged'), |
|
|
|
('title', 'Title Case'), |
|
|
|
('upper', 'Upper Case'), |
|
|
|
], string='Letter Case', default='unchanged', |
|
|
|
related="country_id.geonames_state_code_column", readonly=True |
|
|
|
) |
|
|
|
name_row_index = fields.Integer(related="country_id.geonames_state_name_column") |
|
|
|
|
|
|
|
letter_case = fields.Selection( |
|
|
|
[("unchanged", "Unchanged"), ("title", "Title Case"), ("upper", "Upper Case")], |
|
|
|
string="Letter Case", |
|
|
|
default="unchanged", |
|
|
|
help="Converts retreived city and state names to Title Case " |
|
|
|
"(upper case on each first letter of a word) or Upper Case " |
|
|
|
"(all letters upper case).") |
|
|
|
"(all letters upper case).", |
|
|
|
) |
|
|
|
|
|
|
|
@api.model |
|
|
|
def transform_city_name(self, city, country): |
|
|
@ -49,94 +50,94 @@ class CityZipGeonamesImport(models.TransientModel): |
|
|
|
:return: Transformed city name |
|
|
|
""" |
|
|
|
res = city |
|
|
|
if self.letter_case == 'title': |
|
|
|
if self.letter_case == "title": |
|
|
|
res = city.title() |
|
|
|
elif self.letter_case == 'upper': |
|
|
|
elif self.letter_case == "upper": |
|
|
|
res = city.upper() |
|
|
|
return res |
|
|
|
|
|
|
|
@api.model |
|
|
|
def _domain_search_res_city(self, row, country): |
|
|
|
return [('name', '=', self.transform_city_name(row[2], country)), |
|
|
|
('country_id', '=', country.id)] |
|
|
|
return [ |
|
|
|
("name", "=", self.transform_city_name(row[2], country)), |
|
|
|
("country_id", "=", country.id), |
|
|
|
] |
|
|
|
|
|
|
|
@api.model |
|
|
|
def _domain_search_city_zip(self, row, res_city): |
|
|
|
domain = [('name', '=', row[1])] |
|
|
|
domain = [("name", "=", row[1])] |
|
|
|
if res_city: |
|
|
|
domain += [('city_id', '=', res_city.id)] |
|
|
|
domain += [("city_id", "=", res_city.id)] |
|
|
|
return domain |
|
|
|
|
|
|
|
@api.model |
|
|
|
def select_state(self, row, country): |
|
|
|
code = row[self.code_row_index or 4] |
|
|
|
return self.env['res.country.state'].search( |
|
|
|
[('country_id', '=', country.id), |
|
|
|
('code', '=', code)], limit=1, |
|
|
|
return self.env["res.country.state"].search( |
|
|
|
[("country_id", "=", country.id), ("code", "=", code)], limit=1 |
|
|
|
) |
|
|
|
|
|
|
|
@api.model |
|
|
|
def select_city(self, row, country): |
|
|
|
res_city_model = self.env['res.city'] |
|
|
|
return res_city_model.search(self._domain_search_res_city( |
|
|
|
row, country), limit=1) |
|
|
|
res_city_model = self.env["res.city"] |
|
|
|
return res_city_model.search( |
|
|
|
self._domain_search_res_city(row, country), limit=1 |
|
|
|
) |
|
|
|
|
|
|
|
@api.model |
|
|
|
def select_zip(self, row, country): |
|
|
|
city = self.select_city(row, country) |
|
|
|
return self.env['res.city.zip'].search(self._domain_search_city_zip( |
|
|
|
row, city)) |
|
|
|
return self.env["res.city.zip"].search(self._domain_search_city_zip(row, city)) |
|
|
|
|
|
|
|
@api.model |
|
|
|
def prepare_state(self, row, country): |
|
|
|
return { |
|
|
|
'name': row[self.name_row_index or 3], |
|
|
|
'code': row[self.code_row_index or 4], |
|
|
|
'country_id': country.id, |
|
|
|
"name": row[self.name_row_index or 3], |
|
|
|
"code": row[self.code_row_index or 4], |
|
|
|
"country_id": country.id, |
|
|
|
} |
|
|
|
|
|
|
|
@api.model |
|
|
|
def prepare_city(self, row, country, state_id): |
|
|
|
vals = { |
|
|
|
'name': self.transform_city_name(row[2], country), |
|
|
|
'state_id': state_id, |
|
|
|
'country_id': country.id, |
|
|
|
"name": self.transform_city_name(row[2], country), |
|
|
|
"state_id": state_id, |
|
|
|
"country_id": country.id, |
|
|
|
} |
|
|
|
return vals |
|
|
|
|
|
|
|
@api.model |
|
|
|
def prepare_zip(self, row, city_id): |
|
|
|
vals = { |
|
|
|
'name': row[1], |
|
|
|
'city_id': city_id, |
|
|
|
} |
|
|
|
vals = {"name": row[1], "city_id": city_id} |
|
|
|
return vals |
|
|
|
|
|
|
|
@api.model |
|
|
|
def get_and_parse_csv(self): |
|
|
|
country_code = self.country_id.code |
|
|
|
config_url = self.env['ir.config_parameter'].get_param( |
|
|
|
'geonames.url', |
|
|
|
default='http://download.geonames.org/export/zip/%s.zip') |
|
|
|
config_url = self.env["ir.config_parameter"].get_param( |
|
|
|
"geonames.url", default="http://download.geonames.org/export/zip/%s.zip" |
|
|
|
) |
|
|
|
url = config_url % country_code |
|
|
|
logger.info('Starting to download %s' % url) |
|
|
|
logger.info("Starting to download %s" % url) |
|
|
|
res_request = requests.get(url) |
|
|
|
if res_request.status_code != requests.codes.ok: |
|
|
|
raise UserError( |
|
|
|
_('Got an error %d when trying to download the file %s.') |
|
|
|
% (res_request.status_code, url)) |
|
|
|
_("Got an error %d when trying to download the file %s.") |
|
|
|
% (res_request.status_code, url) |
|
|
|
) |
|
|
|
|
|
|
|
f_geonames = zipfile.ZipFile(io.BytesIO(res_request.content)) |
|
|
|
tempdir = tempfile.mkdtemp(prefix='odoo') |
|
|
|
f_geonames.extract('%s.txt' % country_code, tempdir) |
|
|
|
tempdir = tempfile.mkdtemp(prefix="odoo") |
|
|
|
f_geonames.extract("%s.txt" % country_code, tempdir) |
|
|
|
|
|
|
|
data_file = open(os.path.join(tempdir, '%s.txt' % country_code), 'r', |
|
|
|
encoding='utf-8') |
|
|
|
data_file = open( |
|
|
|
os.path.join(tempdir, "%s.txt" % country_code), "r", encoding="utf-8" |
|
|
|
) |
|
|
|
data_file.seek(0) |
|
|
|
reader = csv.reader(data_file, delimiter=' ') |
|
|
|
reader = csv.reader(data_file, delimiter=" ") |
|
|
|
parsed_csv = [row for i, row in enumerate(reader)] |
|
|
|
data_file.close() |
|
|
|
logger.info('The geonames zipfile has been decompressed') |
|
|
|
logger.info("The geonames zipfile has been decompressed") |
|
|
|
return parsed_csv |
|
|
|
|
|
|
|
def _create_states(self, parsed_csv, search_states, max_import): |
|
|
@ -146,8 +147,7 @@ class CityZipGeonamesImport(models.TransientModel): |
|
|
|
for i, row in enumerate(parsed_csv): |
|
|
|
if max_import and i == max_import: |
|
|
|
break |
|
|
|
state = self.select_state( |
|
|
|
row, self.country_id) if search_states else False |
|
|
|
state = self.select_state(row, self.country_id) if search_states else False |
|
|
|
if not state: |
|
|
|
state_vals = self.prepare_state(row, self.country_id) |
|
|
|
if state_vals not in state_vals_list: |
|
|
@ -155,13 +155,12 @@ class CityZipGeonamesImport(models.TransientModel): |
|
|
|
else: |
|
|
|
state_dict[state.code] = state.id |
|
|
|
|
|
|
|
created_states = self.env['res.country.state'].create(state_vals_list) |
|
|
|
created_states = self.env["res.country.state"].create(state_vals_list) |
|
|
|
for i, vals in enumerate(state_vals_list): |
|
|
|
state_dict[vals['code']] = created_states[i].id |
|
|
|
state_dict[vals["code"]] = created_states[i].id |
|
|
|
return state_dict |
|
|
|
|
|
|
|
def _create_cities(self, parsed_csv, |
|
|
|
search_cities, max_import, state_dict): |
|
|
|
def _create_cities(self, parsed_csv, search_cities, max_import, state_dict): |
|
|
|
# Cities |
|
|
|
city_vals_list = [] |
|
|
|
city_dict = {} |
|
|
@ -169,18 +168,16 @@ class CityZipGeonamesImport(models.TransientModel): |
|
|
|
if max_import and i == max_import: |
|
|
|
break |
|
|
|
state_id = state_dict[row[self.code_row_index or 4]] |
|
|
|
city = self.select_city( |
|
|
|
row, self.country_id) if search_cities else False |
|
|
|
city = self.select_city(row, self.country_id) if search_cities else False |
|
|
|
if not city: |
|
|
|
city_vals = self.prepare_city( |
|
|
|
row, self.country_id, state_id) |
|
|
|
city_vals = self.prepare_city(row, self.country_id, state_id) |
|
|
|
if city_vals not in city_vals_list: |
|
|
|
city_vals_list.append(city_vals) |
|
|
|
else: |
|
|
|
city_dict[(city.name, state_id)] = city.id |
|
|
|
created_cities = self.env['res.city'].create(city_vals_list) |
|
|
|
created_cities = self.env["res.city"].create(city_vals_list) |
|
|
|
for i, vals in enumerate(city_vals_list): |
|
|
|
city_dict[(vals['name'], vals['state_id'])] = created_cities[i].id |
|
|
|
city_dict[(vals["name"], vals["state_id"])] = created_cities[i].id |
|
|
|
return city_dict |
|
|
|
|
|
|
|
def run_import(self): |
|
|
@ -189,28 +186,29 @@ class CityZipGeonamesImport(models.TransientModel): |
|
|
|
return self._process_csv(parsed_csv) |
|
|
|
|
|
|
|
def _process_csv(self, parsed_csv): |
|
|
|
state_model = self.env['res.country.state'] |
|
|
|
zip_model = self.env['res.city.zip'] |
|
|
|
res_city_model = self.env['res.city'] |
|
|
|
state_model = self.env["res.country.state"] |
|
|
|
zip_model = self.env["res.city.zip"] |
|
|
|
res_city_model = self.env["res.city"] |
|
|
|
|
|
|
|
# Store current record list |
|
|
|
current_zips = zip_model.search( |
|
|
|
[('city_id.country_id', '=', self.country_id.id)]) |
|
|
|
[("city_id.country_id", "=", self.country_id.id)] |
|
|
|
) |
|
|
|
search_zips = True and len(current_zips) > 0 or False |
|
|
|
current_cities = res_city_model.search( |
|
|
|
[('country_id', '=', self.country_id.id)]) |
|
|
|
[("country_id", "=", self.country_id.id)] |
|
|
|
) |
|
|
|
search_cities = True and len(current_cities) > 0 or False |
|
|
|
current_states = state_model.search( |
|
|
|
[('country_id', '=', self.country_id.id)]) |
|
|
|
current_states = state_model.search([("country_id", "=", self.country_id.id)]) |
|
|
|
search_states = True and len(current_states) > 0 or False |
|
|
|
|
|
|
|
max_import = self.env.context.get('max_import', 0) |
|
|
|
logger.info('Starting to create the cities and/or city zip entries') |
|
|
|
max_import = self.env.context.get("max_import", 0) |
|
|
|
logger.info("Starting to create the cities and/or city zip entries") |
|
|
|
|
|
|
|
state_dict = self._create_states(parsed_csv, |
|
|
|
search_states, max_import) |
|
|
|
city_dict = self._create_cities(parsed_csv, |
|
|
|
search_cities, max_import, state_dict) |
|
|
|
state_dict = self._create_states(parsed_csv, search_states, max_import) |
|
|
|
city_dict = self._create_cities( |
|
|
|
parsed_csv, search_cities, max_import, state_dict |
|
|
|
) |
|
|
|
|
|
|
|
# Zips |
|
|
|
zip_vals_list = [] |
|
|
@ -223,21 +221,22 @@ class CityZipGeonamesImport(models.TransientModel): |
|
|
|
zip_code = self.select_zip(row, self.country_id) |
|
|
|
if not zip_code: |
|
|
|
state_id = state_dict[row[self.code_row_index or 4]] |
|
|
|
city_id = city_dict[( |
|
|
|
self.transform_city_name(row[2], self.country_id), |
|
|
|
state_id, |
|
|
|
)] |
|
|
|
city_id = city_dict[ |
|
|
|
(self.transform_city_name(row[2], self.country_id), state_id) |
|
|
|
] |
|
|
|
zip_vals = self.prepare_zip(row, city_id) |
|
|
|
if zip_vals not in zip_vals_list: |
|
|
|
zip_vals_list.append(zip_vals) |
|
|
|
|
|
|
|
delete_zips = self.env['res.city.zip'].create(zip_vals_list) |
|
|
|
delete_zips = self.env["res.city.zip"].create(zip_vals_list) |
|
|
|
current_zips -= delete_zips |
|
|
|
|
|
|
|
if not max_import: |
|
|
|
current_zips.unlink() |
|
|
|
logger.info('%d city zip entries deleted for country %s' % |
|
|
|
(len(current_zips), self.country_id.name)) |
|
|
|
logger.info( |
|
|
|
"%d city zip entries deleted for country %s" |
|
|
|
% (len(current_zips), self.country_id.name) |
|
|
|
) |
|
|
|
|
|
|
|
# Since we wrapped the entire cities |
|
|
|
# creation in a function we need |
|
|
@ -245,14 +244,19 @@ class CityZipGeonamesImport(models.TransientModel): |
|
|
|
# order to know which are the new ones so |
|
|
|
# we can delete the old ones |
|
|
|
created_cities = res_city_model.search( |
|
|
|
[('country_id', '=', self.country_id.id), |
|
|
|
('id', 'in', list(city_dict.values()))] |
|
|
|
[ |
|
|
|
("country_id", "=", self.country_id.id), |
|
|
|
("id", "in", list(city_dict.values())), |
|
|
|
] |
|
|
|
) |
|
|
|
current_cities -= created_cities |
|
|
|
current_cities.unlink() |
|
|
|
logger.info('%d res.city entries deleted for country %s' % |
|
|
|
(len(current_cities), self.country_id.name)) |
|
|
|
logger.info( |
|
|
|
'The wizard to create cities and/or city zip entries from ' |
|
|
|
'geonames has been successfully completed.') |
|
|
|
"%d res.city entries deleted for country %s" |
|
|
|
% (len(current_cities), self.country_id.name) |
|
|
|
) |
|
|
|
logger.info( |
|
|
|
"The wizard to create cities and/or city zip entries from " |
|
|
|
"geonames has been successfully completed." |
|
|
|
) |
|
|
|
return True |