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:
@@ -63,6 +63,31 @@ class FpPortalJob(models.Model):
|
||||
string='Actual Ship Date',
|
||||
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(
|
||||
'fusion.plating.process.type',
|
||||
'fp_portal_job_process_type_rel',
|
||||
@@ -164,3 +189,36 @@ class FpPortalJob(models.Model):
|
||||
walk(child, depth + 1)
|
||||
walk(mo.x_fc_recipe_id, 0)
|
||||
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
|
||||
|
||||
@@ -57,3 +57,20 @@ class TestPortalDashboard(TransactionCase):
|
||||
self.assertEqual(active, 2)
|
||||
self.assertEqual(awaiting_review, 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)
|
||||
|
||||
Reference in New Issue
Block a user