# -*- coding: utf-8 -*- # Copyright 2026 Nexa Systems Inc. # License OPL-1 (Odoo Proprietary License v1.0) # Part of the Fusion Plating product family. """Timezone helpers for Fusion Plating. The Postgres database stores all datetimes naive-UTC. Anything that is shown to a user — dashboards, PDFs, emails, OWL frontends — must be converted to a human's local timezone first. Resolution order for "what timezone does this user see": 1. The current user's `res.users.tz` 2. The current company's `x_fc_default_tz` (Fusion Plating setting) 3. UTC Use ``fp_user_tz(env)`` to get the resolved pytz tzinfo, then either convert datetimes yourself or use the convenience helpers ``fp_format`` / ``fp_isoformat_utc``. """ import pytz def fp_user_tz(env): """Return a pytz tzinfo for the current user (or company fallback).""" name = ( (env.user.tz if env and env.user else None) or (env.company.x_fc_default_tz if env and env.company else None) or 'UTC' ) try: return pytz.timezone(name) except Exception: return pytz.UTC def fp_to_user_tz(env, dt): """Convert a naive UTC datetime to a tz-aware datetime in the user's tz. Returns ``None`` if ``dt`` is falsy. Datetimes that already carry a tzinfo are converted in place; naive ones are assumed to be UTC (matching Odoo's storage convention). """ if not dt: return None tz = fp_user_tz(env) if dt.tzinfo is None: dt = pytz.UTC.localize(dt) return dt.astimezone(tz) def fp_format(env, dt, fmt='%Y-%m-%d %H:%M'): """Format a naive UTC datetime as a string in the user's tz. Returns an empty string when ``dt`` is falsy so callers can use the result directly in dicts / templates without ``if`` guards. """ if not dt: return '' return fp_to_user_tz(env, dt).strftime(fmt) def fp_isoformat_utc(dt): """Return an ISO-8601 string with an explicit UTC marker. Naive datetimes from Odoo are assumed UTC. Adding the ``+00:00`` suffix tells JavaScript ``new Date(...)`` to parse the string as UTC (without it, the browser interprets the string as *local* wall time and silently shifts it). Pair this with frontend code that calls ``.toLocaleString()`` on the resulting Date object so the user sees their own local time. """ if not dt: return '' if dt.tzinfo is None: dt = pytz.UTC.localize(dt) return dt.isoformat() def fp_time_ago(env, dt): """Return a 'just now / 5m ago / 2h ago / 3d ago' string. Both sides of the comparison are converted to UTC tz-aware first so that the delta is meaningful regardless of where Odoo or the user happens to be running. """ if not dt: return '' if dt.tzinfo is None: dt = pytz.UTC.localize(dt) from datetime import datetime now = datetime.now(pytz.UTC) delta = now - dt total_seconds = int(delta.total_seconds()) if total_seconds < 0: total_seconds = 0 if total_seconds < 60: return 'just now' minutes = total_seconds // 60 if minutes < 60: return f'{minutes}m ago' hours = minutes // 60 if hours < 24: return f'{hours}h ago' days = hours // 24 if days < 7: return f'{days}d ago' weeks = days // 7 remaining_days = days % 7 if remaining_days: return f'{weeks}w {remaining_days}d ago' return f'{weeks}w ago' # --------------------------------------------------------------------------- # Auto-detection used by the post_init_hook # --------------------------------------------------------------------------- _FALLBACK_TZ = 'America/Toronto' def detect_default_tz(env=None): """Best guess at a sensible default tz when the module is installed. Tries, in order: 1. The admin user's ``tz`` (Odoo sets it from the browser on login). 2. The current company's ``partner_id.tz``. 3. The host server's IANA timezone (Linux typical). 4. ``America/Toronto`` as the final fallback (this project is Canada-focused and that's the most likely correct guess). """ if env is not None: admin = env.ref('base.user_admin', raise_if_not_found=False) if admin and admin.tz: return admin.tz try: partner_tz = env.company.partner_id.tz except Exception: partner_tz = None if partner_tz: return partner_tz # Server-side detection — works on most Linux hosts. try: from datetime import datetime local = datetime.now().astimezone() name = str(local.tzinfo) if name and name in pytz.all_timezones: return name except Exception: pass try: with open('/etc/timezone', 'r') as fh: tz = fh.read().strip() if tz in pytz.all_timezones: return tz except Exception: pass return _FALLBACK_TZ