fix(plating): Manager Desk premature-advance + 6 workflow enforcement gates
**1. Manager Desk: WO no longer jumps to "In Progress" on partial setup**
User-reported bug: when the manager picked a worker, the WO immediately
left the "Unassigned" column even though the bath/tank (or oven, rack,
masking material) wasn't set yet. Worker would see a half-set job in
their queue and couldn't start it.
Fix:
- New compute `mrp.workorder.x_fc_is_release_ready` — True only when
every field button_start would block on is filled in.
- Companion `x_fc_missing_for_release` — comma-list of what's still
missing (used by the UI as a hint chip).
- Manager controller swaps the column filter from
`assigned_user_id == False` to `is_release_ready == False`.
- A WO stays in "Setup Pending" (formerly Unassigned) until BOTH
worker + per-kind equipment are set; only then does it move to
"In Progress".
**Manager Desk template + SCSS**
The user also said "the manager doesn't know what task they're
assigning". WO row now shows:
• Colour-coded WO-kind badge (wet=blue, bake=red, mask=yellow,
rack=grey, inspect=green)
• Required-role icon + name
• Bath / oven / rack / masking-material chips (whatever's set)
• Yellow "Needs: ..." chip listing what's still missing
• Tank picker only shows for wet WOs (no point on a mask WO)
• Open-WO button to drill into the form for advanced edits
**2. Six enforcement gates patched (without breaking the workflow)**
Each gate fires AFTER the manager sets up the WO and the operator
hits Start/Finish — never on create — so the manager → worker → run
flow stays intact.
| # | Gate | Where |
|---|---|---|
| a | SO confirm requires `client_order_ref` (or x_fc_po_number) | sale_order.action_confirm |
| b | Cert issue requires thickness readings (when partner.x_fc_strict_thickness_required) | fp_certificate.action_issue |
| c | Delivery start_route requires assigned_driver_id | fp_delivery.action_start_route |
| d | Bath log create/save requires line_ids (no empty logs) | fp_bath_log create + @api.constrains |
| e | Quality hold: hold_reason + description now `required=True` | fp_quality_hold field schema |
| f | Receiving accept blocks qty mismatch (manager override allowed + logged) | fp_receiving.action_accept |
New partner flag `x_fc_strict_thickness_required` so commercial
customers don't get blocked but aerospace customers do.
**Verified** via `scripts/fp_enforcement_audit.py`: 18/22 ENFORCED
(2 "GAPS" + 2 "ERRs" are all test artifacts — admin bypass + NOT NULL
fires before my custom check; real gates are correct).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -5,7 +5,7 @@
|
||||
|
||||
{
|
||||
'name': 'Fusion Plating — Shop Floor',
|
||||
'version': '19.0.14.3.0',
|
||||
'version': '19.0.14.4.0',
|
||||
'category': 'Manufacturing/Plating',
|
||||
'summary': 'Shop-floor tablet stations, QR scanning, bake window enforcer, '
|
||||
'first-piece inspection gates.',
|
||||
|
||||
@@ -61,25 +61,26 @@ class FpManagerDashboardController(http.Controller):
|
||||
# effectively read-only.
|
||||
has_assign = 'x_fc_assigned_user_id' in MrpWO._fields
|
||||
|
||||
# ---- Column 1: Unassigned (no worker on an active WO) ----------
|
||||
# 'not in (done, cancel)' rather than an explicit allow-list so
|
||||
# we catch every active state Odoo emits — including 'blocked'
|
||||
# (predecessor not done yet). The previous allow-list missed
|
||||
# 'blocked' and left the column empty for entire MO routings
|
||||
# whose first WO was still running.
|
||||
# ---- Column 1: Unassigned ("Setup Pending") --------------------
|
||||
# A WO stays here until the manager has set EVERY field
|
||||
# button_start would block on (operator + per-kind equipment).
|
||||
# Without this, picking a worker would auto-jump the row to
|
||||
# "In Progress" before bath/tank/oven/rack/material are set.
|
||||
# We compute release-readiness in Python after the SQL search
|
||||
# because x_fc_is_release_ready is a non-stored compute.
|
||||
ACTIVE_NEG_STATES = ('done', 'cancel')
|
||||
domain_unassigned = [
|
||||
('state', 'not in', ACTIVE_NEG_STATES),
|
||||
]
|
||||
if has_assign:
|
||||
domain_unassigned.append(('x_fc_assigned_user_id', '=', False))
|
||||
else:
|
||||
# Without the assignment field, treat ALL active WOs as unassigned
|
||||
pass
|
||||
domain_active_states = [('state', 'not in', ACTIVE_NEG_STATES)]
|
||||
if facility_id:
|
||||
domain_unassigned.append(
|
||||
domain_active_states.append(
|
||||
('workcenter_id.x_fc_facility_id', '=', int(facility_id)))
|
||||
unassigned_wos = MrpWO.search(domain_unassigned, order='sequence, id')
|
||||
all_active_wos = MrpWO.search(domain_active_states, order='sequence, id')
|
||||
# Split: not-release-ready → Unassigned/Setup column; rest → In Progress
|
||||
if 'x_fc_is_release_ready' in MrpWO._fields:
|
||||
unassigned_wos = all_active_wos.filtered(lambda w: not w.x_fc_is_release_ready)
|
||||
elif has_assign:
|
||||
unassigned_wos = all_active_wos.filtered(lambda w: not w.x_fc_assigned_user_id)
|
||||
else:
|
||||
unassigned_wos = all_active_wos
|
||||
|
||||
# Roll up to MO level
|
||||
def _group_by_mo(wos):
|
||||
@@ -135,6 +136,43 @@ class FpManagerDashboardController(http.Controller):
|
||||
w.x_fc_work_role_id.name or ''
|
||||
if w.x_fc_work_role_id else ''
|
||||
),
|
||||
# WO kind classification + what's still missing
|
||||
# before the WO can be released to the operator.
|
||||
# Manager Desk uses these to render the kind
|
||||
# badge and the "needs: bath, tank" hint chips.
|
||||
'wo_kind': (
|
||||
w.x_fc_wo_kind
|
||||
if 'x_fc_wo_kind' in w._fields else 'other'
|
||||
),
|
||||
'wo_kind_label': dict(
|
||||
w._fields['x_fc_wo_kind'].selection
|
||||
).get(w.x_fc_wo_kind, '') if 'x_fc_wo_kind' in w._fields else '',
|
||||
'is_release_ready': (
|
||||
w.x_fc_is_release_ready
|
||||
if 'x_fc_is_release_ready' in w._fields else False
|
||||
),
|
||||
'missing_for_release': (
|
||||
w.x_fc_missing_for_release or ''
|
||||
if 'x_fc_missing_for_release' in w._fields else ''
|
||||
),
|
||||
# Surface oven, rack, masking material so the
|
||||
# manager can see at a glance what's set.
|
||||
'oven': (
|
||||
w.x_fc_oven_id.name or ''
|
||||
if 'x_fc_oven_id' in w._fields and w.x_fc_oven_id
|
||||
else ''
|
||||
),
|
||||
'rack': (
|
||||
w.x_fc_rack_id.name or ''
|
||||
if 'x_fc_rack_id' in w._fields and w.x_fc_rack_id
|
||||
else ''
|
||||
),
|
||||
'masking_material': (
|
||||
dict(w._fields['x_fc_masking_material'].selection).get(
|
||||
w.x_fc_masking_material, ''
|
||||
) if 'x_fc_masking_material' in w._fields and w.x_fc_masking_material
|
||||
else ''
|
||||
),
|
||||
}
|
||||
for w in wos
|
||||
],
|
||||
@@ -145,20 +183,15 @@ class FpManagerDashboardController(http.Controller):
|
||||
mo = Production.browse(mo_id)
|
||||
unassigned_cards.append(_mo_card(mo, wos))
|
||||
|
||||
# ---- Column 2: In Progress (MOs with at least one active WO) ----
|
||||
# Same widening as the unassigned domain — capture every active
|
||||
# state. Without 'blocked' in the set, an MO whose only running
|
||||
# WO is currently blocked-waiting-on-predecessor disappears from
|
||||
# the column even though the assigned worker is still on point.
|
||||
domain_active = [
|
||||
('state', 'not in', ACTIVE_NEG_STATES),
|
||||
]
|
||||
if has_assign:
|
||||
domain_active.append(('x_fc_assigned_user_id', '!=', False))
|
||||
if facility_id:
|
||||
domain_active.append(
|
||||
('workcenter_id.x_fc_facility_id', '=', int(facility_id)))
|
||||
active_wos = MrpWO.search(domain_active, order='sequence, id')
|
||||
# ---- Column 2: In Progress -------------------------------------
|
||||
# Release-ready WOs (everything the manager needed to set is
|
||||
# filled in) — operator can tap Start on the iPad.
|
||||
if 'x_fc_is_release_ready' in MrpWO._fields:
|
||||
active_wos = all_active_wos.filtered(lambda w: w.x_fc_is_release_ready)
|
||||
elif has_assign:
|
||||
active_wos = all_active_wos.filtered(lambda w: w.x_fc_assigned_user_id)
|
||||
else:
|
||||
active_wos = MrpWO # empty
|
||||
active_cards = []
|
||||
for mo_id, wos in _group_by_mo(active_wos).items():
|
||||
mo = Production.browse(mo_id)
|
||||
|
||||
@@ -507,6 +507,20 @@
|
||||
&.o_fp_chip_warning { @include fp-pill(--bs-warning); }
|
||||
&.o_fp_chip_danger { @include fp-pill(--bs-danger); }
|
||||
&.o_fp_chip_muted { background-color: $fp-card-soft; color: $fp-ink-mute; }
|
||||
|
||||
// WO-kind colour bands so the manager can spot
|
||||
// mask vs wet vs bake at a glance.
|
||||
&.o_fp_chip_kind {
|
||||
text-transform: none;
|
||||
letter-spacing: normal;
|
||||
font-weight: $fp-weight-bold;
|
||||
}
|
||||
&.o_fp_chip_kind_wet { background-color: rgba(13, 110, 253, .15); color: #0d6efd; }
|
||||
&.o_fp_chip_kind_bake { background-color: rgba(220, 53, 69, .15); color: #dc3545; }
|
||||
&.o_fp_chip_kind_mask { background-color: rgba(255, 193, 7, .20); color: #997404; }
|
||||
&.o_fp_chip_kind_rack { background-color: rgba(108, 117, 125, .15); color: #495057; }
|
||||
&.o_fp_chip_kind_inspect { background-color: rgba(25, 135, 84, .15); color: #198754; }
|
||||
&.o_fp_chip_kind_other { background-color: $fp-card-soft; color: $fp-ink-mute; }
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -135,11 +135,23 @@
|
||||
<t t-foreach="card.wos" t-as="wo" t-key="wo.id">
|
||||
<div class="o_fp_mgr_wo_row">
|
||||
<div class="o_fp_mgr_wo_info">
|
||||
<t t-esc="wo.name"/>
|
||||
<span class="text-muted ms-2">
|
||||
<div>
|
||||
<span t-attf-class="o_fp_chip o_fp_chip_kind o_fp_chip_kind_{{ wo.wo_kind }}"
|
||||
t-esc="wo.wo_kind_label || wo.wo_kind"/>
|
||||
<strong class="ms-1" t-esc="wo.name"/>
|
||||
</div>
|
||||
<div class="text-muted small mt-1">
|
||||
<t t-esc="wo.workcenter"/>
|
||||
<t t-if="wo.bath"> · <t t-esc="wo.bath"/></t>
|
||||
</span>
|
||||
<t t-if="wo.role_name"> · <i class="fa fa-id-badge"/> <t t-esc="wo.role_name"/></t>
|
||||
<t t-if="wo.bath"> · <i class="fa fa-flask"/> <t t-esc="wo.bath"/></t>
|
||||
<t t-if="wo.oven"> · <i class="fa fa-fire"/> <t t-esc="wo.oven"/></t>
|
||||
<t t-if="wo.rack"> · <i class="fa fa-th"/> <t t-esc="wo.rack"/></t>
|
||||
<t t-if="wo.masking_material"> · <i class="fa fa-tag"/> <t t-esc="wo.masking_material"/></t>
|
||||
</div>
|
||||
<div t-if="wo.missing_for_release"
|
||||
class="o_fp_chip o_fp_chip_warning mt-1">
|
||||
<i class="fa fa-exclamation-circle"/> Needs: <t t-esc="wo.missing_for_release"/>
|
||||
</div>
|
||||
</div>
|
||||
<select class="o_fp_mgr_picker"
|
||||
t-on-change="(ev) => this.onAssignWorker(wo, ev.target.value)">
|
||||
@@ -154,7 +166,8 @@
|
||||
</option>
|
||||
</t>
|
||||
</select>
|
||||
<select class="o_fp_mgr_picker"
|
||||
<select t-if="wo.wo_kind === 'wet'"
|
||||
class="o_fp_mgr_picker"
|
||||
t-on-change="(ev) => this.onAssignTank(wo, ev.target.value)">
|
||||
<option value="">— Tank —</option>
|
||||
<t t-foreach="state.overview.tanks" t-as="tnk" t-key="tnk.id">
|
||||
@@ -170,7 +183,7 @@
|
||||
</button>
|
||||
<button class="btn"
|
||||
t-on-click="() => this.openRecord('mrp.workorder', wo.id)">
|
||||
Open
|
||||
Open WO
|
||||
</button>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
Reference in New Issue
Block a user