From ba6f39375a6925c3c183a5af0e5806ff3b1b6e9c Mon Sep 17 00:00:00 2001 From: gsinghpal Date: Sun, 17 May 2026 03:49:54 -0400 Subject: [PATCH] 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) --- .../controllers/portal.py | 48 ++++++++++++++----- 1 file changed, 36 insertions(+), 12 deletions(-) diff --git a/fusion_plating/fusion_plating_portal/controllers/portal.py b/fusion_plating/fusion_plating_portal/controllers/portal.py index b53aabc8..10d16c57 100644 --- a/fusion_plating/fusion_plating_portal/controllers/portal.py +++ b/fusion_plating/fusion_plating_portal/controllers/portal.py @@ -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,