Replace em-dashes and en-dashes with hyphens across 789 shipped source files (py/xml/js/scss) so the delivered module reads as human-written; em-dashes had become a recognizable AI-generated tell. Internal .md dev notes are excluded. The WO-sticker mojibake strippers keep their dash search targets (now written — / –). No logic changes: comments and display strings only; validated with py_compile + lxml parse. Rewrite the 7 customer notification emails to be intake-neutral (ship-in / drop-off / pickup) and repair-aware, and fix the Shipped email documents line (packing slip vs bill of lading; certificate only when issued). Subjects use a hyphen separator. Co-Authored-By: Claude Opus 4.8 (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)
|