bemade_geo_distance: new module for getting driving distance and time from one partner to another.
This commit is contained in:
parent
a2d9f37ef2
commit
8fa832ccac
6 changed files with 186 additions and 0 deletions
1
bemade_geo_distance/__init__.py
Normal file
1
bemade_geo_distance/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
from . import models
|
||||
35
bemade_geo_distance/__manifest__.py
Normal file
35
bemade_geo_distance/__manifest__.py
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
#
|
||||
# Bemade Inc.
|
||||
#
|
||||
# Copyright (C) July 2023 Bemade Inc. (<https://www.bemade.org>).
|
||||
# Author: Marc Durepos (Contact : marc@bemade.org)
|
||||
#
|
||||
# This program is under the terms of the Odoo Proprietary License v1.0 (OPL-1)
|
||||
# It is forbidden to publish, distribute, sublicense, or sell copies of the Software
|
||||
# or modified copies of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||
# DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
||||
# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
# DEALINGS IN THE SOFTWARE.
|
||||
#
|
||||
{
|
||||
'name': 'Bemade Geo Distance',
|
||||
'version': '15.0.1.0.0',
|
||||
'summary': 'Utility module for calculating the distance driving between addresses.',
|
||||
'description': """Module for calculating the driving distance between addresses using the Google Route API.
|
||||
**IMPORTANT NOTE:** You must have an active Google Route API key configured in the Geo Localization
|
||||
settings (under General Settings).""",
|
||||
'category': 'Generic Modules/Others',
|
||||
'author': 'Bemade Inc.',
|
||||
'website': 'https://www.bemade.org',
|
||||
'license': 'OPL-1',
|
||||
'depends': ['base_geolocalize'],
|
||||
'data': [],
|
||||
'demo': [],
|
||||
'installable': True,
|
||||
'auto_install': False,
|
||||
}
|
||||
1
bemade_geo_distance/models/__init__.py
Normal file
1
bemade_geo_distance/models/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
from . import bemade_geo_router
|
||||
95
bemade_geo_distance/models/bemade_geo_router.py
Normal file
95
bemade_geo_distance/models/bemade_geo_router.py
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
from odoo import models, fields, tools, _, api
|
||||
from odoo.exceptions import UserError
|
||||
import requests
|
||||
import logging
|
||||
import datetime
|
||||
from typing import Literal
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class GeoRouter(models.AbstractModel):
|
||||
""" Abstract class used to call Google Route API and convert responses
|
||||
into useful distance and route data.
|
||||
"""
|
||||
_name = 'base.geo_router'
|
||||
_description = "Geographic Router"
|
||||
|
||||
units = Literal['metric', 'imperial']
|
||||
|
||||
@api.model
|
||||
def _get_api_key(self):
|
||||
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."
|
||||
))
|
||||
return apikey
|
||||
|
||||
@api.model
|
||||
def get_driving_distance_time(self, origin, destination, departure_time: datetime.datetime = None,
|
||||
arrival_time: datetime.date = None, units: units = 'metric',
|
||||
avoid_tolls=False, avoid_highways=False, avoid_ferries=False) -> (float, float):
|
||||
""" Calculates the route between two addresses and returns the distance it either in Kilometers or Miles and
|
||||
the time in minutes.
|
||||
|
||||
:param origin: The origin address (res.partner) from which to drive (origin).
|
||||
:param destination: The destination address (res.partner).
|
||||
:param departure_time: The departure time to use when planning the route (traffic aware).
|
||||
Cannot be set simultaneously with arrival_time.
|
||||
:param arrival_time: The arrival time to use when planning the route (traffic aware).
|
||||
Cannot be set simultaneously with departure_time.
|
||||
:param units: Type of units to return: 'imperial' for Miles, 'metric' for Kilometers.
|
||||
:param avoid_tolls: Whether Google should calculate a route avoiding tolls.
|
||||
:param avoid_highways: Whether Google should calculate a route avoiding highways.
|
||||
:param avoid_ferries: Whether Google should calculate a route avoiding ferries.
|
||||
:returns: tuple(distance: float, time: float)
|
||||
"""
|
||||
if departure_time and arrival_time:
|
||||
raise ValueError("Route cannot be calculated with both an arrival time and departure time.")
|
||||
apikey = self._get_api_key()
|
||||
origin_addr = self.env['base.geocoder'].geo_query_address(street=origin.street, zip=origin.zip,
|
||||
city=origin.city, state=origin.state_id.name,
|
||||
country=origin.country_id.name)
|
||||
destination_addr = self.env['base.geocoder'].geo_query_address(street=destination.street, zip=destination.zip,
|
||||
city=destination.city,
|
||||
state=destination.state_id.name,
|
||||
country=destination.country_id.name)
|
||||
params = {
|
||||
'origin': {
|
||||
'address': origin_addr,
|
||||
},
|
||||
'destination': {
|
||||
'address': destination_addr,
|
||||
},
|
||||
'travelMode': 'DRIVE',
|
||||
'computeAlternativeRoutes': False,
|
||||
'routeModifiers': {
|
||||
'avoidTolls': avoid_tolls,
|
||||
'avoidHighways': avoid_highways,
|
||||
'avoidFerries': avoid_ferries,
|
||||
},
|
||||
'languageCode': 'en-US',
|
||||
'units': units,
|
||||
}
|
||||
if departure_time or arrival_time:
|
||||
params.update({
|
||||
'routingPreference': 'TRAFFIC_AWARE',
|
||||
'departureTime' if departure_time else 'arrivalTime': departure_time or arrival_time
|
||||
})
|
||||
|
||||
url = "https://routes.googleapis.com/directions/v2:computeRoutes"
|
||||
distance_field = 'distanceMeters' if units == 'metric' else 'distanceMiles'
|
||||
result = requests.post(url,
|
||||
json=params,
|
||||
headers={'X-Goog-Api-Key': apikey,
|
||||
'Content-Type': 'application/json',
|
||||
'X-Goog-FieldMask': f"routes.duration,routes.{distance_field}"}).json()
|
||||
if 'routes' not in result:
|
||||
_logger.error(f"Failed to retrieve route between {origin_addr} and {destination_addr}. "
|
||||
f"Google response: {result}.")
|
||||
route = result['routes'][0]
|
||||
distance = (route[distance_field] / 1000) if units == 'metric' else route[distance_field]
|
||||
time = int(route['duration'].strip('s')) / 60
|
||||
return distance, time
|
||||
1
bemade_geo_distance/tests/__init__.py
Normal file
1
bemade_geo_distance/tests/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
from . import test_geo_router
|
||||
53
bemade_geo_distance/tests/test_geo_router.py
Normal file
53
bemade_geo_distance/tests/test_geo_router.py
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
from odoo.tests import TransactionCase, tagged
|
||||
import datetime
|
||||
import pytz
|
||||
|
||||
@tagged('-at_install', 'post_install')
|
||||
class TestGeoRouter(TransactionCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.apikey = cls.env['base.geo_router']._get_api_key()
|
||||
|
||||
def test_distance_time_basic(self):
|
||||
bemade, montreal = self._generate_partners()
|
||||
|
||||
distance, time = self.env['base.geo_router'].get_driving_distance_time(bemade, montreal)
|
||||
|
||||
self.assertTrue(20 < distance < 40)
|
||||
self.assertTrue(12 < time < 60)
|
||||
|
||||
def test_time_traffic(self):
|
||||
bemade, montreal = self._generate_partners()
|
||||
est = pytz.timezone('US/Eastern')
|
||||
today_naive = datetime.date.today()
|
||||
today = datetime.datetime(today_naive.year, today_naive.month, today_naive.day, 8, 30, tzinfo=est)
|
||||
arrival_time_local = today + datetime.timedelta(days=(2 - today.weekday()) % 7)
|
||||
arrival_time_utc = arrival_time_local.astimezone(pytz.utc)
|
||||
arrival_time = arrival_time_utc.strftime('%Y-%m-%dT%H:%M:%S.%fZ')
|
||||
print(arrival_time)
|
||||
|
||||
distance, time = self.env['base.geo_router'].get_driving_distance_time(bemade, montreal,
|
||||
arrival_time=arrival_time)
|
||||
|
||||
self.assertTrue(25 < time < 90)
|
||||
|
||||
@classmethod
|
||||
def _generate_partners(self):
|
||||
bemade = self.env['res.partner'].create({
|
||||
'name': 'Bemade Inc.',
|
||||
'street': '255 North Montcalm Blvd.',
|
||||
'city': 'Candiac',
|
||||
'zip': 'J5R 3L6',
|
||||
'state_id': self.env.ref('base.state_ca_qc').id,
|
||||
'country_id': self.env.ref('base.ca').id,
|
||||
})
|
||||
montreal = self.env['res.partner'].create({
|
||||
'name': 'City of Montreal',
|
||||
'street': '220 East René-Lévesque Blvd.',
|
||||
'city': 'Montreal',
|
||||
'zip': 'H2X 3A7',
|
||||
'state_id': self.env.ref('base.state_ca_qc').id,
|
||||
'country_id': self.env.ref('base.ca').id,
|
||||
})
|
||||
return bemade, montreal
|
||||
Loading…
Reference in a new issue