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:
@@ -113,6 +113,63 @@ class FpCustomerPortal(CustomerPortal):
|
|||||||
partner = request.env.user.partner_id
|
partner = request.env.user.partner_id
|
||||||
return partner.commercial_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
|
# DASHBOARD
|
||||||
# ==========================================================================
|
# ==========================================================================
|
||||||
|
|||||||
@@ -74,3 +74,45 @@ class TestPortalDashboard(TransactionCase):
|
|||||||
self.assertGreaterEqual(job.in_progress_started_at, before)
|
self.assertGreaterEqual(job.in_progress_started_at, before)
|
||||||
# received_at must not be overwritten when state advances
|
# received_at must not be overwritten when state advances
|
||||||
self.assertTrue(job.received_at)
|
self.assertTrue(job.received_at)
|
||||||
|
|
||||||
|
def test_stage_timeline_for_job_in_quality_check(self):
|
||||||
|
"""Timeline returns 5 entries aligned with dashboard stepper labels."""
|
||||||
|
from odoo.addons.fusion_plating_portal.controllers.portal import FpCustomerPortal
|
||||||
|
Job = self.env['fusion.plating.portal.job']
|
||||||
|
job = Job.create({
|
||||||
|
'name': 'WO-TL-001',
|
||||||
|
'partner_id': self.partner.id,
|
||||||
|
'state': 'received',
|
||||||
|
})
|
||||||
|
job.state = 'in_progress'
|
||||||
|
job.state = 'quality_check'
|
||||||
|
timeline = FpCustomerPortal()._fp_get_stage_timeline(job)
|
||||||
|
self.assertEqual(len(timeline), 5)
|
||||||
|
labels = [s['label'] for s in timeline]
|
||||||
|
# Must match dashboard stepper labels in fp_portal_dashboard.xml
|
||||||
|
self.assertEqual(labels, ['Received', 'Inspected', 'Plating', 'QC', 'Shipped'])
|
||||||
|
statuses = [s['status'] for s in timeline]
|
||||||
|
# state=quality_check -> step_idx=3 -> QC active, 3 prior done, 1 after pending
|
||||||
|
self.assertEqual(statuses, ['done', 'done', 'done', 'active', 'pending'])
|
||||||
|
# Done + active stages have started_at set
|
||||||
|
self.assertIsNotNone(timeline[0]['started_at'], 'Received timestamp')
|
||||||
|
self.assertIsNotNone(timeline[1]['started_at'], 'Inspected timestamp')
|
||||||
|
self.assertIsNotNone(timeline[2]['started_at'], 'Plating timestamp')
|
||||||
|
self.assertIsNotNone(timeline[3]['started_at'], 'QC active timestamp')
|
||||||
|
# Pending stage has no started_at
|
||||||
|
self.assertIsNone(timeline[4]['started_at'], 'Shipped pending - no timestamp')
|
||||||
|
|
||||||
|
def test_stage_timeline_complete_state_marks_all_done(self):
|
||||||
|
"""state='complete' shows all 5 stages done (no active)."""
|
||||||
|
from odoo.addons.fusion_plating_portal.controllers.portal import FpCustomerPortal
|
||||||
|
Job = self.env['fusion.plating.portal.job']
|
||||||
|
job = Job.create({
|
||||||
|
'name': 'WO-TL-002',
|
||||||
|
'partner_id': self.partner.id,
|
||||||
|
'state': 'received',
|
||||||
|
})
|
||||||
|
for s in ('in_progress', 'quality_check', 'ready_to_ship', 'shipped', 'complete'):
|
||||||
|
job.state = s
|
||||||
|
timeline = FpCustomerPortal()._fp_get_stage_timeline(job)
|
||||||
|
statuses = [t['status'] for t in timeline]
|
||||||
|
self.assertEqual(statuses, ['done', 'done', 'done', 'done', 'done'])
|
||||||
|
|||||||
Reference in New Issue
Block a user