feat(fusion_plating): Express masking reference images → mask step + workstation viewer
Order-entry shortcut: when masking is toggled ON for an Express order line, an amber "MASK" button appears to attach reference image(s)/PDF(s). The files ride the existing _fp_apply_express_overrides_to_job path onto the job's masking step, so the operator sees exactly what to mask — no recipe edit or custom prompt needed. - configurator: masking_attachment_ids on the wizard line + SO line; action_upload_masking_ref; override branch writes refs onto mask steps; amber multi-file MASK button (express_action_btns) shown when masking is on. - jobs: x_fc_masking_attachment_ids on fp.job.step (per-step) + computed rollup on fp.job; office "Masking Refs" form page (readonly preview). - shopfloor: workspace step payload carries masking_refs (sudo'd attachment read, rule 13m); operator sees thumbnail/PDF tiles on the mask step that open in Odoo's full-screen FileViewer (zoom + swipe). Verified end-to-end on entech: SO-line refs land on the mask step + job rollup (WO-30091); payload mask_refs shape correct (is_image, /web/image). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -91,6 +91,67 @@ export class FpExpressActionBtns extends Component {
|
||||
);
|
||||
if (action) await this.action.doAction(action);
|
||||
}
|
||||
|
||||
// ---- Masking reference upload (2026-06-03) ----
|
||||
// Visible only when masking is toggled ON for this line. Accepts MULTIPLE
|
||||
// image/PDF files; each is attached to the line and (on order confirm)
|
||||
// copied onto the job's masking step so the operator sees it in the
|
||||
// workstation. Mirrors onUpload but loops over the file list.
|
||||
get maskingEnabled() {
|
||||
return !!this.props.record.data.masking_enabled;
|
||||
}
|
||||
|
||||
get maskCount() {
|
||||
const m = this.props.record.data.masking_attachment_ids;
|
||||
return (m && m.count) || 0;
|
||||
}
|
||||
|
||||
async onUploadMask(ev) {
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
const input = document.createElement("input");
|
||||
input.type = "file";
|
||||
input.multiple = true;
|
||||
input.accept = ".pdf,.png,.jpg,.jpeg,application/pdf,image/*";
|
||||
input.onchange = async () => {
|
||||
const files = Array.from(input.files || []);
|
||||
if (!files.length) return;
|
||||
if (!(await this._ensureSaved())) return;
|
||||
let ok = 0;
|
||||
for (const file of files) {
|
||||
try {
|
||||
const base64 = await new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => resolve(reader.result.split(",")[1]);
|
||||
reader.onerror = reject;
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
await this.orm.call(
|
||||
this.props.record.resModel,
|
||||
"action_upload_masking_ref",
|
||||
[[this.props.record.resId]],
|
||||
{
|
||||
context: {
|
||||
fp_masking_file: base64,
|
||||
fp_masking_filename: file.name,
|
||||
},
|
||||
},
|
||||
);
|
||||
ok += 1;
|
||||
} catch (e) {
|
||||
this.notification.add(
|
||||
`Masking upload failed for "${file.name}": ${e.message || e}`,
|
||||
{ type: "danger" },
|
||||
);
|
||||
}
|
||||
}
|
||||
if (ok) {
|
||||
this.notification.add(`${ok} masking reference(s) added.`, { type: "success" });
|
||||
await this.props.record.load();
|
||||
}
|
||||
};
|
||||
input.click();
|
||||
}
|
||||
}
|
||||
|
||||
export const fpExpressActionBtns = {
|
||||
|
||||
@@ -441,6 +441,21 @@
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
// MASK upload — amber so order-entry notices the "attach reference"
|
||||
// affordance the moment masking is toggled on. Solid amber works on
|
||||
// both the light and dark backend bundles (dark text on amber fill).
|
||||
.o_fp_xpr_mask_btn {
|
||||
color: #1f2937;
|
||||
border-color: #d97706;
|
||||
background: #fbbf24;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
color: #1f2937;
|
||||
border-color: #b45309;
|
||||
background: #f59e0b;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
|
||||
@@ -16,6 +16,13 @@
|
||||
title="Open the part record in a modal">
|
||||
OPEN
|
||||
</button>
|
||||
<button t-if="maskingEnabled"
|
||||
class="o_fp_xpr_action_stack_btn o_fp_xpr_mask_btn"
|
||||
t-on-click="onUploadMask"
|
||||
t-att-disabled="!hasPart"
|
||||
title="Attach masking reference image(s)/PDF(s) — shown to the operator on the masking step">
|
||||
MASK<t t-if="maskCount"> (<t t-esc="maskCount"/>)</t>
|
||||
</button>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user