fix(manager-desk): unstick the spinner + live updates that don't flash
Root cause of the stuck "Loading manager data..." spinner: the overview
endpoint included a search_count on sale.order.x_fc_workflow_stage,
which is a non-stored computed field. Odoo 19 raised:
ValueError: Cannot convert sale.order.x_fc_workflow_stage to SQL
because it is not stored
The controller silently logged the error; the JS caught and swallowed
the RPC failure, leaving state.overview=null forever. So the UI just
kept spinning while production changed around the manager.
Fixes:
1. Controller (manager_controller.py)
- "Awaiting assignment SOs" is now computed from STORED fields only:
state='sale' AND x_fc_receiving_status='inspected'
AND x_fc_assigned_manager_id=False
Same stage, legal SQL.
- Whole endpoint wrapped in try/except; failures return
{'ok': False, 'error': '...'} so the UI can surface them instead
of dying silently.
- Response carries a payload_hash (md5 of the JSON body minus
user_name). If the client sends back known_hash and nothing has
moved, the server returns {'unchanged': True, 'payload_hash': ...}
and the client skips the repaint entirely. Keeps the UI quiet
between polls.
2. OWL component (manager_dashboard.js)
- Poll cadence tightened from 30s → 8s (production-pace).
- Unchanged payloads don't mutate state.overview → no re-render,
no flash. Live dot just updates its tooltip.
- Changed payloads do an in-place MERGE of the overview (copying
scalars/arrays onto the existing reactive object) instead of
replacing it wholesale. OWL's diff only re-renders rows that
actually moved.
- isFetching guard so overlapping polls can't stack up.
- state.loadError surfaces backend errors in a red banner with a
Retry button — no more silent spinner.
3. UX
- Live dot next to the title: soft green at rest, bright green
pulsing during a fetch.
- "Updated Xs ago" subtitle uses a getter so the label freshens
between polls.
- Manual Refresh button next to Quick/Detailed toggle.
- Spinner only appears on the genuine first load; gone forever
once the first payload lands.
Verified: the old crashing query now runs clean on demo data; odoo
logs show zero errors for the last 5 minutes of polling.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -22,7 +22,22 @@ class FpManagerDashboardController(http.Controller):
|
||||
# Overview snapshot — used on initial load + 30s auto-refresh
|
||||
# ------------------------------------------------------------------
|
||||
@http.route('/fp/manager/overview', type='jsonrpc', auth='user')
|
||||
def overview(self, facility_id=None):
|
||||
def overview(self, facility_id=None, known_hash=None):
|
||||
"""Build the manager dashboard payload.
|
||||
|
||||
`known_hash`: if the client sends back the hash of its last
|
||||
overview, we compare and return `{'unchanged': True}` when
|
||||
nothing has moved. Keeps the UI flicker-free between polls
|
||||
while still catching every shop-floor change within a few
|
||||
seconds.
|
||||
"""
|
||||
try:
|
||||
return self._overview_payload(facility_id, known_hash)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
_logger.exception('Manager overview failed')
|
||||
return {'ok': False, 'error': str(exc)}
|
||||
|
||||
def _overview_payload(self, facility_id, known_hash):
|
||||
env = request.env
|
||||
MrpWO = env.get('mrp.workorder')
|
||||
Production = env.get('mrp.production')
|
||||
@@ -35,6 +50,7 @@ class FpManagerDashboardController(http.Controller):
|
||||
'operators': [], 'tanks': [],
|
||||
'user_name': env.user.name,
|
||||
'mrp_missing': True,
|
||||
'payload_hash': '',
|
||||
}
|
||||
# The assignment field lives in fusion_plating_bridge_mrp. If it's
|
||||
# missing, the dashboard still renders but the worker pickers are
|
||||
@@ -160,7 +176,23 @@ class FpManagerDashboardController(http.Controller):
|
||||
for t in (Tank.search([]) if Tank is not None else [])
|
||||
]
|
||||
|
||||
# KPI summary
|
||||
# KPI summary — every query must use STORED fields only, otherwise
|
||||
# Odoo raises "Cannot convert … to SQL because it is not stored".
|
||||
# x_fc_workflow_stage is computed (non-stored); replicate the
|
||||
# "awaiting assignment" stage directly via its stored antecedents.
|
||||
SO = env['sale.order']
|
||||
so_fields = SO._fields
|
||||
if ('x_fc_receiving_status' in so_fields
|
||||
and 'x_fc_assigned_manager_id' in so_fields):
|
||||
pending_accept_domain = [
|
||||
('state', '=', 'sale'),
|
||||
('x_fc_receiving_status', '=', 'inspected'),
|
||||
('x_fc_assigned_manager_id', '=', False),
|
||||
]
|
||||
pending_accept_sos = SO.search_count(pending_accept_domain)
|
||||
else:
|
||||
pending_accept_sos = 0
|
||||
|
||||
kpis = {
|
||||
'unassigned_wos': MrpWO.search_count(domain_unassigned),
|
||||
'active_wos': MrpWO.search_count(domain_active),
|
||||
@@ -171,12 +203,10 @@ class FpManagerDashboardController(http.Controller):
|
||||
('state', '=', 'done'),
|
||||
('x_fc_portal_job_id.state', '=', 'ready_to_ship'),
|
||||
]),
|
||||
'pending_accept_sos': env['sale.order'].search_count(
|
||||
[('x_fc_workflow_stage', '=', 'assign_work')]
|
||||
) if 'x_fc_workflow_stage' in env['sale.order']._fields else 0,
|
||||
'pending_accept_sos': pending_accept_sos,
|
||||
}
|
||||
|
||||
return {
|
||||
payload = {
|
||||
'ok': True,
|
||||
'kpis': kpis,
|
||||
'unassigned': unassigned_cards,
|
||||
@@ -187,6 +217,18 @@ class FpManagerDashboardController(http.Controller):
|
||||
'user_name': env.user.name,
|
||||
}
|
||||
|
||||
# Short-circuit: if nothing changed since last poll, skip repaint.
|
||||
import hashlib, json
|
||||
hashable = json.dumps(
|
||||
{k: v for k, v in payload.items() if k != 'user_name'},
|
||||
sort_keys=True, default=str,
|
||||
)
|
||||
payload_hash = hashlib.md5(hashable.encode('utf-8')).hexdigest()
|
||||
payload['payload_hash'] = payload_hash
|
||||
if known_hash and known_hash == payload_hash:
|
||||
return {'ok': True, 'unchanged': True, 'payload_hash': payload_hash}
|
||||
return payload
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Assign a worker to a WO
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user