feat(sub12b): OWL Rack Parts sub-dialog (Task 12)
Mirrors screens 7-8. Searchable empty-rack picker with debounced typeahead via /fp/tablet/rack/list_empty. QR Scan button prompts operator for FP-RACK:<name> token, resolves via /fp/tablet/rack/ scan_qr. Save commits the racking via /fp/tablet/rack_parts/commit. Save+Print opens /web/report/pdf/fp.rack.travel/<id> in a new tab — that report ships in Sub 12c, returns 404 until then. Plain Save works today. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,90 @@
|
||||
/** @odoo-module */
|
||||
/*
|
||||
* Sub 12b — Rack Parts sub-dialog (OWL).
|
||||
*
|
||||
* Mirrors Steelhead screens 7-8. Searchable empty-rack picker,
|
||||
* QR-scan input, Unit + Amount fields. Save assigns the step → rack;
|
||||
* Save + Print also opens the rack travel ticket PDF (Sub 12c).
|
||||
*/
|
||||
|
||||
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 FpRackPartsDialog extends Component {
|
||||
static template = "fusion_plating_shopfloor.FpRackPartsDialog";
|
||||
static components = { Dialog };
|
||||
static props = ["fromStepId", "qty", "onRacked?", "close"];
|
||||
|
||||
setup() {
|
||||
this.notification = useService("notification");
|
||||
this.state = useState({
|
||||
racks: [],
|
||||
search: "",
|
||||
selectedRackId: false,
|
||||
unit: "Count",
|
||||
amount: this.props.qty || 0,
|
||||
saving: false,
|
||||
});
|
||||
onWillStart(async () => {
|
||||
await this.refreshRacks("");
|
||||
});
|
||||
}
|
||||
|
||||
async refreshRacks(query) {
|
||||
const data = await rpc("/fp/tablet/rack/list_empty", { query });
|
||||
if (data.ok) {
|
||||
this.state.racks = data.racks;
|
||||
}
|
||||
}
|
||||
|
||||
async onSearch(ev) {
|
||||
this.state.search = ev.target.value;
|
||||
await this.refreshRacks(this.state.search);
|
||||
}
|
||||
|
||||
async onScan() {
|
||||
const code = window.prompt(_t("Scan or type FP-RACK:<name>:"));
|
||||
if (!code) return;
|
||||
const data = await rpc("/fp/tablet/rack/scan_qr", { qr_code: code });
|
||||
if (data.ok) {
|
||||
this.state.selectedRackId = data.rack_id;
|
||||
this.notification.add(
|
||||
_t("Selected %s", data.rack_name), { type: "success" });
|
||||
} else {
|
||||
this.notification.add(data.error, { type: "danger" });
|
||||
}
|
||||
}
|
||||
|
||||
async onSave(printAfter) {
|
||||
if (!this.state.selectedRackId) return;
|
||||
this.state.saving = true;
|
||||
const result = await rpc("/fp/tablet/rack_parts/commit", {
|
||||
from_step_id: this.props.fromStepId,
|
||||
rack_id: this.state.selectedRackId,
|
||||
qty: this.state.amount,
|
||||
});
|
||||
if (result.ok) {
|
||||
this.notification.add(
|
||||
_t("Racked onto %s", result.rack_name),
|
||||
{ type: "success" });
|
||||
if (this.props.onRacked) {
|
||||
this.props.onRacked(result);
|
||||
}
|
||||
this.props.close();
|
||||
if (printAfter) {
|
||||
// Sub 12c report — until it ships, this returns 404.
|
||||
window.open(
|
||||
`/web/report/pdf/fp.rack.travel/${this.state.selectedRackId}`,
|
||||
"_blank",
|
||||
);
|
||||
}
|
||||
} else {
|
||||
this.notification.add(result.error, { type: "danger" });
|
||||
this.state.saving = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="fusion_plating_shopfloor.FpRackPartsDialog">
|
||||
<Dialog title.translate="Rack Parts" size="'md'">
|
||||
<div class="o_fp_rack_dialog">
|
||||
|
||||
<div class="o_fp_move_field">
|
||||
<label>To Rack</label>
|
||||
<input type="text" placeholder="Search racks…"
|
||||
t-on-input="onSearch" t-att-value="state.search"/>
|
||||
<button class="btn btn-sm btn-secondary"
|
||||
t-on-click="onScan">
|
||||
QR Scan
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="o_fp_move_field">
|
||||
<label/>
|
||||
<select t-model.number="state.selectedRackId">
|
||||
<option value="">— Select empty rack —</option>
|
||||
<t t-foreach="state.racks" t-as="r" t-key="r.id">
|
||||
<option t-att-value="r.id">
|
||||
<t t-esc="r.name"/> (<t t-esc="r.rack_type"/>)
|
||||
</option>
|
||||
</t>
|
||||
</select>
|
||||
<span/>
|
||||
</div>
|
||||
|
||||
<div class="o_fp_move_field">
|
||||
<label>Unit</label>
|
||||
<select t-model="state.unit">
|
||||
<option value="Count">Count</option>
|
||||
<option value="Pieces">Pieces</option>
|
||||
<option value="Lbs">Lbs</option>
|
||||
<option value="Kg">Kg</option>
|
||||
</select>
|
||||
<span/>
|
||||
</div>
|
||||
|
||||
<div class="o_fp_move_field">
|
||||
<label>Amount</label>
|
||||
<input type="number" t-model.number="state.amount"/>
|
||||
<span class="text-muted" t-esc="state.unit"/>
|
||||
</div>
|
||||
</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="!state.selectedRackId or state.saving"
|
||||
t-on-click="() => this.onSave(false)">
|
||||
Save
|
||||
</button>
|
||||
<button class="btn btn-warning"
|
||||
t-att-disabled="!state.selectedRackId or state.saving"
|
||||
t-on-click="() => this.onSave(true)">
|
||||
Save + Print
|
||||
</button>
|
||||
</t>
|
||||
</Dialog>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
Reference in New Issue
Block a user