# Verify predecessor enforcement from odoo import fields W = env['fp.direct.order.wizard'] Line = env['fp.direct.order.line'] P = env['res.partner'] Part = env['fp.part.catalog'] target = P.browse(2529) part = Part.search([('x_fc_default_coating_config_id', '!=', False)], limit=1) w = W.create({ 'partner_id': target.id, 'po_pending': True, 'po_number': 'PO-S14V-' + fields.Datetime.now().strftime('%H%M%S'), 'invoice_strategy': 'net_terms', }) w._onchange_partner_id() Line.create({ 'wizard_id': w.id, 'part_catalog_id': part.id, 'coating_config_id': part.x_fc_default_coating_config_id.id, 'quantity': 5, 'unit_price': 20.0, }) r = w.action_create_order() so = env['sale.order'].browse(r['res_id']) so.action_confirm() job = env['fp.job'].search([('sale_order_id', '=', so.id)], limit=1) # Find plating step + flag its recipe node as serial-required plating = job.step_ids.filtered(lambda s: 'plating' in (s.name or '').lower())[:1] if plating and plating.recipe_node_id: plating.recipe_node_id.requires_predecessor_done = True print(f' Recipe author flagged "{plating.name}" requires_predecessor_done') plating.invalidate_recordset() print(f' Step picks it up via related: {plating.requires_predecessor_done}') # Try to start plating with earlier steps still ready print() print(f' [Operator] Tries to start plating WITHOUT finishing earlier steps:') try: plating.button_start() print(f' ❌ Allowed early start! state={plating.state}') except Exception as e: print(f' ✓ Blocked: {str(e)[:200]}') # Walk earlier steps to done print() print(f' [Operator] Walks earlier steps to done:') for s in job.step_ids.sorted('sequence'): if s == plating: break if s.state in ('pending', 'ready'): s.button_start() if s.state == 'in_progress': s.button_finish() print(f' Earlier steps now: {set(job.step_ids.filtered(lambda x: x.sequence < plating.sequence).mapped("state"))}') # Try plating again print() print(f' [Operator] Tries plating again after earlier steps done:') try: plating.button_start() print(f' ✓ Allowed: state={plating.state}') except Exception as e: print(f' ❌ Still blocked: {e}') # Test manager bypass via context print() print(f' Test manager bypass on a fresh job:') w2 = W.create({ 'partner_id': target.id, 'po_pending': True, 'po_number': 'PO-S14B-' + fields.Datetime.now().strftime('%H%M%S'), 'invoice_strategy': 'net_terms', }) w2._onchange_partner_id() Line.create({ 'wizard_id': w2.id, 'part_catalog_id': part.id, 'coating_config_id': part.x_fc_default_coating_config_id.id, 'quantity': 5, 'unit_price': 20.0, }) r2 = w2.action_create_order() so2 = env['sale.order'].browse(r2['res_id']) so2.action_confirm() job2 = env['fp.job'].search([('sale_order_id', '=', so2.id)], limit=1) plating2 = job2.step_ids.filtered(lambda s: 'plating' in (s.name or '').lower())[:1] # (the recipe_node already has requires_predecessor_done=True from earlier write) print(f' Plating step requires_predecessor_done: {plating2.requires_predecessor_done}') try: plating2.with_context(fp_skip_predecessor_check=True).button_start() print(f' ✓ Manager bypass: state={plating2.state}') except Exception as e: print(f' ❌ Bypass failed: {e}') env.cr.commit()