ems.datetime_utils (models/shared/datetime_utils.py)EmsDatetimeUtils (_name = 'ems.datetime_utils') is a stateless AbstractModel of timezone
and float-hour helpers. Odoo stores Datetime fields as naive UTC and represents a “time of
day” (e.g. a schedule slot) as a plain float (8.5 = 8:30), so every consumer needing to
convert between “a float hour in the company/user’s local timezone” and “a naive UTC
datetime Odoo can store” goes through this mixin instead of re-deriving the conversion.
None of its methods read or write fields on self — some consumers inherit it as a mixin
(ems.attendance_schedule, ems.attendance_session, working_schedule.py, employee_autocheckout.py);
others just grab an instance directly, e.g. models/settings/settings.py’s
self.env['ems.datetime_utils']. Both are equally valid, since there’s no per-record state.
| Method | Purpose |
|---|---|
company_tz_name() |
The company’s own partner_id.tz (read with sudo()), else 'UTC'. |
current_tz() |
Always the company’s timezone, never the acting user’s nor the one in the context: see timezones.md. |
time_float_to_local_datetime(date, time_float) |
Combines a date and a float hour into an aware local datetime. |
time_float_to_utc_datetime(date, time_float) |
Same, converted to UTC. |
local_datetime_to_utc(dt) / utc_datetime_to_local(dt) |
Plain dt.astimezone(...) calls. |
datetime_to_odoo(dt) |
Strips tzinfo — Odoo’s Datetime fields are naive UTC. |
get_local_datetime() |
datetime.now(self.current_tz()). |
get_local_today() |
Today’s date in the company’s timezone. Use it (and get_local_datetime()) instead of date.today()/datetime.now(), which are UTC in Odoo. |
get_server_epoch_ms() |
The server’s clock (ms since epoch), for the web client’s server_clock.js. |
time_to_float(time) |
datetime.time → float hour. |
next_occurrence_utc(time_float) |
Given a float hour, the next naive-UTC moment that time occurs — today if not yet passed, otherwise tomorrow. Used for scheduling one-off cron-like triggers at a fixed local time. |
ranges_overlap(start_a, end_a, start_b, end_b) |
Plain half-open interval overlap test — no timezone awareness needed, both ranges must already be in the same units. |
time_string_to_float(value) |
"17:45" → 17.75. |
local_datetime_to_utc/utc_datetime_to_local/datetime_to_odoo named their parameter
datetime, shadowing the module-level from datetime import datetime import inside each
method’s own body. Harmless as written — none of the three actually needed the datetime
class inside their bodies, only the parameter instance’s own methods (.astimezone(),
.replace()) — but the same latent-trap pattern already flagged and fixed elsewhere in this
rollout (LimesurveyApi.count_participants’s list variable, ems.base.persistent_hash’s
bytes/hash). Renamed the parameter to dt; behavior unchanged, covered by
tests/test_shared_mixins.py::TestEmsDatetimeUtils::test_local_utc_roundtrip_does_not_shadow_the_datetime_class.
Class renamed ems_datetime_utils → EmsDatetimeUtils. Tabs → spaces.
tests/test_shared_mixins.py::TestEmsDatetimeUtils (new, 5 tests) — all against the bare
self.env['ems.datetime_utils'] recordset, since no method here touches instance state.