Update model: add category field matching extension fields
This commit is contained in:
@@ -2,42 +2,62 @@ 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 = {
|
||||||
|
'IN_PASSENGER_VEHICLE': 'In Vehicle',
|
||||||
|
'IN_VEHICLE': 'In Vehicle',
|
||||||
|
'IN_ROAD_VEHICLE': 'In Vehicle',
|
||||||
|
'IN_RAIL_VEHICLE': 'Rail / Transit',
|
||||||
|
'IN_TWO_WHEELER_VEHICLE': 'Motorcycle / Scooter',
|
||||||
|
'WALKING': 'Walking',
|
||||||
|
'ON_FOOT': 'Walking',
|
||||||
|
'RUNNING': 'Running',
|
||||||
|
'ON_BICYCLE': 'Cycling',
|
||||||
|
'STILL': 'Stationary',
|
||||||
|
'UNKNOWN': 'Unknown',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class WtLocationLog(models.Model):
|
class WtLocationLog(models.Model):
|
||||||
_name = 'wt.location.log'
|
_name = 'wt.location.log'
|
||||||
_description = 'WorkTrace Location Log'
|
_description = 'WorkTrace Location Log'
|
||||||
_order = 'arrived_at asc'
|
_order = 'arrived_at asc'
|
||||||
|
|
||||||
name = fields.Char(
|
name = fields.Char(
|
||||||
string='Location',
|
string='Name',
|
||||||
compute='_compute_name',
|
compute='_compute_name',
|
||||||
store=True
|
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='Arrived At', required=True)
|
arrived_at = fields.Datetime(string='Begin Time', required=True)
|
||||||
departed_at = fields.Datetime(string='Departed At')
|
departed_at = fields.Datetime(string='End Time')
|
||||||
|
|
||||||
latitude = fields.Float(string='Latitude', digits=(10, 7))
|
latitude = fields.Float(string='Latitude', digits=(10, 7))
|
||||||
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')
|
||||||
|
|
||||||
time_at_location = fields.Float(
|
time_at_location = fields.Float(
|
||||||
string='Time at Location (hrs)',
|
string='Duration (hrs)',
|
||||||
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 from Previous (mi)',
|
string='Distance (mi)',
|
||||||
digits=(10, 2)
|
digits=(10, 2)
|
||||||
)
|
)
|
||||||
travel_time_from_previous = fields.Float(
|
travel_time_from_previous = fields.Float(
|
||||||
string='Travel Time from Previous (hrs)',
|
string='Travel Time (hrs)',
|
||||||
digits=(10, 2)
|
digits=(10, 2)
|
||||||
)
|
)
|
||||||
travel_mode = fields.Selection([
|
travel_mode = fields.Selection([
|
||||||
('driving', 'Driving'),
|
('driving', 'Driving'),
|
||||||
('walking', 'Walking'),
|
('walking', 'Walking'),
|
||||||
('transit', 'Transit'),
|
('transit', 'Transit'),
|
||||||
|
('cycling', 'Cycling'),
|
||||||
('unknown', 'Unknown'),
|
('unknown', 'Unknown'),
|
||||||
], string='Travel Mode', default='unknown')
|
], string='Travel Mode', default='unknown')
|
||||||
|
|
||||||
@@ -54,7 +74,7 @@ class WtLocationLog(models.Model):
|
|||||||
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')
|
||||||
|
|
||||||
@api.depends('address', 'place_name', 'latitude', 'longitude')
|
@api.depends('place_name', 'address', 'latitude', 'longitude')
|
||||||
def _compute_name(self):
|
def _compute_name(self):
|
||||||
for rec in self:
|
for rec in self:
|
||||||
rec.name = (
|
rec.name = (
|
||||||
@@ -75,7 +95,7 @@ class WtLocationLog(models.Model):
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def _haversine_distance(lat1, lon1, lat2, lon2):
|
def _haversine_distance(lat1, lon1, lat2, lon2):
|
||||||
"""Calculate distance in miles between two GPS coordinates."""
|
"""Calculate distance in miles between two GPS coordinates."""
|
||||||
R = 3958.8 # Earth radius in miles
|
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 = lat2 - lat1
|
||||||
dlon = lon2 - lon1
|
dlon = lon2 - lon1
|
||||||
@@ -83,8 +103,9 @@ class WtLocationLog(models.Model):
|
|||||||
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 and category using OpenStreetMap Nominatim."""
|
||||||
import requests
|
import requests
|
||||||
|
import time
|
||||||
for rec in self:
|
for rec in self:
|
||||||
if not rec.latitude or not rec.longitude:
|
if not rec.latitude or not rec.longitude:
|
||||||
continue
|
continue
|
||||||
@@ -100,19 +121,40 @@ class WtLocationLog(models.Model):
|
|||||||
resp = requests.get(url, params=params, headers=headers, timeout=10)
|
resp = requests.get(url, params=params, headers=headers, timeout=10)
|
||||||
resp.raise_for_status()
|
resp.raise_for_status()
|
||||||
result = resp.json()
|
result = resp.json()
|
||||||
|
addr = result.get('address', {})
|
||||||
|
|
||||||
rec.address = result.get('display_name', '')
|
rec.address = result.get('display_name', '')
|
||||||
|
|
||||||
rec.place_name = (
|
rec.place_name = (
|
||||||
result.get('name')
|
result.get('name')
|
||||||
or result.get('address', {}).get('amenity')
|
or addr.get('amenity')
|
||||||
or result.get('address', {}).get('shop')
|
or addr.get('shop')
|
||||||
or result.get('address', {}).get('building')
|
or addr.get('building')
|
||||||
|
or addr.get('office')
|
||||||
|
or addr.get('tourism')
|
||||||
or ''
|
or ''
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Category: match OSM types to Google Timeline-style categories
|
||||||
|
osm_type = (
|
||||||
|
addr.get('amenity')
|
||||||
|
or addr.get('shop')
|
||||||
|
or addr.get('tourism')
|
||||||
|
or addr.get('leisure')
|
||||||
|
or addr.get('office')
|
||||||
|
or addr.get('building')
|
||||||
|
or result.get('type', '')
|
||||||
|
or result.get('class', '')
|
||||||
|
)
|
||||||
|
rec.category = osm_type.replace('_', ' ').title() if osm_type else rec.category
|
||||||
|
|
||||||
|
# Nominatim rate limit: 1 request per second
|
||||||
|
time.sleep(1)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def action_compute_distances(self):
|
def action_compute_distances(self):
|
||||||
"""Compute distances and travel times between sequential location logs for the same date."""
|
"""Recompute distances and travel times for all logs grouped by date."""
|
||||||
dates = self.mapped('date')
|
dates = self.mapped('date')
|
||||||
for date in set(dates):
|
for date in set(dates):
|
||||||
logs = self.search([('date', '=', date)], order='arrived_at asc')
|
logs = self.search([('date', '=', date)], order='arrived_at asc')
|
||||||
|
|||||||
Reference in New Issue
Block a user