# Comprehensive internal-process walk. # # Phases: # A) Pause / resume — multiple intervals merge into duration_actual # B) Skip an opt-in step # C) Skipped steps don't block job mark-done # D) Wet plating step finish auto-spawns bake.window with right window_hours # E) Bake-window state evolves (awaiting_bake → bake_in_progress → baked) # F) Failure: try to start a step already done import time from datetime import timedelta 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'] Coating = env['fp.coating.config'] target = P.browse(2529) # Find or build a coating that requires bake relief. coating = Coating.search([('requires_bake_relief', '=', True)], limit=1) if not coating: coating = Coating.search([], limit=1) coating.requires_bake_relief = True coating.bake_window_hours = 4.0 coating.bake_temperature = 375.0 coating.bake_temperature_uom = 'F' coating.bake_duration_hours = 4.0 print(f'[setup] Configured {coating.name} to require bake relief (4h window @ 375°F for 4h)') else: print(f'[setup] Using existing bake-required coating: {coating.name} ({coating.bake_window_hours}h window)') # Build a part using this coating as default. part = Part.create({ 'partner_id': target.id, 'part_number': 'INT-' + fields.Datetime.now().strftime('%H%M%S'), 'revision': 'A', 'name': 'Internal-process test bracket', 'substrate_material': 'steel', 'x_fc_default_coating_config_id': coating.id, }) w = W.create({ 'partner_id': target.id, 'po_pending': True, 'po_number': 'PO-INT-' + fields.Datetime.now().strftime('%H%M%S'), 'invoice_strategy': 'net_terms', }) w._onchange_partner_id() ln = Line.new({'wizard_id': w.id}) ln.part_catalog_id = part ln._onchange_part_clears_variant() Line.create({ 'wizard_id': w.id, 'part_catalog_id': part.id, 'coating_config_id': coating.id, 'quantity': 5, 'unit_price': 22.0, }) result = w.action_create_order() so = env['sale.order'].browse(result['res_id']) so.action_confirm() job = env['fp.job'].search([('sale_order_id', '=', so.id)], limit=1) print(f'[setup] Job {job.name} with {len(job.step_ids)} steps') # ====================================================================== A print() print('='*72) print('A — Pause + resume on a step. Multiple intervals must merge.') print('='*72) masking = job.step_ids.sorted('sequence')[0] masking.button_start() print(f' start → state={masking.state}, open logs={len(masking.time_log_ids)}') time.sleep(2) masking.button_pause() print(f' pause → state={masking.state}, logs={len(masking.time_log_ids)}, ' f'log[0]={masking.time_log_ids[0].duration_minutes:.3f} min') time.sleep(2) masking.button_start() # resume print(f' resume → state={masking.state}, logs={len(masking.time_log_ids)}') time.sleep(2) masking.button_finish() print(f' finish → state={masking.state}, logs={len(masking.time_log_ids)}') total = sum(masking.time_log_ids.mapped('duration_minutes')) print(f' duration_actual={masking.duration_actual:.3f} min (sum of logs={total:.3f} min)') if abs(masking.duration_actual - total) < 0.001: print(f' ✓ Pause/resume merged correctly') else: print(f' ❌ Mismatch') # ====================================================================== B print() print('='*72) print('B — Skip an opt-in step') print('='*72) racking = job.step_ids.sorted('sequence')[1] print(f' Step: {racking.name} state={racking.state}') racking.button_skip() print(f' After Skip: state={racking.state}') if racking.state == 'skipped': print(f' ✓ Skip works') # ====================================================================== C — walk rest, then mark-done print() print('='*72) print('C — Walk remaining steps (some will spawn bake-window). Mark job done.') print('='*72) spawn_count_before = env['fusion.plating.bake.window'].search_count([]) for s in job.step_ids.sorted('sequence'): if s.state in ('done', 'skipped', 'cancelled'): continue if s.state in ('pending', 'ready'): s.button_start() if s.state == 'in_progress': s.button_finish() spawn_count_after = env['fusion.plating.bake.window'].search_count([]) created_bw = spawn_count_after - spawn_count_before print(f' Walked all remaining steps to done') print(f' Bake windows spawned during walk: {created_bw}') bws = env['fusion.plating.bake.window'].search([('part_ref', '=', job.name)]) for bw in bws: print(f' {bw.name}: state={bw.state}, plate_exit={bw.plate_exit_time}, required_by={bw.bake_required_by}, time_remaining={bw.time_remaining_display}') # ====================================================================== D — try to mark job done print() print('='*72) print('D — Mark job done (skipped+done steps both count as terminal)') print('='*72) try: job.button_mark_done() print(f' ✓ Job done — state={job.state}') except Exception as e: print(f' ❌ {e}') # ====================================================================== E — bake-window lifecycle if bws: bw = bws[0] print() print('='*72) print('E — Bake-window lifecycle: start → end') print('='*72) print(f' Before start: state={bw.state}, color={bw.status_color}') bw.action_start_bake() print(f' After start_bake: state={bw.state}, bake_start={bw.bake_start_time}, color={bw.status_color}') time.sleep(1) bw.action_end_bake() print(f' After end_bake: state={bw.state}, bake_end={bw.bake_end_time}, duration_h={bw.bake_duration_hours:.4f}') # ====================================================================== F — failure: start a done step print() print('='*72) print('F — Failure paths') print('='*72) done_step = job.step_ids.filtered(lambda s: s.state == 'done')[:1] if done_step: try: done_step.button_start() print(f' ❌ Allowed re-start of a done step') except Exception as e: print(f' ✓ Blocked: {str(e)[:80]}') # Try to skip an already-done step try: done_step.button_skip() print(f' ❌ Allowed skip of done step') except Exception as e: print(f' ✓ Blocked: {str(e)[:80]}') env.cr.commit() print() print('== Internal-process walk complete ==')