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:
@@ -5,7 +5,7 @@
|
||||
|
||||
{
|
||||
'name': 'Fusion Plating — Shop Floor',
|
||||
'version': '19.0.37.0.3',
|
||||
'version': '19.0.37.1.0',
|
||||
'category': 'Manufacturing/Plating',
|
||||
'summary': 'Shop-floor tablet stations, QR scanning, bake window enforcer.',
|
||||
'description': """
|
||||
|
||||
@@ -68,6 +68,18 @@ class FpWorkspaceController(http.Controller):
|
||||
override = job.override_ids.filtered(
|
||||
lambda o, n=step.recipe_node_id: o.node_id.id == n.id
|
||||
) if 'override_ids' in job._fields else env['fp.job.node.override']
|
||||
# Masking reference image(s)/PDF(s) attached at Express order entry.
|
||||
# sudo: low-priv operators can read fp.job.step but not always the
|
||||
# linked ir.attachment (rule 13m). The files are safe to surface.
|
||||
mask_atts = (step.sudo().x_fc_masking_attachment_ids
|
||||
if 'x_fc_masking_attachment_ids' in step._fields
|
||||
else env['ir.attachment'])
|
||||
mask_refs = [{
|
||||
'id': a.id,
|
||||
'name': a.name or '',
|
||||
'mimetype': a.mimetype or '',
|
||||
'is_image': (a.mimetype or '').startswith('image/'),
|
||||
} for a in mask_atts]
|
||||
steps.append({
|
||||
'id': step.id,
|
||||
'sequence': step.sequence,
|
||||
@@ -109,6 +121,7 @@ class FpWorkspaceController(http.Controller):
|
||||
'quick_look_prompt_count': len(
|
||||
getattr(step, 'quick_look_prompt_ids', step.browse())
|
||||
),
|
||||
'masking_refs': mask_refs,
|
||||
})
|
||||
|
||||
# ---- Spec + attachments + chatter -------------------------------
|
||||
|
||||
@@ -31,6 +31,8 @@ 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 { 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";
|
||||
@@ -42,6 +44,8 @@ export class FpJobWorkspace extends Component {
|
||||
this.action = useService("action");
|
||||
this.dialog = useService("dialog");
|
||||
this.tabletSessionManager = useService("fp_tablet_session_manager");
|
||||
// Full-screen image/PDF viewer (zoom + swipe) for masking refs.
|
||||
this.fileViewer = useFileViewer();
|
||||
|
||||
this.state = useState({
|
||||
data: null,
|
||||
@@ -199,6 +203,24 @@ export class FpJobWorkspace extends Component {
|
||||
});
|
||||
}
|
||||
|
||||
// Open masking reference image(s)/PDF(s) in the full-screen viewer.
|
||||
// Builds FileModel descriptors so the operator gets zoom + swipe across
|
||||
// every reference on this step, starting at the tile they tapped.
|
||||
openMaskRef(step, ref) {
|
||||
const files = (step.masking_refs || []).map((r) => {
|
||||
const f = new FileModel();
|
||||
f.id = r.id;
|
||||
f.name = r.name;
|
||||
f.mimetype = r.mimetype;
|
||||
f.type = "binary";
|
||||
return f;
|
||||
});
|
||||
const clicked = files.find((f) => f.id === ref.id) || files[0];
|
||||
if (clicked) {
|
||||
this.fileViewer.open(clicked, files);
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Step state helpers ------------------------------------------------
|
||||
iconForStepState(state) {
|
||||
const map = {
|
||||
|
||||
@@ -317,6 +317,89 @@ $_ws-text-hex: #1d1d1f;
|
||||
.o_fp_ws_step_instr { font-size: 0.78rem; color: var(--text-secondary, #555); font-style: italic; }
|
||||
.o_fp_ws_step_actions { display: flex; gap: 0.35rem; flex-wrap: wrap; }
|
||||
|
||||
// ---- Masking reference tiles (tap → full-screen FileViewer) -----------
|
||||
.o_fp_ws_mask_refs {
|
||||
margin-top: 0.5rem;
|
||||
padding: 0.5rem 0.6rem;
|
||||
border: 1px solid #d97706;
|
||||
border-left: 4px solid #f59e0b;
|
||||
border-radius: 8px;
|
||||
background: rgba(245, 158, 11, 0.08);
|
||||
}
|
||||
.o_fp_ws_mask_refs_label {
|
||||
font-size: 0.78rem;
|
||||
font-weight: 600;
|
||||
color: #b06600;
|
||||
margin-bottom: 0.4rem;
|
||||
i { margin-right: 0.3rem; }
|
||||
}
|
||||
.o_fp_ws_mask_refs_grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
.o_fp_ws_mask_ref {
|
||||
position: relative;
|
||||
width: 104px;
|
||||
height: 104px;
|
||||
border: 1px solid $_ws-border-hex;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
background: $_ws-card-hex;
|
||||
transition: transform 0.08s ease, box-shadow 0.08s ease;
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.04);
|
||||
box-shadow: 0 4px 14px rgba(0, 0, 0, 0.18);
|
||||
border-color: #f59e0b;
|
||||
}
|
||||
}
|
||||
.o_fp_ws_mask_ref_thumb {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
.o_fp_ws_mask_ref_pdf {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.25rem;
|
||||
padding: 0.3rem;
|
||||
text-align: center;
|
||||
|
||||
i { font-size: 1.8rem; color: #d9534f; }
|
||||
}
|
||||
.o_fp_ws_mask_ref_pdfname {
|
||||
font-size: 0.6rem;
|
||||
color: $_ws-text-hex;
|
||||
line-height: 1.1;
|
||||
word-break: break-word;
|
||||
max-height: 2.2em;
|
||||
overflow: hidden;
|
||||
}
|
||||
.o_fp_ws_mask_ref_zoom {
|
||||
position: absolute;
|
||||
right: 4px;
|
||||
bottom: 4px;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border-radius: 50%;
|
||||
background: rgba(0, 0, 0, 0.55);
|
||||
color: #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
@if $o-webclient-color-scheme == dark {
|
||||
.o_fp_ws_mask_refs_label { color: #f0a93a; }
|
||||
}
|
||||
|
||||
.o_fp_ws_step_excluded {
|
||||
font-size: 0.78rem;
|
||||
color: var(--text-secondary, #888);
|
||||
|
||||
@@ -369,6 +369,32 @@
|
||||
<t t-esc="step.instructions"/>
|
||||
</div>
|
||||
|
||||
<!-- Masking reference(s) — attached at order entry; tap to enlarge -->
|
||||
<div t-if="step.masking_refs and step.masking_refs.length"
|
||||
class="o_fp_ws_mask_refs">
|
||||
<div class="o_fp_ws_mask_refs_label">
|
||||
<i class="fa fa-paint-brush"/>
|
||||
Masking reference<t t-if="step.masking_refs.length > 1">s</t> — tap to enlarge
|
||||
</div>
|
||||
<div class="o_fp_ws_mask_refs_grid">
|
||||
<t t-foreach="step.masking_refs" t-as="ref" t-key="ref.id">
|
||||
<div class="o_fp_ws_mask_ref"
|
||||
t-on-click="() => this.openMaskRef(step, ref)"
|
||||
t-att-title="ref.name">
|
||||
<img t-if="ref.is_image"
|
||||
class="o_fp_ws_mask_ref_thumb"
|
||||
t-att-src="'/web/image/' + ref.id + '/200x200'"
|
||||
t-att-alt="ref.name"/>
|
||||
<div t-else="" class="o_fp_ws_mask_ref_pdf">
|
||||
<i class="fa fa-file-pdf-o"/>
|
||||
<span class="o_fp_ws_mask_ref_pdfname" t-esc="ref.name"/>
|
||||
</div>
|
||||
<div class="o_fp_ws_mask_ref_zoom"><i class="fa fa-search-plus"/></div>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Opt-out notice -->
|
||||
<div t-if="step.override_excluded" class="o_fp_ws_step_excluded">
|
||||
<i class="fa fa-ban"/> Skipped per recipe override for this WO
|
||||
|
||||
Reference in New Issue
Block a user