feat(portal): _fp_get_stage_timeline helper for detail-page timeline

Builds a 5-entry list (label, status, started_at, time_label, notes)
ordered by stage. Labels match the dashboard stepper exactly
(Received/Inspected/Plating/QC/Shipped) so the two surfaces tell
the same story. Inspected and Plating share in_progress_started_at
since state in_progress means both transitions happened.

Time labels use lowercase am/pm matching the mockup typography.
'complete' state correctly shows all 5 stages as done (caught by
new test).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsinghpal
2026-05-17 02:48:42 -04:00
parent 9d58f5f61e
commit 8c6718e352
2 changed files with 99 additions and 0 deletions

View File

@@ -113,6 +113,63 @@ class FpCustomerPortal(CustomerPortal):
partner = request.env.user.partner_id
return partner.commercial_partner_id
# ==========================================================================
# Customer-visible stage timeline (detail page)
# ==========================================================================
# 5 customer-facing stages aligned with the dashboard stepper.
# Each entry: (label, timestamp_field_name_on_fp_portal_job).
# Inspected and Plating share `in_progress_started_at` — when state moves
# away from 'received' it means inspection finished and plating started.
_FP_STAGES = [
('Received', 'received_at'),
('Inspected', 'in_progress_started_at'),
('Plating', 'in_progress_started_at'),
('QC', 'qc_started_at'),
('Shipped', 'shipped_at'),
]
# State -> active step index (matches the dashboard stepper logic in
# fp_portal_dashboard.xml so dashboard and detail page agree).
_FP_STATE_TO_STEP_IDX = {
'received': 0,
'in_progress': 2,
'quality_check': 3,
'ready_to_ship': 4,
'shipped': 5,
'complete': 5,
}
def _fp_get_stage_timeline(self, job):
"""Build a 5-entry timeline for the detail-page vertical view.
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).
"""
state_idx = self._FP_STATE_TO_STEP_IDX.get(job.state, 0)
out = []
for i, (label, ts_field) in enumerate(self._FP_STAGES):
if i < state_idx:
status = 'done'
elif i == state_idx:
status = 'active'
else:
status = 'pending'
ts = job[ts_field] if hasattr(job, ts_field) else None
time_label = ''
if ts and status in ('done', 'active'):
# "Mar 14 · 8:00a" — lowercase am/pm + truncated to single letter.
time_label = ts.strftime('%b %d · %-I:%M%p').lower().replace('am', 'a').replace('pm', 'p')
elif status == 'pending' and label == 'Shipped' and job.target_ship_date:
time_label = 'est. ' + job.target_ship_date.strftime('%b %d')
out.append({
'label': label,
'status': status,
'started_at': ts if ts and status in ('done', 'active') else None,
'time_label': time_label,
'notes': '',
})
return out
# ==========================================================================
# DASHBOARD
# ==========================================================================