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:
@@ -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': """
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 '
|
||||
|
||||
@@ -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 & 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 & 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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user