feat(sub12b): Move Rack + Stop Timer OWL dialogs (Task 13)

Move Rack: rack name in title via getter, tag chips, batches list
(read-only), Type + To Node + To Station picker. Atomic Save commits
all batches via /fp/tablet/move_rack/commit.

Stop Timer: opens with state already at 'stopped' (server flipped on
load via /labor_timer/stop), pre-fills billed_* from accrued.
Operator edits → Save (state → reconciled).

Save & Start New Timer chains into a fresh timer for the same step
via the start_new=True flag — mirrors screen 10's right-most button.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsinghpal
2026-04-27 21:14:30 -04:00
parent 48c06c40c9
commit 270f427d7f
4 changed files with 299 additions and 0 deletions

View File

@@ -0,0 +1,78 @@
/** @odoo-module */
/*
* Sub 12b — Move Rack dialog (OWL).
*
* Mirrors screens 11, 13, 14. Same shape as Move Parts but no
* transition prompts (rack moves are rack-level). Title carries
* rack name; parts list (read-only) shows all batches on the rack.
*/
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 FpMoveRackDialog extends Component {
static template = "fusion_plating_shopfloor.FpMoveRackDialog";
static components = { Dialog };
static props = ["rackId", "toStepId", "onCommit?", "close"];
setup() {
this.notification = useService("notification");
this.state = useState({
loading: true,
rack: { tag_ids: [] },
batches: [],
toStep: { tank_options: [] },
transferType: "step",
toTankId: false,
saving: false,
});
onWillStart(async () => {
const data = await rpc("/fp/tablet/move_rack/preview", {
rack_id: this.props.rackId,
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.rack = data.rack;
this.state.batches = data.batches;
this.state.toStep = data.to_step;
const opts = data.to_step.tank_options || [];
this.state.toTankId = opts.length ? opts[0].id : false;
this.state.loading = false;
});
}
get title() {
return `Move Rack: ${this.state.rack.name || ""}`;
}
async onSave() {
this.state.saving = true;
const result = await rpc("/fp/tablet/move_rack/commit", {
rack_id: this.props.rackId,
to_step_id: this.props.toStepId,
transfer_type: this.state.transferType,
to_tank_id: this.state.toTankId || false,
});
if (result.ok) {
this.notification.add(
_t("Moved %s batches", result.count),
{ type: "success" });
if (this.props.onCommit) {
this.props.onCommit(result);
}
this.props.close();
} else {
this.notification.add(result.error, { type: "danger" });
this.state.saving = false;
}
}
}

View File

@@ -0,0 +1,92 @@
/** @odoo-module */
/*
* Sub 12b — Stop User Labor Timer dialog (OWL).
*
* Mirrors screen 10. Opens with state already at 'stopped' (server-side
* flip on /labor_timer/stop), pre-fills billed_* from accrued. Operator
* edits → Save (state → reconciled). Save & Start New Timer chains
* into a fresh timer for the same step.
*/
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 FpStopTimerDialog extends Component {
static template = "fusion_plating_shopfloor.FpStopTimerDialog";
static components = { Dialog };
static props = ["timerId", "onReconciled?", "close"];
setup() {
this.notification = useService("notification");
this.state = useState({
loading: true,
accruedSeconds: 0,
billedHrs: 0,
billedMin: 0,
billedSec: 0,
notes: "",
saving: false,
});
onWillStart(async () => {
const data = await rpc("/fp/tablet/labor_timer/stop",
{ timer_id: this.props.timerId });
if (data.ok) {
this.state.accruedSeconds = data.accrued_seconds;
this.state.billedHrs = data.billed_hrs;
this.state.billedMin = data.billed_min;
this.state.billedSec = data.billed_sec;
this.state.loading = false;
} else {
this.notification.add(data.error || _t("Stop failed"),
{ type: "danger" });
this.props.close();
}
});
}
get billedPct() {
const total = this.state.billedHrs * 3600
+ this.state.billedMin * 60
+ this.state.billedSec;
if (!this.state.accruedSeconds) return 0;
return Math.round(100 * total / this.state.accruedSeconds);
}
get accruedDisplay() {
const s = this.state.accruedSeconds;
const h = Math.floor(s / 3600);
const m = Math.floor((s % 3600) / 60);
const sec = s % 60;
return `${h}h ${m}m ${sec}s`;
}
async commit(startNew) {
this.state.saving = true;
const result = await rpc("/fp/tablet/labor_timer/reconcile", {
timer_id: this.props.timerId,
billed_hrs: this.state.billedHrs,
billed_min: this.state.billedMin,
billed_sec: this.state.billedSec,
notes: this.state.notes,
start_new: startNew,
});
if (result.ok) {
this.notification.add(_t("Timer reconciled"),
{ type: "success" });
if (this.props.onReconciled) {
this.props.onReconciled(result);
}
this.props.close();
} else {
this.notification.add(result.error, { type: "danger" });
this.state.saving = false;
}
}
onSave() { return this.commit(false); }
onSaveAndStartNew() { return this.commit(true); }
}

View File

@@ -0,0 +1,76 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="fusion_plating_shopfloor.FpMoveRackDialog">
<Dialog t-att-title="title" size="'lg'">
<div class="o_fp_move_dialog" t-if="!state.loading">
<div class="o_fp_move_field">
<label>Rack Tags</label>
<div>
<span t-foreach="state.rack.tag_ids" t-as="t" t-key="t.id"
class="o_fp_rack_tag_chip"
t-att-data-color="t.color">
<t t-esc="t.name"/>
</span>
</div>
<span/>
</div>
<div class="o_fp_move_field">
<label>Parts</label>
<ul class="o_fp_rack_parts_list">
<t t-foreach="state.batches" t-as="b" t-key="b.step_id">
<li>
<t t-esc="b.qty"/> <t t-esc="b.part_number"/>
on WO <t t-esc="b.wo_number"/>
</li>
</t>
</ul>
<span/>
</div>
<div class="o_fp_move_field">
<label>Type</label>
<select t-model="state.transferType">
<option value="step">Step</option>
<option value="hold">Hold</option>
<option value="rework">Rework</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>
<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="state.saving"
t-on-click="onSave">
Save
</button>
</t>
</Dialog>
</t>
</templates>

View File

@@ -0,0 +1,53 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="fusion_plating_shopfloor.FpStopTimerDialog">
<Dialog title.translate="Stop User Labor Timer" size="'md'">
<div class="o_fp_timer_dialog" t-if="!state.loading">
<div class="o_fp_timer_summary">
Accrued: <t t-esc="accruedDisplay"/>
· Billed: <t t-esc="billedPct"/>%
</div>
<div class="o_fp_move_field">
<label>Billed Time</label>
<div class="o_fp_billed_inputs">
<input type="number" t-model.number="state.billedHrs"
min="0"/> hrs
<input type="number" t-model.number="state.billedMin"
min="0" max="59"/> min
<input type="number" t-model.number="state.billedSec"
min="0" max="59"/> sec
</div>
<span/>
</div>
<div class="o_fp_move_field">
<label>Notes</label>
<textarea t-model="state.notes" rows="2"/>
<span/>
</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="state.saving"
t-on-click="onSave">
Save
</button>
<button class="btn btn-warning"
t-att-disabled="state.saving"
t-on-click="onSaveAndStartNew">
Save &amp; Start New Timer
</button>
</t>
</Dialog>
</t>
</templates>