feat(portal): per-stage timestamps on fp.portal.job

Adds received_at, in_progress_started_at, qc_started_at,
ready_to_ship_at, shipped_at - snapshotted on state change via
write() override using super().write() to avoid recursion. Required
for the vertical-timeline rendering on the job detail page (Phase 3).

Idempotent: re-transitioning to a state already-stamped does not
overwrite the original timestamp.

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

View File

@@ -63,6 +63,31 @@ class FpPortalJob(models.Model):
string='Actual Ship Date', string='Actual Ship Date',
tracking=True, tracking=True,
) )
# Per-stage Datetime timestamps for the customer-facing timeline.
# Snapshotted by write()/create() on state changes (idempotent).
received_at = fields.Datetime(
string='Received Timestamp',
readonly=True,
help='Auto-set when state first reaches received.',
)
in_progress_started_at = fields.Datetime(
string='In Progress Started At',
readonly=True,
)
qc_started_at = fields.Datetime(
string='QC Started At',
readonly=True,
)
ready_to_ship_at = fields.Datetime(
string='Ready to Ship At',
readonly=True,
)
shipped_at = fields.Datetime(
string='Shipped At',
readonly=True,
)
process_type_ids = fields.Many2many( process_type_ids = fields.Many2many(
'fusion.plating.process.type', 'fusion.plating.process.type',
'fp_portal_job_process_type_rel', 'fp_portal_job_process_type_rel',
@@ -164,3 +189,36 @@ class FpPortalJob(models.Model):
walk(child, depth + 1) walk(child, depth + 1)
walk(mo.x_fc_recipe_id, 0) walk(mo.x_fc_recipe_id, 0)
return result return result
# ==========================================================================
# Per-stage timestamp snapshots
# ==========================================================================
_STATE_TO_TS_FIELD = {
'received': 'received_at',
'in_progress': 'in_progress_started_at',
'quality_check': 'qc_started_at',
'ready_to_ship': 'ready_to_ship_at',
'shipped': 'shipped_at',
}
def write(self, vals):
if 'state' in vals:
ts_field = self._STATE_TO_TS_FIELD.get(vals['state'])
if ts_field:
now = fields.Datetime.now()
# Snapshot the timestamp only for records that don't have it yet,
# so re-transitioning to the same state doesn't overwrite history.
for rec in self:
if not rec[ts_field]:
super(FpPortalJob, rec).write({ts_field: now})
return super().write(vals)
@api.model_create_multi
def create(self, vals_list):
records = super().create(vals_list)
now = fields.Datetime.now()
for rec in records:
ts_field = self._STATE_TO_TS_FIELD.get(rec.state)
if ts_field and not rec[ts_field]:
super(FpPortalJob, rec).write({ts_field: now})
return records

View File

@@ -57,3 +57,20 @@ class TestPortalDashboard(TransactionCase):
self.assertEqual(active, 2) self.assertEqual(active, 2)
self.assertEqual(awaiting_review, 1) self.assertEqual(awaiting_review, 1)
self.assertEqual(ready_to_ship, 1) self.assertEqual(ready_to_ship, 1)
def test_state_change_snapshots_timestamp(self):
"""write({'state': 'in_progress'}) sets in_progress_started_at."""
from odoo import fields as odoo_fields
Job = self.env['fusion.plating.portal.job']
job = Job.create({
'name': 'WO-TS-001',
'partner_id': self.partner.id,
'state': 'received',
})
self.assertTrue(job.received_at, 'received_at set on create')
before = odoo_fields.Datetime.now()
job.state = 'in_progress'
self.assertTrue(job.in_progress_started_at, 'in_progress_started_at set')
self.assertGreaterEqual(job.in_progress_started_at, before)
# received_at must not be overwritten when state advances
self.assertTrue(job.received_at)