diff --git a/work_trace/models/wt_location_log.py b/work_trace/models/wt_location_log.py new file mode 100644 index 00000000..2a887dfa --- /dev/null +++ b/work_trace/models/wt_location_log.py @@ -0,0 +1,129 @@ +from odoo import models, fields, api +from math import radians, sin, cos, sqrt, atan2 + + +class WtLocationLog(models.Model): + _name = 'wt.location.log' + _description = 'WorkTrace Location Log' + _order = 'arrived_at asc' + + name = fields.Char( + string='Location', + compute='_compute_name', + store=True + ) + date = fields.Date(string='Date', required=True, index=True) + arrived_at = fields.Datetime(string='Arrived At', required=True) + departed_at = fields.Datetime(string='Departed At') + + 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') + + time_at_location = fields.Float( + string='Time at Location (hrs)', + compute='_compute_time_at_location', + store=True + ) + distance_from_previous = fields.Float( + string='Distance from Previous (mi)', + digits=(10, 2) + ) + travel_time_from_previous = fields.Float( + string='Travel Time from Previous (hrs)', + digits=(10, 2) + ) + travel_mode = fields.Selection([ + ('driving', 'Driving'), + ('walking', 'Walking'), + ('transit', 'Transit'), + ('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('address', 'place_name', '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 # Earth radius in miles + 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 using OpenStreetMap Nominatim.""" + import requests + 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() + rec.address = result.get('display_name', '') + rec.place_name = ( + result.get('name') + or result.get('address', {}).get('amenity') + or result.get('address', {}).get('shop') + or result.get('address', {}).get('building') + or '' + ) + except Exception: + pass + + def action_compute_distances(self): + """Compute distances and travel times between sequential location logs for the same 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