feat(sub12b): OWL Move Parts dialog (Task 11)
Mirrors Steelhead screens 1-3, 14-15. Loads preview on mount,
re-checks hard-blockers on commit. MOVE (n) button disabled when
hard-blocked OR required prompt blank — improvement over Steelhead's
silent disabled state (we show a tooltip listing reasons).
Inline 'Resolve' button next to each blocker. For rack-required,
fires a window CustomEvent ('fp-resolve-rack') the parent tablet
catches to open the Rack Parts sub-dialog.
Typed input rendering by input_type — text/number/checkbox/select/
datetime, plus support for time_hms and signature/photo (text input
for now; full upload widget in Sub 12c).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,155 @@
|
||||
/** @odoo-module */
|
||||
/*
|
||||
* Sub 12b — Move Parts dialog (OWL).
|
||||
*
|
||||
* Mirror of Steelhead screens 1-3, 14-15. Loads preview on mount,
|
||||
* re-checks hard-blockers on commit. MOVE (n) button disabled when
|
||||
* hard-blocked OR required prompt blank — improvement over Steelhead's
|
||||
* silent disabled state (we show a tooltip listing blockers).
|
||||
*
|
||||
* Inline 'Resolve' button next to each blocker with a resolve_action.
|
||||
* For rack-required, fires a window CustomEvent ('fp-resolve-rack')
|
||||
* the parent tablet catches to open the Rack Parts sub-dialog.
|
||||
*/
|
||||
|
||||
import { Component, onWillStart, useState } from "@odoo/owl";
|
||||
import { Dialog } from "@web/core/dialog/dialog";
|
||||
import { rpc } from "@web/core/network/rpc";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
import { _t } from "@web/core/l10n/translation";
|
||||
|
||||
|
||||
export class FpMovePartsDialog extends Component {
|
||||
static template = "fusion_plating_shopfloor.FpMovePartsDialog";
|
||||
static components = { Dialog };
|
||||
static props = ["fromStepId", "toStepId", "onCommit?", "close"];
|
||||
|
||||
setup() {
|
||||
this.notification = useService("notification");
|
||||
this.state = useState({
|
||||
loading: true,
|
||||
qty: 0,
|
||||
qtyAvailable: 0,
|
||||
fromStep: { tank_options: [] },
|
||||
toStep: { tank_options: [] },
|
||||
transferType: "step",
|
||||
toTankId: false,
|
||||
toLocation: "global",
|
||||
customerWoCount: 0,
|
||||
transitionPrompts: [],
|
||||
promptValues: {},
|
||||
blockers: [],
|
||||
committing: false,
|
||||
});
|
||||
onWillStart(async () => {
|
||||
await this.loadPreview();
|
||||
});
|
||||
}
|
||||
|
||||
async loadPreview() {
|
||||
this.state.loading = true;
|
||||
const data = await rpc("/fp/tablet/move_parts/preview", {
|
||||
from_step_id: this.props.fromStepId,
|
||||
to_step_id: this.props.toStepId,
|
||||
});
|
||||
if (!data.ok) {
|
||||
this.notification.add(data.error || _t("Preview failed"),
|
||||
{ type: "danger" });
|
||||
this.props.close();
|
||||
return;
|
||||
}
|
||||
this.state.qtyAvailable = data.qty_available;
|
||||
this.state.qty = data.qty_available;
|
||||
this.state.fromStep = data.from_step;
|
||||
this.state.toStep = data.to_step;
|
||||
this.state.transitionPrompts = data.transition_prompts;
|
||||
this.state.blockers = data.blockers;
|
||||
const opts = data.to_step.tank_options || [];
|
||||
this.state.toTankId = opts.length ? opts[0].id : false;
|
||||
this.state.loading = false;
|
||||
}
|
||||
|
||||
get hardBlocked() {
|
||||
return this.state.blockers.some((b) => b.severity === "hard");
|
||||
}
|
||||
|
||||
get requiredPromptsBlank() {
|
||||
return this.state.transitionPrompts.some((p) => {
|
||||
return p.required && !this.state.promptValues[p.id];
|
||||
});
|
||||
}
|
||||
|
||||
get blockerTooltip() {
|
||||
const reasons = [];
|
||||
if (this.hardBlocked) {
|
||||
reasons.push(...this.state.blockers
|
||||
.filter((b) => b.severity === "hard")
|
||||
.map((b) => b.message));
|
||||
}
|
||||
if (this.requiredPromptsBlank) {
|
||||
reasons.push(_t("Required compliance prompt(s) blank."));
|
||||
}
|
||||
if (this.state.qty <= 0) {
|
||||
reasons.push(_t("Part Count must be > 0."));
|
||||
}
|
||||
if (this.state.qty > this.state.qtyAvailable) {
|
||||
reasons.push(_t("Part Count exceeds available."));
|
||||
}
|
||||
return reasons.join(" · ");
|
||||
}
|
||||
|
||||
get canCommit() {
|
||||
return !this.state.committing &&
|
||||
!this.hardBlocked &&
|
||||
!this.requiredPromptsBlank &&
|
||||
this.state.qty > 0 &&
|
||||
this.state.qty <= this.state.qtyAvailable;
|
||||
}
|
||||
|
||||
async onCommit() {
|
||||
if (!this.canCommit) return;
|
||||
this.state.committing = true;
|
||||
const result = await rpc("/fp/tablet/move_parts/commit", {
|
||||
from_step_id: this.props.fromStepId,
|
||||
to_step_id: this.props.toStepId,
|
||||
qty: this.state.qty,
|
||||
transfer_type: this.state.transferType,
|
||||
to_tank_id: this.state.toTankId || false,
|
||||
to_location: this.state.toLocation,
|
||||
customer_wo_count: this.state.customerWoCount,
|
||||
prompt_values: this.state.promptValues,
|
||||
});
|
||||
if (result.ok) {
|
||||
this.notification.add(
|
||||
_t("Move %s committed", result.move_name),
|
||||
{ type: "success" },
|
||||
);
|
||||
if (this.props.onCommit) {
|
||||
this.props.onCommit(result);
|
||||
}
|
||||
this.props.close();
|
||||
} else {
|
||||
this.notification.add(
|
||||
result.error || _t("Move failed"),
|
||||
{ type: "danger" });
|
||||
this.state.committing = false;
|
||||
}
|
||||
}
|
||||
|
||||
onResolveBlocker(blocker) {
|
||||
if (blocker.resolve_action === "open_rack_parts_dialog") {
|
||||
const ev = new CustomEvent("fp-resolve-rack", {
|
||||
detail: {
|
||||
fromStepId: this.props.fromStepId,
|
||||
qty: this.state.qty,
|
||||
},
|
||||
});
|
||||
window.dispatchEvent(ev);
|
||||
this.props.close();
|
||||
} else {
|
||||
this.notification.add(
|
||||
blocker.message + " " + _t("(Open the related step manually.)"),
|
||||
{ type: "warning" });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="fusion_plating_shopfloor.FpMovePartsDialog">
|
||||
<Dialog title.translate="Move Parts" size="'lg'">
|
||||
<div class="o_fp_move_dialog" t-if="!state.loading">
|
||||
|
||||
<div class="o_fp_move_field">
|
||||
<label>Part Count</label>
|
||||
<input type="number" t-model.number="state.qty"
|
||||
t-att-min="1" t-att-max="state.qtyAvailable"/>
|
||||
<span class="text-muted">Available: <t t-esc="state.qtyAvailable"/></span>
|
||||
</div>
|
||||
|
||||
<div class="o_fp_move_field">
|
||||
<label>From Node</label>
|
||||
<span t-esc="state.fromStep.name"/>
|
||||
<span/>
|
||||
</div>
|
||||
|
||||
<div class="o_fp_move_field" t-if="state.fromStep.tank_name">
|
||||
<label>From Station</label>
|
||||
<span t-esc="state.fromStep.tank_name"/>
|
||||
<span/>
|
||||
</div>
|
||||
|
||||
<div class="o_fp_move_field">
|
||||
<label>Transfer Type</label>
|
||||
<select t-model="state.transferType">
|
||||
<option value="step">Step</option>
|
||||
<option value="hold">Hold</option>
|
||||
<option value="scrap">Scrap</option>
|
||||
<option value="rework">Rework</option>
|
||||
<option value="split">Split</option>
|
||||
<option value="return">Return</option>
|
||||
</select>
|
||||
<span/>
|
||||
</div>
|
||||
|
||||
<div class="o_fp_move_field">
|
||||
<label>To Node</label>
|
||||
<span t-esc="state.toStep.name"/>
|
||||
<span/>
|
||||
</div>
|
||||
|
||||
<div class="o_fp_move_field"
|
||||
t-if="state.toStep.tank_options and state.toStep.tank_options.length > 1">
|
||||
<label>To Station</label>
|
||||
<select t-model.number="state.toTankId">
|
||||
<t t-foreach="state.toStep.tank_options"
|
||||
t-as="tk" t-key="tk.id">
|
||||
<option t-att-value="tk.id"><t t-esc="tk.name"/></option>
|
||||
</t>
|
||||
</select>
|
||||
<span/>
|
||||
</div>
|
||||
|
||||
<div class="o_fp_move_field">
|
||||
<label>To Location</label>
|
||||
<select t-model="state.toLocation">
|
||||
<option value="global">Global</option>
|
||||
<option value="quarantine">Quarantine</option>
|
||||
<option value="staging_a">Staging A</option>
|
||||
<option value="staging_b">Staging B</option>
|
||||
<option value="shipping_dock">Shipping Dock</option>
|
||||
<option value="scrap_bin">Scrap Bin</option>
|
||||
</select>
|
||||
<span/>
|
||||
</div>
|
||||
|
||||
<div class="o_fp_compliance_prompts"
|
||||
t-if="state.transitionPrompts.length">
|
||||
<h5>Compliance Prompts</h5>
|
||||
<t t-foreach="state.transitionPrompts" t-as="p" t-key="p.id">
|
||||
<div class="o_fp_move_field">
|
||||
<label>
|
||||
<t t-esc="p.name"/>
|
||||
<span t-if="p.required" class="text-danger">*</span>
|
||||
</label>
|
||||
<input t-if="['text','customer_wo','time_hms','location_picker','signature','photo'].includes(p.input_type)"
|
||||
type="text" t-model="state.promptValues[p.id]"/>
|
||||
<input t-elif="['number','temperature','thickness','time_seconds'].includes(p.input_type)"
|
||||
type="number" t-model.number="state.promptValues[p.id]"/>
|
||||
<input t-elif="p.input_type === 'boolean' or p.input_type === 'pass_fail'"
|
||||
type="checkbox" t-model="state.promptValues[p.id]"/>
|
||||
<input t-elif="p.input_type === 'date'"
|
||||
type="datetime-local" t-model="state.promptValues[p.id]"/>
|
||||
<select t-elif="p.input_type === 'selection'"
|
||||
t-model="state.promptValues[p.id]">
|
||||
<option value="">— Select —</option>
|
||||
<t t-foreach="p.selection_options.split(',')"
|
||||
t-as="opt" t-key="opt_index">
|
||||
<option t-att-value="opt.trim()"><t t-esc="opt.trim()"/></option>
|
||||
</t>
|
||||
</select>
|
||||
<span class="text-muted" t-if="p.hint"><t t-esc="p.hint"/></span>
|
||||
<span t-else=""/>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
|
||||
<div class="o_fp_blockers" t-if="state.blockers.length">
|
||||
<h5>Blockers</h5>
|
||||
<t t-foreach="state.blockers" t-as="b" t-key="b_index">
|
||||
<div class="o_fp_blocker_row"
|
||||
t-att-class="b.severity === 'hard' ? 'o_fp_blocker_hard' : 'o_fp_blocker_soft'">
|
||||
<span class="o_fp_blocker_icon">⚠</span>
|
||||
<span class="o_fp_blocker_msg" t-esc="b.message"/>
|
||||
<button t-if="b.resolve_action"
|
||||
class="btn btn-sm btn-warning"
|
||||
t-on-click="() => this.onResolveBlocker(b)">
|
||||
Resolve
|
||||
</button>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div t-if="state.loading">Loading…</div>
|
||||
|
||||
<t t-set-slot="footer">
|
||||
<button class="btn btn-secondary" t-on-click="props.close">
|
||||
Cancel
|
||||
</button>
|
||||
<button class="btn btn-primary"
|
||||
t-att-disabled="!canCommit"
|
||||
t-att-title="blockerTooltip"
|
||||
t-on-click="onCommit">
|
||||
MOVE (<t t-esc="state.qty"/>)
|
||||
</button>
|
||||
</t>
|
||||
</Dialog>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
Reference in New Issue
Block a user