diff --git a/fusion_plating/fusion_plating_portal/controllers/portal.py b/fusion_plating/fusion_plating_portal/controllers/portal.py index ab557089..cfb064e2 100644 --- a/fusion_plating/fusion_plating_portal/controllers/portal.py +++ b/fusion_plating/fusion_plating_portal/controllers/portal.py @@ -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 # ========================================================================== diff --git a/fusion_plating/fusion_plating_portal/tests/test_portal_dashboard.py b/fusion_plating/fusion_plating_portal/tests/test_portal_dashboard.py index 347fbfb8..7d8f68cf 100644 --- a/fusion_plating/fusion_plating_portal/tests/test_portal_dashboard.py +++ b/fusion_plating/fusion_plating_portal/tests/test_portal_dashboard.py @@ -74,3 +74,45 @@ class TestPortalDashboard(TransactionCase): self.assertGreaterEqual(job.in_progress_started_at, before) # received_at must not be overwritten when state advances 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'])