feat(plating): hard-required fields on WO start — operator + bath + tank
Some checks failed
fusion_accounting CI / test (fusion_accounting_ai) (push) Has been cancelled
fusion_accounting CI / test (fusion_accounting_core) (push) Has been cancelled
fusion_accounting CI / test (fusion_accounting_migration) (push) Has been cancelled

User audit caught: in the workforce E2E run we had no idea which bath /
which tank ran the job. For aerospace traceability that's a deal-
breaker. Add a validation gate on mrp.workorder.button_start so
operators can't tap START without the data the shop floor MUST capture.

**Three new pieces on mrp.workorder:**

1. `_fp_is_wet_process()` — best-effort "does this WO involve a
   chemistry bath?" check. Three signals in priority order:
   a. A bath is already linked → definitely wet
   b. The workcenter's FP work-centre supports a wet process family
      (plating, pre/post-treatment, strip, passivation)
   c. WO name contains a wet-process keyword (plat, nickel, chrome,
      anodiz, zinc, etch, clean, rinse, strip, passivat, electroless…)
   The keyword fallback is needed because most existing recipes have
   no process_type_id set on their operation nodes.

2. `_fp_check_required_fields_before_start()` — runs before the
   existing certification check. Rules:
   • Every WO needs an assigned operator (x_fc_assigned_user_id).
     Without it, productivity records can't be attributed and the
     proficiency tracker has no employee to credit.
   • Wet WOs additionally need x_fc_bath_id + x_fc_tank_id. So we
     know exactly which chemistry bath ran the job and which physical
     tank it sat in.
   Raises a clear UserError listing the missing fields if any.

3. `x_fc_requires_bath` (compute, non-stored) — surfaces the wet check
   to the form view so bath + tank fields render with `required=`.

**View changes:**
- `x_fc_assigned_user_id` is now `required="1"` on the form
- `x_fc_bath_id` + `x_fc_tank_id` use `required="x_fc_requires_bath"`
  → red asterisk only when the WO is actually wet

**Simulator updates** (scripts/fp_e2e_workforce.py):
- Hannah now explicitly assigns bath + tank to wet WOs during planning,
  AND pre-issues operator certifications for the bath's process type
  (real shop manager workflow).
- Two negative tests added that PROVE the gates fire:
  • Test 1: strip the operator → button_start raises "missing Assigned Operator"
  • Test 2: strip bath/tank on a wet WO → button_start raises "missing Bath/Tank"

**Final E2E:** 42 PASS / 2 WARN / 0 FAIL out of 44 checks.
Both remaining WARNs (bake-window auto-create, first-piece gate) are
expected behaviour — those are coating-driven and the test coating
intentionally doesn't trigger them.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsinghpal
2026-04-19 09:47:31 -04:00
parent fe003567a9
commit 4161f04b0f
5 changed files with 195 additions and 6 deletions

View File

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

View File

@@ -26,6 +26,13 @@ class MrpWorkorder(models.Model):
# ------------------------------------------------------------------
# Plating-specific fields
# ------------------------------------------------------------------
x_fc_requires_bath = fields.Boolean(
string='Requires Bath/Tank',
compute='_compute_requires_bath',
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_bath_id = fields.Many2one(
'fusion.plating.bath', string='Bath', tracking=True,
)
@@ -512,10 +519,82 @@ class MrpWorkorder(models.Model):
# ------------------------------------------------------------------
# T2.2 — Certification gate on WO start
# T2.3 — Required-field gate (bath/tank for wet WOs, assigned operator)
# ------------------------------------------------------------------
WET_FAMILIES = (
'plating', 'pre_treatment', 'post_treatment',
'strip', 'passivation',
)
# Keyword fallback used when the workcenter / process-type metadata
# is missing — covers most shop floor naming conventions. Lowercased.
WET_NAME_KEYWORDS = (
'plat', 'nickel', 'chrome', 'anodiz', 'zinc',
'etch', 'clean', 'rinse', 'strip', 'passivat',
'zincate', 'alkalin', 'acid', 'electroless',
)
@api.depends('x_fc_bath_id', 'name', 'workcenter_id')
def _compute_requires_bath(self):
for wo in self:
wo.x_fc_requires_bath = wo._fp_is_wet_process()
def _fp_is_wet_process(self):
"""Best-effort check: does this WO involve a chemistry bath?
Three signals, in priority order:
1. A bath is already linked → definitely wet
2. The workcenter's FP work-centre supports a wet process family
3. The WO's name contains a wet-process keyword
"""
self.ensure_one()
if self.x_fc_bath_id:
return True
wc = self.workcenter_id
fpwc = getattr(wc, 'x_fc_fp_work_center_id', False)
if fpwc:
families = set(fpwc.supported_process_ids.mapped('process_family'))
if families & set(self.WET_FAMILIES):
return True
name = (self.name or '').lower()
return any(k in name for k in self.WET_NAME_KEYWORDS)
def _fp_check_required_fields_before_start(self):
"""Block button_start if the WO is missing data the shop must
record for traceability + compliance.
Rules:
• Every WO needs an assigned operator (x_fc_assigned_user_id) —
without it, productivity records can't be attributed and
proficiency tracking goes nowhere.
• 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).
"""
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():
if not wo.x_fc_bath_id:
missing.append(_('Bath'))
if not wo.x_fc_tank_id:
missing.append(_('Tank'))
if missing:
raise UserError(_(
'Cannot start work order "%(wo)s" — please fill these '
'required fields first:\n%(fields)s\n\n'
'Open the work order form and have the planner set them.'
) % {
'wo': wo.display_name or wo.name,
'fields': '\n'.join(missing),
})
def button_start(self):
"""Block start unless the current user's linked employee holds
an active certification for this WO's process type."""
an active certification for this WO's process type AND every
required field for traceability is filled in."""
self._fp_check_required_fields_before_start()
self._fp_check_operator_certification()
res = super().button_start()
# Capture audit AFTER the super call so we don't stamp WOs that

View File

@@ -93,8 +93,10 @@
<field name="x_fc_priority" widget="priority"/>
<field name="x_fc_assigned_user_id"
string="Assigned To"
required="1"
options="{'no_create': True}"/>
<field name="x_fc_work_role_id" readonly="1"/>
<field name="x_fc_requires_bath" invisible="1"/>
</xpath>
<!-- ============================================================
@@ -166,8 +168,10 @@
<group>
<group string="Bath &amp; Tank">
<field name="x_fc_facility_id"/>
<field name="x_fc_bath_id"/>
<field name="x_fc_tank_id"/>
<field name="x_fc_bath_id"
required="x_fc_requires_bath"/>
<field name="x_fc_tank_id"
required="x_fc_requires_bath"/>
<field name="x_fc_rack_id"/>
<field name="x_fc_rack_ref"/>
</group>