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:
gsinghpal
2026-05-17 03:49:54 -04:00
parent cbed74e5eb
commit ba6f39375a

View File

@@ -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,