feat(jobs): add fp.job.step model with state machine
Per-operation model replacing mrp.workorder for plating. Each step instantiates from a recipe operation node (recipe_node_id link). Container/step nodes from the recipe template are rendered at view time via that link — they don't get rows here. See spec §5.2 Option A. 7-state machine: pending → ready → in_progress → done, plus paused, skipped, cancelled. button_start/button_finish enforce the transitions. Job header gets step_ids + step_count, step_done_count, step_progress_pct, current_step_id (computed from steps). Equipment, audit fields, plating-spec fields, time logs, and release-ready validation come in Tasks 1.6 and 1.7. Manifest 19.0.8.3.1 → 19.0.8.4.0. Part of: native job model migration (spec 2026-04-25) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from . import test_fp_work_centre
|
||||
from . import test_fp_job_state_machine
|
||||
from . import test_fp_job_step_state_machine
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from odoo.tests.common import TransactionCase
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
|
||||
class TestFpJobStepStateMachine(TransactionCase):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.partner = self.env['res.partner'].create({'name': 'Cust'})
|
||||
self.product = self.env['product.product'].create({'name': 'Widget'})
|
||||
self.wc = self.env['fp.work.centre'].create({
|
||||
'name': 'WC', 'code': 'WC', 'kind': 'wet_line',
|
||||
})
|
||||
self.job = self.env['fp.job'].create({
|
||||
'partner_id': self.partner.id,
|
||||
'product_id': self.product.id,
|
||||
'qty': 1.0,
|
||||
})
|
||||
|
||||
def _make_step(self, **kw):
|
||||
vals = {
|
||||
'job_id': self.job.id,
|
||||
'name': 'Plating Bath',
|
||||
'sequence': 10,
|
||||
'work_centre_id': self.wc.id,
|
||||
}
|
||||
vals.update(kw)
|
||||
return self.env['fp.job.step'].create(vals)
|
||||
|
||||
def test_step_starts_pending(self):
|
||||
step = self._make_step()
|
||||
self.assertEqual(step.state, 'pending')
|
||||
|
||||
def test_button_start_requires_ready_or_paused(self):
|
||||
step = self._make_step()
|
||||
# state is 'pending' — should raise
|
||||
with self.assertRaises(UserError):
|
||||
step.button_start()
|
||||
|
||||
def test_button_start_moves_ready_to_in_progress(self):
|
||||
step = self._make_step()
|
||||
step.state = 'ready'
|
||||
step.button_start()
|
||||
self.assertEqual(step.state, 'in_progress')
|
||||
self.assertTrue(step.date_started)
|
||||
self.assertEqual(step.started_by_user_id, self.env.user)
|
||||
|
||||
def test_button_finish_requires_in_progress(self):
|
||||
step = self._make_step()
|
||||
with self.assertRaises(UserError):
|
||||
step.button_finish() # state is pending
|
||||
|
||||
def test_button_finish_moves_to_done(self):
|
||||
step = self._make_step()
|
||||
step.state = 'ready'
|
||||
step.button_start()
|
||||
step.button_finish()
|
||||
self.assertEqual(step.state, 'done')
|
||||
self.assertTrue(step.date_finished)
|
||||
self.assertEqual(step.finished_by_user_id, self.env.user)
|
||||
|
||||
def test_job_step_counts_update(self):
|
||||
# Add 3 steps; finish 1; verify computed counts on job header.
|
||||
s1 = self._make_step(name='Step 1', sequence=10)
|
||||
s2 = self._make_step(name='Step 2', sequence=20)
|
||||
s3 = self._make_step(name='Step 3', sequence=30)
|
||||
self.assertEqual(self.job.step_count, 3)
|
||||
self.assertEqual(self.job.step_done_count, 0)
|
||||
s1.state = 'done'
|
||||
# Force recompute
|
||||
self.job.invalidate_recordset(['step_done_count', 'step_progress_pct'])
|
||||
self.assertEqual(self.job.step_done_count, 1)
|
||||
self.assertAlmostEqual(self.job.step_progress_pct, 33.33, places=1)
|
||||
Reference in New Issue
Block a user