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:
gsinghpal
2026-04-19 10:47:01 -04:00
parent 6e964c230f
commit 2804168d9e
5 changed files with 252 additions and 21 deletions

View File

@@ -5,7 +5,7 @@
{
"name": "Fusion Plating — MRP Bridge",
'version': '19.0.6.5.0',
'version': '19.0.6.6.0',
'category': 'Manufacturing/Plating',
'summary': 'Bridge Fusion Plating facilities, baths and tanks to Odoo MRP work orders.',
'description': """

View File

@@ -420,8 +420,13 @@ class MrpProduction(models.Model):
# Bulk create work orders
if wo_vals_list:
created_wos = WorkOrder.create(wo_vals_list)
# Post step instructions to each WO's chatter where present
# Post step instructions to each WO's chatter where present.
# Also auto-fill the default equipment per WO (bath / tank /
# oven) when there's only one option for the facility — saves
# the planner a click on single-line shops.
for wo in created_wos:
if hasattr(wo, '_fp_autofill_default_equipment'):
wo._fp_autofill_default_equipment()
steps_txt = wo_steps.get(wo.sequence)
if steps_txt:
wo.message_post(

View File

@@ -28,11 +28,46 @@ class MrpWorkorder(models.Model):
# ------------------------------------------------------------------
x_fc_requires_bath = fields.Boolean(
string='Requires Bath/Tank',
compute='_compute_requires_bath',
compute='_compute_wo_kind',
store=False,
help='True when this WO involves a chemistry bath. Surfaced to '
'the form view so bath/tank fields render as required.',
)
x_fc_requires_oven = fields.Boolean(
string='Requires Oven',
compute='_compute_wo_kind',
store=False,
help='True when this WO is a bake/cure step. Surfaced to the '
'form view so the oven field renders as required.',
)
x_fc_wo_kind = fields.Selection(
[('wet', 'Wet / Bath'),
('bake', 'Oven / Bake'),
('mask', 'Mask / De-mask'),
('rack', 'Rack / De-rack'),
('inspect', 'Inspection / QC'),
('other', 'Other')],
string='WO Kind',
compute='_compute_wo_kind',
store=False,
help='High-level classification used by the form view to show '
'only the equipment fields that apply to this kind of WO.',
)
x_fc_oven_id = fields.Many2one(
'fusion.plating.bake.oven', string='Oven',
domain="[('facility_id', '=', x_fc_facility_id)]",
help='The specific oven this bake / cure WO ran in. Required '
'for bake WOs — multiple ovens means we need to pin '
'which one for the chart-recorder trail.',
)
x_fc_bake_temp = fields.Float(
string='Bake Temp (°F)', digits=(5, 1),
help='Setpoint temperature recorded for this bake WO.',
)
x_fc_bake_duration_hours = fields.Float(
string='Bake Duration (h)', digits=(5, 2),
help='Total bake time at temperature.',
)
x_fc_bath_id = fields.Many2one(
'fusion.plating.bath', string='Bath', tracking=True,
)
@@ -547,10 +582,97 @@ class MrpWorkorder(models.Model):
'zincate', 'alkalin', 'acid', 'electroless',
)
@api.depends('x_fc_bath_id', 'name', 'workcenter_id')
def _compute_requires_bath(self):
# Keyword fallbacks per kind. Lowercased name match.
BAKE_KEYWORDS = ('bake', 'oven', 'cure', 'heat treat')
MASK_KEYWORDS = ('mask', 'de-mask', 'demask', 'tape')
RACK_KEYWORDS = ('rack', 'de-rack', 'derack', 'fixture')
INSPECT_KEYWORDS = ('inspect', 'qa', 'qc', 'fai', 'final check')
@api.depends('x_fc_bath_id', 'x_fc_oven_id', 'name', 'workcenter_id')
def _compute_wo_kind(self):
for wo in self:
wo.x_fc_requires_bath = wo._fp_is_wet_process()
kind = wo._fp_classify_kind()
wo.x_fc_wo_kind = kind
wo.x_fc_requires_bath = kind == 'wet'
wo.x_fc_requires_oven = kind == 'bake'
@api.onchange('workcenter_id', 'x_fc_facility_id', 'x_fc_bath_id')
def _onchange_autofill_equipment(self):
"""If the facility has exactly ONE choice for the equipment this
WO needs, pre-pick it so the planner doesn't have to. Saves a
click per WO on shops that run a single line."""
for wo in self:
wo._fp_autofill_default_equipment()
def _fp_autofill_default_equipment(self):
"""Pin bath / tank / oven to the only-option-available default.
Rules (none of these overwrite an already-set value):
• Wet WO with no bath: if the facility has exactly one active
bath (or globally one bath when no facility set), pick it.
• Wet WO with a bath but no tank: if the bath has exactly
one tank, pick it.
• Bake WO with no oven: if the facility has exactly one
active oven, pick it.
Idempotent and safe to call repeatedly.
"""
self.ensure_one()
kind = self._fp_classify_kind()
Bath = self.env.get('fusion.plating.bath')
Tank = self.env.get('fusion.plating.tank')
Oven = self.env.get('fusion.plating.bake.oven')
facility = self.x_fc_facility_id
# ---- Bath ----
if kind == 'wet' and not self.x_fc_bath_id and Bath is not None:
bath_domain = [('active', '=', True)]
if facility and 'facility_id' in Bath._fields:
bath_domain.append(('facility_id', '=', facility.id))
baths = Bath.search(bath_domain, limit=2)
if len(baths) == 1:
self.x_fc_bath_id = baths.id
# ---- Tank ----
if kind == 'wet' and self.x_fc_bath_id and not self.x_fc_tank_id and Tank is not None:
tank_domain = [('active', '=', True)]
if 'bath_id' in Tank._fields:
tank_domain.append(('bath_id', '=', self.x_fc_bath_id.id))
tanks = Tank.search(tank_domain, limit=2)
if len(tanks) == 1:
self.x_fc_tank_id = tanks.id
# ---- Oven ----
if kind == 'bake' and not self.x_fc_oven_id and Oven is not None:
oven_domain = [('active', '=', True)]
if facility and 'facility_id' in Oven._fields:
oven_domain.append(('facility_id', '=', facility.id))
ovens = Oven.search(oven_domain, limit=2)
if len(ovens) == 1:
self.x_fc_oven_id = ovens.id
def _fp_classify_kind(self):
"""Bucket this WO into one of: wet / bake / mask / rack / inspect / other.
Priority: explicit linked equipment > workcenter process family >
WO name keyword. Wet wins over bake when both signals appear
(you can have an "alkaline clean before bake" — that's still wet).
"""
self.ensure_one()
if self._fp_is_wet_process():
return 'wet'
if self.x_fc_oven_id:
return 'bake'
name = (self.name or '').lower()
if any(k in name for k in self.BAKE_KEYWORDS):
return 'bake'
if any(k in name for k in self.MASK_KEYWORDS):
return 'mask'
if any(k in name for k in self.RACK_KEYWORDS):
return 'rack'
if any(k in name for k in self.INSPECT_KEYWORDS):
return 'inspect'
return 'other'
def _fp_is_wet_process(self):
"""Best-effort check: does this WO involve a chemistry bath?
@@ -583,17 +705,23 @@ class MrpWorkorder(models.Model):
• Wet (bath) WOs additionally need x_fc_bath_id + x_fc_tank_id —
for chemistry traceability and physical-location audit
(which exact tank ran the job).
• Bake WOs need x_fc_oven_id — multiple ovens means we have
to pin which one for the chart-recorder trail.
"""
from odoo.exceptions import UserError
for wo in self:
missing = []
if not wo.x_fc_assigned_user_id:
missing.append(_('Assigned Operator'))
if wo._fp_is_wet_process():
kind = wo._fp_classify_kind()
if kind == 'wet':
if not wo.x_fc_bath_id:
missing.append(_('Bath'))
if not wo.x_fc_tank_id:
missing.append(_('Tank'))
elif kind == 'bake':
if not wo.x_fc_oven_id:
missing.append(_('Oven'))
if missing:
raise UserError(_(
'Cannot start work order "%(wo)s" — please fill these '

View File

@@ -96,7 +96,12 @@
required="1"
options="{'no_create': True}"/>
<field name="x_fc_work_role_id" readonly="1"/>
<field name="x_fc_wo_kind" widget="badge" readonly="1"
decoration-info="x_fc_wo_kind == 'wet'"
decoration-warning="x_fc_wo_kind == 'bake'"
decoration-muted="x_fc_wo_kind in ('mask', 'rack', 'inspect', 'other')"/>
<field name="x_fc_requires_bath" invisible="1"/>
<field name="x_fc_requires_oven" invisible="1"/>
</xpath>
<!-- ============================================================
@@ -162,12 +167,21 @@
</group>
</xpath>
<!-- 5b. Plating Details tab (insert AFTER Time & Cost) -->
<!-- 5b. Process Details tab — content adapts to WO kind so
operators see only the equipment fields that matter for
their step (bath/tank for wet, oven for bake, etc.). -->
<xpath expr="//notebook/page[@name='time_tracking']" position="after">
<page string="Plating Details" name="plating_details">
<page string="Process Details" name="plating_details">
<!-- Always-visible: facility (set everywhere) -->
<group>
<group string="Bath &amp; Tank">
<group string="Where">
<field name="x_fc_facility_id"/>
</group>
</group>
<!-- Wet / bath WOs (plating, etch, rinse, strip, ...) -->
<group invisible="x_fc_wo_kind != 'wet'">
<group string="Bath &amp; Tank">
<field name="x_fc_bath_id"
required="x_fc_requires_bath"/>
<field name="x_fc_tank_id"
@@ -181,6 +195,54 @@
<field name="x_fc_dwell_time_minutes"/>
</group>
</group>
<!-- Bake / cure WOs -->
<group invisible="x_fc_wo_kind != 'bake'">
<group string="Oven">
<field name="x_fc_oven_id"
required="x_fc_requires_oven"/>
</group>
<group string="Bake Parameters">
<field name="x_fc_bake_temp"/>
<field name="x_fc_bake_duration_hours"/>
</group>
</group>
<!-- Rack / de-rack WOs -->
<group invisible="x_fc_wo_kind != 'rack'">
<group string="Rack">
<field name="x_fc_rack_id"/>
<field name="x_fc_rack_ref"/>
</group>
</group>
<!-- Mask / De-mask WOs — workcenter is the bench;
no extra equipment fields, just a hint -->
<group invisible="x_fc_wo_kind != 'mask'">
<div class="alert alert-info" role="alert">
Masking / de-masking — work centre identifies
the bench. Use chatter for any per-job notes
on tape pattern, masking material, etc.
</div>
</group>
<!-- Inspection / QC WOs -->
<group invisible="x_fc_wo_kind != 'inspect'">
<div class="alert alert-info" role="alert">
Inspection — record Fischerscope readings via
the Tablet Station (calibration std + n
measurements per part). Readings auto-link
to the CoC at MO done.
</div>
</group>
<!-- Generic WOs that don't fit any bucket -->
<group invisible="x_fc_wo_kind != 'other'">
<div class="alert alert-light text-muted" role="alert">
Generic operation — equipment is identified
by the work centre. Use chatter for job notes.
</div>
</group>
</page>
</xpath>