feat(jobs): add fp.job._generate_steps_from_recipe (Task 2.4)
Native port of fusion_plating_bridge_mrp's _generate_workorders_from_recipe method. Walks the recipe tree, creates one fp.job.step per 'operation' node, formats child 'step' nodes as step instructions on chatter, respects opt-in/out overrides from fp.job.node.override. Adaptations from the original: - Creates fp.job.step (not mrp.workorder) - Maps fusion.plating.work.center to fp.work.centre via forward link (x_fc_fp_work_centre_id) or code fallback - Uses native field names (job_id, work_centre_id, etc.) - Drops work_role_id (not on fp.job.step yet — Task 2.6+) - Drops _fp_autofill_default_equipment (not yet on step) 5 new tests cover: basic generation, idempotency, no-recipe skip, opt-in override behaviour, recipe_node_id link. Manifest 19.0.1.2.0 → 19.0.1.3.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:
@@ -154,3 +154,109 @@ class TestFpJobNodeOverride(TransactionCase):
|
||||
})
|
||||
self.job.invalidate_recordset(['override_ids'])
|
||||
self.assertIn(ovr, self.job.override_ids)
|
||||
|
||||
|
||||
class TestFpJobStepsGenerator(TransactionCase):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.partner = self.env['res.partner'].create({'name': 'C'})
|
||||
self.product = self.env['product.product'].create({'name': 'W'})
|
||||
self.wc = self.env['fp.work.centre'].create({
|
||||
'name': 'Bath', 'code': 'BATH', 'kind': 'wet_line',
|
||||
})
|
||||
# Build a simple recipe: recipe → 2 operations + 1 opt-in op
|
||||
self.recipe = self.env['fusion.plating.process.node'].create({
|
||||
'name': 'TestRecipe',
|
||||
'node_type': 'recipe',
|
||||
})
|
||||
# Legacy work centre (recipe nodes still point at the legacy model).
|
||||
# Match the new fp.work.centre.code so the resolver picks it up.
|
||||
facility = self.env['fusion.plating.facility'].search([], limit=1)
|
||||
if not facility:
|
||||
facility = self.env['fusion.plating.facility'].create({
|
||||
'name': 'TestFacility',
|
||||
'code': 'TF',
|
||||
})
|
||||
legacy_wc = self.env['fusion.plating.work.center'].search(
|
||||
[('code', '=', 'BATH')], limit=1)
|
||||
if not legacy_wc:
|
||||
legacy_wc = self.env['fusion.plating.work.center'].create({
|
||||
'name': 'Bath',
|
||||
'code': 'BATH',
|
||||
'facility_id': facility.id,
|
||||
})
|
||||
self.legacy_wc = legacy_wc
|
||||
self.op1 = self.env['fusion.plating.process.node'].create({
|
||||
'name': 'Plating Bath',
|
||||
'node_type': 'operation',
|
||||
'parent_id': self.recipe.id,
|
||||
'sequence': 10,
|
||||
'estimated_duration': 30.0,
|
||||
'work_center_id': self.legacy_wc.id,
|
||||
})
|
||||
self.op2 = self.env['fusion.plating.process.node'].create({
|
||||
'name': 'Bake',
|
||||
'node_type': 'operation',
|
||||
'parent_id': self.recipe.id,
|
||||
'sequence': 20,
|
||||
'estimated_duration': 60.0,
|
||||
})
|
||||
self.opt_in = self.env['fusion.plating.process.node'].create({
|
||||
'name': 'Optional Inspect',
|
||||
'node_type': 'operation',
|
||||
'parent_id': self.recipe.id,
|
||||
'sequence': 30,
|
||||
'opt_in_out': 'opt_in',
|
||||
})
|
||||
|
||||
def _make_job(self, **kw):
|
||||
vals = {
|
||||
'partner_id': self.partner.id,
|
||||
'product_id': self.product.id,
|
||||
'qty': 1.0,
|
||||
'recipe_id': self.recipe.id,
|
||||
}
|
||||
vals.update(kw)
|
||||
return self.env['fp.job'].create(vals)
|
||||
|
||||
def test_generator_creates_steps(self):
|
||||
job = self._make_job()
|
||||
job._generate_steps_from_recipe()
|
||||
# 2 ops by default; opt_in skipped without an override
|
||||
self.assertEqual(len(job.step_ids), 2)
|
||||
|
||||
def test_generator_idempotent(self):
|
||||
job = self._make_job()
|
||||
job._generate_steps_from_recipe()
|
||||
first_count = len(job.step_ids)
|
||||
job._generate_steps_from_recipe()
|
||||
self.assertEqual(len(job.step_ids), first_count)
|
||||
|
||||
def test_generator_skips_no_recipe(self):
|
||||
job = self.env['fp.job'].create({
|
||||
'partner_id': self.partner.id,
|
||||
'product_id': self.product.id,
|
||||
'qty': 1.0,
|
||||
})
|
||||
job._generate_steps_from_recipe()
|
||||
self.assertFalse(job.step_ids)
|
||||
|
||||
def test_generator_respects_opt_in_override(self):
|
||||
job = self._make_job()
|
||||
self.env['fp.job.node.override'].create({
|
||||
'job_id': job.id,
|
||||
'node_id': self.opt_in.id,
|
||||
'included': True,
|
||||
})
|
||||
job._generate_steps_from_recipe()
|
||||
# 3 steps: 2 default + 1 opted-in
|
||||
self.assertEqual(len(job.step_ids), 3)
|
||||
|
||||
def test_generator_recipe_node_link(self):
|
||||
job = self._make_job()
|
||||
job._generate_steps_from_recipe()
|
||||
first_step = job.step_ids.sorted('sequence')[0]
|
||||
self.assertEqual(first_step.recipe_node_id, self.op1)
|
||||
self.assertEqual(first_step.duration_expected, 30.0)
|
||||
# Work centre resolved by code from legacy model
|
||||
self.assertEqual(first_step.work_centre_id, self.wc)
|
||||
|
||||
Reference in New Issue
Block a user