feat: add 'flying' travel mode to selection
This commit is contained in:
@@ -2,7 +2,6 @@ from odoo import models, fields, api, _
|
|||||||
from math import radians, sin, cos, sqrt, atan2
|
from math import radians, sin, cos, sqrt, atan2
|
||||||
|
|
||||||
|
|
||||||
# Maps Google/OSM activity types to human-readable categories
|
|
||||||
CATEGORY_MAP = {
|
CATEGORY_MAP = {
|
||||||
'IN_PASSENGER_VEHICLE': 'In Vehicle',
|
'IN_PASSENGER_VEHICLE': 'In Vehicle',
|
||||||
'IN_VEHICLE': 'In Vehicle',
|
'IN_VEHICLE': 'In Vehicle',
|
||||||
@@ -25,11 +24,7 @@ class WtLocationLog(models.Model):
|
|||||||
_description = 'WorkTrace Location Log'
|
_description = 'WorkTrace Location Log'
|
||||||
_order = 'arrived_at asc'
|
_order = 'arrived_at asc'
|
||||||
|
|
||||||
name = fields.Char(
|
name = fields.Char(string='Name', compute='_compute_name', store=True)
|
||||||
string='Name',
|
|
||||||
compute='_compute_name',
|
|
||||||
store=True
|
|
||||||
)
|
|
||||||
date = fields.Date(string='Date', required=True, index=True)
|
date = fields.Date(string='Date', required=True, index=True)
|
||||||
arrived_at = fields.Datetime(string='Begin Time', required=True)
|
arrived_at = fields.Datetime(string='Begin Time', required=True)
|
||||||
departed_at = fields.Datetime(string='End Time')
|
departed_at = fields.Datetime(string='End Time')
|
||||||
@@ -38,8 +33,6 @@ class WtLocationLog(models.Model):
|
|||||||
longitude = fields.Float(string='Longitude', digits=(10, 7))
|
longitude = fields.Float(string='Longitude', digits=(10, 7))
|
||||||
address = fields.Char(string='Address')
|
address = fields.Char(string='Address')
|
||||||
place_name = fields.Char(string='Place / Business Name')
|
place_name = fields.Char(string='Place / Business Name')
|
||||||
|
|
||||||
# Category: OSM-derived for stops, activity-type for travel segments
|
|
||||||
category = fields.Char(string='Category')
|
category = fields.Char(string='Category')
|
||||||
|
|
||||||
time_at_location = fields.Float(
|
time_at_location = fields.Float(
|
||||||
@@ -47,19 +40,14 @@ class WtLocationLog(models.Model):
|
|||||||
compute='_compute_time_at_location',
|
compute='_compute_time_at_location',
|
||||||
store=True
|
store=True
|
||||||
)
|
)
|
||||||
distance_from_previous = fields.Float(
|
distance_from_previous = fields.Float(string='Distance (mi)', digits=(10, 2))
|
||||||
string='Distance (mi)',
|
travel_time_from_previous = fields.Float(string='Travel Time (hrs)', digits=(10, 2))
|
||||||
digits=(10, 2)
|
|
||||||
)
|
|
||||||
travel_time_from_previous = fields.Float(
|
|
||||||
string='Travel Time (hrs)',
|
|
||||||
digits=(10, 2)
|
|
||||||
)
|
|
||||||
travel_mode = fields.Selection([
|
travel_mode = fields.Selection([
|
||||||
('driving', 'Driving'),
|
('driving', 'Driving'),
|
||||||
('walking', 'Walking'),
|
('walking', 'Walking'),
|
||||||
('transit', 'Transit'),
|
|
||||||
('cycling', 'Cycling'),
|
('cycling', 'Cycling'),
|
||||||
|
('transit', 'Transit'),
|
||||||
|
('flying', 'Flying'),
|
||||||
('unknown', 'Unknown'),
|
('unknown', 'Unknown'),
|
||||||
], string='Travel Mode', default='unknown')
|
], string='Travel Mode', default='unknown')
|
||||||
|
|
||||||
@@ -68,11 +56,7 @@ class WtLocationLog(models.Model):
|
|||||||
('manual', 'Manual'),
|
('manual', 'Manual'),
|
||||||
], string='Source', default='google_timeline')
|
], string='Source', default='google_timeline')
|
||||||
|
|
||||||
calendar_event_id = fields.Many2one(
|
calendar_event_id = fields.Many2one('calendar.event', string='Calendar Event', ondelete='set null')
|
||||||
'calendar.event',
|
|
||||||
string='Calendar Event',
|
|
||||||
ondelete='set null'
|
|
||||||
)
|
|
||||||
add_to_calendar = fields.Boolean(string='Add to Calendar', default=True)
|
add_to_calendar = fields.Boolean(string='Add to Calendar', default=True)
|
||||||
notes = fields.Text(string='Notes')
|
notes = fields.Text(string='Notes')
|
||||||
|
|
||||||
@@ -89,35 +73,27 @@ class WtLocationLog(models.Model):
|
|||||||
def _compute_time_at_location(self):
|
def _compute_time_at_location(self):
|
||||||
for rec in self:
|
for rec in self:
|
||||||
if rec.arrived_at and rec.departed_at:
|
if rec.arrived_at and rec.departed_at:
|
||||||
delta = rec.departed_at - rec.arrived_at
|
rec.time_at_location = (rec.departed_at - rec.arrived_at).total_seconds() / 3600
|
||||||
rec.time_at_location = delta.total_seconds() / 3600
|
|
||||||
else:
|
else:
|
||||||
rec.time_at_location = 0.0
|
rec.time_at_location = 0.0
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _haversine_distance(lat1, lon1, lat2, lon2):
|
def _haversine_distance(lat1, lon1, lat2, lon2):
|
||||||
"""Calculate distance in miles between two GPS coordinates."""
|
|
||||||
R = 3958.8
|
R = 3958.8
|
||||||
lat1, lon1, lat2, lon2 = map(radians, [lat1, lon1, lat2, lon2])
|
lat1, lon1, lat2, lon2 = map(radians, [lat1, lon1, lat2, lon2])
|
||||||
dlat = lat2 - lat1
|
dlat, dlon = lat2 - lat1, lon2 - lon1
|
||||||
dlon = lon2 - lon1
|
|
||||||
a = sin(dlat / 2) ** 2 + cos(lat1) * cos(lat2) * sin(dlon / 2) ** 2
|
a = sin(dlat / 2) ** 2 + cos(lat1) * cos(lat2) * sin(dlon / 2) ** 2
|
||||||
return R * 2 * atan2(sqrt(a), sqrt(1 - a))
|
return R * 2 * atan2(sqrt(a), sqrt(1 - a))
|
||||||
|
|
||||||
def action_geocode(self):
|
def action_geocode(self):
|
||||||
"""Resolve coordinates to address using OpenStreetMap Nominatim.
|
"""Resolve coordinates to address using OpenStreetMap Nominatim.
|
||||||
|
Processes up to GEOCODE_BATCH_SIZE records per call to avoid HTTP worker timeouts.
|
||||||
Processes up to GEOCODE_BATCH_SIZE records per call to avoid HTTP
|
|
||||||
worker timeouts (Nominatim enforces a 1 req/sec rate limit).
|
|
||||||
Call repeatedly until the notification reports 0 remaining.
|
|
||||||
"""
|
"""
|
||||||
import requests
|
import requests
|
||||||
import time
|
import time
|
||||||
|
|
||||||
# From the selected/passed recordset, take only those needing geocoding
|
|
||||||
to_geocode = self.filtered(lambda r: r.latitude and r.longitude and not r.address)
|
to_geocode = self.filtered(lambda r: r.latitude and r.longitude and not r.address)
|
||||||
if not to_geocode:
|
if not to_geocode:
|
||||||
# If all selected already have addresses, allow re-geocoding but still batch
|
|
||||||
to_geocode = self.filtered(lambda r: r.latitude and r.longitude)
|
to_geocode = self.filtered(lambda r: r.latitude and r.longitude)
|
||||||
|
|
||||||
batch = to_geocode[:GEOCODE_BATCH_SIZE]
|
batch = to_geocode[:GEOCODE_BATCH_SIZE]
|
||||||
@@ -125,12 +101,7 @@ class WtLocationLog(models.Model):
|
|||||||
|
|
||||||
for rec in batch:
|
for rec in batch:
|
||||||
url = 'https://nominatim.openstreetmap.org/reverse'
|
url = 'https://nominatim.openstreetmap.org/reverse'
|
||||||
params = {
|
params = {'lat': rec.latitude, 'lon': rec.longitude, 'format': 'json', 'addressdetails': 1}
|
||||||
'lat': rec.latitude,
|
|
||||||
'lon': rec.longitude,
|
|
||||||
'format': 'json',
|
|
||||||
'addressdetails': 1,
|
|
||||||
}
|
|
||||||
headers = {'User-Agent': 'WorkTrace/1.0 (Odoo Module)'}
|
headers = {'User-Agent': 'WorkTrace/1.0 (Odoo Module)'}
|
||||||
try:
|
try:
|
||||||
resp = requests.get(url, params=params, headers=headers, timeout=10)
|
resp = requests.get(url, params=params, headers=headers, timeout=10)
|
||||||
@@ -148,16 +119,10 @@ class WtLocationLog(models.Model):
|
|||||||
or addr.get('tourism')
|
or addr.get('tourism')
|
||||||
or ''
|
or ''
|
||||||
)
|
)
|
||||||
|
|
||||||
osm_type = (
|
osm_type = (
|
||||||
addr.get('amenity')
|
addr.get('amenity') or addr.get('shop') or addr.get('tourism')
|
||||||
or addr.get('shop')
|
or addr.get('leisure') or addr.get('office') or addr.get('building')
|
||||||
or addr.get('tourism')
|
or result.get('type', '') or result.get('class', '')
|
||||||
or addr.get('leisure')
|
|
||||||
or addr.get('office')
|
|
||||||
or addr.get('building')
|
|
||||||
or result.get('type', '')
|
|
||||||
or result.get('class', '')
|
|
||||||
)
|
)
|
||||||
if osm_type and not rec.category:
|
if osm_type and not rec.category:
|
||||||
rec.category = osm_type.replace('_', ' ').title()
|
rec.category = osm_type.replace('_', ' ').title()
|
||||||
@@ -167,11 +132,7 @@ class WtLocationLog(models.Model):
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# Count all location logs still missing an address
|
remaining = self.search_count([('latitude', '!=', 0), ('address', '=', False)])
|
||||||
remaining = self.search_count([
|
|
||||||
('latitude', '!=', 0),
|
|
||||||
('address', '=', False),
|
|
||||||
])
|
|
||||||
|
|
||||||
if remaining:
|
if remaining:
|
||||||
msg = _('%d address(es) resolved. %d still need geocoding — click "Geocode Next %d" again to continue.') % (
|
msg = _('%d address(es) resolved. %d still need geocoding — click "Geocode Next %d" again to continue.') % (
|
||||||
@@ -193,11 +154,8 @@ class WtLocationLog(models.Model):
|
|||||||
}
|
}
|
||||||
|
|
||||||
def action_geocode_all_pending(self):
|
def action_geocode_all_pending(self):
|
||||||
"""Action to geocode the next batch of ALL unresolved records (ignores selection)."""
|
"""Geocode the next batch of ALL unresolved records (ignores selection)."""
|
||||||
pending = self.search([
|
pending = self.search([('latitude', '!=', 0), ('address', '=', False)], limit=GEOCODE_BATCH_SIZE)
|
||||||
('latitude', '!=', 0),
|
|
||||||
('address', '=', False),
|
|
||||||
], limit=GEOCODE_BATCH_SIZE)
|
|
||||||
if not pending:
|
if not pending:
|
||||||
return {
|
return {
|
||||||
'type': 'ir.actions.client',
|
'type': 'ir.actions.client',
|
||||||
@@ -220,8 +178,7 @@ class WtLocationLog(models.Model):
|
|||||||
for log in logs:
|
for log in logs:
|
||||||
if prev and prev.latitude and log.latitude:
|
if prev and prev.latitude and log.latitude:
|
||||||
log.distance_from_previous = self._haversine_distance(
|
log.distance_from_previous = self._haversine_distance(
|
||||||
prev.latitude, prev.longitude,
|
prev.latitude, prev.longitude, log.latitude, log.longitude
|
||||||
log.latitude, log.longitude
|
|
||||||
)
|
)
|
||||||
if prev.departed_at and log.arrived_at:
|
if prev.departed_at and log.arrived_at:
|
||||||
delta = log.arrived_at - prev.departed_at
|
delta = log.arrived_at - prev.departed_at
|
||||||
|
|||||||
Reference in New Issue
Block a user