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>
147 lines
5.6 KiB
Python
147 lines
5.6 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
|
|
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 ===')
|