From 5bcd8a5548c3c935ed59528949aeb61c8c64af06 Mon Sep 17 00:00:00 2001 From: tocmo0nlord Date: Sat, 14 Mar 2026 02:44:41 +0000 Subject: [PATCH] fix: batch geocoding (max 20/call) to prevent worker timeout, add action_geocode_all_pending --- work_trace/models/wt_location_log.py | 80 ++++++++++++++++++++++++---- 1 file changed, 69 insertions(+), 11 deletions(-) diff --git a/work_trace/models/wt_location_log.py b/work_trace/models/wt_location_log.py index 5bc85007..d3a68247 100644 --- a/work_trace/models/wt_location_log.py +++ b/work_trace/models/wt_location_log.py @@ -1,4 +1,4 @@ -from odoo import models, fields, api +from odoo import models, fields, api, _ from math import radians, sin, cos, sqrt, atan2 @@ -17,6 +17,8 @@ CATEGORY_MAP = { 'UNKNOWN': 'Unknown', } +GEOCODE_BATCH_SIZE = 20 + class WtLocationLog(models.Model): _name = 'wt.location.log' @@ -103,12 +105,25 @@ class WtLocationLog(models.Model): return R * 2 * atan2(sqrt(a), sqrt(1 - a)) def action_geocode(self): - """Resolve coordinates to address and category using OpenStreetMap Nominatim.""" + """Resolve coordinates to address using OpenStreetMap Nominatim. + + 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 time - for rec in self: - if not rec.latitude or not rec.longitude: - continue + + # 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) + 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) + + batch = to_geocode[:GEOCODE_BATCH_SIZE] + processed = 0 + + for rec in batch: url = 'https://nominatim.openstreetmap.org/reverse' params = { 'lat': rec.latitude, @@ -124,8 +139,7 @@ class WtLocationLog(models.Model): addr = result.get('address', {}) rec.address = result.get('display_name', '') - - rec.place_name = ( + rec.place_name = rec.place_name or ( result.get('name') or addr.get('amenity') or addr.get('shop') @@ -135,7 +149,6 @@ class WtLocationLog(models.Model): or '' ) - # Category: match OSM types to Google Timeline-style categories osm_type = ( addr.get('amenity') or addr.get('shop') @@ -146,13 +159,58 @@ class WtLocationLog(models.Model): or result.get('type', '') or result.get('class', '') ) - rec.category = osm_type.replace('_', ' ').title() if osm_type else rec.category + if osm_type and not rec.category: + rec.category = osm_type.replace('_', ' ').title() - # Nominatim rate limit: 1 request per second + processed += 1 time.sleep(1) except Exception: pass + # Count all location logs still missing an address + remaining = self.search_count([ + ('latitude', '!=', 0), + ('address', '=', False), + ]) + + if remaining: + msg = _('%d address(es) resolved. %d still need geocoding — click "Geocode Next %d" again to continue.') % ( + processed, remaining, GEOCODE_BATCH_SIZE) + notif_type = 'warning' + else: + msg = _('All done! %d address(es) resolved.') % processed + notif_type = 'success' + + return { + 'type': 'ir.actions.client', + 'tag': 'display_notification', + 'params': { + 'title': _('Geocoding Batch Complete'), + 'message': msg, + 'type': notif_type, + 'sticky': True, + }, + } + + def action_geocode_all_pending(self): + """Action to geocode the next batch of ALL unresolved records (ignores selection).""" + pending = self.search([ + ('latitude', '!=', 0), + ('address', '=', False), + ], limit=GEOCODE_BATCH_SIZE) + if not pending: + return { + 'type': 'ir.actions.client', + 'tag': 'display_notification', + 'params': { + 'title': _('Nothing to Geocode'), + 'message': _('All records with coordinates already have addresses.'), + 'type': 'info', + 'sticky': False, + }, + } + return pending.action_geocode() + def action_compute_distances(self): """Recompute distances and travel times for all logs grouped by date.""" dates = self.mapped('date') @@ -168,4 +226,4 @@ class WtLocationLog(models.Model): 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 + prev = log \ No newline at end of file