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:
gsinghpal
2026-06-03 15:12:18 -04:00
parent b52b8758a1
commit 235c8fba39
16 changed files with 331 additions and 2 deletions

View File

@@ -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 = {

View File

@@ -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;
}
}
}
// ============================================================

View File

@@ -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>