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:
gsinghpal
2026-04-27 21:13:04 -04:00
parent 6d046f2881
commit 48c06c40c9
2 changed files with 157 additions and 0 deletions

View File

@@ -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;
}
}
}

View File

@@ -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>