feat(fusion_plating_shopfloor): mobile responsiveness, boxes stepper, racking panel

- Plant Kanban + Job Workspace made phone-responsive: height:100% + single
  internal scroll (was 100vh, broke mobile scroll), compact header/workflow
  bar, receiving part-line stacking so fields don't overflow, responsive
  lock-screen tile grid.
- +/- stepper on the receiving "Boxes received" field.
- Multi-rack Racking panel (Phase 1): split a WO's parts across racks
  (+Add Rack / Divide Equally / manual qty + Unassigned counter) on the Job
  Workspace, shown only when the WO is at the Racking step (area_kind based,
  excludes De-Racking). New /fp/racking/* controller.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsinghpal
2026-06-03 08:37:11 -04:00
parent ae256b4480
commit 5424c785d9
12 changed files with 514 additions and 14 deletions

View File

@@ -10,3 +10,4 @@ from . import workspace_controller
from . import landing_controller
from . import tablet_controller
from . import plant_kanban
from . import racking_controller

View File

@@ -0,0 +1,91 @@
# -*- coding: utf-8 -*-
# Copyright 2026 Nexa Systems Inc.
# License OPL-1 (Odoo Proprietary License v1.0)
#
# Multi-rack splitting at Racking — Phase 1 controller. Endpoints run as the
# technician (request.env.user); the rack-load + division logic lives on
# fp.rack.load (core + fusion_plating_jobs extension).
from odoo import http
from odoo.http import request
from odoo.exceptions import UserError
class FpRackingController(http.Controller):
def _job(self, job_id):
return request.env['fp.job'].browse(int(job_id))
def _payload(self, job):
Load = request.env['fp.rack.load']
loads = Load._fp_job_loads(job)
total = Load._fp_racking_total(job)
return {
'ok': True,
'job_id': job.id,
'wo_name': job.display_wo_name,
'total': total,
'unassigned': max(total - sum(loads.mapped('qty_total')), 0),
'loads': [{
'id': load.id,
'name': load.name,
'rack_id': load.rack_id.id or False,
'rack_name': load.rack_id.name or '',
'rack_capacity': load.rack_id.capacity or 0,
'qty': load.qty_total,
'over_capacity': bool(
load.rack_id and load.rack_id.capacity
and load.qty_total > load.rack_id.capacity),
'moved': bool(load.current_step_id),
} for load in loads],
}
@http.route('/fp/racking/load', type='jsonrpc', auth='user')
def load(self, job_id):
job = self._job(job_id)
request.env['fp.rack.load']._fp_ensure_seeded(job)
return self._payload(job)
@http.route('/fp/racking/add_rack', type='jsonrpc', auth='user')
def add_rack(self, job_id):
job = self._job(job_id)
try:
request.env['fp.rack.load']._fp_add_rack(job)
except UserError as e:
return {'ok': False, 'error': str(e.args[0])}
return self._payload(job)
@http.route('/fp/racking/divide_equally', type='jsonrpc', auth='user')
def divide_equally(self, job_id):
job = self._job(job_id)
request.env['fp.rack.load']._fp_divide_equally(job)
return self._payload(job)
@http.route('/fp/racking/set_qty', type='jsonrpc', auth='user')
def set_qty(self, load_id, qty):
load = request.env['fp.rack.load'].browse(int(load_id))
job = load.line_ids[:1].job_id
try:
load._fp_set_qty(qty)
except UserError as e:
return {'ok': False, 'error': str(e.args[0])}
return self._payload(job)
@http.route('/fp/racking/remove_rack', type='jsonrpc', auth='user')
def remove_rack(self, load_id):
load = request.env['fp.rack.load'].browse(int(load_id))
job = load.line_ids[:1].job_id
try:
load._fp_remove_rack()
except UserError as e:
return {'ok': False, 'error': str(e.args[0])}
return self._payload(job)
@http.route('/fp/racking/assign_rack', type='jsonrpc', auth='user')
def assign_rack(self, load_id, rack_id):
load = request.env['fp.rack.load'].browse(int(load_id))
rack = request.env['fusion.plating.rack'].browse(int(rack_id))
load.rack_id = rack.id
if 'racking_state' in rack._fields:
rack.racking_state = 'loaded'
return self._payload(load.line_ids[:1].job_id)

View File

@@ -283,6 +283,14 @@ class FpWorkspaceController(http.Controller):
'is_manager': env.user.has_group(
'fusion_plating.group_fusion_plating_manager',
),
# Racking panel (multi-rack split) shows when the WO is at the
# racking step and it's the current actionable work. Detect by
# area_kind == 'racking' (corrected classification) — NOT
# _fp_is_racking_step(), which would also match mis-tagged
# De-Racking steps (kind='racking' in the data).
'is_at_racking': bool(job.step_ids.filtered(
lambda s: s.area_kind == 'racking'
and s.state in ('ready', 'in_progress', 'paused'))),
}
# ======================================================================