Files
Odoo-Modules/fusion_plating/docs/superpowers/tests/2026-04-22-sub4-smoke.py
gsinghpal ee80673579 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>
2026-05-03 20:02:52 -04:00

190 lines
7.3 KiB
Python

"""Sub 4 smoke test — runs inside odoo-shell on entech.
Verifies: toggle-triggered banner, lazy review creation, roster-enforced
sign flow, compute-driven auto-dismiss on confirmed MO, re-open flow,
risk-band matrix.
"""
env = env # odoo-shell injects this
Partner = env['res.partner']
Part = env['fp.part.catalog']
Review = env['fp.contract.review']
Users = env['res.users']
Company = env.company
# ---- Roster: add admin to both rosters ---------------------------------
admin = env.ref('base.user_admin')
Company.write({
'x_fc_qa_assistant_user_ids': [(6, 0, [admin.id])],
'x_fc_qa_manager_user_ids': [(6, 0, [admin.id])],
})
assert admin in Company.x_fc_qa_assistant_user_ids
assert admin in Company.x_fc_qa_manager_user_ids
print('[OK] Company rosters populated')
# ---- Customer with contract_review_required ----------------------------
cust = Partner.create({
'name': 'Sub4 Smoke Customer',
'is_company': True,
'customer_rank': 1,
'x_fc_contract_review_required': True,
})
assert cust.x_fc_contract_review_required
print('[OK] Customer toggle set')
# ---- Part under that customer → banner visible -------------------------
part = Part.create({
'partner_id': cust.id,
'part_number': 'SUB4-SMOKE-001',
'revision': 'A',
})
part.invalidate_recordset()
assert part.x_fc_customer_requires_contract_review
# v19.0.4.10.0 — create() now auto-stages the review when the customer
# has enforcement enabled, so the review record exists immediately and
# the banner correspondingly hides (banner = "no review yet"). The
# action_start_contract_review path remains valid for parts created
# before enforcement was enabled or via flows that bypass create().
assert part.x_fc_contract_review_id, 'review should be auto-created on create()'
print('[OK] Review auto-created on part create')
# ---- Start contract review → opens the existing record ---------------
action = part.action_start_contract_review()
part.invalidate_recordset()
review = part.x_fc_contract_review_id
assert review, 'review should exist'
assert review.state == 'assistant_review'
assert review.customer_id == cust
assert review.part_number == 'SUB4-SMOKE-001'
print('[OK] action_start_contract_review opens the auto-created review')
# ---- Sign section 2.0 as admin (roster ok) -----------------------------
review.with_user(admin).action_sign_section_20()
review.invalidate_recordset()
assert review.s20_locked
assert review.s20_signed_by == admin
assert review.state == 'manager_review'
print('[OK] Section 2.0 signed; state advanced')
# ---- Sign section 3.0 → state = complete -------------------------------
review.s30_risk_consequence = '4'
review.s30_risk_likelihood = '4'
assert review.s30_risk_band == 'red', f'expected red, got {review.s30_risk_band}'
review.with_user(admin).action_sign_section_30()
review.invalidate_recordset()
assert review.s30_locked
assert review.state == 'complete'
print('[OK] Section 3.0 signed; review complete; risk matrix computed')
# ---- Banner now hidden (review complete) -------------------------------
part.invalidate_recordset()
assert not part.x_fc_contract_review_banner_visible
print('[OK] Banner hidden after completion')
# ---- Re-open (admin is manager) ----------------------------------------
review.action_reopen()
review.invalidate_recordset()
assert review.state == 'assistant_review'
assert not review.s20_locked
assert not review.s30_locked
print('[OK] Re-open cleared both sections')
# ---- Risk matrix parity with paper form --------------------------------
expected = {
(1, 1): 'green', (1, 2): 'green', (1, 3): 'green', (1, 4): 'green', (1, 5): 'green',
(2, 1): 'green', (2, 2): 'green', (2, 3): 'yellow', (2, 4): 'yellow', (2, 5): 'yellow',
(3, 1): 'green', (3, 2): 'yellow', (3, 3): 'yellow', (3, 4): 'yellow', (3, 5): 'red',
(4, 1): 'yellow', (4, 2): 'yellow', (4, 3): 'yellow', (4, 4): 'red', (4, 5): 'red',
(5, 1): 'yellow', (5, 2): 'yellow', (5, 3): 'red', (5, 4): 'red', (5, 5): 'red',
}
for (c, l), band in expected.items():
review.s30_risk_consequence = str(c)
review.s30_risk_likelihood = str(l)
assert review.s30_risk_band == band, (
f'matrix mismatch at c={c} l={l}: got {review.s30_risk_band} expected {band}'
)
print('[OK] 25-cell risk matrix matches paper form')
# ---- Dismiss flow ------------------------------------------------------
part2 = Part.create({
'partner_id': cust.id,
'part_number': 'SUB4-SMOKE-002',
'revision': 'A',
})
part2.invalidate_recordset()
assert part2.x_fc_contract_review_banner_visible
part2.action_dismiss_contract_review()
part2.invalidate_recordset()
assert part2.x_fc_contract_review_dismissed
assert not part2.x_fc_contract_review_banner_visible
print('[OK] Dismiss hides banner')
# ---- Non-roster user blocked ------------------------------------------
demo_user = Users.search([('login', '=', 'demo')], limit=1)
if demo_user:
part3 = Part.create({
'partner_id': cust.id,
'part_number': 'SUB4-SMOKE-003',
'revision': 'A',
})
part3.action_start_contract_review()
part3.invalidate_recordset()
rev3 = part3.x_fc_contract_review_id
try:
rev3.with_user(demo_user).action_sign_section_20()
assert False, 'non-roster user should have been blocked'
except Exception as e:
assert 'authorised' in str(e) or 'Plating Manager' in str(e)
print('[OK] Non-roster user blocked')
else:
print('[SKIP] No demo user for non-roster check')
# ---- Bulk-toggle (Check All / Clear All) buttons ----------------------
part4 = Part.create({
'partner_id': cust.id,
'part_number': 'SUB4-SMOKE-004',
'revision': 'A',
})
part4.action_start_contract_review()
part4.invalidate_recordset()
rev4 = part4.x_fc_contract_review_id
rev4.action_check_all_section_20()
for f in rev4._SECTION_20_CHECKLIST:
assert rev4[f] is True, f'{f} should be True after Check All'
print('[OK] Section 2.0 Check All ticks all 10 boxes')
rev4.action_clear_all_section_20()
for f in rev4._SECTION_20_CHECKLIST:
assert rev4[f] is False, f'{f} should be False after Clear All'
print('[OK] Section 2.0 Clear All clears all 10 boxes')
rev4.action_check_all_section_30()
for f in rev4._SECTION_30_CHECKLIST:
assert rev4[f] is True, f'{f} should be True after Check All'
print('[OK] Section 3.0 Check All ticks all 11 boxes')
rev4.action_clear_all_section_30()
for f in rev4._SECTION_30_CHECKLIST:
assert rev4[f] is False, f'{f} should be False after Clear All'
print('[OK] Section 3.0 Clear All clears all 11 boxes')
# Lock section 2.0, bulk toggle should refuse
rev4.with_user(admin).action_sign_section_20()
try:
rev4.action_check_all_section_20()
assert False, 'bulk toggle should fail on locked section'
except Exception as e:
assert 'locked' in str(e).lower() or 'signed' in str(e).lower()
print('[OK] Bulk toggle blocked on locked section')
# ---- QWeb render -------------------------------------------------------
report = env.ref('fusion_plating_quality.action_report_contract_review')
pdf_bytes, mime = report._render_qweb_pdf('fusion_plating_quality.report_contract_review_qa005', [review.id])
assert pdf_bytes and pdf_bytes[:4] == b'%PDF'
print(f'[OK] QA-005 PDF rendered ({len(pdf_bytes)} bytes)')
# ---- Cleanup -----------------------------------------------------------
env.cr.rollback()
print('\n=== SUB 4 SMOKE PASS — all assertions held ===')