From 8109b3ec7645baf0a755f2d8075becf4736f8f30 Mon Sep 17 00:00:00 2001 From: gsinghpal Date: Fri, 22 May 2026 21:47:29 -0400 Subject: [PATCH] feat(fusion_plating_shopfloor): HoldComposer shared OWL service Plan task P1.6. Modal hold-creation form: reason picker, qty split, optional photo (camera input on mobile), description, mark-for-scrap toggle. Calls /fp/workspace/hold (added in P1.9). Reason list kept client-side, keep in sync with fusion.plating.quality.hold.hold_reason. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../fusion_plating_shopfloor/__manifest__.py | 3 + .../static/src/js/components/hold_composer.js | 102 ++++++++++++++++++ .../src/scss/components/_hold_composer.scss | 21 ++++ .../src/xml/components/hold_composer.xml | 51 +++++++++ 4 files changed, 177 insertions(+) create mode 100644 fusion_plating/fusion_plating_shopfloor/static/src/js/components/hold_composer.js create mode 100644 fusion_plating/fusion_plating_shopfloor/static/src/scss/components/_hold_composer.scss create mode 100644 fusion_plating/fusion_plating_shopfloor/static/src/xml/components/hold_composer.xml diff --git a/fusion_plating/fusion_plating_shopfloor/__manifest__.py b/fusion_plating/fusion_plating_shopfloor/__manifest__.py index 87fa7560..1ec19d85 100644 --- a/fusion_plating/fusion_plating_shopfloor/__manifest__.py +++ b/fusion_plating/fusion_plating_shopfloor/__manifest__.py @@ -74,6 +74,9 @@ Copyright (c) 2026 Nexa Systems Inc. All rights reserved. 'fusion_plating_shopfloor/static/src/scss/components/_signature_pad.scss', 'fusion_plating_shopfloor/static/src/xml/components/signature_pad.xml', 'fusion_plating_shopfloor/static/src/js/components/signature_pad.js', + 'fusion_plating_shopfloor/static/src/scss/components/_hold_composer.scss', + 'fusion_plating_shopfloor/static/src/xml/components/hold_composer.xml', + 'fusion_plating_shopfloor/static/src/js/components/hold_composer.js', 'fusion_plating_shopfloor/static/src/scss/qr_scanner.scss', 'fusion_plating_shopfloor/static/src/scss/fusion_plating_shopfloor.scss', 'fusion_plating_shopfloor/static/src/scss/plant_overview.scss', diff --git a/fusion_plating/fusion_plating_shopfloor/static/src/js/components/hold_composer.js b/fusion_plating/fusion_plating_shopfloor/static/src/js/components/hold_composer.js new file mode 100644 index 00000000..32667b88 --- /dev/null +++ b/fusion_plating/fusion_plating_shopfloor/static/src/js/components/hold_composer.js @@ -0,0 +1,102 @@ +/** @odoo-module **/ +// ============================================================================= +// Fusion Plating — HoldComposer (shared OWL service) +// +// Modal form to create a fusion.plating.quality.hold with reason picker, +// qty split, optional photo, description, and mark-for-scrap toggle. +// Calls /fp/workspace/hold; caller passes onCreated(res) to refresh. +// +// Mounted via the dialog service: +// this.dialog.add(FpHoldComposer, { +// jobId, stepId?, defaultQty, partRef, onCreated, +// }); +// ============================================================================= + +import { Component, 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"; + +// Hold reasons kept here so the picker doesn't need a server roundtrip. +// Keep in sync with the fusion.plating.quality.hold.hold_reason Selection. +const HOLD_REASONS = [ + { value: "dimensional", label: "Dimensional" }, + { value: "thickness", label: "Thickness fail" }, + { value: "plating_defect", label: "Plating defect" }, + { value: "contamination", label: "Contamination" }, + { value: "wrong_part", label: "Wrong part" }, + { value: "other", label: "Other" }, +]; + +export class FpHoldComposer extends Component { + static template = "fusion_plating_shopfloor.HoldComposer"; + static components = { Dialog }; + static props = { + close: Function, + jobId: { type: Number, optional: false }, + stepId: { type: [Number, Boolean], optional: true }, + defaultQty: { type: Number, optional: true }, + partRef: { type: String, optional: true }, + onCreated: { type: Function, optional: true }, + }; + + setup() { + this.notification = useService("notification"); + this.reasons = HOLD_REASONS; + this.state = useState({ + reason: "dimensional", + qty: this.props.defaultQty || 1, + description: "", + photoDataUri: null, + photoFilename: "", + markForScrap: false, + submitting: false, + }); + } + + onPhotoChange(ev) { + const file = ev.target.files && ev.target.files[0]; + if (!file) return; + const reader = new FileReader(); + reader.onload = (e) => { + // Strip "data:...;base64," prefix — backend expects raw base64 + const dataUri = e.target.result; + const base64 = dataUri.split(",", 2)[1] || ""; + this.state.photoDataUri = base64; + this.state.photoFilename = file.name; + }; + reader.readAsDataURL(file); + } + + async onSubmit() { + if (!this.state.qty || this.state.qty < 1) { + this.notification.add("Qty on hold must be at least 1", { type: "warning" }); + return; + } + this.state.submitting = true; + try { + const res = await rpc("/fp/workspace/hold", { + job_id: this.props.jobId, + step_id: this.props.stepId || null, + part_ref: this.props.partRef || "", + reason: this.state.reason, + qty_on_hold: this.state.qty, + description: this.state.description || "", + mark_for_scrap: this.state.markForScrap, + photo_data: this.state.photoDataUri, + photo_filename: this.state.photoFilename, + }); + if (res && res.ok) { + this.notification.add(`Hold ${res.hold_name} created.`, { type: "success" }); + if (this.props.onCreated) this.props.onCreated(res); + this.props.close(); + } else { + this.notification.add((res && res.error) || "Hold creation failed", { type: "danger" }); + } + } catch (err) { + this.notification.add(err.message || String(err), { type: "danger" }); + } finally { + this.state.submitting = false; + } + } +} diff --git a/fusion_plating/fusion_plating_shopfloor/static/src/scss/components/_hold_composer.scss b/fusion_plating/fusion_plating_shopfloor/static/src/scss/components/_hold_composer.scss new file mode 100644 index 00000000..9160efcf --- /dev/null +++ b/fusion_plating/fusion_plating_shopfloor/static/src/scss/components/_hold_composer.scss @@ -0,0 +1,21 @@ +// ============================================================================= +// HoldComposer — modal hold-create form +// ============================================================================= + +.o_fp_hc { + display: flex; + flex-direction: column; + gap: 0.75rem; +} + +.o_fp_hc_row { + display: flex; + flex-direction: column; + gap: 0.3rem; +} + +.o_fp_hc_row label { + font-size: 0.8rem; + font-weight: 600; + color: var(--text-secondary, #666); +} diff --git a/fusion_plating/fusion_plating_shopfloor/static/src/xml/components/hold_composer.xml b/fusion_plating/fusion_plating_shopfloor/static/src/xml/components/hold_composer.xml new file mode 100644 index 00000000..73f1fdd6 --- /dev/null +++ b/fusion_plating/fusion_plating_shopfloor/static/src/xml/components/hold_composer.xml @@ -0,0 +1,51 @@ + + + + + +
+
+ + +
+
+ + +
+
+ +