diff --git a/fusion_plating/fusion_plating_shopfloor/__manifest__.py b/fusion_plating/fusion_plating_shopfloor/__manifest__.py index 36d6eac3..e53d7548 100644 --- a/fusion_plating/fusion_plating_shopfloor/__manifest__.py +++ b/fusion_plating/fusion_plating_shopfloor/__manifest__.py @@ -5,7 +5,7 @@ { 'name': 'Fusion Plating — Shop Floor', - 'version': '19.0.36.1.1', + 'version': '19.0.37.0.1', 'category': 'Manufacturing/Plating', 'summary': 'Shop-floor tablet stations, QR scanning, bake window enforcer.', 'description': """ @@ -109,6 +109,11 @@ Copyright (c) 2026 Nexa Systems Inc. All rights reserved. 'fusion_plating_shopfloor/static/src/js/tablet_lock.js', 'fusion_plating_shopfloor/static/src/xml/components/pin_setup.xml', 'fusion_plating_shopfloor/static/src/js/components/pin_setup.js', + # ---- Racking panel (multi-rack split, Phase 1 — 2026-06-03) ---- + # Loaded before job_workspace.js (which imports RackingPanel). + 'fusion_plating_shopfloor/static/src/scss/components/_racking_panel.scss', + 'fusion_plating_shopfloor/static/src/xml/components/racking_panel.xml', + 'fusion_plating_shopfloor/static/src/js/components/racking_panel.js', # ---- Job Workspace (Phase 1 — tablet redesign) ---- 'fusion_plating_shopfloor/static/src/scss/job_workspace.scss', 'fusion_plating_shopfloor/static/src/xml/job_workspace.xml', diff --git a/fusion_plating/fusion_plating_shopfloor/controllers/__init__.py b/fusion_plating/fusion_plating_shopfloor/controllers/__init__.py index 17fe6476..fbbdbad4 100644 --- a/fusion_plating/fusion_plating_shopfloor/controllers/__init__.py +++ b/fusion_plating/fusion_plating_shopfloor/controllers/__init__.py @@ -10,3 +10,4 @@ from . import workspace_controller from . import landing_controller from . import tablet_controller from . import plant_kanban +from . import racking_controller diff --git a/fusion_plating/fusion_plating_shopfloor/controllers/racking_controller.py b/fusion_plating/fusion_plating_shopfloor/controllers/racking_controller.py new file mode 100644 index 00000000..1028a651 --- /dev/null +++ b/fusion_plating/fusion_plating_shopfloor/controllers/racking_controller.py @@ -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) diff --git a/fusion_plating/fusion_plating_shopfloor/controllers/workspace_controller.py b/fusion_plating/fusion_plating_shopfloor/controllers/workspace_controller.py index 6357b7af..08dfe0be 100644 --- a/fusion_plating/fusion_plating_shopfloor/controllers/workspace_controller.py +++ b/fusion_plating/fusion_plating_shopfloor/controllers/workspace_controller.py @@ -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'))), } # ====================================================================== diff --git a/fusion_plating/fusion_plating_shopfloor/static/src/js/components/racking_panel.js b/fusion_plating/fusion_plating_shopfloor/static/src/js/components/racking_panel.js new file mode 100644 index 00000000..bb8367c6 --- /dev/null +++ b/fusion_plating/fusion_plating_shopfloor/static/src/js/components/racking_panel.js @@ -0,0 +1,53 @@ +/** @odoo-module **/ +// Racking panel — split a WO's parts across multiple racks (Phase 1). +// Lives on the Job Workspace, shown when the WO is at the Racking step. +import { Component, useState, onWillStart } from "@odoo/owl"; +import { rpc } from "@web/core/network/rpc"; + +export class RackingPanel extends Component { + static template = "fusion_plating_shopfloor.RackingPanel"; + static props = ["jobId"]; + + setup() { + this.state = useState({ data: null, error: "", busy: false }); + onWillStart(() => this.reload()); + } + + _apply(d) { + if (d && d.ok) { + this.state.data = d; + this.state.error = ""; + } else { + this.state.error = (d && d.error) || "Something went wrong."; + } + } + + async _call(route, params) { + if (this.state.busy) { + return; + } + this.state.busy = true; + try { + this._apply(await rpc(route, params)); + } finally { + this.state.busy = false; + } + } + + reload() { + return this._call("/fp/racking/load", { job_id: this.props.jobId }); + } + addRack() { + return this._call("/fp/racking/add_rack", { job_id: this.props.jobId }); + } + divideEqually() { + return this._call("/fp/racking/divide_equally", { job_id: this.props.jobId }); + } + setQty(load, ev) { + const qty = parseInt(ev.target.value, 10) || 0; + return this._call("/fp/racking/set_qty", { load_id: load.id, qty }); + } + removeRack(load) { + return this._call("/fp/racking/remove_rack", { load_id: load.id }); + } +} diff --git a/fusion_plating/fusion_plating_shopfloor/static/src/js/job_workspace.js b/fusion_plating/fusion_plating_shopfloor/static/src/js/job_workspace.js index e55595f1..b52bbae4 100644 --- a/fusion_plating/fusion_plating_shopfloor/static/src/js/job_workspace.js +++ b/fusion_plating/fusion_plating_shopfloor/static/src/js/job_workspace.js @@ -30,11 +30,12 @@ import { FpTabletLock } from "./tablet_lock"; import { FpRackPartsDialog } from "./rack_parts_dialog"; import { FpDamageDialog } from "./fp_damage_dialog"; import { FpFinishBlockDialog } from "./fp_finish_block_dialog"; +import { RackingPanel } from "./components/racking_panel"; export class FpJobWorkspace extends Component { static template = "fusion_plating_shopfloor.JobWorkspace"; static props = ["*"]; - static components = { WorkflowChip, GateViz, FpSignaturePad, FpHoldComposer, FpTabletLock, FpRackPartsDialog, FpDamageDialog, FpFinishBlockDialog }; + static components = { WorkflowChip, GateViz, FpSignaturePad, FpHoldComposer, FpTabletLock, FpRackPartsDialog, FpDamageDialog, FpFinishBlockDialog, RackingPanel }; setup() { this.notification = useService("notification"); @@ -471,6 +472,17 @@ export class FpJobWorkspace extends Component { async onReceivingBoxCountBlur(rcv, ev) { const newVal = parseInt(ev.target.value, 10) || 0; + await this._saveReceivingBoxCount(rcv, newVal); + } + + // +/- stepper on the Boxes-received field. Clamps at 0 and reuses the + // same persist path as the typed-in blur handler. + async onReceivingBoxStep(rcv, delta) { + const newVal = Math.max(0, (rcv.box_count_in || 0) + delta); + await this._saveReceivingBoxCount(rcv, newVal); + } + + async _saveReceivingBoxCount(rcv, newVal) { if (newVal === (rcv.box_count_in || 0)) return; rcv.box_count_in = newVal; try { diff --git a/fusion_plating/fusion_plating_shopfloor/static/src/scss/components/_racking_panel.scss b/fusion_plating/fusion_plating_shopfloor/static/src/scss/components/_racking_panel.scss new file mode 100644 index 00000000..74069ba8 --- /dev/null +++ b/fusion_plating/fusion_plating_shopfloor/static/src/scss/components/_racking_panel.scss @@ -0,0 +1,70 @@ +// Racking panel (Job Workspace) — split a WO across racks. Self-contained +// tokens with a compile-time dark-mode branch (Odoo 19 compiles this file +// into both web.assets_backend and web.assets_web_dark). + +$o-webclient-color-scheme: bright !default; + +$_rkp-card-hex: #ffffff; +$_rkp-border-hex: #d8dadd; +$_rkp-text-hex: #1d1f1e; +$_rkp-page-hex: #f3f4f6; + +@if $o-webclient-color-scheme == dark { + $_rkp-card-hex: #22262d !global; + $_rkp-border-hex: #3a3f47 !global; + $_rkp-text-hex: #e6e6e6 !global; + $_rkp-page-hex: #1a1d21 !global; +} + +.o_fp_racking_panel { + background: $_rkp-card-hex; + border: 1px solid $_rkp-border-hex; + border-left: 4px solid #0071e3; + border-radius: 8px; + padding: 0.7rem 0.9rem; + margin-bottom: 0.7rem; + color: $_rkp-text-hex; + + .o_fp_rkp_head { + display: flex; + justify-content: space-between; + align-items: center; + gap: 0.5rem; + flex-wrap: wrap; + margin-bottom: 0.5rem; + } + .o_fp_rkp_title { font-weight: 700; } + .o_fp_rkp_unassigned { + font-size: 0.85rem; + color: #1d6e2f; + &.has { color: #b06600; font-weight: 600; } + } + .o_fp_rkp_err { + color: #b00018; + font-size: 0.85rem; + margin-bottom: 0.4rem; + } + .o_fp_rkp_row { + display: flex; + align-items: center; + gap: 0.6rem; + padding: 0.4rem 0; + border-bottom: 1px solid $_rkp-border-hex; + &:last-of-type { border-bottom: 0; } + &.over { border-left: 3px solid #ff9f0a; padding-left: 0.4rem; } + } + .o_fp_rkp_rk { flex: 1; font-weight: 600; } + .o_fp_rkp_qty { + width: 6rem; + text-align: center; + font-weight: 600; + background: $_rkp-page-hex; + } + .o_fp_rkp_cap { color: #888; font-size: 0.85rem; } + .o_fp_rkp_actions { + display: flex; + gap: 0.5rem; + margin-top: 0.6rem; + flex-wrap: wrap; + } +} diff --git a/fusion_plating/fusion_plating_shopfloor/static/src/scss/job_workspace.scss b/fusion_plating/fusion_plating_shopfloor/static/src/scss/job_workspace.scss index 7fcad9a5..4fe22cf5 100644 --- a/fusion_plating/fusion_plating_shopfloor/static/src/scss/job_workspace.scss +++ b/fusion_plating/fusion_plating_shopfloor/static/src/scss/job_workspace.scss @@ -219,19 +219,38 @@ $_ws-text-hex: #1d1d1f; grid-template-columns: 1.7fr 1fr; overflow: hidden; - @media (max-width: 900px) { grid-template-columns: 1fr; } + // Single column on tablets/phones, and make MAIN itself the one scroll + // container — the work (steps/receiving) sits at the top, Notes stack + // below and scroll into view when needed. The old layout kept + // overflow:hidden with two nested auto-height scroll panes, which + // clipped the notes and broke scrolling on narrow screens. + @media (max-width: 900px) { + display: flex; + flex-direction: column; + overflow-y: auto; + -webkit-overflow-scrolling: touch; + overscroll-behavior: contain; + } } .o_fp_ws_steps { padding: 0.7rem 1rem; overflow-y: auto; border-right: 1px solid $_ws-border-hex; + + // MAIN owns the scroll on narrow screens; don't nest a second one. + @media (max-width: 900px) { + overflow-y: visible; + border-right: none; + } } .o_fp_ws_side { padding: 0.7rem 1rem; overflow-y: auto; background: $_ws-page-hex; + + @media (max-width: 900px) { overflow-y: visible; } } .o_fp_ws_empty { @@ -385,6 +404,61 @@ $_ws-text-hex: #1d1d1f; flex-wrap: wrap; } +// ===== Phone optimization (2026-06-02) ============================== +// Shrink the fixed chrome (header + workflow bar + rail) so the operator +// sees the actual work (receiving / step cards) without scrolling. Notes +// sit below the work in the single scroll column — present, scroll for more. +@media (max-width: 600px) { + .o_fp_ws_head { + padding: 0.45rem 0.7rem; + gap: 0.4rem 0.6rem; + } + .o_fp_ws_head_l, .o_fp_ws_head_r { gap: 0.35rem; } + .o_fp_ws_wo { font-size: 1.05rem; } + .o_fp_ws_cust, .o_fp_ws_part { font-size: 0.85rem; } + .o_fp_ws_back, .o_fp_ws_handoff { padding: 0.4rem 0.7rem; font-size: 0.85rem; } + .o_fp_ws_pill { padding: 0.2rem 0.5rem; font-size: 0.76rem; } + + // Workflow bar: tighter + horizontally scrollable so every stage is + // reachable (it was clipped) while taking far less vertical room. + .o_fp_ws_bar { padding: 0.4rem 0.7rem; gap: 0.5rem; } + .o_fp_ws_bar_line { + overflow-x: auto; + overscroll-behavior-x: contain; + -webkit-overflow-scrolling: touch; + padding-bottom: 2px; + } + .o_fp_ws_dot_wrap { min-width: 56px; } + .o_fp_ws_dot_wrap .o_fp_ws_bar_dot { width: 14px; height: 14px; } + .o_fp_ws_dot_wrap .o_fp_ws_bar_label { font-size: 0.66rem; margin-top: 0.2rem; } + .o_fp_ws_next { white-space: nowrap; } + + // Work + notes stack tighter; rail stays tappable but compact. + .o_fp_ws_steps, .o_fp_ws_side { padding: 0.5rem 0.6rem; } + .o_fp_ws_rail { padding: 0.45rem 0.6rem; gap: 0.4rem; } + + // Receiving card part lines: stack vertically so the Received input and + // the Good/Damaged select wrap INSIDE the card instead of overflowing + // off the right edge. The part description now wraps across full width. + .o_fp_ws_rcv { padding: 0.7rem; } + .o_fp_ws_rcv_line { + flex-direction: column; + align-items: stretch; + gap: 0.45rem; + } + .o_fp_ws_rcv_line_part { min-width: 0; overflow-wrap: anywhere; } + .o_fp_ws_rcv_line_qty { + flex-wrap: wrap; + gap: 0.5rem 0.75rem; + } + .o_fp_ws_rcv_line_qty label { flex: 1 1 8rem; } + .o_fp_ws_rcv_qty_input { width: auto; flex: 1 1 4rem; min-width: 3.5rem; } + .o_fp_ws_rcv_cond_select { width: auto; flex: 1 1 7rem; min-width: 6rem; } + + // Shipping panel fields already wrap; keep them inside the card too. + .o_fp_ws_ship_fields label { min-width: 0; } +} + // ============================================================================= // SHIPPING PANEL (tablet receiving+shipping 2026-05-29) // ============================================================================= @@ -519,6 +593,45 @@ $_ws-text-hex: #1d1d1f; text-align: center; } +// +/- stepper around the Boxes-received input (and any future numeric step +// field). Big touch targets; the input grows to fill between the buttons. +.o_fp_ws_stepper { + display: flex; + align-items: stretch; + gap: 0.4rem; + max-width: 18rem; + + .o_fp_ws_rcv_box_input { + flex: 1 1 auto; + max-width: none; // override the standalone 12rem cap inside the stepper + margin: 0; + } +} + +.o_fp_ws_stepper_btn { + flex: 0 0 auto; + width: 3rem; + min-height: 3rem; // comfortable touch target + border: 1px solid $_ws-border-hex; + border-radius: 8px; + background: linear-gradient(135deg, $_ws-card-hex 0%, $_ws-page-hex 100%); + color: $_ws-text-hex; + font-size: 1.2rem; + font-weight: 700; + line-height: 1; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: transform 0.1s ease, background 0.1s ease, box-shadow 0.1s ease; + + &:hover { + transform: translateY(-1px); + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.12); + } + &:active { transform: scale(0.95); } +} + .o_fp_ws_rcv_lines { background: $_ws-page-hex; border-radius: 6px; diff --git a/fusion_plating/fusion_plating_shopfloor/static/src/scss/plant_kanban.scss b/fusion_plating/fusion_plating_shopfloor/static/src/scss/plant_kanban.scss index 3020e352..a3c4eca3 100644 --- a/fusion_plating/fusion_plating_shopfloor/static/src/scss/plant_kanban.scss +++ b/fusion_plating/fusion_plating_shopfloor/static/src/scss/plant_kanban.scss @@ -3,11 +3,16 @@ .o_fp_plant_kanban { padding: 8px; background: $plant-bg; - // Full viewport height + flex column so .board can grow to fill all - // remaining vertical space. min-height: 100vh would let .board's - // intrinsic height bubble up and put the horizontal scrollbar - // mid-page; height + flex pins the scrollbar to the viewport bottom. - height: 100vh; + // Fill the Odoo action area (below the navbar) and own the scroll + // internally — NOT 100vh. 100vh is taller than the available area by + // the navbar height, so the board bottom + its horizontal scrollbar + // overflowed off-screen and scrolling broke — badly on phones, where + // Odoo also re-lays-out at the md breakpoint and the scroll gets lost + // up the tree. height:100% + internal overflow is the same pattern + // job_workspace / manager_dashboard / .o_fp_tablet use. flex column so + // .board fills the remaining space under the sticky header. + height: 100%; + min-height: 0; display: flex; flex-direction: column; color: $plant-text; @@ -97,7 +102,26 @@ // 8 tiles — Work Orders, At My Station, Bakes Due, On Hold, // Awaiting QC, Awaiting CoC, Ready to Ship, Overdue. - .kpi-strip { display: grid; grid-template-columns: repeat(8, 1fr); gap: 8px; } + .kpi-strip { + display: grid; + grid-template-columns: repeat(8, 1fr); + gap: 8px; + // 8 tiles can't fit a narrow row. Drop to 4-up on tablets, then on + // phones make it one horizontally-scrollable row so the header stays + // short and the board keeps the screen. + @media (max-width: 1180px) { grid-template-columns: repeat(4, 1fr); } + @media (max-width: 600px) { + grid-template-columns: none; + grid-auto-flow: column; + grid-auto-columns: 42%; + overflow-x: auto; + overscroll-behavior-x: contain; + -webkit-overflow-scrolling: touch; + scroll-snap-type: x proximity; + padding-bottom: 2px; + > * { scroll-snap-align: start; } + } + } .search-row { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; } .search-input { @@ -134,7 +158,23 @@ flex: 1 1 auto; min-height: 0; overflow-x: auto; + overflow-y: hidden; // columns scroll internally, not the board + overscroll-behavior-x: contain; // don't bounce the whole page sideways + -webkit-overflow-scrolling: touch; padding-bottom: 4px; // room for the horizontal scrollbar + // Tablet: slightly narrower columns so more fit per swipe. + @media (max-width: 900px) { + grid-template-columns: repeat(9, minmax(260px, 1fr)); + } + // Phone: one full-width stage per screen; swipe between stages. + // 86vw leaves a peek of the next column so it's clearly scrollable. + // Cards are width:100% so they never overflow the narrower column. + @media (max-width: 600px) { + grid-template-columns: repeat(9, 86vw); + gap: 10px; + scroll-snap-type: x proximity; + > .col { scroll-snap-align: start; } + } } // Each .col is now a proper bordered card that runs full board // height — same visual treatment as Trello / Asana columns. The @@ -214,3 +254,29 @@ } } } + +// ===== Responsive — phones / small screens (2026-06-02) ============== +// Compact the header so the board keeps the screen, and make the toolbar +// controls full-width + tappable (>=40px) on a phone. +.o_fp_plant_kanban { + @media (max-width: 600px) { + padding: 6px; + + .floor-header { padding: 8px; margin-bottom: 6px; gap: 6px; } + .floor-title { font-size: 15px; } + + .floor-header-top { gap: 8px; } + .floor-controls { gap: 5px; width: 100%; } + + // 3-segment mode toggle spans the row; each segment stays tappable. + .mode-toggle { flex: 1 1 100%; } + .mode-toggle .mode-btn { flex: 1 1 0; padding: 10px 6px; } + + .station-picker, + .toolbar-btn { padding: 10px 12px; } + + // Search fills its own row; filter chips wrap underneath. + .search-row { gap: 6px; } + .search-input { min-width: 0; flex: 1 1 100%; } + } +} diff --git a/fusion_plating/fusion_plating_shopfloor/static/src/scss/tablet_lock.scss b/fusion_plating/fusion_plating_shopfloor/static/src/scss/tablet_lock.scss index 8b2c074b..e1f5456b 100644 --- a/fusion_plating/fusion_plating_shopfloor/static/src/scss/tablet_lock.scss +++ b/fusion_plating/fusion_plating_shopfloor/static/src/scss/tablet_lock.scss @@ -328,3 +328,22 @@ &:hover { color: $lock-text; } } } + +// ===== Responsive — phones / small screens (2026-06-02) ============== +// The lock screen is position:fixed + overflow-y:auto, so it already +// scrolls; it just needs the 5-up operator grid + chrome to step down +// on narrow screens. Ordered descending max-width so the smaller query +// wins the cascade. +@media (max-width: 900px) { + .o_fp_lock_tiles { grid-template-columns: repeat(4, 1fr); } +} +@media (max-width: 600px) { + .o_fp_tablet_lock { padding: 16px 12px; gap: 16px; } + .o_fp_lock_tiles { grid-template-columns: repeat(3, 1fr); gap: 10px; } + .o_fp_lock_clock { font-size: 34px; } + .o_fp_lock_logo_frame { width: 220px; height: 92px; } + .o_fp_lock_wizard { padding: 24px 20px; } +} +@media (max-width: 380px) { + .o_fp_lock_tiles { grid-template-columns: repeat(2, 1fr); } +} diff --git a/fusion_plating/fusion_plating_shopfloor/static/src/xml/components/racking_panel.xml b/fusion_plating/fusion_plating_shopfloor/static/src/xml/components/racking_panel.xml new file mode 100644 index 00000000..fe00be87 --- /dev/null +++ b/fusion_plating/fusion_plating_shopfloor/static/src/xml/components/racking_panel.xml @@ -0,0 +1,43 @@ + + + + +
+
+ 🧰 Racking — split across racks + + Unassigned: / + +
+ +
+ + +
+ + Rack : + + + + / + + +
+
+ +
+ + +
+
+ + + diff --git a/fusion_plating/fusion_plating_shopfloor/static/src/xml/job_workspace.xml b/fusion_plating/fusion_plating_shopfloor/static/src/xml/job_workspace.xml index 86b7cb99..8344c447 100644 --- a/fusion_plating/fusion_plating_shopfloor/static/src/xml/job_workspace.xml +++ b/fusion_plating/fusion_plating_shopfloor/static/src/xml/job_workspace.xml @@ -80,6 +80,11 @@
+ + +