Merge remote-tracking branch 'origin/main'

# Conflicts:
#	fusion_plating/fusion_plating/__manifest__.py
#	fusion_plating/fusion_plating_jobs/__manifest__.py
#	fusion_plating/fusion_plating_jobs/models/fp_job_step.py
#	fusion_plating/fusion_plating_shopfloor/__manifest__.py
#	fusion_plating/fusion_plating_shopfloor/static/src/js/job_workspace.js
This commit is contained in:
gsinghpal
2026-06-03 15:37:38 -04:00
187 changed files with 6060 additions and 12550 deletions

View File

@@ -60,11 +60,15 @@ export class FpPlantCard extends Component {
onCardClick() {
const c = this.props.card;
if (!c.job_id) return;
// Open the workspace focused on THIS stage's step (partial-order
// handling) — tapping the Baking card lands on the Baking step,
// not the job's global active step. The workspace already accepts
// focus_step_id (see the FP-STEP scan path in plant_kanban.js).
this.action.doAction({
type: "ir.actions.client",
tag: "fp_job_workspace",
target: "current",
params: { job_id: c.job_id },
params: { job_id: c.job_id, focus_step_id: c.focus_step_id || false },
});
}
}

View File

@@ -31,13 +31,14 @@ import { FpRackPartsDialog } from "./rack_parts_dialog";
import { FpDamageDialog } from "./fp_damage_dialog";
import { FpFinishBlockDialog } from "./fp_finish_block_dialog";
import { RackingPanel } from "./components/racking_panel";
import { FpMovePartsDialog } from "./move_parts_dialog";
import { useFileViewer } from "@web/core/file_viewer/file_viewer_hook";
import { FileModel } from "@web/core/file_viewer/file_model";
export class FpJobWorkspace extends Component {
static template = "fusion_plating_shopfloor.JobWorkspace";
static props = ["*"];
static components = { WorkflowChip, GateViz, FpSignaturePad, FpHoldComposer, FpTabletLock, FpRackPartsDialog, FpDamageDialog, FpFinishBlockDialog, RackingPanel };
static components = { WorkflowChip, GateViz, FpSignaturePad, FpHoldComposer, FpTabletLock, FpRackPartsDialog, FpDamageDialog, FpFinishBlockDialog, RackingPanel, FpMovePartsDialog };
setup() {
this.notification = useService("notification");
@@ -248,7 +249,21 @@ export class FpJobWorkspace extends Component {
if (step.override_excluded) return [];
const actions = [];
// Partial-order handling — "Send to next →" advances parts parked
// at this step to the next stage. Only shown when parts are here
// AND a next stage exists. The destination name is on the button
// so there's nothing to guess; qty defaults to all parked here.
const advanceAction = () => {
const nxt = this.nextStepFor(step);
if (nxt && (step.qty_at_step || 0) > 0) {
return { key: "advance", label: "Send → " + nxt.name,
icon: "fa fa-arrow-right", cssClass: "btn btn-primary" };
}
return null;
};
if (step.state === "in_progress") {
const adv = advanceAction();
if (adv) actions.push(adv);
actions.push({ key: "record_inputs", label: "Record Inputs",
icon: "fa fa-pencil", cssClass: "btn btn-secondary" });
actions.push({ key: "pause", label: "Pause",
@@ -263,6 +278,8 @@ export class FpJobWorkspace extends Component {
if (step.state === "paused") {
actions.push({ key: "resume", label: "Resume",
icon: "fa fa-play", cssClass: "btn btn-primary" });
const adv = advanceAction();
if (adv) actions.push(adv);
actions.push({ key: "record_inputs", label: "Record Inputs",
icon: "fa fa-pencil", cssClass: "btn btn-secondary" });
actions.push({
@@ -304,6 +321,7 @@ export class FpJobWorkspace extends Component {
case "mark_passed": return this.onMarkPassed(step);
case "open_contract_review": return this.onOpenContractReview(step);
case "start_with_rack": return this.onStartWithRack(step);
case "advance": return this.onAdvanceStep(step);
}
}
@@ -486,6 +504,44 @@ export class FpJobWorkspace extends Component {
});
}
// ---- Partial-order advance (2026-06-02) -------------------------------
// "Send to next →" — moves parts parked at this step to the next stage.
// The destination auto-readies server-side (move_controller), so the
// receiving operator sees a Ready card immediately; the source
// auto-finishes when it drains to zero. Pure client-side next-step
// resolution off the loaded step list — no extra RPC.
nextStepFor(step) {
// The next stage parts flow into: lowest-sequence non-terminal step
// after this one. Returns null at the end of the line (parts finish
// in place there and close out at job mark-done).
const steps = (this.state.data && this.state.data.steps) || [];
const candidates = steps
.filter((s) => s.sequence > step.sequence
&& ["pending", "ready", "paused", "in_progress"].includes(s.state))
.sort((a, b) => a.sequence - b.sequence);
return candidates.length ? candidates[0] : null;
}
onAdvanceStep(step) {
const nxt = this.nextStepFor(step);
if (!nxt) {
this.notification.add(
"This is the last stage — parts finish here and close out at job completion.",
{ type: "warning" },
);
return;
}
// Open the slim Move dialog pre-set to advance to the next stage.
// Qty defaults to all parked here (qty_at_step) via the preview
// endpoint; the operator confirms or trims it with the steppers.
this.dialog.add(FpMovePartsDialog, {
fromStepId: step.id,
toStepId: nxt.id,
onCommit: async () => { await this.refresh(); },
});
}
// ---- Receiving handlers (Spec C1+C2 2026-05-24) -----------------------
// The receiver card at the top of the workspace lets the dock receiver
// count boxes, set per-line received quantities + condition, log damage

View File

@@ -40,6 +40,11 @@ export class FpMovePartsDialog extends Component {
promptValues: {},
blockers: [],
committing: false,
// Advanced fields (Transfer Type, To Location) stay collapsed
// by default — the everyday flow is "advance all to the next
// stage", which needs none of them. Keeps the dialog to a qty
// confirm + SEND for the 95% case.
showAdvanced: false,
});
onWillStart(async () => {
await this.loadPreview();
@@ -152,4 +157,20 @@ export class FpMovePartsDialog extends Component {
{ type: "warning" });
}
}
// ---- Qty steppers (no keyboard) ---------------------------------------
// The operator taps / + or "All". Clamped to [1, qtyAvailable] so the
// count can never exceed what's parked here.
incQty() {
if (this.state.qty < this.state.qtyAvailable) this.state.qty += 1;
}
decQty() {
if (this.state.qty > 1) this.state.qty -= 1;
}
setQtyAll() {
this.state.qty = this.state.qtyAvailable;
}
toggleAdvanced() {
this.state.showAdvanced = !this.state.showAdvanced;
}
}

View File

@@ -26,5 +26,5 @@ $_gate-text-hex: #b06600;
.o_fp_gate_icon { color: $_gate-border-hex; margin-top: 0.15rem; }
.o_fp_gate_body { flex: 1; }
.o_fp_gate_title { font-weight: 600; color: $_gate-text-hex; font-size: 0.85rem; }
.o_fp_gate_reason { color: var(--text-secondary, #666); font-size: 0.78rem; margin-top: 0.1rem; }
.o_fp_gate_reason { color: var(--bs-secondary-color, #666); font-size: 0.78rem; margin-top: 0.1rem; }
.o_fp_gate_jump { flex-shrink: 0; }

View File

@@ -17,5 +17,5 @@
.o_fp_hc_row label {
font-size: 0.8rem;
font-weight: 600;
color: var(--text-secondary, #666);
color: var(--bs-secondary-color, #666);
}

View File

@@ -34,18 +34,18 @@ $_kc-hover-hex: #f5f5f7;
}
.o_fp_kcard_h2 {
color: var(--text-secondary, #666);
color: var(--bs-secondary-color, #666);
font-size: 0.75rem;
margin-top: 0.15rem;
}
.o_fp_kcard_qty {
display: flex; justify-content: space-between;
font-size: 0.7rem; color: var(--text-secondary, #777);
font-size: 0.7rem; color: var(--bs-secondary-color, #777);
margin-top: 0.3rem;
}
.o_fp_kcard_due { color: var(--text-secondary, #999); }
.o_fp_kcard_due { color: var(--bs-secondary-color, #999); }
.o_fp_kcard_bar {
height: 4px; background: rgba(0,0,0,0.08);
@@ -74,7 +74,7 @@ $_kc-hover-hex: #f5f5f7;
}
.o_fp_kcard_wc {
color: var(--text-secondary, #999);
color: var(--bs-secondary-color, #999);
font-size: 0.7rem;
}

View File

@@ -38,7 +38,7 @@ $_pin-dot-fill-hex: #1d1d1f;
}
.o_fp_pin_title { font-size: 1.1rem; font-weight: 600; }
.o_fp_pin_subtitle { font-size: 0.85rem; color: var(--text-secondary, #666); text-align: center; }
.o_fp_pin_subtitle { font-size: 0.85rem; color: var(--bs-secondary-color, #666); text-align: center; }
.o_fp_pin_dots {
display: flex;
@@ -83,8 +83,8 @@ $_pin-dot-fill-hex: #1d1d1f;
&:disabled { opacity: 0.5; cursor: wait; }
}
.o_fp_pin_key_clear { font-size: 0.95rem; color: var(--text-secondary, #666); }
.o_fp_pin_key_cancel { font-size: 0.95rem; color: var(--text-secondary, #666); }
.o_fp_pin_key_clear { font-size: 0.95rem; color: var(--bs-secondary-color, #666); }
.o_fp_pin_key_cancel { font-size: 0.95rem; color: var(--bs-secondary-color, #666); }
@keyframes o_fp_pin_shake_kf {
0%, 100% { transform: translateX(0); }

View File

@@ -91,6 +91,21 @@
.card-sub-em { color: $plant-text; font-weight: 600; }
.card-meta { font-size: 11px; color: $plant-muted; }
.card-step { font-size: 14px; font-weight: 600; color: $plant-text; margin-top: 2px; }
// Partial-order handling — "20 of 50 here" per-stage count. The big
// number pops so an operator scanning their column instantly sees how
// many of a job's parts are at their station. Uses existing tokens so
// dark mode is handled at compile time by _plant_tokens.scss.
.card-qty-here {
font-size: 12px;
color: $plant-muted;
margin-top: 1px;
.qty-here-num {
font-size: 16px;
font-weight: 800;
color: $plant-mine-border;
letter-spacing: -0.01em;
}
}
.card-chips { display: flex; flex-wrap: wrap; gap: 4px; }
.chip {

View File

@@ -21,7 +21,7 @@ $_sig-canvas-border-hex: #d8dadd;
.o_fp_sig_ctx {
font-size: 0.85rem;
color: var(--text-secondary, #666);
color: var(--bs-secondary-color, #666);
}
.o_fp_sig_canvas {
@@ -36,6 +36,6 @@ $_sig-canvas-border-hex: #d8dadd;
.o_fp_sig_hint {
font-size: 0.75rem;
color: var(--text-secondary, #999);
color: var(--bs-secondary-color, #999);
text-align: center;
}

View File

@@ -29,7 +29,7 @@ $_ws-text-hex: #1d1d1f;
.o_fp_ws_loading {
margin: auto;
text-align: center;
color: var(--text-secondary, #666);
color: var(--bs-secondary-color, #666);
> div { margin-top: 0.6rem; }
}
@@ -87,8 +87,8 @@ $_ws-text-hex: #1d1d1f;
}
.o_fp_ws_wo { font-weight: 700; font-size: 1.3rem; letter-spacing: 0.01em; }
.o_fp_ws_dot { color: var(--text-secondary, #999); }
.o_fp_ws_cust, .o_fp_ws_part { color: var(--text-secondary, #555); font-size: 0.95rem; }
.o_fp_ws_dot { color: var(--bs-secondary-color, #999); }
.o_fp_ws_cust, .o_fp_ws_part { color: var(--bs-secondary-color, #555); font-size: 0.95rem; }
.o_fp_ws_pill {
background: linear-gradient(135deg, $_ws-card-hex 0%, $_ws-page-hex 100%);
@@ -97,7 +97,7 @@ $_ws-text-hex: #1d1d1f;
border-radius: 6px;
font-size: 0.9rem;
font-weight: 500;
color: var(--text-secondary, #555);
color: var(--bs-secondary-color, #555);
box-shadow: 0 1px 2px rgba(0,0,0,0.04);
}
@@ -152,7 +152,7 @@ $_ws-text-hex: #1d1d1f;
.o_fp_ws_bar_label {
font-size: 0.8rem;
font-weight: 500;
color: var(--text-secondary, #888);
color: var(--bs-secondary-color, #888);
margin-top: 0.35rem;
text-align: center;
}
@@ -256,7 +256,7 @@ $_ws-text-hex: #1d1d1f;
.o_fp_ws_empty {
text-align: center;
padding: 2rem 1rem;
color: var(--text-secondary, #999);
color: var(--bs-secondary-color, #999);
> div { margin-top: 0.5rem; }
}
@@ -289,9 +289,9 @@ $_ws-text-hex: #1d1d1f;
}
.o_fp_ws_step_icon { width: 18px; text-align: center; font-weight: 700; }
.o_fp_ws_step_num { color: var(--text-secondary, #999); font-size: 0.78rem; min-width: 50px; }
.o_fp_ws_step_num { color: var(--bs-secondary-color, #999); font-size: 0.78rem; min-width: 50px; }
.o_fp_ws_step_name { font-weight: 600; }
.o_fp_ws_step_meta { color: var(--text-secondary, #999); font-size: 0.78rem; margin-left: auto; }
.o_fp_ws_step_meta { color: var(--bs-secondary-color, #999); font-size: 0.78rem; margin-left: auto; }
.o_fp_ws_step_badge {
background: #0071e3;
@@ -314,7 +314,7 @@ $_ws-text-hex: #1d1d1f;
}
.o_fp_ws_step_chips { display: flex; gap: 0.3rem; flex-wrap: wrap; }
.o_fp_ws_step_instr { font-size: 0.78rem; color: var(--text-secondary, #555); font-style: italic; }
.o_fp_ws_step_instr { font-size: 0.78rem; color: var(--bs-secondary-color, #555); font-style: italic; }
.o_fp_ws_step_actions { display: flex; gap: 0.35rem; flex-wrap: wrap; }
// ---- Masking reference tiles (tap → full-screen FileViewer) -----------
@@ -402,7 +402,7 @@ $_ws-text-hex: #1d1d1f;
.o_fp_ws_step_excluded {
font-size: 0.78rem;
color: var(--text-secondary, #888);
color: var(--bs-secondary-color, #888);
font-style: italic;
}
@@ -433,7 +433,7 @@ $_ws-text-hex: #1d1d1f;
font-size: 0.7rem;
text-transform: uppercase;
letter-spacing: 0.04em;
color: var(--text-secondary, #777);
color: var(--bs-secondary-color, #777);
margin-bottom: 0.35rem;
}
}
@@ -464,14 +464,14 @@ $_ws-text-hex: #1d1d1f;
display: flex;
gap: 0.4rem;
font-size: 0.72rem;
color: var(--text-secondary, #777);
color: var(--bs-secondary-color, #777);
}
.o_fp_ws_note .author { font-weight: 600; }
.o_fp_ws_note .body { color: var(--text-secondary, #555); margin-top: 0.15rem; }
.o_fp_ws_note .body { color: var(--bs-secondary-color, #555); margin-top: 0.15rem; }
.o_fp_ws_empty_small {
color: var(--text-secondary, #999);
color: var(--bs-secondary-color, #999);
font-size: 0.75rem;
font-style: italic;
}
@@ -574,7 +574,7 @@ $_ws-text-hex: #1d1d1f;
align-items: center;
gap: 0.4rem;
font-size: 0.85rem;
color: var(--text-secondary, #777);
color: var(--bs-secondary-color, #777);
}
.o_fp_ws_ship_fields {
@@ -645,8 +645,8 @@ $_ws-text-hex: #1d1d1f;
.o_fp_ws_rcv_status {
padding: 0.2rem 0.6rem;
border-radius: 4px;
background: #fef3c7;
color: #78350f;
background-color: color-mix(in srgb, #f59e0b 18%, var(--bs-body-bg));
color: var(--bs-body-color);
font-weight: 600;
font-size: 0.85rem;
text-transform: uppercase;
@@ -664,7 +664,7 @@ $_ws-text-hex: #1d1d1f;
label {
font-weight: 600;
color: var(--text-secondary, #555);
color: var(--bs-secondary-color, #555);
font-size: 0.9rem;
}
}
@@ -725,7 +725,7 @@ $_ws-text-hex: #1d1d1f;
font-size: 0.8rem;
font-weight: 700;
text-transform: uppercase;
color: var(--text-secondary, #666);
color: var(--bs-secondary-color, #666);
margin-bottom: 0.5rem;
letter-spacing: 0.05em;
}
@@ -794,7 +794,7 @@ $_ws-text-hex: #1d1d1f;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--text-secondary, #666);
color: var(--bs-secondary-color, #666);
margin-bottom: 0.5rem;
display: flex;
align-items: center;
@@ -840,7 +840,7 @@ $_ws-text-hex: #1d1d1f;
}
.o_fp_ws_rcv_damage_photos {
color: var(--text-secondary, #666);
color: var(--bs-secondary-color, #666);
font-size: 0.85rem;
}
@@ -876,7 +876,7 @@ $_ws-text-hex: #1d1d1f;
}
.o_fp_dmg_field { display: flex; flex-direction: column; gap: 0.4rem; }
.o_fp_dmg_label { font-weight: 600; color: var(--text-secondary, #555); }
.o_fp_dmg_label { font-weight: 600; color: var(--bs-secondary-color, #555); }
.o_fp_dmg_req { color: #dc2626; }
.o_fp_dmg_pills {
@@ -980,8 +980,8 @@ $_ws-text-hex: #1d1d1f;
}
.o_fp_ws_step_timer_over {
background: #fee2e2;
color: #7f1d1d;
background-color: color-mix(in srgb, #ef4444 16%, var(--bs-body-bg));
color: var(--bs-body-color);
animation: o_fp_ws_timer_pulse 1.5s ease-in-out infinite;
}
@@ -1002,17 +1002,25 @@ $_ws-text-hex: #1d1d1f;
gap: 1rem;
}
// NOTE: Odoo's backend CSS does NOT define --bs-body-color /
// --bs-secondary-color / --bs-*-bg as custom properties (verified: 0
// definitions in the compiled bundle — they're SCSS literals + two
// bundles + [data-bs-theme]). So var(--bs-body-color, #hex) ALWAYS
// resolves to the dark #hex fallback, in light AND dark mode. The fix
// for dialog text is to INHERIT the modal's theme-correct colour (the
// dialog title and the "Count the Parts" list items do exactly this and
// are readable in both modes). Tinted boxes use translucent rgba() so
// they work over whatever the live theme background is.
.o_fp_finish_block_step {
font-size: 1.1rem;
color: #b45309;
background: #fef3c7;
background-color: rgba(245, 158, 11, 0.16);
padding: 0.7rem 1rem;
border-radius: 6px;
border-left: 4px solid #f59e0b;
}
.o_fp_finish_block_msg {
color: var(--text-secondary, #333);
font-weight: 500;
}
.o_fp_finish_block_list {
@@ -1027,9 +1035,9 @@ $_ws-text-hex: #1d1d1f;
}
.o_fp_finish_block_action_note {
color: var(--text-secondary, #555);
// Inherit text colour; translucent neutral box works in both themes.
font-style: italic;
padding: 0.6rem 0.8rem;
background: #f3f4f6;
background: rgba(128, 128, 128, 0.12);
border-radius: 4px;
}

View File

@@ -173,3 +173,89 @@ $fp-md-page: var(--fp-page-bg, #{$_fp_md_page_hex});
}
}
}
// ============================ Partial-order handling — easy-advance layout
// "Send Parts Forward" dialog: destination banner + big-tap qty stepper
// (no keyboard) + collapsed advanced fields. Reuses the $fp-md-* tokens so
// dark mode is handled at compile time.
.o_fp_move_dialog {
.o_fp_move_route {
display: flex;
align-items: center;
justify-content: center;
gap: .5rem;
flex-wrap: wrap;
padding: .6rem .75rem;
background: $fp-md-page;
border: 1px solid $fp-md-border;
border-radius: 6px;
font-weight: 600;
.route-from { color: $fp-md-muted; }
.route-arrow { color: $fp-md-accent; font-weight: 800; }
.route-to { color: $fp-md-accent; }
}
.o_fp_move_qty {
display: flex;
flex-direction: column;
align-items: center;
gap: .35rem;
label { font-weight: 600; margin: 0; }
}
.o_fp_qty_stepper {
display: flex;
align-items: center;
gap: .5rem;
.qty-btn {
width: 3rem;
height: 3rem;
font-size: 1.5rem;
font-weight: 700;
line-height: 1;
border: 1px solid $fp-md-border;
border-radius: 8px;
background: $fp-md-card;
color: $fp-md-accent;
&:disabled { opacity: .4; }
}
.qty-value {
min-width: 3.5rem;
text-align: center;
font-size: 1.75rem;
font-weight: 800;
}
.qty-all {
margin-left: .5rem;
padding: .5rem .9rem;
border: 1px solid $fp-md-border;
border-radius: 8px;
background: $fp-md-card;
font-weight: 600;
}
}
.o_fp_qty_hint {
color: $fp-md-muted;
font-size: .85rem;
}
.o_fp_move_advanced_toggle {
text-align: center;
.btn-link { color: $fp-md-muted; text-decoration: none; }
}
.o_fp_move_advanced {
display: flex;
flex-direction: column;
gap: .5rem;
padding: .6rem .75rem;
border: 1px dashed $fp-md-border;
border-radius: 6px;
}
}

View File

@@ -77,6 +77,8 @@
}
}
.toolbar-btn {
display: inline-flex;
align-items: center;
padding: 8px 14px;
font-size: 14px;
font-weight: 500;
@@ -86,8 +88,10 @@
cursor: pointer;
color: $plant-text;
font-family: inherit;
white-space: nowrap;
box-shadow: 0 1px 2px rgba(0,0,0,0.05);
transition: transform 0.1s ease, box-shadow 0.1s ease;
i { font-size: 15px; line-height: 1; }
&:hover {
transform: translateY(-1px);
box-shadow: 0 2px 4px rgba(0,0,0,0.08);
@@ -98,6 +102,24 @@
color: #5e4400;
font-weight: 700;
}
// Scan pair — matched look. "Scan QR" (camera, the primary way to
// scan a printed job sticker) is accent-filled so it stands out;
// "Enter Code" (manual / hardware scanner-gun) is the accent-tinted
// secondary. Matched FA icons (fa-qrcode / fa-keyboard-o), no emoji.
&.o_fp_qr_btn {
background: linear-gradient(135deg, #2563eb 0%, #1d4ed8 100%);
border-color: #1d4ed8;
color: #fff;
font-weight: 600;
i { color: #fff; }
&:hover { box-shadow: 0 3px 8px rgba(29, 78, 216, 0.32); }
}
&.scan-alt {
background: linear-gradient(135deg, $plant-mine-bg 0%, $plant-card-bg 100%);
border-color: $plant-mine-border;
font-weight: 600;
i { color: #1d4ed8; }
}
}
// 8 tiles — Work Orders, At My Station, Bakes Due, On Hold,

View File

@@ -47,6 +47,14 @@
<!-- Step name -->
<div class="card-step" t-esc="props.card.step_name"/>
<!-- Parts at THIS stage (partial-order handling). "20 of 50"
so a per-stage presence is never mistaken for a whole job.
Hidden when nothing is parked here (post-shop / empty). -->
<div t-if="props.card.qty_here" class="card-qty-here">
<span class="qty-here-num" t-esc="props.card.qty_here"/>
<span class="qty-here-of"> of <t t-esc="props.card.job_qty"/> here</span>
</div>
<!-- Tank + state chip -->
<div class="card-chips">
<span t-if="props.card.tank_label" class="chip tank" t-esc="props.card.tank_label"/>

View File

@@ -2,75 +2,48 @@
<templates xml:space="preserve">
<t t-name="fusion_plating_shopfloor.FpMovePartsDialog">
<Dialog title.translate="Move Parts" size="'lg'">
<Dialog title.translate="Send Parts Forward" size="'md'">
<div class="o_fp_move_dialog" t-if="!state.loading">
<div class="o_fp_move_field">
<label>Part Count</label>
<input type="number" t-model.number="state.qty"
t-att-min="1" t-att-max="state.qtyAvailable"/>
<span class="text-muted">Available: <t t-esc="state.qtyAvailable"/></span>
<!-- Destination banner — operator sees exactly where parts go,
nothing to guess. -->
<div class="o_fp_move_route">
<span class="route-from" t-esc="state.fromStep.name"/>
<span class="route-arrow"></span>
<span class="route-to" t-esc="state.toStep.name"/>
</div>
<div class="o_fp_move_field">
<label>From Node</label>
<span t-esc="state.fromStep.name"/>
<span/>
</div>
<div class="o_fp_move_field" t-if="state.fromStep.tank_name">
<label>From Station</label>
<span t-esc="state.fromStep.tank_name"/>
<span/>
</div>
<div class="o_fp_move_field">
<label>Transfer Type</label>
<select t-model="state.transferType">
<option value="step">Step</option>
<option value="hold">Hold</option>
<option value="scrap">Scrap</option>
<option value="rework">Rework</option>
<option value="split">Split</option>
<option value="return">Return</option>
</select>
<span/>
</div>
<div class="o_fp_move_field">
<label>To Node</label>
<span t-esc="state.toStep.name"/>
<span/>
<!-- Qty stepper — no keyboard. Defaults to all parked here. -->
<div class="o_fp_move_qty">
<label>How many to send?</label>
<div class="o_fp_qty_stepper">
<button class="qty-btn" t-on-click="decQty"
t-att-disabled="state.qty &lt;= 1"></button>
<span class="qty-value" t-esc="state.qty"/>
<button class="qty-btn" t-on-click="incQty"
t-att-disabled="state.qty &gt;= state.qtyAvailable">+</button>
<button class="qty-all" t-on-click="setQtyAll">
All (<t t-esc="state.qtyAvailable"/>)
</button>
</div>
<span class="o_fp_qty_hint"><t t-esc="state.qtyAvailable"/> parked here</span>
</div>
<!-- To Station (tank) — only when the recipe offers a choice -->
<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">
<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 class="o_fp_move_field">
<label>To Location</label>
<select t-model="state.toLocation">
<option value="global">Global</option>
<option value="quarantine">Quarantine</option>
<option value="staging_a">Staging A</option>
<option value="staging_b">Staging B</option>
<option value="shipping_dock">Shipping Dock</option>
<option value="scrap_bin">Scrap Bin</option>
</select>
<span/>
</div>
<div class="o_fp_compliance_prompts"
t-if="state.transitionPrompts.length">
<h5>Compliance Prompts</h5>
<!-- Compliance prompts — only when the recipe author required
them. Pickers/checkboxes, minimal free text. -->
<div class="o_fp_compliance_prompts" t-if="state.transitionPrompts.length">
<h5>Required before sending</h5>
<t t-foreach="state.transitionPrompts" t-as="p" t-key="p.id">
<div class="o_fp_move_field">
<label>
@@ -94,13 +67,12 @@
</t>
</select>
<span class="text-muted" t-if="p.hint"><t t-esc="p.hint"/></span>
<span t-else=""/>
</div>
</t>
</div>
<!-- Blockers — inline resolve where possible -->
<div class="o_fp_blockers" t-if="state.blockers.length">
<h5>Blockers</h5>
<t t-foreach="state.blockers" t-as="b" t-key="b_index">
<div class="o_fp_blocker_row"
t-att-class="b.severity === 'hard' ? 'o_fp_blocker_hard' : 'o_fp_blocker_soft'">
@@ -114,6 +86,39 @@
</div>
</t>
</div>
<!-- More options (advanced) — hold / scrap / rework / location.
Collapsed by default so the everyday "advance all" flow is
a qty confirm + SEND. -->
<div class="o_fp_move_advanced_toggle">
<button class="btn btn-link btn-sm" t-on-click="toggleAdvanced">
<t t-if="state.showAdvanced">▾ Hide options</t>
<t t-else="">▸ More options (hold / scrap / location)</t>
</button>
</div>
<div t-if="state.showAdvanced" class="o_fp_move_advanced">
<div class="o_fp_move_field">
<label>Transfer Type</label>
<select t-model="state.transferType">
<option value="step">Send to next step</option>
<option value="hold">Hold</option>
<option value="scrap">Scrap</option>
<option value="rework">Rework</option>
<option value="return">Return</option>
</select>
</div>
<div class="o_fp_move_field">
<label>To Location</label>
<select t-model="state.toLocation">
<option value="global">Global</option>
<option value="quarantine">Quarantine</option>
<option value="staging_a">Staging A</option>
<option value="staging_b">Staging B</option>
<option value="shipping_dock">Shipping Dock</option>
<option value="scrap_bin">Scrap Bin</option>
</select>
</div>
</div>
</div>
<div t-if="state.loading">Loading…</div>
@@ -126,7 +131,7 @@
t-att-disabled="!canCommit"
t-att-title="blockerTooltip"
t-on-click="onCommit">
MOVE (<t t-esc="state.qty"/>)
SEND (<t t-esc="state.qty"/>)
</button>
</t>
</Dialog>

View File

@@ -23,13 +23,18 @@
<button t-att-class="modeClass('manager')"
t-on-click="() => this.setMode('manager')">Manager</button>
</div>
<!-- Text/wedge scan drawer toggle. Camera path
is the QrScanner inline below — it
opens its own modal + decoder. -->
<button class="toolbar-btn"
t-att-class="state.showScan ? 'toolbar-btn active' : 'toolbar-btn'"
t-on-click="toggleScan">⌨️ Scan Code</button>
<QrScanner cssClass="'toolbar-btn'" label="'📷 Camera'"/>
<!-- "Scan QR" = the QrScanner camera path (the
primary way to scan a printed job sticker).
The component renders its own fa-qrcode
icon, so the label must be plain text — an
emoji here would double up the icon.
"Enter Code" = the manual / hardware-scanner-
gun text drawer (a wedge gun types the code;
no camera). -->
<QrScanner cssClass="'toolbar-btn'" label="'Scan QR'"/>
<button class="toolbar-btn scan-alt"
t-att-class="state.showScan ? 'active' : ''"
t-on-click="toggleScan"><i class="fa fa-keyboard-o me-1"/>Enter Code</button>
<button class="toolbar-btn handoff" t-on-click="onHandOff">🔓 Hand Off</button>
</div>
</div>