feat(plating): per-WO-kind equipment fields + smart auto-fill defaults
User caught two related issues from screenshots of the WO form:
1. The "Plating Details" tab was meaningless for non-wet WOs —
bath/tank/dwell/thickness all show as empty for masking, oven
bake, racking, and inspection steps. A shop with multiple ovens
had no way to record which oven a bake WO ran in.
2. When there's only ONE option (single oven, single bath), forcing
the manager to pick it on every WO is busywork — pin it
automatically.
**1. WO classification + per-kind equipment**
New `x_fc_wo_kind` (compute, non-stored) Selection field that buckets
each WO into one of: wet / bake / mask / rack / inspect / other.
Classification by priority:
• bath linked → wet
• oven linked → bake
• workcenter's process families wet → wet
• WO name keyword match (bake/oven/cure → bake;
mask/de-mask → mask; rack/de-rack → rack;
inspect/qa/qc/fai → inspect; default → other)
New equipment fields per kind:
• `x_fc_oven_id` (m2o fp.bake.oven) for bake WOs
• `x_fc_bake_temp`, `x_fc_bake_duration_hours` — bake parameters
• Existing bath/tank/rack/thickness reused for wet
• Existing rack reused for rack WOs
**2. Required-fields gate extended**
button_start now also requires `x_fc_oven_id` for bake WOs (alongside
the existing operator + bath/tank rules). Without an oven the
chart-recorder trail can't be tied back to the WO for compliance.
**3. View reorganized**
Process Details tab now shows only the equipment groups that apply
to this WO's kind (using `invisible="x_fc_wo_kind != 'bake'"` etc.).
Mask + Inspection + Other show informational alerts instead of
empty form fields. WO header shows a colour-coded kind badge.
**4. Smart auto-fill defaults**
New `_fp_autofill_default_equipment()` method on mrp.workorder. When
the facility has exactly ONE active option, it pre-pins:
• Bath → if facility has 1 active bath
• Tank → if the chosen bath has 1 active tank
• Oven → if facility has 1 active oven
Hooked from:
• `@api.onchange('workcenter_id', 'x_fc_facility_id', 'x_fc_bath_id')`
→ fills as user edits in the form
• Recipe → WO generation `_generate_workorders_from_recipe()`
→ fills at creation time so single-line shops never see an
empty bath/oven field
None of this overwrites an already-set value. Multi-line shops still
get a blank field to choose from.
**Simulator updates** (scripts/fp_e2e_workforce.py)
• Creates an oven if none exists
• Pins per-kind equipment in Hannah's planning step
• New PASS check: bake-WO auto-pinned to default oven
• New negative test 2b: bake WO with oven stripped → blocked
**Final E2E**: 54 PASS / 2 WARN / 0 FAIL out of 56 checks.
12 negative tests passing — all gates fire when triggered:
Tests 1-2 + 2b: WO start (operator + bath/tank + oven)
Tests 3-7: MO facility, cert spec, delivery POD, invoice
payment terms, thickness cal std
Tests 8-11: NCR close, CAPA close, discharge close, invoice ref
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -235,9 +235,19 @@ WO_OPERATORS = {
|
||||
}
|
||||
|
||||
step('HANNAH', 'Assigns each WO to a specific operator')
|
||||
# Pick a bath + a tank for any WO that needs wet-process traceability
|
||||
# Pick equipment for each WO kind so the gate fires only for missing ones
|
||||
test_bath = env['fusion.plating.bath'].search([], limit=1)
|
||||
test_tank = env['fusion.plating.tank'].search([], limit=1)
|
||||
test_oven = env['fusion.plating.bake.oven'].search([], limit=1)
|
||||
if not test_oven:
|
||||
# Create one if none exists yet — the test recipe needs it
|
||||
f = env['fusion.plating.facility'].search([], limit=1)
|
||||
test_oven = env['fusion.plating.bake.oven'].sudo().create({
|
||||
'name': 'Bake Oven 1', 'code': 'OVEN-1',
|
||||
'facility_id': f.id if f else False,
|
||||
'target_temp_min': 350.0, 'target_temp_max': 380.0,
|
||||
})
|
||||
show(' oven created', test_oven.name)
|
||||
|
||||
# Issue operator certifications for the bath's process type so the cert
|
||||
# gate doesn't block legitimate operators (in real life the manager
|
||||
@@ -279,23 +289,21 @@ for wo in mo.workorder_ids:
|
||||
op_user = users[operator_key]
|
||||
wo.sudo().x_fc_assigned_user_id = op_user.id
|
||||
|
||||
# If this is a wet-process WO (E-Nickel Plating, etch, rinse, etc.)
|
||||
# Hannah must also pin the exact bath + tank for traceability.
|
||||
is_wet = wo._fp_is_wet_process() if hasattr(wo, '_fp_is_wet_process') else False
|
||||
bath_assigned = tank_assigned = False
|
||||
if is_wet and test_bath and test_tank:
|
||||
# Pin per-kind equipment based on the WO's classification.
|
||||
kind = wo._fp_classify_kind() if hasattr(wo, '_fp_classify_kind') else 'other'
|
||||
extras = f' [{kind}]'
|
||||
if kind == 'wet' and test_bath and test_tank:
|
||||
wo.sudo().write({
|
||||
'x_fc_bath_id': test_bath.id,
|
||||
'x_fc_tank_id': test_tank.id,
|
||||
})
|
||||
bath_assigned = True
|
||||
tank_assigned = True
|
||||
wet_assignments.append(wo)
|
||||
extras = f' [WET — bath={test_bath.name}, tank={test_tank.name}]'
|
||||
elif kind == 'bake' and test_oven:
|
||||
wo.sudo().write({'x_fc_oven_id': test_oven.id})
|
||||
extras = f' [BAKE — oven={test_oven.name}]'
|
||||
|
||||
assignments.append((wo, op_user, operator_key))
|
||||
extras = ''
|
||||
if is_wet:
|
||||
extras = f' [WET — bath={test_bath.name if bath_assigned else "MISSING"}, tank={test_tank.name if tank_assigned else "MISSING"}]'
|
||||
show(f' WO {wo.id}', f'"{wo.name}" → {op_user.name}{extras}')
|
||||
|
||||
assigned_count = sum(1 for w, _, _ in assignments if w.x_fc_assigned_user_id)
|
||||
@@ -307,6 +315,15 @@ finding('PASS' if (not wet_assignments) or (wet_with_bath == len(wet_assignments
|
||||
'wet-WO bath+tank set',
|
||||
f'{wet_with_bath}/{len(wet_assignments)} wet WOs have both bath + tank')
|
||||
|
||||
# ===== Smart-default verification: bake WOs auto-pinned to the only oven? =====
|
||||
bake_wos_check = mo.workorder_ids.filtered(
|
||||
lambda w: hasattr(w, '_fp_classify_kind') and w._fp_classify_kind() == 'bake'
|
||||
)
|
||||
auto_oven = sum(1 for w in bake_wos_check if w.x_fc_oven_id)
|
||||
finding('PASS' if (not bake_wos_check) or (auto_oven == len(bake_wos_check)) else 'FAIL',
|
||||
'bake-WO auto-pinned to default oven',
|
||||
f'{auto_oven}/{len(bake_wos_check)} bake WOs got auto-filled oven')
|
||||
|
||||
# ===== Negative tests: validation MUST block bad starts =====
|
||||
banner('PHASE 4b — Negative tests: validation gates fire correctly')
|
||||
|
||||
@@ -326,6 +343,25 @@ finding('PASS' if gate_fired else 'FAIL',
|
||||
'blocked' if gate_fired else 'NOT blocked — validation broken')
|
||||
test_wo.sudo().x_fc_assigned_user_id = saved_op
|
||||
|
||||
# Test 2b: try to start a BAKE WO without oven → expect UserError
|
||||
bake_wo = mo.workorder_ids.filtered(
|
||||
lambda w: hasattr(w, '_fp_classify_kind') and w._fp_classify_kind() == 'bake'
|
||||
)[:1]
|
||||
if bake_wo:
|
||||
step('SYSTEM', 'Test 2b — bake WO with oven stripped')
|
||||
saved_oven = bake_wo.x_fc_oven_id.id
|
||||
bake_wo.sudo().x_fc_oven_id = False
|
||||
gate_fired = False
|
||||
try:
|
||||
bake_wo.sudo().button_start()
|
||||
except Exception as e:
|
||||
gate_fired = 'Oven' in str(e) or 'oven' in str(e).lower()
|
||||
show(' blocked with', str(e).splitlines()[0][:120])
|
||||
finding('PASS' if gate_fired else 'FAIL',
|
||||
'gate: missing oven on bake WO',
|
||||
'blocked' if gate_fired else 'NOT blocked')
|
||||
bake_wo.sudo().x_fc_oven_id = saved_oven
|
||||
|
||||
# Test 2: try to start a WET WO without bath/tank → expect UserError
|
||||
if wet_assignments:
|
||||
step('SYSTEM', 'Test 2 — wet WO with bath/tank stripped')
|
||||
|
||||
Reference in New Issue
Block a user