fix(manager-desk): include 'blocked' WOs + populate empty columns

Two complementary fixes — a real bug in the Manager Desk and demo
data that exercises the now-correct view.

The bug
=======
manager_controller.py used an explicit allow-list of WO states for
its Unassigned / Active columns and for the per-operator team load
count: ('pending','waiting','ready','progress'). That set MISSED the
'blocked' state Odoo emits when a WO's predecessor isn't done yet.

Result: an MO whose first WO is still running has all its downstream
WOs in 'blocked' state. They literally don't appear on the Manager
Desk — neither in "Needs a Worker" (even when unassigned) nor in
"In Progress" (even when assigned). The team load count also
under-reports because the operator's blocked queue is invisible.

Fix: switch all three domains from an allow-list to a deny-list
('done','cancel'). Same shape Plant Overview already uses, so the
two dashboards now agree on what "active" means.

Demo data
=========
Stage-filler gains two steps so the now-corrected view has obvious
data:

  6e. _populate_active_wos walks the in-flight MO's blocked routing
      and explicitly assigns the seven downstream WOs in sequence
      order — Diego (training), Carlos (plating), James (demask),
      Priya (oven), TWO unassigned (de-rack + post-bake — feed
      "Needs a Worker"), Aisha (final inspection). Earlier
      keyword-fuzzy matching missed WOs whose names didn't carry
      the expected substring.

  6f. _mark_so_awaiting_manager pushes two confirmed SOs to
      receiving_status='inspected' + assigned_manager_id=False so
      the "Awaiting Assignment" KPI is non-zero.

Verified on entech: 2 unassigned WOs, 6 active+assigned, 2
awaiting-assignment SOs. Six of seven operators carry at least one
open queue item; Marie has zero current load but a healthy past
completion history (she's on shift, between jobs).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsinghpal
2026-04-18 22:32:53 -04:00
parent 8f1cb3abd2
commit f8dfff5ce6
2 changed files with 102 additions and 3 deletions

View File

@@ -59,8 +59,14 @@ class FpManagerDashboardController(http.Controller):
has_assign = 'x_fc_assigned_user_id' in MrpWO._fields has_assign = 'x_fc_assigned_user_id' in MrpWO._fields
# ---- Column 1: Unassigned (no worker on an active WO) ---------- # ---- 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.
ACTIVE_NEG_STATES = ('done', 'cancel')
domain_unassigned = [ domain_unassigned = [
('state', 'in', ('pending', 'waiting', 'ready', 'progress')), ('state', 'not in', ACTIVE_NEG_STATES),
] ]
if has_assign: if has_assign:
domain_unassigned.append(('x_fc_assigned_user_id', '=', False)) domain_unassigned.append(('x_fc_assigned_user_id', '=', False))
@@ -126,8 +132,12 @@ class FpManagerDashboardController(http.Controller):
unassigned_cards.append(_mo_card(mo, wos)) unassigned_cards.append(_mo_card(mo, wos))
# ---- Column 2: In Progress (MOs with at least one active WO) ---- # ---- 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 = [ domain_active = [
('state', 'in', ('ready', 'progress')), ('state', 'not in', ACTIVE_NEG_STATES),
] ]
if has_assign: if has_assign:
domain_active.append(('x_fc_assigned_user_id', '!=', False)) domain_active.append(('x_fc_assigned_user_id', '!=', False))
@@ -149,7 +159,7 @@ class FpManagerDashboardController(http.Controller):
for user in operator_group.user_ids.sorted('name'): for user in operator_group.user_ids.sorted('name'):
open_wos = MrpWO.search([ open_wos = MrpWO.search([
('x_fc_assigned_user_id', '=', user.id), ('x_fc_assigned_user_id', '=', user.id),
('state', 'in', ('ready', 'progress', 'waiting')), ('state', 'not in', ACTIVE_NEG_STATES),
]) ])
team.append({ team.append({
'user_id': user.id, 'user_id': user.id,

View File

@@ -451,6 +451,93 @@ def _add_paused_wo(env):
print(f"[6b] Paused-WO marker set on {progress.display_name}") print(f"[6b] Paused-WO marker set on {progress.display_name}")
def _populate_active_wos(env):
"""Make sure the Manager Desk's three columns all have visible data.
Walks the in-flight MO's routing in sequence order and explicitly
assigns each downstream WO to a specific operator (with two
deliberately left unassigned so the "Needs a Worker" column has
cards to pick from). Earlier keyword-based fuzzy matching missed
a few WOs whose names didn't contain the expected substring, so
this rewrite uses a positional plan instead — less clever, more
predictable.
Manager Desk's WO domain was widened to include 'blocked' state in
the same patch, so WOs sitting waiting on a predecessor finally
show up.
"""
Emp = env['hr.employee']
mo = env['mrp.production'].search([('state', '=', 'progress')], limit=1)
if not mo:
print("[6e] No in-progress MO available — skipping")
return
# Sequence-aligned plan: the Nth downstream WO (skipping done +
# progress) goes to the Nth operator. None means leave unassigned.
PLAN = [
'Diego Ramirez', # the training operator gets the first prep step
'Carlos Silva', # senior owns the critical plating step
"James O'Connor", # lead hand for plating_op also covers demask
'Priya Sharma', # lead hand for oven
None, # de-rack — left empty for "Needs a Worker"
None, # post-bake — left empty for "Needs a Worker"
'Aisha Khan', # final inspection
]
# Skip done / cancel / progress (the latter is the live one we
# don't want to disturb mid-flight).
pending = mo.workorder_ids.sorted('sequence').filtered(
lambda w: w.state not in ('done', 'cancel', 'progress')
)
moved = cleared = 0
for wo, name in zip(pending, PLAN):
if name is None:
wo.x_fc_assigned_user_id = False
cleared += 1
continue
emp = Emp.search([('name', '=', name)], limit=1)
if emp and emp.user_id:
wo.x_fc_assigned_user_id = emp.user_id.id
moved += 1
print(f"[6e] Active WOs: redistributed {moved} to new team, "
f"left {cleared} unassigned")
def _mark_so_awaiting_manager(env):
"""Push two confirmed SOs to "inspected, no manager assigned" so the
Manager Desk's "Awaiting Assignment" KPI has a non-zero value.
The KPI's domain on the controller side is:
state == 'sale'
x_fc_receiving_status == 'inspected'
x_fc_assigned_manager_id is False
Set those three on a couple of existing confirmed SOs.
"""
SO = env['sale.order']
if not ('x_fc_receiving_status' in SO._fields
and 'x_fc_assigned_manager_id' in SO._fields):
print("[6f] receiving_status/assigned_manager fields not present")
return
already = SO.search_count([
('state', '=', 'sale'),
('x_fc_receiving_status', '=', 'inspected'),
('x_fc_assigned_manager_id', '=', False),
])
if already >= 2:
print(f"[6f] Awaiting-assignment SOs already populated ({already})")
return
sos = SO.search([
('state', '=', 'sale'),
('x_fc_receiving_status', '!=', 'inspected'),
], limit=2)
for so in sos:
so.write({
'x_fc_receiving_status': 'inspected',
'x_fc_assigned_manager_id': False,
})
print(f"[6f] Marked {len(sos)} SOs as awaiting-manager-assignment")
def _mark_quote_sent(env): def _mark_quote_sent(env):
"""Bump one draft SO into the 'sent' state so the funnel has data """Bump one draft SO into the 'sent' state so the funnel has data
in every workflow column. in every workflow column.
@@ -541,6 +628,8 @@ _safe('6a. add quality holds', _add_quality_holds)
_safe('6b. mark paused WO', _add_paused_wo) _safe('6b. mark paused WO', _add_paused_wo)
_safe('6c. add quote requests', _add_quote_requests) _safe('6c. add quote requests', _add_quote_requests)
_safe('6d. mark one quote sent', _mark_quote_sent) _safe('6d. mark one quote sent', _mark_quote_sent)
_safe('6e. populate active WOs', _populate_active_wos)
_safe('6f. SO awaiting-manager', _mark_so_awaiting_manager)
print("=========================================================") print("=========================================================")
print("Done. Re-run anytime — script is idempotent.") print("Done. Re-run anytime — script is idempotent.")
print("=========================================================\n") print("=========================================================\n")