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>
71 lines
2.9 KiB
Python
71 lines
2.9 KiB
Python
# -*- 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)
|