[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
This commit is contained in:
Robot Odoo
2018-11-17 18:41:18 +01:00
committed by GitHub
15 changed files with 300 additions and 82 deletions
+13
View File
@@ -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.
+6 -3
View File
@@ -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,
}
+12
View File
@@ -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>
@@ -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
@@ -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)
@@ -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."
)
+14 -63
View File
@@ -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],
@@ -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
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_base_geo_provider access_base_geo_provider model_base_geo_provider base.group_user 1 0 0 0
@@ -0,0 +1,3 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import test_geolocalize
@@ -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)
@@ -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>
@@ -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"
@@ -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>
@@ -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],
@@ -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)