diff options
| author | Robot Odoo <xmo+roboto@odoo.com> | 2018-11-17 18:41:18 +0100 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2018-11-17 18:41:18 +0100 |
| commit | 1c1ae3bef91aa6dfd140e21bcba096ccb3b86b31 (patch) | |
| tree | 8bdf8fe91928710435c2434c096e7a740e07d635 | |
| parent | 6faad524cf5e4db02855cb213277c40aee14c42a (diff) | |
| parent | 6c67539b92d52ae6c8449d7d2f63d032f805a903 (diff) | |
[REF] base_geolocalize: support multiple provider
- Make it generic: geo_coder (logic) / geo_provider (list of provider)
- Add support for openstreetmap since google make it more complicated: https://cloud.google.com/maps-platform/user-guide/account-changes
Courtesy of @ecino
closes odoo/odoo#28747
| -rw-r--r-- | addons/base_geolocalize/README.md | 13 | ||||
| -rw-r--r-- | addons/base_geolocalize/__manifest__.py | 9 | ||||
| -rw-r--r-- | addons/base_geolocalize/data/data.xml | 12 | ||||
| -rw-r--r-- | addons/base_geolocalize/models/__init__.py | 2 | ||||
| -rw-r--r-- | addons/base_geolocalize/models/base_geocoder.py | 147 | ||||
| -rw-r--r-- | addons/base_geolocalize/models/res_config_settings.py | 21 | ||||
| -rw-r--r-- | addons/base_geolocalize/models/res_partner.py | 77 | ||||
| -rw-r--r-- | addons/base_geolocalize/security/ir.model.access.csv | 2 | ||||
| -rw-r--r-- | addons/base_geolocalize/tests/__init__.py | 3 | ||||
| -rw-r--r-- | addons/base_geolocalize/tests/test_geolocalize.py | 35 | ||||
| -rw-r--r-- | addons/base_geolocalize/views/res_config_settings_views.xml | 19 | ||||
| -rw-r--r-- | addons/base_setup/models/res_config_settings.py | 1 | ||||
| -rw-r--r-- | addons/base_setup/views/res_config_settings_views.xml | 20 | ||||
| -rw-r--r-- | addons/website_crm_partner_assign/models/crm_lead.py | 12 | ||||
| -rw-r--r-- | addons/website_crm_partner_assign/tests/test_partner_assign.py | 9 |
15 files changed, 300 insertions, 82 deletions
diff --git a/addons/base_geolocalize/README.md b/addons/base_geolocalize/README.md new file mode 100644 index 00000000000..90d54f5c726 --- /dev/null +++ b/addons/base_geolocalize/README.md @@ -0,0 +1,13 @@ +Partner geolocalize +=================== + +Contacts geolocation API to convert partner addresses into GPS coordinates. + +Configure +--------- +You can configure in General Settings the default provider of the geolocation API service. + +A method `_call_<service>` should be implemented in object `base.geocoder` that accepts an address string as parameter and return (latitude, longitude) tuple for this to work. +If no default provider is set, the first one will be used by default. + +An optional method `_geo_query_address_<service>` which takes address fields as parameters can be defined to encode the query string for the provider.
\ No newline at end of file diff --git a/addons/base_geolocalize/__manifest__.py b/addons/base_geolocalize/__manifest__.py index c7d279cb43e..a5ed52e1df3 100644 --- a/addons/base_geolocalize/__manifest__.py +++ b/addons/base_geolocalize/__manifest__.py @@ -2,15 +2,18 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. { 'name': 'Partners Geolocation', - 'version': '2.0', - 'category': 'Sales', + 'version': '2.1', + 'category': 'Extra Tools', 'description': """ Partners Geolocation ======================== """, - 'depends': ['base'], + 'depends': ['base_setup'], 'data': [ + 'security/ir.model.access.csv', 'views/res_partner_views.xml', + 'views/res_config_settings_views.xml', + 'data/data.xml', ], 'installable': True, } diff --git a/addons/base_geolocalize/data/data.xml b/addons/base_geolocalize/data/data.xml new file mode 100644 index 00000000000..dd2dec16505 --- /dev/null +++ b/addons/base_geolocalize/data/data.xml @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="utf-8"?> +<odoo> + <record id="geoprovider_open_street" model="base.geo_provider"> + <field name="tech_name">openstreetmap</field> + <field name="name">Open Street Map</field> + </record> + + <record id="geoprovider_google_map" model="base.geo_provider"> + <field name="tech_name">googlemap</field> + <field name="name">Google Place Map</field> + </record> +</odoo> diff --git a/addons/base_geolocalize/models/__init__.py b/addons/base_geolocalize/models/__init__.py index 3f2b7ae35b3..47cc19bfc62 100644 --- a/addons/base_geolocalize/models/__init__.py +++ b/addons/base_geolocalize/models/__init__.py @@ -1,3 +1,5 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. +from . import base_geocoder +from . import res_config_settings from . import res_partner diff --git a/addons/base_geolocalize/models/base_geocoder.py b/addons/base_geolocalize/models/base_geocoder.py new file mode 100644 index 00000000000..ee54fcf6dbd --- /dev/null +++ b/addons/base_geolocalize/models/base_geocoder.py @@ -0,0 +1,147 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. +import requests +import logging + +from odoo import api, fields, models, tools, _ +from odoo.exceptions import UserError + + +_logger = logging.getLogger(__name__) + + +class GeoProvider(models.Model): + _name = "base.geo_provider" + _description = "Geo Provider" + + tech_name = fields.Char() + name = fields.Char() + + +class GeoCoder(models.AbstractModel): + """ + Abstract class used to call Geolocalization API and convert addresses + into GPS coordinates. + """ + _name = "base.geocoder" + _description = "Geo Coder" + + @api.model + def _get_provider(self): + prov_id = self.env['ir.config_parameter'].sudo().get_param('base_geolocalize.geo_provider') + if prov_id: + provider = self.env['base.geo_provider'].browse(int(prov_id)) + if not prov_id or not provider.exists(): + provider = self.env['base.geo_provider'].search([], limit=1) + return provider + + @api.model + def geo_query_address(self, street=None, zip=None, city=None, state=None, country=None): + """ Converts address fields into a valid string for querying + geolocation APIs. + :param street: street address + :param zip: zip code + :param city: city + :param state: state + :param country: country + :return: formatted string + """ + provider = self._get_provider().tech_name + if hasattr(self, '_geo_query_address_' + provider): + # Makes the transformation defined for provider + return getattr(self, '_geo_query_address_' + provider)(street, zip, city, state, country) + else: + # By default, join the non-empty parameters + return self._geo_query_address_default(street=street, zip=zip, city=city, state=state, country=country) + + @api.model + def geo_find(self, addr): + """Use a location provider API to convert an address string into a latitude, longitude tuple. + Here we use Openstreetmap Nominatim by default. + :param addr: Address string passed to API + :return: (latitude, longitude) or None if not found + """ + provider = self._get_provider().tech_name + try: + service = getattr(self, '_call_' + provider) + result = service(addr) + except AttributeError: + raise UserError(_( + 'Provider %s is not implemented for geolocation service.' + ) % provider) + except UserError: + raise + except Exception: + _logger.debug('Geolocalize call failed', exc_info=True) + result = None + return result + + @api.model + def _call_openstreetmap(self, addr): + """ + Use Openstreemap Nominatim service to retrieve location + :return: (latitude, longitude) or None if not found + """ + if not addr: + _logger.info('invalid address given') + return None + url = 'https://nominatim.openstreetmap.org/search' + try: + result = requests.get(url, {'format': 'json', 'q': addr}).json() + _logger.info('openstreetmap nominatim service called') + except Exception as e: + self._raise_query_error(e) + geo = result[0] + return float(geo['lat']), float(geo['lon']) + + @api.model + def _call_googlemap(self, addr): + """ Use google maps API. It won't work without a valid API key. + :return: (latitude, longitude) or None if not found + """ + apikey = self.env['ir.config_parameter'].sudo().get_param('base_geolocalize.google_map_api_key') + if not apikey: + raise UserError(_( + "API key for GeoCoding (Places) required.\n" + "Visit https://developers.google.com/maps/documentation/geocoding/get-api-key for more information." + )) + url = "https://maps.googleapis.com/maps/api/geocode/json" + try: + result = requests.get(url, params={'sensor': 'false', 'address': addr, 'key': apikey}).json() + except Exception as e: + self._raise_query_error(e) + + try: + if result['status'] != 'OK': + _logger.debug('Invalid Gmaps call: %s - %s', + result['status'], result.get('error_message', '')) + return None + geo = result['results'][0]['geometry']['location'] + return float(geo['lat']), float(geo['lng']) + except (KeyError, ValueError): + _logger.debug('Unexpected Gmaps API answer %s', result.get('error_message', '')) + return None + + @api.model + def _geo_query_address_default(self, street=None, zip=None, city=None, state=None, country=None): + address_list = [ + street, + ("%s %s" % (zip or '', city or '')).strip(), + state, + country + ] + address_list = [item for item in address_list if item] + return tools.ustr(', '.join(address_list)) + + @api.model + def _geo_query_address_googlemap(self, street=None, zip=None, city=None, state=None, country=None): + # put country qualifier in front, otherwise GMap gives wrong# results + # e.g. 'Congo, Democratic Republic of the' => 'Democratic Republic of the Congo' + if country and ',' in country and ( + country.endswith(' of') or country.endswith(' of the')): + country = '{1} {0}'.format(*country.split(',', 1)) + return self._geo_query_address_default(street=street, zip=zip, city=city, state=state, country=country) + + def _raise_query_error(self, error): + raise UserError(_('Error with geolocation server:') + ' %s' % error) + diff --git a/addons/base_geolocalize/models/res_config_settings.py b/addons/base_geolocalize/models/res_config_settings.py new file mode 100644 index 00000000000..4cd6cdb87da --- /dev/null +++ b/addons/base_geolocalize/models/res_config_settings.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import fields, models + + +class ResConfigSettings(models.TransientModel): + _inherit = 'res.config.settings' + + geoloc_provider_id = fields.Many2one( + 'base.geo_provider', + string='API', + config_parameter='base_geolocalize.geo_provider', + default=lambda x: x.env['base.geocoder']._get_provider() + ) + geoloc_provider_techname = fields.Char(related='geoloc_provider_id.tech_name', readonly=1) + geoloc_provider_googlemap_key = fields.Char( + string='Google Map API Key', + config_parameter='base_geolocalize.google_map_api_key', + help="Visit https://developers.google.com/maps/documentation/geocoding/get-api-key for more information." + ) diff --git a/addons/base_geolocalize/models/res_partner.py b/addons/base_geolocalize/models/res_partner.py index 3594e1cb61b..6c7cbad5aac 100644 --- a/addons/base_geolocalize/models/res_partner.py +++ b/addons/base_geolocalize/models/res_partner.py @@ -1,53 +1,4 @@ -# -*- coding: utf-8 -*- -# Part of Odoo. See LICENSE file for full copyright and licensing details. -import json -import logging - -import requests - -from odoo import api, fields, models, tools, _ -from odoo.exceptions import UserError - -_logger = logging.getLogger(__name__) - - -def geo_find(addr, apikey=False): - if not addr: - return None - - if not apikey: - raise UserError(_('''API key for GeoCoding (Places) required.\n - Save this key in System Parameters with key: google.api_key_geocode, value: <your api key> - Visit https://developers.google.com/maps/documentation/geocoding/get-api-key for more information. - ''')) - - url = "https://maps.googleapis.com/maps/api/geocode/json" - try: - result = requests.get(url, params={'sensor': 'false', 'address': addr, 'key': apikey}).json() - except Exception as e: - raise UserError(_('Cannot contact geolocation servers. Please make sure that your Internet connection is up and running (%s).') % e) - - if result['status'] != 'OK': - if result.get('error_message'): - _logger.error(result['error_message']) - return None - - try: - geo = result['results'][0]['geometry']['location'] - return float(geo['lat']), float(geo['lng']) - except (KeyError, ValueError): - return None - - -def geo_query_address(street=None, zip=None, city=None, state=None, country=None): - if country and ',' in country and (country.endswith(' of') or country.endswith(' of the')): - # put country qualifier in front, otherwise GMap gives wrong results, - # e.g. 'Congo, Democratic Republic of the' => 'Democratic Republic of the Congo' - country = '{1} {0}'.format(*country.split(',', 1)) - return tools.ustr(', '.join( - field for field in [street, ("%s %s" % (zip or '', city or '')).strip(), state, country] - if field - )) +from odoo import api, fields, models class ResPartner(models.Model): @@ -57,26 +8,26 @@ class ResPartner(models.Model): partner_longitude = fields.Float(string='Geo Longitude', digits=(16, 5)) date_localization = fields.Date(string='Geolocation Date') - @classmethod - def _geo_localize(cls, apikey, street='', zip='', city='', state='', country=''): - search = geo_query_address(street=street, zip=zip, city=city, state=state, country=country) - result = geo_find(search, apikey) + @api.model + def _geo_localize(self, street='', zip='', city='', state='', country=''): + geo_obj = self.env['base.geocoder'] + search = geo_obj.geo_query_address(street=street, zip=zip, city=city, state=state, country=country) + result = geo_obj.geo_find(search) if result is None: - search = geo_query_address(city=city, state=state, country=country) - result = geo_find(search, apikey) + search = geo_obj.geo_query_address(city=city, state=state, country=country) + result = geo_obj.geo_find(search) return result @api.multi def geo_localize(self): # We need country names in English below - apikey = self.env['ir.config_parameter'].sudo().get_param('google.api_key_geocode') for partner in self.with_context(lang='en_US'): - result = partner._geo_localize(apikey, - partner.street, - partner.zip, - partner.city, - partner.state_id.name, - partner.country_id.name) + result = self._geo_localize(partner.street, + partner.zip, + partner.city, + partner.state_id.name, + partner.country_id.name) + if result: partner.write({ 'partner_latitude': result[0], diff --git a/addons/base_geolocalize/security/ir.model.access.csv b/addons/base_geolocalize/security/ir.model.access.csv new file mode 100644 index 00000000000..b6ad51578ce --- /dev/null +++ b/addons/base_geolocalize/security/ir.model.access.csv @@ -0,0 +1,2 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
+access_base_geo_provider,access_base_geo_provider,model_base_geo_provider,base.group_user,1,0,0,0
diff --git a/addons/base_geolocalize/tests/__init__.py b/addons/base_geolocalize/tests/__init__.py new file mode 100644 index 00000000000..44d3f6100bc --- /dev/null +++ b/addons/base_geolocalize/tests/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. +from . import test_geolocalize diff --git a/addons/base_geolocalize/tests/test_geolocalize.py b/addons/base_geolocalize/tests/test_geolocalize.py new file mode 100644 index 00000000000..8052e49f3b4 --- /dev/null +++ b/addons/base_geolocalize/tests/test_geolocalize.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. +from odoo.tests import TransactionCase +from odoo.exceptions import UserError + +import odoo.tests + + +@odoo.tests.tagged('external', '-standard') +class TestGeoLocalize(TransactionCase): + + def test_default_openstreetmap(self): + """ Test that openstreetmap localize service works. """ + test_partner = self.env.ref('base.res_partner_2') + test_partner.geo_localize() + self.assertTrue(test_partner.partner_longitude) + self.assertTrue(test_partner.partner_latitude) + self.assertTrue(test_partner.date_localization) + + # we don't check here that the localization is at right place + # but just that result is realistic float coordonates + self.assertTrue(float(test_partner.partner_longitude) != 0.0) + self.assertTrue(float(test_partner.partner_latitude) != 0.0) + + def test_googlemap_without_api_key(self): + """ Without providing API key to google maps, + the service doesn't work.""" + test_partner = self.env.ref('base.res_partner_address_4') + google_map = self.env.ref('base_geolocalize.geoprovider_google_map').id + self.env['ir.config_parameter'].set_param('base_geolocalize.geo_provider', google_map) + with self.assertRaises(UserError): + test_partner.geo_localize() + self.assertFalse(test_partner.partner_longitude) + self.assertFalse(test_partner.partner_latitude) + self.assertFalse(test_partner.date_localization) diff --git a/addons/base_geolocalize/views/res_config_settings_views.xml b/addons/base_geolocalize/views/res_config_settings_views.xml new file mode 100644 index 00000000000..68eb238e4bf --- /dev/null +++ b/addons/base_geolocalize/views/res_config_settings_views.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="utf-8"?> +<odoo> + <record id="res_config_settings_view_form" model="ir.ui.view"> + <field name="name">res.config.settings.view.form.inherit.web.geoloclaize</field> + <field name="model">res.config.settings</field> + <field name="inherit_id" ref="base_setup.res_config_settings_view_form"/> + <field name="arch" type="xml"> + <xpath expr="//div[@name='base_geolocalize_warning']" position="replace"> + <div class="content-group" attrs="{'invisible': [('module_base_geolocalize', '=', False)]}"> + API: <field name="geoloc_provider_id"/> + <field name="geoloc_provider_techname" invisible="1"/> + <div attrs="{'invisible': [('geoloc_provider_techname', '!=', 'googlemap')]}"> + Key: <field name='geoloc_provider_googlemap_key'/> + </div> + </div> + </xpath> + </field> + </record> +</odoo>
\ No newline at end of file diff --git a/addons/base_setup/models/res_config_settings.py b/addons/base_setup/models/res_config_settings.py index bb6c718b29d..9743cbc3b68 100644 --- a/addons/base_setup/models/res_config_settings.py +++ b/addons/base_setup/models/res_config_settings.py @@ -32,6 +32,7 @@ class ResConfigSettings(models.TransientModel): module_voip = fields.Boolean("Asterisk (VoIP)") module_web_unsplash = fields.Boolean("Unsplash Image Library") module_partner_autocomplete = fields.Boolean("Auto-populate company data") + module_base_geolocalize = fields.Boolean("GeoLocalize") company_share_partner = fields.Boolean(string='Share partners to all companies', help="Share your partners to all companies defined in your instance.\n" " * Checked : Partners are visible for every companies, even if a company is defined on the partner.\n" diff --git a/addons/base_setup/views/res_config_settings_views.xml b/addons/base_setup/views/res_config_settings_views.xml index de090d491d5..c986850b9c1 100644 --- a/addons/base_setup/views/res_config_settings_views.xml +++ b/addons/base_setup/views/res_config_settings_views.xml @@ -219,7 +219,7 @@ </div> </div> </div> - <div class="col-12 col-lg-6 o_setting_box" id="oauth"> + <div class="col-12 col-lg-6 o_setting_box" id="gengo"> <div class="o_setting_left_pane"> <field name="module_base_gengo"/> </div> @@ -233,7 +233,7 @@ </div> </div> </div> - <div class="col-12 col-lg-6 o_setting_box"> + <div class="col-12 col-lg-6 o_setting_box" id="unsplash"> <div class="o_setting_left_pane"> <field name="module_web_unsplash"/> </div> @@ -244,7 +244,7 @@ </div> </div> </div> - <div class="col-xs-12 col-md-6 o_setting_box" title="When populating your address book, Odoo provides a list of matching companies. When selecting one item, the company data and logo are auto-filled."> + <div class="col-xs-12 col-md-6 o_setting_box" title="When populating your address book, Odoo provides a list of matching companies. When selecting one item, the company data and logo are auto-filled." id="partner_autocomplete"> <div class="o_setting_left_pane"> <field name="module_partner_autocomplete"/> </div> @@ -255,6 +255,20 @@ </div> </div> </div> + <div class="col-12 col-lg-6 o_setting_box" id="base_geolocalize"> + <div class="o_setting_left_pane"> + <field name="module_base_geolocalize"/> + </div> + <div class="o_setting_right_pane" id="web_geolocalize_settings"> + <label string="Geo Localization" for="module_base_geolocalize"/> + <div class="text-muted"> + GeoLocalize your partners + </div> + <div class="content-group" attrs="{'invisible': [('module_base_geolocalize','=', False)]}" name="base_geolocalize_warning"> + <div class="mt16 text-warning"><strong>Save</strong> this page and come back here to choose your Geo Provider.</div> + </div> + </div> + </div> </div> </div> </xpath> diff --git a/addons/website_crm_partner_assign/models/crm_lead.py b/addons/website_crm_partner_assign/models/crm_lead.py index 215b96e9457..cd90ea185ef 100644 --- a/addons/website_crm_partner_assign/models/crm_lead.py +++ b/addons/website_crm_partner_assign/models/crm_lead.py @@ -4,9 +4,9 @@ import random from odoo import api, fields, models, _ -from odoo.addons.base_geolocalize.models.res_partner import geo_find, geo_query_address from odoo.exceptions import AccessDenied + class CrmLead(models.Model): _inherit = "crm.lead" partner_latitude = fields.Float('Geo Latitude', digits=(16, 5)) @@ -65,7 +65,7 @@ class CrmLead(models.Model): tag_to_add = self.env.ref('website_crm_partner_assign.tag_portal_lead_partner_unavailable', False) lead.write({'tag_ids': [(4, tag_to_add.id, False)]}) continue - lead.assign_geo_localize(lead.partner_latitude, lead.partner_longitude,) + lead.assign_geo_localize(lead.partner_latitude, lead.partner_longitude) partner = self.env['res.partner'].browse(partner_id) if partner.user_id: lead.allocate_salesman(partner.user_id.ids, team_id=partner.team_id.id) @@ -86,10 +86,10 @@ class CrmLead(models.Model): if lead.partner_latitude and lead.partner_longitude: continue if lead.country_id: - apikey = self.env['ir.config_parameter'].sudo().get_param('google.api_key_geocode') - result = self.env['res.partner']._geo_localize(apikey, - lead.street, lead.zip, lead.city, - lead.state_id.name, lead.country_id.name) + result = self.env['res.partner']._geo_localize( + lead.street, lead.zip, lead.city, + lead.state_id.name, lead.country_id.name + ) if result: lead.write({ 'partner_latitude': result[0], diff --git a/addons/website_crm_partner_assign/tests/test_partner_assign.py b/addons/website_crm_partner_assign/tests/test_partner_assign.py index 1945e6bd2e4..52ddbe427e1 100644 --- a/addons/website_crm_partner_assign/tests/test_partner_assign.py +++ b/addons/website_crm_partner_assign/tests/test_partner_assign.py @@ -16,18 +16,13 @@ class TestPartnerAssign(TransactionCase): def setUp(self): super(TestPartnerAssign, self).setUp() - def geo_find(addr, apikey): + def geo_find(addr): return { 'Wavre, Belgium': (50.7158956, 4.6128075), 'Cannon Hill Park, B46 3AG Birmingham, United Kingdom': (52.45216, -1.898578), }.get(addr) - patcher = patch('odoo.addons.base_geolocalize.models.res_partner.geo_find', wraps=geo_find) - patcher.start() - self.addCleanup(patcher.stop) - - patcher = patch('odoo.addons.website_crm_partner_assign.models.crm_lead.geo_find', - wraps=geo_find) + patcher = patch('odoo.addons.base_geolocalize.models.base_geocoder.GeoCoder.geo_find', wraps=geo_find) patcher.start() self.addCleanup(patcher.stop) |
