feat(plating): Sub 4 — Contract Review (optional, QA-005 1:1 PDF)

Per-part contract review record (fp.contract.review) gated by a
customer-level toggle, signed in two sections (QA Assistant → QA
Manager), settings-based signer rosters (no new res.groups), banner on
the part form that auto-dismisses once the first MO for the part hits
confirmed. QA-005 Rev. 0 paper form reproduced 1:1 in a QWeb PDF.

Never blocks MO/SO/WO — review is purely an audit artefact.

Smoke test run on entech: 12 assertions pass including the 25-cell
risk matrix parity with the paper form and 22 KB PDF render.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsinghpal
2026-04-22 21:43:06 -04:00
parent 98a8bc234b
commit 21da526aa7
17 changed files with 1472 additions and 2 deletions

View File

@@ -0,0 +1,146 @@
"""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
assert part.x_fc_contract_review_banner_visible, 'banner should be visible'
assert not part.x_fc_contract_review_id
print('[OK] Banner visible on fresh part')
# ---- Start contract review → record created + state = assistant_review
action = part.action_start_contract_review()
part.invalidate_recordset()
review = part.x_fc_contract_review_id
assert review, 'review should be created'
assert review.state == 'assistant_review'
assert review.customer_id == cust
assert review.part_number == 'SUB4-SMOKE-001'
print('[OK] Review created by action_start_contract_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')
# ---- 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 ===')