[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:
@@ -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.
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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."
|
||||
)
|
||||
@@ -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
|
||||
|
@@ -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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user