This commit is contained in:
gsinghpal
2026-04-26 15:05:17 -04:00
parent 160198edb1
commit d9f58b9851
110 changed files with 6210 additions and 1182 deletions

View File

@@ -0,0 +1,349 @@
/** @odoo-module **/
// =============================================================================
// Fusion Plating — Mobile QC Checklist (OWL backend client action)
// Copyright 2026 Nexa Systems Inc.
// License OPL-1 (Odoo Proprietary License v1.0)
//
// Matches the existing Tablet Station / Plant Overview conventions:
// * `static template` + `static props = ["*"]`
// * Standalone rpc() from @web/core/network/rpc
// * Design tokens from _fp_shopfloor_tokens.scss (no borders, shadow
// elevation, 48 px touch targets)
//
// Invoked either via the MO "Open QC" smart-button (action_open_tablet)
// or directly with `ir.actions.client` tag `fp_qc_checklist` and the
// action's params.check_id.
// =============================================================================
import { Component, useState, onMounted, useRef } from "@odoo/owl";
import { registry } from "@web/core/registry";
import { rpc } from "@web/core/network/rpc";
import { useService } from "@web/core/utils/hooks";
export class FpQcChecklist extends Component {
static template = "fusion_plating_quality.FpQcChecklist";
static props = ["*"];
setup() {
this.notification = useService("notification");
this.action = useService("action");
this.fileInput = useRef("fileInput");
this.pdfInput = useRef("pdfInput");
this.photoLineId = null;
this.state = useState({
loading: true,
saving: false,
error: null,
check: null,
lines: [],
expandedLineId: null,
showFinalize: false,
finalizeNotes: "",
});
// action.params (from ir.actions.client) is the canonical
// source; fall back to URL query params for deep-linking.
const params = (this.props.action && this.props.action.params) || {};
this.checkId = params.check_id || null;
this.jobId = params.job_id || null;
onMounted(() => this.refresh());
}
// ------------------------------------------------------------------
// Data
// ------------------------------------------------------------------
async refresh() {
this.state.loading = true;
this.state.error = null;
try {
const res = await rpc("/fp/qc/get", {
check_id: this.checkId,
job_id: this.jobId,
});
if (!res.ok) {
this.state.error = res.error === "no_qc"
? "No QC checklist exists for this MO yet."
: (res.error || "QC not found");
return;
}
this.state.check = res.check;
this.state.lines = res.lines || [];
this.checkId = res.check.id;
} catch (err) {
this.state.error = err && err.message ? err.message : String(err);
} finally {
this.state.loading = false;
}
}
// ------------------------------------------------------------------
// Line actions
// ------------------------------------------------------------------
async markLine(line, result) {
if (this.state.saving) return;
this.state.saving = true;
try {
const payload = {
check_id: this.checkId,
line_id: line.id,
result,
};
if (line.requires_value) {
payload.value = line.value;
}
if (line.notes !== undefined) payload.notes = line.notes;
const res = await rpc("/fp/qc/line/mark", payload);
if (!res.ok) {
this.notification.add(res.error || "Mark failed", {
type: "danger",
title: line.name,
});
return;
}
// Merge updated line into state
const idx = this.state.lines.findIndex((l) => l.id === line.id);
if (idx >= 0) this.state.lines[idx] = res.line;
this.state.check = res.check;
this.notification.add(
result === "pass" ? "Passed" : result === "fail" ? "Failed" : "Marked",
{ type: result === "fail" ? "danger" : "success" },
);
} catch (err) {
this.notification.add(
err && err.message ? err.message : String(err),
{ type: "danger" },
);
} finally {
this.state.saving = false;
}
}
// Value input — debounced write on blur. Pending result stays until
// operator taps pass/fail.
onValueInput(line, ev) {
const v = parseFloat(ev.target.value);
line.value = isNaN(v) ? 0 : v;
if (line.requires_value) {
const inRange =
(!line.value_min || line.value >= line.value_min) &&
(!line.value_max || line.value <= line.value_max);
line.value_in_range = inRange;
}
}
onNotesInput(line, ev) {
line.notes = ev.target.value;
}
toggleExpanded(line) {
this.state.expandedLineId =
this.state.expandedLineId === line.id ? null : line.id;
}
// ------------------------------------------------------------------
// Photo upload
// ------------------------------------------------------------------
triggerPhoto(line) {
this.photoLineId = line.id;
if (this.fileInput.el) {
this.fileInput.el.value = "";
this.fileInput.el.click();
}
}
async onPhotoSelected(ev) {
const file = ev.target.files && ev.target.files[0];
if (!file || !this.photoLineId) return;
const fd = new FormData();
fd.append("file", file);
fd.append("line_id", this.photoLineId);
try {
const resp = await fetch("/fp/qc/line/photo", {
method: "POST",
body: fd,
credentials: "same-origin",
});
const json = await resp.json();
if (!json.ok) {
this.notification.add(json.error || "Upload failed", {
type: "danger",
});
return;
}
this.notification.add("Photo uploaded", { type: "success" });
await this.refresh();
} catch (err) {
this.notification.add(
err && err.message ? err.message : String(err),
{ type: "danger" },
);
} finally {
this.photoLineId = null;
}
}
// ------------------------------------------------------------------
// Fischerscope PDF upload
// ------------------------------------------------------------------
triggerPdfUpload() {
if (this.pdfInput.el) {
this.pdfInput.el.value = "";
this.pdfInput.el.click();
}
}
async onPdfSelected(ev) {
const file = ev.target.files && ev.target.files[0];
if (!file) return;
const fd = new FormData();
fd.append("file", file);
fd.append("check_id", this.checkId);
try {
this.state.saving = true;
const resp = await fetch("/fp/qc/thickness_pdf", {
method: "POST",
body: fd,
credentials: "same-origin",
});
const json = await resp.json();
if (!json.ok) {
this.notification.add(json.error || "Upload failed", {
type: "danger",
});
return;
}
this.notification.add(
`Uploaded — ${json.reading_count || 0} reading(s) extracted`,
{ type: "success" },
);
await this.refresh();
} catch (err) {
this.notification.add(
err && err.message ? err.message : String(err),
{ type: "danger" },
);
} finally {
this.state.saving = false;
}
}
// ------------------------------------------------------------------
// Finalize
// ------------------------------------------------------------------
openFinalize() {
this.state.showFinalize = true;
this.state.finalizeNotes = this.state.check
? this.state.check.notes || ""
: "";
}
closeFinalize() {
this.state.showFinalize = false;
}
async finalize(result) {
try {
this.state.saving = true;
const res = await rpc("/fp/qc/finalize", {
check_id: this.checkId,
result,
notes: this.state.finalizeNotes,
});
if (!res.ok) {
this.notification.add(res.error || "Finalize failed", {
type: "danger",
});
return;
}
this.state.check = res.check;
this.state.showFinalize = false;
this.notification.add(
result === "pass"
? "QC passed. MO can now be marked Done."
: result === "fail"
? "QC failed. Go to the MO to decide scrap/rework."
: "QC flagged for rework.",
{ type: result === "pass" ? "success" : "warning" },
);
await this.refresh();
} catch (err) {
this.notification.add(
err && err.message ? err.message : String(err),
{ type: "danger" },
);
} finally {
this.state.saving = false;
}
}
// ------------------------------------------------------------------
// Navigation
// ------------------------------------------------------------------
async openJob() {
if (!this.state.check || !this.state.check.job_id) return;
this.action.doAction({
type: "ir.actions.act_window",
res_model: "fp.job",
res_id: this.state.check.job_id,
views: [[false, "form"]],
target: "current",
});
}
// ------------------------------------------------------------------
// Helpers used by the template
// ------------------------------------------------------------------
resultBadgeClass(result) {
return {
pass: "o_fp_qc_badge_pass",
fail: "o_fp_qc_badge_fail",
na: "o_fp_qc_badge_na",
pending: "o_fp_qc_badge_pending",
}[result || "pending"] || "o_fp_qc_badge_pending";
}
checkTypeIcon(type) {
return {
visual: "fa-eye",
dimensional: "fa-arrows-h",
thickness: "fa-bar-chart",
adhesion: "fa-link",
hardness: "fa-diamond",
salt_spray: "fa-tint",
functional: "fa-cogs",
other: "fa-circle-o",
}[type] || "fa-circle-o";
}
get progressPercent() {
if (!this.state.check || !this.state.check.line_count) return 0;
const done = this.state.check.lines_passed +
this.state.check.lines_failed;
return Math.round((done / this.state.check.line_count) * 100);
}
get canFinalize() {
if (!this.state.check) return false;
if (["passed", "failed"].includes(this.state.check.state)) return false;
// Required items must be resolved
const pendingRequired = this.state.lines.filter(
(l) => l.required && (l.result === "pending" || !l.result),
);
if (pendingRequired.length > 0) return false;
// Thickness PDF requirement
if (this.state.check.require_thickness_report_pdf &&
!this.state.check.has_thickness_pdf) return false;
// Thickness readings requirement
if (this.state.check.require_thickness_readings &&
this.state.check.thickness_reading_count === 0) return false;
return true;
}
get anyFailed() {
return this.state.lines.some((l) => l.result === "fail");
}
}
registry.category("actions").add("fp_qc_checklist", FpQcChecklist);