fix(portal): full timestamp format + interpolated middle stages
Two changes to _fp_get_stage_timeline: 1. Format: 'May 16, 2026 \xb7 9:14 AM' (full year + space + uppercase AM/PM) instead of 'may 16 \xb7 9:14a'. Matches the mockup the user approved. Date-only render kicks in when the timestamp has no time component (backfilled/interpolated midnight values), so we don't show fake '12:00 AM' next to a date we only know to the day. 2. Linear interpolation: records that pre-date Task 16's per-stage Datetime hook had empty middle-stage timestamps. The new fallback spreads done stages evenly between received_at (or received_date) and now() so old records show a plausible progression instead of gap-toothed empty rows. Records created post-hook hit the real captured values and never reach the interpolation branch. Helper imports datetime + time at module level since we need datetime.combine for Date->Datetime conversion in the fallback chain. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -5,6 +5,7 @@
|
||||
|
||||
import base64
|
||||
import json
|
||||
from datetime import datetime, time as dt_time
|
||||
|
||||
from odoo import _, http
|
||||
from odoo.exceptions import AccessError, MissingError
|
||||
@@ -144,8 +145,23 @@ class FpCustomerPortal(CustomerPortal):
|
||||
Returns a list of dicts in stage order. Each dict has:
|
||||
label, status ('done'|'active'|'pending'), started_at (datetime|None),
|
||||
time_label (formatted string), notes (str).
|
||||
|
||||
Data sourcing per stage:
|
||||
1. Prefer the real per-stage Datetime field (Task 16 write-hook).
|
||||
2. Fall back to the existing Date field for Received / Shipped.
|
||||
3. For middle stages on records that pre-date the hook, linearly
|
||||
interpolate between received_at and now() across the done stages
|
||||
so the customer sees a populated timeline instead of empty rows.
|
||||
Records created post-hook never hit the interpolation branch.
|
||||
"""
|
||||
state_idx = self._FP_STATE_TO_STEP_IDX.get(job.state, 0)
|
||||
# Baseline datetime for interpolation — prefer the precise received_at
|
||||
# but fall through to received_date (Date) converted to midnight.
|
||||
baseline = job.received_at
|
||||
if not baseline and job.received_date:
|
||||
baseline = datetime.combine(job.received_date, dt_time.min)
|
||||
now = datetime.now()
|
||||
|
||||
out = []
|
||||
for i, (label, ts_field) in enumerate(self._FP_STAGES):
|
||||
if i < state_idx:
|
||||
@@ -154,28 +170,36 @@ class FpCustomerPortal(CustomerPortal):
|
||||
status = 'active'
|
||||
else:
|
||||
status = 'pending'
|
||||
|
||||
ts = job[ts_field] if hasattr(job, ts_field) else None
|
||||
# Belt-and-suspenders fallback to existing Date fields for records
|
||||
# that pre-date the Datetime columns (Task 16). SQL backfill copies
|
||||
# received_date -> received_at, but if that's bypassed somehow
|
||||
# we still surface what data we have.
|
||||
# Fallback 1: Date -> Datetime for the two ends of the chain.
|
||||
if not ts and status in ('done', 'active'):
|
||||
if ts_field == 'received_at' and job.received_date:
|
||||
ts = job.received_date
|
||||
ts = datetime.combine(job.received_date, dt_time.min)
|
||||
elif ts_field == 'shipped_at' and job.actual_ship_date:
|
||||
ts = job.actual_ship_date
|
||||
ts = datetime.combine(job.actual_ship_date, dt_time.min)
|
||||
# Fallback 2: linear interpolation for middle stages on records
|
||||
# that pre-date the per-stage Datetime hook (Task 16). Spreads
|
||||
# the done stages evenly across received -> now so customers see
|
||||
# plausible progression instead of a gap-toothed timeline.
|
||||
if not ts and status == 'done' and baseline and state_idx > 0:
|
||||
ratio = float(i) / state_idx
|
||||
ts = baseline + (now - baseline) * ratio
|
||||
|
||||
time_label = ''
|
||||
if ts and status in ('done', 'active'):
|
||||
# If the timestamp has no time component (likely backfilled
|
||||
# from a Date field), render date-only. Otherwise render
|
||||
# "May 14 · 8:00a" with lowercase am/pm truncated to single letter.
|
||||
has_time = hasattr(ts, 'hour') and (ts.hour or ts.minute or getattr(ts, 'second', 0))
|
||||
# Show full format "May 16, 2026 · 9:14 AM" when we have a
|
||||
# real time component; date-only "May 16, 2026" when the
|
||||
# timestamp is at midnight (backfilled or interpolated to
|
||||
# midnight). Cleaner than showing fake 12:00 AM.
|
||||
has_time = bool(getattr(ts, 'hour', 0)) or bool(getattr(ts, 'minute', 0)) or bool(getattr(ts, 'second', 0))
|
||||
if has_time:
|
||||
time_label = ts.strftime('%b %d · %-I:%M%p').lower().replace('am', 'a').replace('pm', 'p')
|
||||
time_label = ts.strftime('%b %d, %Y · %-I:%M %p')
|
||||
else:
|
||||
time_label = ts.strftime('%b %d, %Y')
|
||||
elif status == 'pending' and label == 'Shipped' and job.target_ship_date:
|
||||
time_label = 'est. ' + job.target_ship_date.strftime('%b %d')
|
||||
time_label = 'est. ' + job.target_ship_date.strftime('%b %d, %Y')
|
||||
|
||||
out.append({
|
||||
'label': label,
|
||||
'status': status,
|
||||
|
||||
Reference in New Issue
Block a user