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],
|
||||
})
|
||||
|
||||
# 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 {
|
||||
'facility_name': facility_name,
|
||||
'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)
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
@@ -23,6 +23,7 @@ import { registry } from "@web/core/registry";
|
||||
import { rpc } from "@web/core/network/rpc";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
import { QrScanner } from "./qr_scanner";
|
||||
import { FpMoveRackDialog } from "./move_rack_dialog";
|
||||
|
||||
// =============================================================================
|
||||
// TimerChip — per-card live elapsed-in-stage chip (v19.0.24.10.0)
|
||||
@@ -132,10 +133,12 @@ export class PlantOverview extends Component {
|
||||
setup() {
|
||||
this.notification = useService("notification");
|
||||
this.action = useService("action");
|
||||
this.dialog = useService("dialog");
|
||||
|
||||
this.state = useState({
|
||||
facilityName: "",
|
||||
columns: [],
|
||||
racks: [], // Sub 12b — Racks pane payload
|
||||
searchTerm: "",
|
||||
loading: false,
|
||||
lastRefresh: null,
|
||||
@@ -192,6 +195,7 @@ export class PlantOverview extends Component {
|
||||
if (result) {
|
||||
this.state.facilityName = result.facility_name || "Plant 1";
|
||||
this.state.columns = result.columns || [];
|
||||
this.state.racks = result.racks || [];
|
||||
this.state.lastRefresh = new Date().toLocaleTimeString();
|
||||
}
|
||||
} catch (err) {
|
||||
@@ -215,6 +219,23 @@ export class PlantOverview extends Component {
|
||||
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() {
|
||||
if (this._searchTimer) clearTimeout(this._searchTimer);
|
||||
this._searchTimer = setTimeout(() => this.loadData(), 200);
|
||||
|
||||
@@ -129,3 +129,47 @@ $fp-md-page: var(--fp-page-bg, #{$_fp_md_page_hex});
|
||||
font-size: .875rem;
|
||||
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>
|
||||
</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) ========== -->
|
||||
<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">
|
||||
|
||||
Reference in New Issue
Block a user