feat(plating): hard-required fields on WO start — operator + bath + tank
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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user