# Internal-process walk — test time tracking, pause, skip, bake-window # auto-spawn, duration overrun. Persona: Carlos (operator) walking the # tablet station for a real plating job. # # Goals: # 1) Time tracking captures every start/stop interval correctly # 2) Multiple intervals (start/finish/start/finish) sum to duration_actual # 3) Pause / resume flow works (currently NOT implemented — gap to fix) # 4) Skip flow works for opt-in steps (currently NOT implemented) # 5) Wet plating step finishing auto-spawns a bake.window when the # coating requires hydrogen embrittlement relief # 6) Bake-window state machine reflects elapsed time import time from datetime import timedelta from odoo import fields # Set up a fresh job to walk. 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-INTERNAL-' + 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': part.x_fc_default_coating_config_id.id, 'quantity': 5, 'unit_price': 18.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] Fresh job {job.name} with {len(job.step_ids)} steps') # ====================================================================== STEP 1 print() print('='*72) print('STEP 1 — Carlos opens the first step on the tablet, clicks Start') print('='*72) first = job.step_ids.sorted('sequence')[0] print(f' Step: {first.name} (kind={first.kind}, state={first.state})') print(f' duration_expected: {first.duration_expected} min') before = fields.Datetime.now() first.button_start() print(f' After Start: state={first.state}, date_started={first.date_started}, started_by={first.started_by_user_id.name}') print(f' Open time-log rows: {len(first.time_log_ids.filtered(lambda l: not l.date_finished))}') # ====================================================================== STEP 2 print() print('='*72) print('STEP 2 — Carlos works for 6 seconds, then clicks Finish') print('='*72) time.sleep(6) first.button_finish() print(f' After Finish: state={first.state}, date_finished={first.date_finished}') print(f' Time-log rows: {len(first.time_log_ids)}') for log in first.time_log_ids: print(f' - {log.user_id.name} {log.date_started} → {log.date_finished or "OPEN"} = {log.duration_minutes:.3f} min') print(f' duration_actual: {first.duration_actual:.3f} min') print(f' ✓ Single interval captured cleanly') # ====================================================================== STEP 3 print() print('='*72) print('STEP 3 — Test pause/resume on the next step (currently NotImplementedError)') print('='*72) second = job.step_ids.sorted('sequence')[1] second.button_start() print(f' Started step: {second.name} (state={second.state})') print(f' Carlos now needs a smoke break — clicks Pause') try: second.button_pause() print(f' ✓ Paused: state={second.state}, open timelog={len(second.time_log_ids.filtered(lambda l: not l.date_finished))}') except NotImplementedError as e: print(f' ❌ button_pause not implemented: {e}') except Exception as e: print(f' ❌ {type(e).__name__}: {e}') # ====================================================================== STEP 4 print() print('='*72) print('STEP 4 — Test skip (currently NotImplementedError)') print('='*72) third = job.step_ids.sorted('sequence')[2] print(f' Step: {third.name}, state={third.state}') print(f' Planner wants to skip this opt-in step') try: third.button_skip() print(f' ✓ Skipped: state={third.state}') except NotImplementedError as e: print(f' ❌ button_skip not implemented: {e}') except Exception as e: print(f' ❌ {type(e).__name__}: {e}') # ====================================================================== STEP 5 print() print('='*72) print('STEP 5 — Wet plating step finishes, does a bake.window auto-spawn?') print('='*72) # Find a step with kind='wet' (or use step #4 as plating analog) wet_step = job.step_ids.filtered(lambda s: 'plating' in (s.name or '').lower())[:1] if not wet_step: wet_step = job.step_ids.sorted('sequence')[3:4] print(f' Using as plating step: {wet_step.name} (kind={wet_step.kind})') coating = job.coating_config_id print(f' Coating: {coating.name}') print(f' coating.requires_bake_relief: {coating.requires_bake_relief}') print(f' coating.bake_window_hours: {coating.bake_window_hours}') # Count bake.window before BW = env['fusion.plating.bake.window'] bw_before = BW.search_count([('part_ref', '=', job.name)]) print(f' Bake windows for this job BEFORE finish: {bw_before}') # Skip if currently in_progress (it is — paused step #2 still open) if wet_step.state in ('pending', 'ready'): wet_step.button_start() if wet_step.state == 'in_progress': wet_step.button_finish() print(f' After Finish: state={wet_step.state}') bw_after = BW.search_count([('part_ref', '=', job.name)]) print(f' Bake windows for this job AFTER finish: {bw_after}') if coating.requires_bake_relief and bw_after == bw_before: print(f' ❌ Coating requires bake relief BUT no bake.window was auto-created!') elif not coating.requires_bake_relief: print(f' (coating doesn\'t require bake relief — auto-spawn would skip anyway)') else: print(f' ✓ Bake window spawned') env.cr.commit() print() print('== Walk complete ==')