feat: add 'flying' travel mode to selection
Some checks failed
pre-commit / pre-commit (push) Has been cancelled
tests / Detect unreleased dependencies (push) Has been cancelled
tests / test with OCB (push) Has been cancelled
tests / test with Odoo (push) Has been cancelled

This commit is contained in:
2026-03-14 03:25:24 +00:00
parent 988d26df4e
commit cc8bd4ef00

View File

@@ -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