feat(plating): per-step compliance gates + backfill — 0 CRITICAL gaps

Per-step audit caught real enforcement bugs across all 9 WO kinds in
the recipe (Masking, Racking, Plating, De-Masking, Oven baking, etc.).
Five gates added or fixed; 0 CRITICAL gaps remain after a verification
run on a fresh MO.

**1. Bake-WO finish gate** (`_fp_check_required_fields_before_finish`)
button_finish on a bake WO now blocks unless:
  • x_fc_bake_temp set (Nadcap req — actual setpoint, not just oven)
  • x_fc_bake_duration_hours set (actual run time at temp)
  • x_fc_oven_id.chart_recorder_ref set (so the chart for THIS run
    can be retrieved by an auditor — required for AS9100/Nadcap)

Run-time data lives at FINISH, not START — operators don't know
temp/duration until the bake is done.

**2. Rack-WO start gate** added to the existing button_start gate.
Per-rack life tracking + which physical fixture handled the parts.

**3. Classifier priority fix** (`_fp_classify_kind`)
"Post-plate Inspection" was matching the `plat` wet keyword and
getting kind=wet (then required to have bath/tank). Reordered:
  1. Explicit equipment links (bath_id/oven_id)
  2. Specific keywords (inspect → mask → bake → rack)
     — bake before rack so "Oven bake (Post de-rack)" → bake
  3. Workcenter wet families
  4. Wet name keywords as last fallback

**4. Auto-populate target_thickness + dwell_time** at recipe→WO
generation. Plating WOs inherit:
  • thickness_target from coating_config.thickness_max
  • thickness_uom from coating_config.thickness_uom
  • dwell_time_minutes from recipe node's estimated_duration

So aerospace QC has the spec target on every WO without paper.

**5. Mask-WO start gate + masking_material field**
New x_fc_masking_material Selection (tape/plug/paint/silicone/wax/
mixed/other). Required to start a mask WO. Needed later when
stripping or replating because each material requires a different
removal process.

**View** (`mrp_workorder_views.xml`)
Process Details tab now branches by kind:
  wet  → Bath/Tank/Rack/Thickness/Dwell
  bake → Oven/Temp/Duration
  rack → Rack/Fixture
  mask → Masking Material
  inspect/other → informational alerts only
WO Kind shows as colour-coded badge in header.

**Backfill** (`scripts/fp_backfill.py`)
Idempotent script that catches up existing data:
  • chart_recorder_ref on every oven
  • rack_id on existing rack/de-rack WOs (91 backfilled)
  • bake_temp + bake_duration_hours on existing bake WOs (33)
  • masking_material on existing mask WOs (62)
  • thickness/dwell on existing plating WOs (38)
  • Cleared 7 legacy bath/tank from inspection WOs that had been
    misclassified by the OLD wet-keyword classifier.

**Per-step audit** (`scripts/fp_per_step_audit.py`)
Walks every WO of the most recent done MO and reports per-kind
which compliance fields are filled vs missing. Re-runnable to
catch regressions.

**Final state on freshly-run MO 00049:**
  • 0 CRITICAL gaps
  • 2 IMPORTANT gaps (dwell_time + rack_id on E-Nickel Plating —
    both inherited from recipe node data, not enforcement bugs)

Negative tests still passing (12 total).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsinghpal
2026-04-19 11:40:01 -04:00
parent 5020129c45
commit 4ffbdc596d
7 changed files with 607 additions and 27 deletions

View File

@@ -238,6 +238,18 @@ step('HANNAH', 'Assigns each WO to a specific operator')
# Pick a bath + a tank for any WO that needs wet-process traceability
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:
f0 = 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': f0.id if f0 else False,
'target_temp_min': 350.0, 'target_temp_max': 380.0,
'chart_recorder_ref': 'CR-OVEN1-2026',
})
# Make sure the oven has a chart_recorder_ref (new gate requirement)
if test_oven and not test_oven.chart_recorder_ref:
test_oven.sudo().chart_recorder_ref = f'CR-{test_oven.code}-2026'
# 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 +291,31 @@ 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 using the new classifier (post inspect/mask/
# rack/bake priority fix), so Post-plate Inspection no longer gets
# bath assigned just because its name contains "plat".
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().x_fc_oven_id = test_oven.id
extras = f' [BAKE — oven={test_oven.name}]'
elif kind == 'rack':
rack = env['fusion.plating.rack'].search([], limit=1)
if rack:
wo.sudo().x_fc_rack_id = rack.id
extras = f' [RACK — fixture={rack.name}]'
elif kind == 'mask':
wo.sudo().x_fc_masking_material = 'tape'
extras = ' [MASK — material=tape]'
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)
@@ -630,6 +650,14 @@ for wo, op_user, op_key in assignments:
n_readings = Reading.search_count([('production_id', '=', mo.id)])
show(' thickness readings', f'{n_readings} logged for {mo.name}')
# Bake operator records actuals BEFORE pressing finish (new gate)
if hasattr(wo, '_fp_classify_kind') and wo._fp_classify_kind() == 'bake':
wo.sudo().write({
'x_fc_bake_temp': 365.0,
'x_fc_bake_duration_hours': 4.0,
})
show(' bake actuals', '365°F × 4h recorded')
step(actor, 'Taps FINISH')
try:
if wo_op.state == 'progress':