feat(fusion_plating_jobs): fp.job.step blocker_kind/reason/jump_target computes
Plan task P1.2. Reuses _fp_should_block_predecessors so the new compute stays in sync with the existing can_start logic. Drives the OWL GateViz component on the tablet — "Can't start yet — Waiting on Step N: X". Future work: extend with explicit branches for contract_review / parts_not_received / racking_required / manager_input as those gate models mature. Tests not run locally (no fusion_plating mount in odoo-modsdev). Verify on entech: -u fusion_plating_jobs --test-tags fp_jobs. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -2,3 +2,5 @@
|
||||
from . import test_fp_job_extensions
|
||||
from . import test_fp_job_milestone_cascade
|
||||
from . import test_qty_received_propagation
|
||||
from . import test_display_wo_name
|
||||
from . import test_blocker_compute
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2026 Nexa Systems Inc. — License OPL-1
|
||||
from odoo.tests.common import TransactionCase, tagged
|
||||
|
||||
|
||||
@tagged('-at_install', 'post_install', 'fp_jobs')
|
||||
class TestBlockerCompute(TransactionCase):
|
||||
"""fp.job.step.blocker_kind / blocker_reason / blocker_jump_target_*
|
||||
— Gate visualizer source of truth for the OWL GateViz component.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.partner = self.env['res.partner'].create({'name': 'Test Cust'})
|
||||
self.product = self.env['product.product'].create({'name': 'Test Prod'})
|
||||
self.job = self.env['fp.job'].create({
|
||||
'name': 'WH/JOB/T1',
|
||||
'partner_id': self.partner.id,
|
||||
'product_id': self.product.id,
|
||||
'qty': 1,
|
||||
})
|
||||
|
||||
def _make_step(self, name, sequence, state='ready'):
|
||||
return self.env['fp.job.step'].create({
|
||||
'job_id': self.job.id,
|
||||
'name': name,
|
||||
'sequence': sequence,
|
||||
'state': state,
|
||||
})
|
||||
|
||||
def test_terminal_step_has_no_blocker(self):
|
||||
step = self._make_step('Done step', 10, state='done')
|
||||
self.assertEqual(step.blocker_kind, 'none')
|
||||
self.assertEqual(step.blocker_reason, '')
|
||||
|
||||
def test_in_progress_step_has_no_blocker(self):
|
||||
step = self._make_step('Running step', 10, state='in_progress')
|
||||
self.assertEqual(step.blocker_kind, 'none')
|
||||
|
||||
def test_solo_ready_step_not_blocked(self):
|
||||
step = self._make_step('Solo', 10, state='ready')
|
||||
# No predecessor → blocker_kind = 'none'
|
||||
self.assertEqual(step.blocker_kind, 'none')
|
||||
self.assertEqual(step.blocker_reason, '')
|
||||
|
||||
def test_predecessor_open_blocks(self):
|
||||
s1 = self._make_step('Earlier', 10, state='in_progress')
|
||||
s2 = self._make_step('Later', 20, state='ready')
|
||||
# If recipe enforces sequential OR step requires predecessor,
|
||||
# blocker_kind should be 'predecessor'. Default depends on job
|
||||
# config; if neither flag triggers we'd see 'none' instead.
|
||||
s2.invalidate_recordset([
|
||||
'blocker_kind', 'blocker_reason',
|
||||
'blocker_jump_target_model', 'blocker_jump_target_id',
|
||||
])
|
||||
if s2._fp_should_block_predecessors():
|
||||
self.assertEqual(s2.blocker_kind, 'predecessor')
|
||||
self.assertIn(s1.name, s2.blocker_reason or '')
|
||||
self.assertEqual(s2.blocker_jump_target_model, 'fp.job.step')
|
||||
self.assertEqual(s2.blocker_jump_target_id, s1.id)
|
||||
else:
|
||||
self.assertEqual(s2.blocker_kind, 'none')
|
||||
|
||||
def test_explicit_requires_predecessor_blocks(self):
|
||||
s1 = self._make_step('Earlier', 10, state='ready')
|
||||
s2 = self._make_step('Later', 20, state='ready')
|
||||
s2.requires_predecessor_done = True
|
||||
s2.invalidate_recordset(['blocker_kind', 'blocker_reason'])
|
||||
self.assertEqual(s2.blocker_kind, 'predecessor')
|
||||
self.assertEqual(s2.blocker_jump_target_id, s1.id)
|
||||
Reference in New Issue
Block a user