import logging import time import requests from odoo import api, models _logger = logging.getLogger(__name__) # System parameter holding the User-Agent sent to Nominatim. # Nominatim's usage policy requires a genuine, identifying User-Agent with a # valid contact. The stock Odoo User-Agent is shared by thousands of instances # and is rate-limited/blocked (HTTP 429), so we let each deployment set its own. USER_AGENT_PARAM = "partner_geolocalize_usability.user_agent" # Placeholder shown until the deployment sets a real value. It intentionally # does NOT identify any real service so an unconfigured instance fails loudly # rather than silently reusing a banned generic agent. USER_AGENT_PLACEHOLDER = ( "SET-ME partner_geolocalize_usability (contact: you@example.invalid)" ) # Nominatim's public API allows at most 1 request per second. We wait slightly # more than a second between calls to stay safely under the limit. NOMINATIM_MIN_INTERVAL = 1.1 class GeoCoder(models.AbstractModel): _inherit = "base.geocoder" def _get_nominatim_user_agent(self): """Return the configured User-Agent for Nominatim requests. Falls back to an obvious placeholder so a misconfigured instance is easy to spot in logs (and so we never silently reuse Odoo's banned agent). """ return ( self.env["ir.config_parameter"] .sudo() .get_param(USER_AGENT_PARAM, USER_AGENT_PLACEHOLDER) ) @api.model def _call_openstreetmap(self, addr, **kw): """Query Nominatim with a configurable User-Agent and rate limiting. Overrides the stock implementation to fix two problems that cause HTTP 429 ("Too many requests"): * the shared/generic Odoo User-Agent is blocked by Nominatim -> use a deployment-specific one from a system parameter; * bursts of requests exceed the 1 req/s policy -> throttle between calls. """ if not addr: _logger.info("invalid address given") return None url = "https://nominatim.openstreetmap.org/search" try: headers = {"User-Agent": self._get_nominatim_user_agent()} # Respect Nominatim's 1 req/s policy. Sleeping before the call keeps # the throttle effective even when callers loop over many records. time.sleep(NOMINATIM_MIN_INTERVAL) response = requests.get( url, headers=headers, params={"format": "json", "q": addr} ) _logger.info("openstreetmap nominatim service called") if response.status_code != 200: _logger.warning( "Request to openstreetmap failed.\nCode: %s\nContent: %s", response.status_code, response.content, ) result = response.json() except Exception as e: self._raise_query_error(e) if not result: return None geo = result[0] return float(geo["lat"]), float(geo["lon"])