Ir al contenido

Fechas y zonas horarias en Odoo: UTC, context_timestamp y trampas

Por qué tu cron dice que hoy es ayer y cómo calcular «hoy» en la zona del usuario
11 de octubre de 2026 por
Fechas y zonas horarias en Odoo: UTC, context_timestamp y trampas
ATEMI, Aitor Atencia

Odoo · Fechas · ORM

Un informe de «ventas de hoy» que a las 00:30 todavía muestra las de ayer. Un test que falla solo cuando CI se ejecuta de madrugada. Un email que anuncia la cita una hora antes de lo que es. Todos tienen la misma causa: Odoo guarda los Datetime en UTC sin zona horaria, y el código los compara como si fueran hora local.

Logo Odoo
En la base de datos todo es UTC; la zona horaria solo existe al mostrar.

La regla

UTC al guardar, zona del usuario al mostrar

Un campo fields.Datetime es un datetime naive (sin tzinfo) que representa un instante en UTC. El cliente web lo convierte a la zona del usuario al pintarlo y lo devuelve a UTC al guardarlo. En el servidor, en cambio, nadie convierte nada: si tu código hace aritmética con esas fechas, está trabajando en UTC lo sepa o no.

Además, Odoo 19 fija TZ=UTC en el arranque de cualquier proceso (servidor, cron o tests), así que datetime.now() y fields.Date.today() devuelven la fecha y hora de UTC aunque el contenedor esté configurado con Europe/Madrid.

from odoo import fields

# A las 00:30 del 12 de octubre en Madrid (22:30 UTC del día 11):
fields.Date.today()              # date(2026, 10, 11)  ← fecha UTC
fields.Date.context_today(self)  # date(2026, 10, 12)  ← fecha del usuario
fields.Datetime.now()            # datetime(2026, 10, 11, 22, 30)  naive UTC

El caso típico

Buscar los registros de «hoy»

El error clásico es filtrar con fields.Date.today() y medianoche UTC. Lo correcto es calcular el inicio y el fin del día en la zona del usuario y convertir esos dos límites a UTC antes de buscar:

from datetime import UTC, datetime, time, timedelta
from zoneinfo import ZoneInfo


def _today_bounds_utc(self):
    tz = ZoneInfo(self.env.user.tz or "UTC")
    today = fields.Date.context_today(self)
    start = datetime.combine(today, time.min, tzinfo=tz)
    end = start + timedelta(days=1)
    # Odoo compara con datetimes naive en UTC
    return (
        start.astimezone(UTC).replace(tzinfo=None),
        end.astimezone(UTC).replace(tzinfo=None),
    )


def _today_orders(self):
    start, end = self._today_bounds_utc()
    return self.env["sale.order"].search([
        ("date_order", ">=", start),
        ("date_order", "<", end),
    ])

Mostrar

Informes, emails y mensajes del chatter

En QWeb, t-field sobre un Datetime ya convierte a la zona del contexto. El problema aparece cuando formateas tú la fecha en Python, por ejemplo para un mensaje del chatter o para Telegram. Ahí hay que pasar por context_timestamp, que devuelve un datetime consciente de la zona del usuario:

# ❌ Muestra la hora UTC: "Cita a las 07:00" cuando en Madrid son las 09:00
body = f"Cita a las {appointment.start:%H:%M}"

# ✅ Convierte a la zona del usuario del entorno
local = fields.Datetime.context_timestamp(appointment, appointment.start)
body = f"Cita a las {local:%H:%M}"

Lo mismo vale para timestamps que llegan de APIs externas en epoch: conviértelos siempre con zona explícita y quítale el tzinfo al guardar, para no depender del TZ=UTC interno de Odoo.

last_seen = datetime.fromtimestamp(payload["ts"] / 1000, tz=UTC).replace(tzinfo=None)
Necesito…UsaEvita
El instante actual para guardarfields.Datetime.now()datetime.now() con zona local
La fecha de hoy para el usuariofields.Date.context_today(record)fields.Date.today()
Mostrar una hora en un textofields.Datetime.context_timestamp()Formatear el valor UTC tal cual
Mostrar una hora en QWebt-fieldt-out con el datetime crudo
Convertir un epoch externofromtimestamp(ts, tz=UTC)fromtimestamp(ts) sin zona

Resumen

Todo Datetime en Odoo es UTC naive y el servidor corre en UTC. «Hoy» es context_today, no today; los límites de un día se calculan en la zona del usuario y se convierten a UTC antes de buscar; y cualquier hora que formatees en Python pasa antes por context_timestamp. Los tests de estos casos, a ser posible, con una hora fija cercana a medianoche.

en Odoo
Refactorizar código Odoo: de legacy a buenas prácticas
Simplificación, separación de responsabilidades y tests que te dejan cambiar sin miedo