Files
gsinghpal f08f328688 changes
2026-04-27 00:11:18 -04:00

211 lines
8.3 KiB
Python

# Battle test — real shop failure modes.
#
# This is the "what if my operator is sloppy / forgetful / lazy" suite.
# We document what the system does TODAY, then identify what's missing.
#
# Persona shorthand:
# Carlos = operator
# Mike = second operator
# Bob = supervisor / manager (admin in this DB)
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']
target = P.browse(2529)
part = Part.search([('x_fc_default_coating_config_id', '!=', False)], limit=1)
def make_job(po_suffix):
w = W.create({
'partner_id': target.id, 'po_pending': True,
'po_number': f'PO-BT-{po_suffix}',
'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()
return env['fp.job'].search([('sale_order_id', '=', so.id)], limit=1)
# ====================================================================== 1
print('='*72)
print('SCENARIO 1 — Carlos forgot to click Start. Realizes 2 hours later.')
print('='*72)
job = make_job('S1-' + fields.Datetime.now().strftime('%H%M%S'))
step = job.step_ids.sorted('sequence')[0]
print(f' Setup: {job.name}, step "{step.name}" state={step.state}')
print(f' Reality: Carlos started masking 2h ago but forgot to click.')
print(f' Now he clicks Start, then immediately Finish.')
step.button_start()
step.button_finish()
print(f' Result: state={step.state}, duration_actual={step.duration_actual:.4f} min')
print(f' → Lost 2h of clock time. NO way to back-date date_started without admin SQL.')
print(f' → date_started field is readonly=True on the form.')
print(f' GAP: No "Adjust Time" affordance for forgetful operators.')
# ====================================================================== 2
print()
print('='*72)
print('SCENARIO 2 — Carlos finished step physically. Forgot Finish. Went home.')
print('='*72)
job = make_job('S2-' + fields.Datetime.now().strftime('%H%M%S'))
step = job.step_ids.sorted('sequence')[0]
step.button_start()
print(f' Carlos starts {step.name} at {step.date_started}')
print(f' ... 12 hours later Mike notices the step is still in_progress ...')
# Simulate the time gap by setting started 12h ago
step.write({'date_started': fields.Datetime.now() - timedelta(hours=12)})
# Mike taps Finish now
step.button_finish()
print(f' Mike clicks Finish: duration_actual = {step.duration_actual:.1f} min')
print(f' Reality was probably 30 min. System recorded {step.duration_actual:.0f} min.')
print(f' Cost rollup is wildly wrong: cost_total = ${step.cost_total or 0:.2f}')
print(f' GAP: No way to retroactively correct the timelog interval.')
# ====================================================================== 3
print()
print('='*72)
print('SCENARIO 3 — Two operators tap Start on the same step.')
print('='*72)
job = make_job('S3-' + fields.Datetime.now().strftime('%H%M%S'))
step = job.step_ids.sorted('sequence')[0]
step.button_start()
print(f' Carlos clicks Start → state={step.state}, '
f'open logs={len(step.time_log_ids.filtered(lambda l: not l.date_finished))}')
try:
# Mike "logs in as himself" then taps Start on the same step
step.button_start()
open_logs = step.time_log_ids.filtered(lambda l: not l.date_finished)
print(f' Mike clicks Start → state={step.state}, open logs={len(open_logs)}')
if len(open_logs) >= 2:
print(f' ❌ TWO open timelogs created. duration_actual will double-count.')
except Exception as e:
print(f' ✓ Blocked: {e}')
# ====================================================================== 4
print()
print('='*72)
print('SCENARIO 4 — Operator finishes step #6 before #5 is started.')
print('='*72)
job = make_job('S4-' + fields.Datetime.now().strftime('%H%M%S'))
steps = job.step_ids.sorted('sequence')
step5 = steps[4]
step6 = steps[5]
print(f' Step #5: {step5.name} state={step5.state}')
print(f' Step #6: {step6.name} state={step6.state}')
try:
step6.button_start()
print(f' ❌ Allowed start of step #6 while step #5 still ready')
step6.button_finish()
print(f' Step #6 done. Step #5 still: {step5.state}')
except Exception as e:
print(f' Blocked: {str(e)[:80]}')
print(f' GAP: No predecessor enforcement. Steps are independent.')
print(f' REALITY: This may be intentional (parallel work in different tanks).')
print(f' But there\'s no "force serial" flag for steps that MUST be in order.')
# ====================================================================== 5
print()
print('='*72)
print('SCENARIO 5 — Job stuck mid-process. Manager wants to take over.')
print('='*72)
job = make_job('S5-' + fields.Datetime.now().strftime('%H%M%S'))
step = job.step_ids.sorted('sequence')[0]
step.write({'assigned_user_id': env.user.id})
step.button_start()
print(f' Step assigned to Carlos, in progress.')
print(f' Carlos is on vacation. Bob needs to reassign + finish.')
print(f' Bob views step → assigned_user_id={step.assigned_user_id.name}')
# Can Bob reassign?
try:
step.write({'assigned_user_id': env.user.id})
print(f' ✓ Bob reassigned step (write to assigned_user_id allowed)')
except Exception as e:
print(f' ❌ Reassign blocked: {e}')
# Bob finishes
step.button_finish()
print(f' Bob finishes: state={step.state}, finished_by={step.finished_by_user_id.name}')
# ====================================================================== 6
print()
print('='*72)
print('SCENARIO 6 — Bake window expired (operator at lunch). Override?')
print('='*72)
BW = env['fusion.plating.bake.window']
Bath = env['fusion.plating.bath']
bath = Bath.search([], limit=1)
expired = BW.create({
'bath_id': bath.id,
'plate_exit_time': fields.Datetime.now() - timedelta(hours=10),
'window_hours': 4.0,
'part_ref': 'BT-EXPIRED',
'quantity': 5,
})
# Cron updates state if past required_by
BW._cron_update_states()
expired.invalidate_recordset()
print(f' Bake window {expired.name}: state={expired.state}, '
f'required_by={expired.bake_required_by} (10h ago)')
# Try to start_bake on a missed_window
try:
expired.action_start_bake()
print(f' ⚠️ action_start_bake worked even on missed_window: state={expired.state}')
print(f' GAP: No guard against starting bake after missing window. Should require manager override.')
except Exception as e:
print(f' ✓ Blocked: {str(e)[:80]}')
# ====================================================================== 7
print()
print('='*72)
print('SCENARIO 7 — Operator clocks 6 hours on a step expected to take 30 min.')
print('='*72)
job = make_job('S7-' + fields.Datetime.now().strftime('%H%M%S'))
step = job.step_ids.sorted('sequence')[0]
step.duration_expected = 30 # 30 min
step.button_start()
# Simulate 6h elapsed
step.write({'date_started': fields.Datetime.now() - timedelta(hours=6)})
step.button_finish()
ratio = (step.duration_actual / step.duration_expected) if step.duration_expected else 0
print(f' duration_expected={step.duration_expected} min, duration_actual={step.duration_actual:.0f} min')
print(f' Ratio: {ratio:.1f}x expected')
print(f' GAP: System silently accepted 12x overrun. No alert, no chatter post.')
# ====================================================================== 8
print()
print('='*72)
print('SCENARIO 8 — Operator did 4 of 5 parts. 1 contaminated. Qty drift.')
print('='*72)
job = make_job('S8-' + fields.Datetime.now().strftime('%H%M%S'))
print(f' Job qty={job.qty}, qty_done={job.qty_done}, qty_scrapped={job.qty_scrapped}')
# Operator finishes all steps
for s in job.step_ids.sorted('sequence'):
if s.state in ('pending', 'ready'):
s.button_start()
if s.state == 'in_progress':
s.button_finish()
# Try to mark done — qty_done is still 0
try:
job.button_mark_done()
print(f' Job done: qty_done={job.qty_done}, qty_scrapped={job.qty_scrapped}')
print(f' ⚠️ System lets job close with qty_done=0 even though qty=5')
print(f' GAP: No reconciliation between qty + qty_done + qty_scrapped at close.')
except Exception as e:
print(f' Blocked: {str(e)[:80]}')
env.cr.commit()
print()
print('== Battle test complete ==')