fix(contract-review): WO step routes to QA-005 + auto-stage on part create
Two bugs fixed in one drop, both targeting the contract review (QA-005)
enforcement gap reported on entech.
## Bug 1 — WO step routed to wrong wizard
Symptom: clicking Finish & Next or Record on a Contract Review step in
WH/JOB/00339 opened the generic measurement wizard with three fake
prompts (Reviewer Initials / Date Reviewed / QA-005 Approved). No path
to the actual QA-005 form from the work order.
Root cause: action_finish_and_advance + action_open_input_wizard had no
branch for recipe_node.default_kind == 'contract_review'. The step.kind
mapping collapses contract_review -> 'other' so kind-based detection
wouldn't have worked either; gate has to live at the recipe-node layer.
Fix in fusion_plating_jobs/models/fp_job_step.py (v19.0.8.14.6):
- action_finish_and_advance:329 calls _fp_contract_review_redirect
before the input-wizard branch
- action_open_input_wizard:844 same gate, keeps Record button consistent
- _fp_contract_review_redirect:866 (new) returns the part's
action_start_contract_review() unless review.state in
(complete, dismissed) — gate clears so the step can finish after
the operator signs QA-005.
## Bug 2 — Part create did not enforce contract review
Symptom: spec called for a banner-only UX. User wanted true automatic
enforcement on first part creation under an enforced customer.
Fix in fusion_plating_quality/models/fp_part_catalog.py (v19.0.4.10.0):
- @api.model_create_multi def create() override
- _fp_enforce_contract_review_on_create() helper auto-stages the
fp.contract.review record AND surfaces three prominent reminders:
1. Sticky bus.bus warning toast (top-right, doesn't auto-dismiss)
2. mail.activity (To Do) on the part for the current user
3. Smart button on the part form lights up (review now exists)
- Idempotent: skips parts that already carry a review id
- Soft-fails: bus or activity outage doesn't block part creation
- create()-only — write/update flows never re-trigger
Sub 4's existing info banner stays as a fourth surface.
## Tests
- fusion_plating_jobs/tests/test_fp_job_extensions.py:
+TestContractReviewStepRouting (5 tests covering both routing methods,
the complete/dismissed gate-clear, and non-CR step regression)
- fusion_plating_quality/tests/test_part_catalog_contract_review_enforcement.py
(NEW): 9 tests covering auto-create, batch create, idempotency,
activity surface, bus surface, write-must-not-retrigger, soft-fail.
- docs/superpowers/tests/2026-04-22-sub4-smoke.py: flipped the
"no review yet" assertion to "review auto-created" to match new
behavior. Sign-flow assertions unchanged.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -681,3 +681,148 @@ class TestPhase7Migration(TransactionCase):
|
||||
Path(__file__).parent.parent / 'scripts' / '__init__.py'
|
||||
)
|
||||
self.assertTrue(init.exists())
|
||||
|
||||
|
||||
class TestContractReviewStepRouting(TransactionCase):
|
||||
"""Regression coverage for the contract-review step gate added on
|
||||
fp.job.step.action_finish_and_advance / action_open_input_wizard.
|
||||
|
||||
Pre-fix behaviour: clicking Finish & Next or Record on a recipe step
|
||||
flagged default_kind='contract_review' opened the generic Record
|
||||
Inputs measurement wizard. Operators could not actually capture the
|
||||
QA-005 review from the work order.
|
||||
|
||||
Post-fix behaviour: both buttons return the part's
|
||||
action_start_contract_review() action when the linked review is not
|
||||
yet complete/dismissed; once signed off they fall through to the
|
||||
normal finish path so the WO can advance.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.partner = self.env['res.partner'].create({
|
||||
'name': 'CR-Test Customer',
|
||||
'x_fc_contract_review_required': True,
|
||||
})
|
||||
self.product = self.env['product.product'].create({'name': 'CR-W'})
|
||||
self.part = self.env['fp.part.catalog'].create({
|
||||
'name': 'CR-Test Part',
|
||||
'partner_id': self.partner.id,
|
||||
'part_number': 'CR-001',
|
||||
})
|
||||
self.wc = self.env['fp.work.centre'].create({
|
||||
'name': 'QA', 'code': 'QA', 'kind': 'inspection',
|
||||
})
|
||||
# Recipe with a single contract-review operation node
|
||||
self.recipe = self.env['fusion.plating.process.node'].create({
|
||||
'name': 'CR-Recipe',
|
||||
'node_type': 'recipe',
|
||||
})
|
||||
self.cr_node = self.env['fusion.plating.process.node'].create({
|
||||
'name': 'Contract Review',
|
||||
'node_type': 'operation',
|
||||
'parent_id': self.recipe.id,
|
||||
'sequence': 10,
|
||||
'default_kind': 'contract_review',
|
||||
})
|
||||
# Job + matching step (skip generator, build directly so the
|
||||
# test stays focused on the routing logic)
|
||||
self.job = self.env['fp.job'].create({
|
||||
'partner_id': self.partner.id,
|
||||
'product_id': self.product.id,
|
||||
'qty': 1.0,
|
||||
'part_catalog_id': self.part.id,
|
||||
'recipe_id': self.recipe.id,
|
||||
})
|
||||
self.step = self.env['fp.job.step'].create({
|
||||
'job_id': self.job.id,
|
||||
'name': 'Contract Review',
|
||||
'recipe_node_id': self.cr_node.id,
|
||||
'work_centre_id': self.wc.id,
|
||||
'sequence': 10,
|
||||
'kind': 'other',
|
||||
'state': 'in_progress',
|
||||
})
|
||||
|
||||
def _is_contract_review_action(self, action):
|
||||
return (
|
||||
isinstance(action, dict)
|
||||
and action.get('res_model') == 'fp.contract.review'
|
||||
and action.get('type') == 'ir.actions.act_window'
|
||||
)
|
||||
|
||||
def test_finish_and_advance_routes_to_qa005_when_no_review(self):
|
||||
action = self.step.action_finish_and_advance()
|
||||
self.assertTrue(
|
||||
self._is_contract_review_action(action),
|
||||
'Finish & Next on a contract_review step must open the '
|
||||
'fp.contract.review form, got: %r' % action,
|
||||
)
|
||||
# Side effect: lazy-create the review record
|
||||
self.assertTrue(self.part.x_fc_contract_review_id)
|
||||
|
||||
def test_open_input_wizard_routes_to_qa005_when_no_review(self):
|
||||
action = self.step.action_open_input_wizard()
|
||||
self.assertTrue(
|
||||
self._is_contract_review_action(action),
|
||||
'Record on a contract_review step must open the '
|
||||
'fp.contract.review form, got: %r' % action,
|
||||
)
|
||||
|
||||
def test_finish_and_advance_falls_through_when_review_complete(self):
|
||||
# Pre-create a complete review so the gate clears
|
||||
review = self.env['fp.contract.review'].create({
|
||||
'part_id': self.part.id,
|
||||
'state': 'complete',
|
||||
})
|
||||
self.part.x_fc_contract_review_id = review.id
|
||||
# With the gate cleared, Finish & Next should NOT return the
|
||||
# contract review action. Either it finishes the step (returns
|
||||
# True) or it opens the input wizard if prompts exist (returns
|
||||
# an act_window for fp.job.step.input.wizard). Both are
|
||||
# acceptable; what matters is it's not the contract review form.
|
||||
action = self.step.action_finish_and_advance()
|
||||
self.assertFalse(
|
||||
self._is_contract_review_action(action),
|
||||
'When review is complete the gate must clear, got: %r'
|
||||
% action,
|
||||
)
|
||||
|
||||
def test_finish_and_advance_falls_through_when_review_dismissed(self):
|
||||
review = self.env['fp.contract.review'].create({
|
||||
'part_id': self.part.id,
|
||||
'state': 'dismissed',
|
||||
})
|
||||
self.part.x_fc_contract_review_id = review.id
|
||||
action = self.step.action_finish_and_advance()
|
||||
self.assertFalse(
|
||||
self._is_contract_review_action(action),
|
||||
'When review is dismissed the gate must clear, got: %r'
|
||||
% action,
|
||||
)
|
||||
|
||||
def test_non_contract_review_step_unaffected(self):
|
||||
# A regular operation step (no default_kind=contract_review)
|
||||
# must not be intercepted by the new gate.
|
||||
plate_node = self.env['fusion.plating.process.node'].create({
|
||||
'name': 'Plating',
|
||||
'node_type': 'operation',
|
||||
'parent_id': self.recipe.id,
|
||||
'sequence': 20,
|
||||
'default_kind': 'plate',
|
||||
})
|
||||
plate_step = self.env['fp.job.step'].create({
|
||||
'job_id': self.job.id,
|
||||
'name': 'Plating',
|
||||
'recipe_node_id': plate_node.id,
|
||||
'work_centre_id': self.wc.id,
|
||||
'sequence': 20,
|
||||
'kind': 'wet',
|
||||
'state': 'in_progress',
|
||||
})
|
||||
action = plate_step.action_open_input_wizard()
|
||||
self.assertFalse(
|
||||
self._is_contract_review_action(action),
|
||||
'Non-CR steps must NOT be redirected to QA-005, got: %r'
|
||||
% action,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user