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