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:
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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); }
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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 & Start New Timer
|
||||
</button>
|
||||
</t>
|
||||
</Dialog>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
Reference in New Issue
Block a user