refactor(shopfloor,jobs): consolidate operator UI into shopfloor
Removes the parallel OWL/controller stack I built in fusion_plating_jobs (job_process_tree, job_plant_overview, job_manager_dashboard, job_tablet, job_*.scss, plus parallel controllers and action XML files). Refactors the existing fusion_plating_shopfloor components in place to bind to fp.job / fp.job.step instead of mrp.production / mrp.workorder. End state: - ONE operator UI module (shopfloor) instead of two parallel ones - Existing token system (_fp_shopfloor_tokens.scss) reused as designed - no duplicate jobs tokens - Existing /fp/shopfloor/* RPC URLs preserved (no integration breakage); workorder_id kwargs accepted as legacy aliases for step_id / job_id so older tablet clients keep working - Existing visual designs preserved - only the data layer underneath changed - Process Tree button on fp.job form now points at fusion_plating_shopfloor's fp_process_tree client action - Bake Windows / First-Piece Gates / Bake Oven / Operator Queue models stay where they were - legacy_menu_hide.xml trimmed: only the bridge_mrp Production Priorities entry remains; the 3 shopfloor menus (Manager Desk, Plant Overview, Tablet Station) are now visible (the canonical native consoles) Manifests: - fusion_plating_jobs 19.0.3.1.0 -> 19.0.4.0.0 (consolidation bump, no more bundled JS/SCSS, only job_scan controller retained) - fusion_plating_shopfloor 19.0.14.4.0 -> 19.0.15.0.0 (asset bundle cache-bust + significant controller refactor) Tests pass on entech: 0 failed, 0 errors of 41 tests. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -2,7 +2,20 @@
|
||||
# Copyright 2026 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
# Part of the Fusion Plating product family.
|
||||
"""JSON-RPC endpoints for the Manager Dashboard (client action)."""
|
||||
"""JSON-RPC endpoints for the Manager Dashboard (client action).
|
||||
|
||||
Native fp.job / fp.job.step edition (consolidated 2026-04-24). All
|
||||
endpoint URLs are preserved (`/fp/manager/*`); the underlying data
|
||||
layer is now fp.job + fp.job.step.
|
||||
|
||||
Manager Desk ergonomics:
|
||||
- Column 1 ("Needs a Worker") = jobs that have at least one step
|
||||
missing the bits a manager has to set before an operator can tap
|
||||
Start (worker, work centre, kind-specific equipment).
|
||||
- Column 2 ("In Progress") = jobs whose steps are release-ready or
|
||||
actively running.
|
||||
- Column 3 ("Team") = operators with their open / in-progress counts.
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
@@ -15,26 +28,45 @@ from odoo.http import request
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class FpManagerDashboardController(http.Controller):
|
||||
"""Manager-level view: unassigned jobs, in-progress jobs, team workload.
|
||||
# --- helpers -----------------------------------------------------------------
|
||||
|
||||
All endpoints require the user to be a manager or above. The UI locks
|
||||
the menu behind group_fusion_plating_manager.
|
||||
"""
|
||||
_NEG_JOB_STATES = ('done', 'cancelled')
|
||||
_ACTIVE_JOB_STATES = ('confirmed', 'in_progress', 'on_hold')
|
||||
|
||||
# A step needs an operator and (for wet/bake/mask) the right equipment
|
||||
# before the operator can tap Start. Mirrors the legacy
|
||||
# x_fc_is_release_ready compute on mrp.workorder.
|
||||
def _step_release_readiness(step):
|
||||
"""Return (is_release_ready, missing_str) for a fp.job.step."""
|
||||
missing = []
|
||||
if not step.assigned_user_id:
|
||||
missing.append('worker')
|
||||
if not step.work_centre_id:
|
||||
missing.append('work centre')
|
||||
if step.kind == 'wet':
|
||||
if not step.bath_id:
|
||||
missing.append('bath')
|
||||
if not step.tank_id:
|
||||
missing.append('tank')
|
||||
elif step.kind == 'rack':
|
||||
if not step.rack_id:
|
||||
missing.append('rack')
|
||||
return (not missing, ', '.join(missing))
|
||||
|
||||
|
||||
def _priority_int(priority):
|
||||
"""fp.job.priority → int 0/1/2 (parallel of legacy x_fc_priority)."""
|
||||
return {'rush': 2, 'high': 1, 'normal': 0, 'low': 0}.get(priority, 0)
|
||||
|
||||
|
||||
class FpManagerDashboardController(http.Controller):
|
||||
"""Manager-level view: unassigned jobs, in-progress jobs, team workload."""
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Overview snapshot — used on initial load + 30s auto-refresh
|
||||
# Overview snapshot — used on initial load + 8s auto-refresh
|
||||
# ------------------------------------------------------------------
|
||||
@http.route('/fp/manager/overview', type='jsonrpc', auth='user')
|
||||
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
|
||||
@@ -43,187 +75,122 @@ class FpManagerDashboardController(http.Controller):
|
||||
|
||||
def _overview_payload(self, facility_id, known_hash):
|
||||
env = request.env
|
||||
MrpWO = env.get('mrp.workorder')
|
||||
Production = env.get('mrp.production')
|
||||
if MrpWO is None or Production is None:
|
||||
return {
|
||||
'ok': True,
|
||||
'kpis': {'unassigned_wos': 0, 'active_wos': 0,
|
||||
'ready_to_ship_mos': 0, 'pending_accept_sos': 0},
|
||||
'unassigned': [], 'active': [], 'team': [],
|
||||
'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
|
||||
# effectively read-only.
|
||||
has_assign = 'x_fc_assigned_user_id' in MrpWO._fields
|
||||
Job = env['fp.job']
|
||||
Step = env['fp.job.step']
|
||||
|
||||
# ---- 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_active_states = [('state', 'not in', ACTIVE_NEG_STATES)]
|
||||
# Pull in-flight jobs (confirmed / in_progress / on_hold)
|
||||
domain = [('state', 'in', _ACTIVE_JOB_STATES)]
|
||||
if facility_id:
|
||||
domain_active_states.append(
|
||||
('workcenter_id.x_fc_facility_id', '=', int(facility_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
|
||||
domain.append(('facility_id', '=', int(facility_id)))
|
||||
jobs = Job.search(domain, order='priority desc, date_deadline asc, id desc')
|
||||
|
||||
# Roll up to MO level
|
||||
def _group_by_mo(wos):
|
||||
groups = {}
|
||||
for wo in wos:
|
||||
mo_id = wo.production_id.id
|
||||
groups.setdefault(mo_id, []).append(wo)
|
||||
return groups
|
||||
# Compute release-readiness per step in a single pass
|
||||
all_steps = jobs.mapped('step_ids').filtered(
|
||||
lambda s: s.state in ('pending', 'ready', 'in_progress', 'paused'),
|
||||
)
|
||||
readiness_by_step = {}
|
||||
for step in all_steps:
|
||||
ready, missing = _step_release_readiness(step)
|
||||
readiness_by_step[step.id] = (ready, missing)
|
||||
|
||||
# Bucket jobs: "needs a worker" vs "in progress".
|
||||
# A job lands in unassigned iff at least one of its open steps
|
||||
# is NOT release-ready. Otherwise it goes to in_progress.
|
||||
unassigned_jobs = jobs.browse([])
|
||||
active_jobs = jobs.browse([])
|
||||
for job in jobs:
|
||||
open_steps = job.step_ids.filtered(
|
||||
lambda s: s.state in ('pending', 'ready', 'in_progress', 'paused'),
|
||||
)
|
||||
if not open_steps:
|
||||
continue
|
||||
not_ready = any(not readiness_by_step.get(s.id, (False, ''))[0]
|
||||
for s in open_steps)
|
||||
if not_ready:
|
||||
unassigned_jobs |= job
|
||||
else:
|
||||
active_jobs |= job
|
||||
|
||||
def _job_card(job, only_open=True):
|
||||
partner = job.partner_id
|
||||
steps_iter = job.step_ids
|
||||
if only_open:
|
||||
steps_iter = steps_iter.filtered(
|
||||
lambda s: s.state in ('pending', 'ready', 'in_progress', 'paused'),
|
||||
)
|
||||
steps_iter = steps_iter.sorted('sequence')
|
||||
|
||||
wo_rows = []
|
||||
for s in steps_iter:
|
||||
ready, missing = readiness_by_step.get(s.id, (False, ''))
|
||||
wo_rows.append({
|
||||
'id': s.id,
|
||||
'name': s.name or '',
|
||||
'workcenter': s.work_centre_id.name or '',
|
||||
'state': s.state,
|
||||
'sequence': s.sequence or 0,
|
||||
'duration_expected': s.duration_expected or 0,
|
||||
'bath': s.bath_id.name or '',
|
||||
'tank': s.tank_id.name or '',
|
||||
'tank_id': s.tank_id.id if s.tank_id else False,
|
||||
'priority': str(_priority_int(job.priority)),
|
||||
'assigned_user_id': s.assigned_user_id.id or False,
|
||||
'assigned_user_name': s.assigned_user_id.name or '',
|
||||
'role_id': False,
|
||||
'role_name': '',
|
||||
'wo_kind': s.kind or 'other',
|
||||
'wo_kind_label': dict(s._fields['kind'].selection).get(
|
||||
s.kind, '',
|
||||
) if s.kind else '',
|
||||
'is_release_ready': ready,
|
||||
'missing_for_release': missing,
|
||||
'oven': '',
|
||||
'rack': s.rack_id.name or '',
|
||||
'masking_material': '',
|
||||
})
|
||||
|
||||
def _mo_card(mo, wos):
|
||||
so_name = mo.origin or ''
|
||||
partner = mo.x_fc_portal_job_id.partner_id if mo.x_fc_portal_job_id else None
|
||||
return {
|
||||
'mo_id': mo.id,
|
||||
'mo_name': mo.name,
|
||||
'so_name': so_name,
|
||||
'mo_id': job.id,
|
||||
'mo_name': job.name or '',
|
||||
'so_name': job.origin or '',
|
||||
'customer': partner.name if partner else '',
|
||||
'product': mo.product_id.display_name if mo.product_id else '',
|
||||
'qty_total': int(mo.product_qty or 0),
|
||||
'date_planned': fp_format(request.env, mo.date_start, fmt='%Y-%m-%d'),
|
||||
'recipe': mo.x_fc_recipe_id.name if mo.x_fc_recipe_id else '',
|
||||
'priority_any': max(
|
||||
[int(w.x_fc_priority or '0') for w in wos] + [0]
|
||||
'product': job.product_id.display_name if job.product_id else '',
|
||||
'qty_total': int(job.qty or 0),
|
||||
'date_planned': fp_format(
|
||||
request.env, job.date_planned_start or job.date_deadline,
|
||||
fmt='%Y-%m-%d',
|
||||
),
|
||||
'current_location': mo.x_fc_current_location or '',
|
||||
'wos': [
|
||||
{
|
||||
'id': w.id,
|
||||
'name': w.display_name or w.name,
|
||||
'workcenter': w.workcenter_id.name or '',
|
||||
'state': w.state,
|
||||
'sequence': w.sequence or 0,
|
||||
'duration_expected': w.duration_expected or 0,
|
||||
'bath': w.x_fc_bath_id.name or '',
|
||||
'tank': w.x_fc_tank_id.name or '',
|
||||
'tank_id': w.x_fc_tank_id.id if w.x_fc_tank_id else False,
|
||||
'priority': w.x_fc_priority or '0',
|
||||
'assigned_user_id': (
|
||||
w.x_fc_assigned_user_id.id
|
||||
if w.x_fc_assigned_user_id else False
|
||||
),
|
||||
'assigned_user_name': (
|
||||
w.x_fc_assigned_user_id.name or ''
|
||||
if w.x_fc_assigned_user_id else ''
|
||||
),
|
||||
# Role required by this step. Used by the
|
||||
# Manager Desk worker dropdown to surface
|
||||
# qualified operators first.
|
||||
'role_id': (
|
||||
w.x_fc_work_role_id.id
|
||||
if w.x_fc_work_role_id else False
|
||||
),
|
||||
'role_name': (
|
||||
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
|
||||
],
|
||||
'recipe': job.recipe_id.name if job.recipe_id else '',
|
||||
'priority_any': _priority_int(job.priority),
|
||||
'current_location': job.current_location or '',
|
||||
'wos': wo_rows,
|
||||
}
|
||||
|
||||
unassigned_cards = []
|
||||
for mo_id, wos in _group_by_mo(unassigned_wos).items():
|
||||
mo = Production.browse(mo_id)
|
||||
unassigned_cards.append(_mo_card(mo, wos))
|
||||
unassigned_cards = [_job_card(j) for j in unassigned_jobs]
|
||||
active_cards = [_job_card(j) for j in active_jobs]
|
||||
|
||||
# ---- 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)
|
||||
active_cards.append(_mo_card(mo, wos))
|
||||
|
||||
# ---- Column 3: Team (operators + their current load) -----------
|
||||
# ---- Column 3: Team --------------------------------------------
|
||||
operator_group = env.ref(
|
||||
'fusion_plating.group_fusion_plating_operator', raise_if_not_found=False,
|
||||
)
|
||||
team = []
|
||||
if operator_group and has_assign:
|
||||
if operator_group:
|
||||
for user in operator_group.user_ids.sorted('name'):
|
||||
open_wos = MrpWO.search([
|
||||
('x_fc_assigned_user_id', '=', user.id),
|
||||
('state', 'not in', ACTIVE_NEG_STATES),
|
||||
open_steps = Step.search([
|
||||
('assigned_user_id', '=', user.id),
|
||||
('state', 'in', ('ready', 'in_progress', 'paused')),
|
||||
])
|
||||
team.append({
|
||||
'user_id': user.id,
|
||||
'name': user.name,
|
||||
'open_count': len(open_wos),
|
||||
'open_count': len(open_steps),
|
||||
'in_progress_count': len(
|
||||
open_wos.filtered(lambda w: w.state == 'progress')
|
||||
open_steps.filtered(lambda s: s.state == 'in_progress'),
|
||||
),
|
||||
'avatar_url': f'/web/image/res.users/{user.id}/avatar_128',
|
||||
})
|
||||
|
||||
# ---- Pickers: operators (with presence + role data) -----------
|
||||
# We send richer operator records so the Manager Desk dropdown can
|
||||
# group qualified-and-present at the top, then lead hands, then
|
||||
# off-shift workers (greyed). Without this the manager has to
|
||||
# remember who's clocked in and who can do what.
|
||||
# ---- Operators picker (with presence + role data) --------------
|
||||
clocked_in_user_ids = (
|
||||
env['hr.employee']._fp_clocked_in_user_ids()
|
||||
if 'hr.employee' in env and hasattr(
|
||||
@@ -237,7 +204,11 @@ class FpManagerDashboardController(http.Controller):
|
||||
operators = []
|
||||
for u in operator_users:
|
||||
emp = u.employee_id
|
||||
role_ids = emp.x_fc_work_role_ids.ids if emp else []
|
||||
role_ids = (
|
||||
emp.x_fc_work_role_ids.ids
|
||||
if emp and 'x_fc_work_role_ids' in emp._fields
|
||||
else []
|
||||
)
|
||||
lead_role_ids = (
|
||||
emp.x_fc_lead_hand_role_ids.ids
|
||||
if emp and 'x_fc_lead_hand_role_ids' in emp._fields
|
||||
@@ -250,12 +221,12 @@ class FpManagerDashboardController(http.Controller):
|
||||
'role_ids': role_ids,
|
||||
'lead_hand_role_ids': lead_role_ids,
|
||||
})
|
||||
# Headline counts so the manager sees at-a-glance who's on shift.
|
||||
present_count = sum(1 for o in operators if o['is_clocked_in'])
|
||||
presence = {
|
||||
'clocked_in': present_count,
|
||||
'total': len(operators),
|
||||
}
|
||||
|
||||
Tank = env.get('fusion.plating.tank')
|
||||
tanks = [
|
||||
{
|
||||
@@ -267,36 +238,32 @@ class FpManagerDashboardController(http.Controller):
|
||||
for t in (Tank.search([]) if Tank is not None else [])
|
||||
]
|
||||
|
||||
# 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.
|
||||
# ---- KPI summary ----------------------------------------------
|
||||
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 = [
|
||||
pending_accept_sos = SO.search_count([
|
||||
('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
|
||||
|
||||
# KPI counts derived from the in-memory split we already have —
|
||||
# don't re-query (the release-ready filter is a Python compute,
|
||||
# not a stored column, so SQL search_count can't see it).
|
||||
# Ready-to-ship: jobs that are done but the portal job hasn't
|
||||
# been marked ready_to_ship yet (or no portal mirror at all).
|
||||
ready_to_ship_jobs = Job.search_count([('state', '=', 'done')])
|
||||
|
||||
kpis = {
|
||||
'unassigned_wos': len(unassigned_wos),
|
||||
'active_wos': len(active_wos),
|
||||
'ready_to_ship_mos': Production.search_count([
|
||||
('state', '=', 'done'),
|
||||
]) if 'x_fc_portal_job_id' not in Production._fields
|
||||
else Production.search_count([
|
||||
('state', '=', 'done'),
|
||||
('x_fc_portal_job_id.state', '=', 'ready_to_ship'),
|
||||
]),
|
||||
'unassigned_wos': len(all_steps.filtered(
|
||||
lambda s: not readiness_by_step.get(s.id, (False, ''))[0],
|
||||
)),
|
||||
'active_wos': len(all_steps.filtered(
|
||||
lambda s: readiness_by_step.get(s.id, (False, ''))[0]
|
||||
and s.state in ('ready', 'in_progress'),
|
||||
)),
|
||||
'ready_to_ship_mos': ready_to_ship_jobs,
|
||||
'pending_accept_sos': pending_accept_sos,
|
||||
}
|
||||
|
||||
@@ -325,45 +292,53 @@ class FpManagerDashboardController(http.Controller):
|
||||
return payload
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Assign a worker to a WO
|
||||
# Assign a worker to a step
|
||||
# ------------------------------------------------------------------
|
||||
@http.route('/fp/manager/assign_worker', type='jsonrpc', auth='user')
|
||||
def assign_worker(self, workorder_id, user_id):
|
||||
wo = request.env['mrp.workorder'].browse(int(workorder_id))
|
||||
if not wo.exists():
|
||||
return {'ok': False, 'error': 'Work order not found.'}
|
||||
wo.x_fc_assigned_user_id = int(user_id) if user_id else False
|
||||
wo.message_post(
|
||||
body=Markup('Worker assigned: <b>%s</b>') % (wo.x_fc_assigned_user_id.name or 'Unassigned'),
|
||||
"""`workorder_id` is the canonical kwarg name from the legacy
|
||||
XML; it now resolves to a fp.job.step id."""
|
||||
step = request.env['fp.job.step'].browse(int(workorder_id))
|
||||
if not step.exists():
|
||||
return {'ok': False, 'error': 'Step not found.'}
|
||||
step.assigned_user_id = int(user_id) if user_id else False
|
||||
step.message_post(
|
||||
body=Markup('Worker assigned: <b>%s</b>') % (
|
||||
step.assigned_user_id.name or 'Unassigned'
|
||||
),
|
||||
)
|
||||
return {'ok': True, 'user_name': wo.x_fc_assigned_user_id.name or ''}
|
||||
return {'ok': True, 'user_name': step.assigned_user_id.name or ''}
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Reassign or swap tank on a WO
|
||||
# Reassign or swap tank on a step
|
||||
# ------------------------------------------------------------------
|
||||
@http.route('/fp/manager/assign_tank', type='jsonrpc', auth='user')
|
||||
def assign_tank(self, workorder_id, tank_id):
|
||||
wo = request.env['mrp.workorder'].browse(int(workorder_id))
|
||||
if not wo.exists():
|
||||
return {'ok': False, 'error': 'Work order not found.'}
|
||||
wo.x_fc_tank_id = int(tank_id) if tank_id else False
|
||||
wo.message_post(
|
||||
body=Markup('Tank assigned: <b>%s</b>') % (wo.x_fc_tank_id.name or 'Unassigned'),
|
||||
step = request.env['fp.job.step'].browse(int(workorder_id))
|
||||
if not step.exists():
|
||||
return {'ok': False, 'error': 'Step not found.'}
|
||||
step.tank_id = int(tank_id) if tank_id else False
|
||||
step.message_post(
|
||||
body=Markup('Tank assigned: <b>%s</b>') % (
|
||||
step.tank_id.name or 'Unassigned'
|
||||
),
|
||||
)
|
||||
return {'ok': True, 'tank_name': wo.x_fc_tank_id.name or ''}
|
||||
return {'ok': True, 'tank_name': step.tank_id.name or ''}
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Manager takes over a WO (no-show coverage)
|
||||
# Manager takes over a step (no-show coverage)
|
||||
# ------------------------------------------------------------------
|
||||
@http.route('/fp/manager/take_over', type='jsonrpc', auth='user')
|
||||
def take_over(self, workorder_id):
|
||||
wo = request.env['mrp.workorder'].browse(int(workorder_id))
|
||||
if not wo.exists():
|
||||
return {'ok': False, 'error': 'Work order not found.'}
|
||||
step = request.env['fp.job.step'].browse(int(workorder_id))
|
||||
if not step.exists():
|
||||
return {'ok': False, 'error': 'Step not found.'}
|
||||
user = request.env.user
|
||||
previous = wo.x_fc_assigned_user_id.name or '—'
|
||||
wo.x_fc_assigned_user_id = user.id
|
||||
wo.message_post(
|
||||
body=Markup('Manager takeover: <b>%s</b> replaces %s.') % (user.name, previous),
|
||||
previous = step.assigned_user_id.name or '—'
|
||||
step.assigned_user_id = user.id
|
||||
step.message_post(
|
||||
body=Markup('Manager takeover: <b>%s</b> replaces %s.') % (
|
||||
user.name, previous,
|
||||
),
|
||||
)
|
||||
return {'ok': True, 'user_name': user.name}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user