Files
Odoo-18.0-20251222/work_trace/models/wt_location_log.py
tocmo0nlord 80ae631d9c
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
Update model: add category field matching extension fields
2026-03-14 00:29:47 +00:00

172 lines
6.0 KiB
Python

from odoo import models, fields, api
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):
_name = 'wt.location.log'
_description = 'WorkTrace Location Log'
_order = 'arrived_at asc'
name = fields.Char(
string='Name',
compute='_compute_name',
store=True
)
date = fields.Date(string='Date', required=True, index=True)
arrived_at = fields.Datetime(string='Begin Time', required=True)
departed_at = fields.Datetime(string='End Time')
latitude = fields.Float(string='Latitude', digits=(10, 7))
longitude = fields.Float(string='Longitude', digits=(10, 7))
address = fields.Char(string='Address')
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(
string='Duration (hrs)',
compute='_compute_time_at_location',
store=True
)
distance_from_previous = fields.Float(
string='Distance (mi)',
digits=(10, 2)
)
travel_time_from_previous = fields.Float(
string='Travel Time (hrs)',
digits=(10, 2)
)
travel_mode = fields.Selection([
('driving', 'Driving'),
('walking', 'Walking'),
('transit', 'Transit'),
('cycling', 'Cycling'),
('unknown', 'Unknown'),
], string='Travel Mode', default='unknown')
source = fields.Selection([
('google_timeline', 'Google Timeline'),
('manual', 'Manual'),
], string='Source', default='google_timeline')
calendar_event_id = fields.Many2one(
'calendar.event',
string='Calendar Event',
ondelete='set null'
)
add_to_calendar = fields.Boolean(string='Add to Calendar', default=True)
notes = fields.Text(string='Notes')
@api.depends('place_name', 'address', 'latitude', 'longitude')
def _compute_name(self):
for rec in self:
rec.name = (
rec.place_name
or rec.address
or (f'{rec.latitude:.5f}, {rec.longitude:.5f}' if rec.latitude else 'Unknown Location')
)
@api.depends('arrived_at', 'departed_at')
def _compute_time_at_location(self):
for rec in self:
if rec.arrived_at and rec.departed_at:
delta = rec.departed_at - rec.arrived_at
rec.time_at_location = delta.total_seconds() / 3600
else:
rec.time_at_location = 0.0
@staticmethod
def _haversine_distance(lat1, lon1, lat2, lon2):
"""Calculate distance in miles between two GPS coordinates."""
R = 3958.8
lat1, lon1, lat2, lon2 = map(radians, [lat1, lon1, lat2, lon2])
dlat = lat2 - lat1
dlon = lon2 - lon1
a = sin(dlat / 2) ** 2 + cos(lat1) * cos(lat2) * sin(dlon / 2) ** 2
return R * 2 * atan2(sqrt(a), sqrt(1 - a))
def action_geocode(self):
"""Resolve coordinates to address and category using OpenStreetMap Nominatim."""
import requests
import time
for rec in self:
if not rec.latitude or not rec.longitude:
continue
url = 'https://nominatim.openstreetmap.org/reverse'
params = {
'lat': rec.latitude,
'lon': rec.longitude,
'format': 'json',
'addressdetails': 1,
}
headers = {'User-Agent': 'WorkTrace/1.0 (Odoo Module)'}
try:
resp = requests.get(url, params=params, headers=headers, timeout=10)
resp.raise_for_status()
result = resp.json()
addr = result.get('address', {})
rec.address = result.get('display_name', '')
rec.place_name = (
result.get('name')
or addr.get('amenity')
or addr.get('shop')
or addr.get('building')
or addr.get('office')
or addr.get('tourism')
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:
pass
def action_compute_distances(self):
"""Recompute distances and travel times for all logs grouped by date."""
dates = self.mapped('date')
for date in set(dates):
logs = self.search([('date', '=', date)], order='arrived_at asc')
prev = None
for log in logs:
if prev and prev.latitude and log.latitude:
log.distance_from_previous = self._haversine_distance(
prev.latitude, prev.longitude,
log.latitude, log.longitude
)
if prev.departed_at and log.arrived_at:
delta = log.arrived_at - prev.departed_at
log.travel_time_from_previous = max(delta.total_seconds() / 3600, 0)
prev = log