Replace em-dashes and en-dashes with hyphens across 789 shipped source files (py/xml/js/scss) so the delivered module reads as human-written; em-dashes had become a recognizable AI-generated tell. Internal .md dev notes are excluded. The WO-sticker mojibake strippers keep their dash search targets (now written — / –). No logic changes: comments and display strings only; validated with py_compile + lxml parse. Rewrite the 7 customer notification emails to be intake-neutral (ship-in / drop-off / pickup) and repair-aware, and fix the Shipped email documents line (packing slip vs bill of lading; certificate only when issued). Subjects use a hyphen separator. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
162 lines
4.9 KiB
Python
162 lines
4.9 KiB
Python
# -*- 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
|