feat(sub12b): plant overview Racks pane (Task 16)
Controller: extend /fp/shopfloor/plant_overview return payload to include 'racks' array (filtered to loaded/in_use/awaiting_unrack states). Each entry has tag chips, part count, current node breadcrumb, current step + tank code, and a precomputed next_step_id (next sequence in the job's recipe — operator overrides at runtime in the Move Rack dialog). JS: state.racks populated from payload. New openMoveRackDialog() method spawns FpMoveRackDialog. Notification when rack has no successor (last step of job). XML: top section above the existing work-centre columns. Renders rack rows with tags, part count, breadcrumb, and primary MOVE RACK button per row. Visible only when state.racks.length > 0. SCSS: minimal styling for the racks pane (extends move_dialogs.scss to keep all Sub 12b styles in one file). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1274,11 +1274,56 @@ class FpShopfloorController(http.Controller):
|
|||||||
'cards': cards_by_wc[0],
|
'cards': cards_by_wc[0],
|
||||||
})
|
})
|
||||||
|
|
||||||
|
# Sub 12b — Racks pane payload alongside the existing parts cards.
|
||||||
|
# Filters to racks currently in active racking-state. Each entry
|
||||||
|
# has the data the OWL plant overview's Racks pane renders:
|
||||||
|
# tag chips, current node breadcrumb, part count, and the
|
||||||
|
# rack-level MOVE RACK button target.
|
||||||
|
rack_domain = [
|
||||||
|
('racking_state', 'in', ('loaded', 'in_use', 'awaiting_unrack')),
|
||||||
|
('active', '=', True),
|
||||||
|
]
|
||||||
|
if facility_id:
|
||||||
|
rack_domain.append(('facility_id', '=', int(facility_id)))
|
||||||
|
racks_payload = []
|
||||||
|
for r in env['fusion.plating.rack'].search(rack_domain):
|
||||||
|
cur_step = r.current_job_step_id
|
||||||
|
racks_payload.append({
|
||||||
|
'id': r.id,
|
||||||
|
'name': r.name,
|
||||||
|
'racking_state': r.racking_state,
|
||||||
|
'tag_ids': [
|
||||||
|
{'id': t.id, 'name': t.name, 'color': t.color}
|
||||||
|
for t in r.tag_ids
|
||||||
|
],
|
||||||
|
'current_part_count': r.current_part_count,
|
||||||
|
'current_node_name': cur_step.name if cur_step else '',
|
||||||
|
'current_tank_code': (
|
||||||
|
r.current_tank_id.code if r.current_tank_id else ''
|
||||||
|
),
|
||||||
|
'current_step_id': cur_step.id if cur_step else False,
|
||||||
|
# Default destination: next sibling step in the recipe
|
||||||
|
# sequence. Falls back to current_step_id if no successor
|
||||||
|
# (operator can pick a different one in the dialog).
|
||||||
|
'next_step_id': self._fp_next_step_id(cur_step) if cur_step else False,
|
||||||
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'facility_name': facility_name,
|
'facility_name': facility_name,
|
||||||
'columns': columns,
|
'columns': columns,
|
||||||
|
'racks': racks_payload,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def _fp_next_step_id(self, step):
|
||||||
|
"""Return the id of the next step in this job's recipe sequence,
|
||||||
|
or False if `step` is the last."""
|
||||||
|
if not step or not step.job_id:
|
||||||
|
return False
|
||||||
|
successors = step.job_id.step_ids.filtered(
|
||||||
|
lambda s: s.sequence > step.sequence
|
||||||
|
).sorted('sequence')
|
||||||
|
return successors[0].id if successors else False
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# Urgency scoring (v19.0.24.8.0)
|
# Urgency scoring (v19.0.24.8.0)
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import { registry } from "@web/core/registry";
|
|||||||
import { rpc } from "@web/core/network/rpc";
|
import { rpc } from "@web/core/network/rpc";
|
||||||
import { useService } from "@web/core/utils/hooks";
|
import { useService } from "@web/core/utils/hooks";
|
||||||
import { QrScanner } from "./qr_scanner";
|
import { QrScanner } from "./qr_scanner";
|
||||||
|
import { FpMoveRackDialog } from "./move_rack_dialog";
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// TimerChip — per-card live elapsed-in-stage chip (v19.0.24.10.0)
|
// TimerChip — per-card live elapsed-in-stage chip (v19.0.24.10.0)
|
||||||
@@ -132,10 +133,12 @@ export class PlantOverview extends Component {
|
|||||||
setup() {
|
setup() {
|
||||||
this.notification = useService("notification");
|
this.notification = useService("notification");
|
||||||
this.action = useService("action");
|
this.action = useService("action");
|
||||||
|
this.dialog = useService("dialog");
|
||||||
|
|
||||||
this.state = useState({
|
this.state = useState({
|
||||||
facilityName: "",
|
facilityName: "",
|
||||||
columns: [],
|
columns: [],
|
||||||
|
racks: [], // Sub 12b — Racks pane payload
|
||||||
searchTerm: "",
|
searchTerm: "",
|
||||||
loading: false,
|
loading: false,
|
||||||
lastRefresh: null,
|
lastRefresh: null,
|
||||||
@@ -192,6 +195,7 @@ export class PlantOverview extends Component {
|
|||||||
if (result) {
|
if (result) {
|
||||||
this.state.facilityName = result.facility_name || "Plant 1";
|
this.state.facilityName = result.facility_name || "Plant 1";
|
||||||
this.state.columns = result.columns || [];
|
this.state.columns = result.columns || [];
|
||||||
|
this.state.racks = result.racks || [];
|
||||||
this.state.lastRefresh = new Date().toLocaleTimeString();
|
this.state.lastRefresh = new Date().toLocaleTimeString();
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -215,6 +219,23 @@ export class PlantOverview extends Component {
|
|||||||
this._debouncedSearch();
|
this._debouncedSearch();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ===================================================== Sub 12b — racks
|
||||||
|
|
||||||
|
openMoveRackDialog(rackId, toStepId) {
|
||||||
|
if (!toStepId) {
|
||||||
|
this.notification.add(
|
||||||
|
"No destination step available — rack is at the last "
|
||||||
|
+ "step of its job. Use the rack form to manually pick "
|
||||||
|
+ "a new step.",
|
||||||
|
{ type: "warning" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.dialog.add(FpMoveRackDialog, {
|
||||||
|
rackId, toStepId,
|
||||||
|
onCommit: () => this.loadData(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
_debouncedSearch() {
|
_debouncedSearch() {
|
||||||
if (this._searchTimer) clearTimeout(this._searchTimer);
|
if (this._searchTimer) clearTimeout(this._searchTimer);
|
||||||
this._searchTimer = setTimeout(() => this.loadData(), 200);
|
this._searchTimer = setTimeout(() => this.loadData(), 200);
|
||||||
|
|||||||
@@ -129,3 +129,47 @@ $fp-md-page: var(--fp-page-bg, #{$_fp_md_page_hex});
|
|||||||
font-size: .875rem;
|
font-size: .875rem;
|
||||||
border-left: 4px solid $fp-md-accent;
|
border-left: 4px solid $fp-md-accent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ===================================================== Plant overview racks pane
|
||||||
|
|
||||||
|
.o_fp_racks_pane {
|
||||||
|
background: $fp-md-card;
|
||||||
|
border: 1px solid $fp-md-border;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: .75rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
|
||||||
|
.o_fp_racks_header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: .5rem;
|
||||||
|
margin-bottom: .5rem;
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1rem;
|
||||||
|
color: $fp-md-accent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_fp_rack_row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: .5rem;
|
||||||
|
padding: .5rem;
|
||||||
|
border-bottom: 1px solid $fp-md-border;
|
||||||
|
font-size: .875rem;
|
||||||
|
|
||||||
|
&:last-child { border-bottom: none; }
|
||||||
|
|
||||||
|
.o_fp_rack_name { font-weight: 600; min-width: 6rem; }
|
||||||
|
.o_fp_rack_count {
|
||||||
|
color: $fp-md-muted;
|
||||||
|
min-width: 5rem;
|
||||||
|
}
|
||||||
|
.o_fp_rack_breadcrumb {
|
||||||
|
flex: 1;
|
||||||
|
color: $fp-md-muted;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -72,6 +72,43 @@
|
|||||||
<p class="mt-3 text-muted">No work centres with active orders found.</p>
|
<p class="mt-3 text-muted">No work centres with active orders found.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- ========== Sub 12b — RACKS PANE ========== -->
|
||||||
|
<!-- Top section above the work-centre columns. Shows racks
|
||||||
|
currently in (loaded / in_use / awaiting_unrack) state
|
||||||
|
with tag chips, part count, current node breadcrumb,
|
||||||
|
and a MOVE RACK button per row. -->
|
||||||
|
<div class="o_fp_racks_pane" t-if="state.racks.length">
|
||||||
|
<div class="o_fp_racks_header">
|
||||||
|
<h3>Racks</h3>
|
||||||
|
<span class="o_fp_po_col_count badge rounded-pill">
|
||||||
|
<t t-esc="state.racks.length"/>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="o_fp_rack_row"
|
||||||
|
t-foreach="state.racks" t-as="rack" t-key="rack.id">
|
||||||
|
<span class="o_fp_rack_name"><t t-esc="rack.name"/></span>
|
||||||
|
<span class="o_fp_rack_count">
|
||||||
|
<t t-esc="rack.current_part_count"/> parts
|
||||||
|
</span>
|
||||||
|
<span class="o_fp_rack_breadcrumb">
|
||||||
|
<t t-esc="rack.current_node_name"/>
|
||||||
|
<t t-if="rack.current_tank_code">
|
||||||
|
/ <t t-esc="rack.current_tank_code"/>
|
||||||
|
</t>
|
||||||
|
</span>
|
||||||
|
<span t-foreach="rack.tag_ids" t-as="tag" t-key="tag.id"
|
||||||
|
class="o_fp_rack_tag_chip"
|
||||||
|
t-att-data-color="tag.color">
|
||||||
|
<t t-esc="tag.name"/>
|
||||||
|
</span>
|
||||||
|
<button class="btn btn-sm btn-primary"
|
||||||
|
t-att-disabled="!rack.next_step_id"
|
||||||
|
t-on-click="() => this.openMoveRackDialog(rack.id, rack.next_step_id)">
|
||||||
|
MOVE RACK
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- ========== COLUMNS (work centres) ========== -->
|
<!-- ========== COLUMNS (work centres) ========== -->
|
||||||
<div class="o_fp_po_columns" t-if="state.columns.length">
|
<div class="o_fp_po_columns" t-if="state.columns.length">
|
||||||
<t t-foreach="state.columns" t-as="col" t-key="col.work_center_id">
|
<t t-foreach="state.columns" t-as="col" t-key="col.work_center_id">
|
||||||
|
|||||||
Reference in New Issue
Block a user