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
# ---- 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 = [
('state', 'in', ('pending', 'waiting', 'ready', 'progress')),
('state', 'not in', ACTIVE_NEG_STATES),
]
if has_assign:
domain_unassigned.append(('x_fc_assigned_user_id', '=', False))
@@ -126,8 +132,12 @@ class FpManagerDashboardController(http.Controller):
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', 'in', ('ready', 'progress')),
('state', 'not in', ACTIVE_NEG_STATES),
]
if has_assign:
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'):
open_wos = MrpWO.search([
('x_fc_assigned_user_id', '=', user.id),
('state', 'in', ('ready', 'progress', 'waiting')),
('state', 'not in', ACTIVE_NEG_STATES),
])
team.append({
'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}")
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):
"""Bump one draft SO into the 'sent' state so the funnel has data
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('6c. add quote requests', _add_quote_requests)
_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("Done. Re-run anytime — script is idempotent.")
print("=========================================================\n")