changes
This commit is contained in:
@@ -5,7 +5,7 @@
|
||||
|
||||
{
|
||||
'name': 'Fusion Plating',
|
||||
'version': '19.0.18.13.8',
|
||||
'version': '19.0.18.13.13',
|
||||
'category': 'Manufacturing/Plating',
|
||||
'summary': 'Core plating / metal finishing ERP: facilities, processes, tanks, baths, jobs, operators.',
|
||||
'description': """
|
||||
|
||||
@@ -37,6 +37,25 @@ _INPUT_SNAPSHOT_FIELDS = [
|
||||
]
|
||||
|
||||
|
||||
def _copy_snapshot_fields(source, fields):
|
||||
"""Copy ``fields`` from ``source`` record into a write-ready dict.
|
||||
|
||||
Many2one values must be unwrapped to their integer id — passing a
|
||||
recordset to ``create`` triggers psycopg2 ``can't adapt type X``
|
||||
because the SQL adapter doesn't know how to serialize a recordset.
|
||||
Scalar fields pass through untouched.
|
||||
"""
|
||||
out = {}
|
||||
for f in fields:
|
||||
field = source._fields[f]
|
||||
val = source[f]
|
||||
if field.type == 'many2one':
|
||||
out[f] = val.id if val else False
|
||||
else:
|
||||
out[f] = val
|
||||
return out
|
||||
|
||||
|
||||
class SimpleRecipeController(http.Controller):
|
||||
|
||||
# ------------------------------------------------------------------ load
|
||||
@@ -115,6 +134,18 @@ class SimpleRecipeController(http.Controller):
|
||||
),
|
||||
'measurements_badge_text': badge_text,
|
||||
'measurements_badge_class': badge_class,
|
||||
# Reference images attached to the step. Operators see
|
||||
# these in the Record Inputs dialog and the step quick-look
|
||||
# modal — recipe authors upload via the inline edit panel.
|
||||
'instruction_images': [
|
||||
{
|
||||
'id': att.id,
|
||||
'name': att.name or '',
|
||||
'mimetype': att.mimetype or '',
|
||||
'url': '/web/image/%s' % att.id,
|
||||
}
|
||||
for att in step.instruction_attachment_ids
|
||||
],
|
||||
'inputs': [
|
||||
{
|
||||
'id': i.id,
|
||||
@@ -457,8 +488,7 @@ class SimpleRecipeController(http.Controller):
|
||||
tpl = False
|
||||
if template_id:
|
||||
tpl = request.env['fp.step.template'].browse(template_id)
|
||||
for f in _SNAPSHOT_FIELDS:
|
||||
new_vals[f] = tpl[f]
|
||||
new_vals.update(_copy_snapshot_fields(tpl, _SNAPSHOT_FIELDS))
|
||||
if tpl.process_type_id:
|
||||
new_vals['process_type_id'] = tpl.process_type_id.id
|
||||
if tpl.tank_ids:
|
||||
@@ -598,8 +628,7 @@ class SimpleRecipeController(http.Controller):
|
||||
'sequence': src_node.sequence,
|
||||
'source_template_id': src_node.source_template_id.id or False,
|
||||
}
|
||||
for f in _SNAPSHOT_FIELDS:
|
||||
new_vals[f] = src_node[f]
|
||||
new_vals.update(_copy_snapshot_fields(src_node, _SNAPSHOT_FIELDS))
|
||||
if src_node.process_type_id:
|
||||
new_vals['process_type_id'] = src_node.process_type_id.id
|
||||
if src_node.tank_ids:
|
||||
@@ -690,6 +719,69 @@ class SimpleRecipeController(http.Controller):
|
||||
rec.unlink()
|
||||
return {'ok': True}
|
||||
|
||||
# ============================================================
|
||||
# Step instruction images — recipe authors attach reference photos
|
||||
# / screenshots / diagrams to a step from the Simple Editor's inline
|
||||
# edit panel. Operators see them on the Record Inputs dialog and
|
||||
# the step quick-look modal at runtime.
|
||||
# ============================================================
|
||||
|
||||
@http.route('/fp/simple_recipe/step/image/add', type='jsonrpc', auth='user')
|
||||
def step_image_add(self, node_id, filename, datas, mimetype=None):
|
||||
"""Upload a new instruction image to a recipe step.
|
||||
|
||||
Args:
|
||||
node_id: recipe node (fusion.plating.process.node) id
|
||||
filename: display name (with extension) for the attachment
|
||||
datas: base64-encoded image payload (no data: URL prefix)
|
||||
mimetype: optional override; falls back to image/png
|
||||
|
||||
Returns the new attachment metadata so the JS can append it to
|
||||
the step's gallery without a full reload.
|
||||
"""
|
||||
node = request.env['fusion.plating.process.node'].browse(int(node_id))
|
||||
node.check_access('write')
|
||||
att = request.env['ir.attachment'].create({
|
||||
'name': filename or 'image.png',
|
||||
'datas': datas,
|
||||
'res_model': 'fusion.plating.process.node',
|
||||
'res_id': node.id,
|
||||
'mimetype': mimetype or 'image/png',
|
||||
})
|
||||
node.instruction_attachment_ids = [(4, att.id)]
|
||||
return {
|
||||
'ok': True,
|
||||
'image': {
|
||||
'id': att.id,
|
||||
'name': att.name,
|
||||
'mimetype': att.mimetype or '',
|
||||
'url': '/web/image/%s' % att.id,
|
||||
},
|
||||
}
|
||||
|
||||
@http.route('/fp/simple_recipe/step/image/remove', type='jsonrpc', auth='user')
|
||||
def step_image_remove(self, node_id, attachment_id):
|
||||
"""Unlink an instruction image from a recipe step.
|
||||
|
||||
Soft-removes from the M2M; the underlying ir.attachment is
|
||||
deleted only if it isn't referenced by any other recipe node.
|
||||
"""
|
||||
node = request.env['fusion.plating.process.node'].browse(int(node_id))
|
||||
node.check_access('write')
|
||||
Att = request.env['ir.attachment']
|
||||
att = Att.browse(int(attachment_id))
|
||||
if not att.exists():
|
||||
return {'ok': False, 'error': 'not_found'}
|
||||
node.instruction_attachment_ids = [(3, att.id)]
|
||||
# Drop the attachment file too if no other node still links to it.
|
||||
Node = request.env['fusion.plating.process.node']
|
||||
still_used = Node.search_count([
|
||||
('instruction_attachment_ids', '=', att.id),
|
||||
])
|
||||
if not still_used:
|
||||
att.sudo().unlink()
|
||||
return {'ok': True}
|
||||
|
||||
@http.route('/fp/simple_recipe/step/reset_to_library', type='jsonrpc', auth='user')
|
||||
def step_reset_to_library(self, node_id):
|
||||
"""Re-sync the recipe step's input_ids + description from the linked
|
||||
|
||||
@@ -129,19 +129,34 @@ class FpProcessNode(models.Model):
|
||||
('fa-th', 'Grid / Racking'),
|
||||
('fa-fire', 'Fire / Bake'),
|
||||
('fa-bolt', 'Bolt / Electric'),
|
||||
('fa-flash', 'Flash / Discharge'),
|
||||
('fa-diamond', 'Diamond / Plating'),
|
||||
('fa-tint', 'Tint / Rinse'),
|
||||
('fa-shower', 'Shower / Clean'),
|
||||
('fa-bullseye', 'Target / Blast'),
|
||||
('fa-search', 'Search / Inspect'),
|
||||
('fa-check-circle', 'Check / Approve'),
|
||||
('fa-check-square-o', 'Checklist / QC'),
|
||||
('fa-clock-o', 'Clock / Wait'),
|
||||
('fa-pause-circle', 'Pause / Hold'),
|
||||
('fa-sun-o', 'Sun / Dry'),
|
||||
('fa-thermometer-half', 'Temp / Heat'),
|
||||
('fa-cloud', 'Cloud / Atmosphere'),
|
||||
('fa-eye', 'Eye / Visual'),
|
||||
('fa-eye-slash', 'Eye-Slash / Hidden'),
|
||||
('fa-hand-paper-o', 'Hand / Manual'),
|
||||
('fa-cube', 'Cube / Part'),
|
||||
('fa-shield', 'Shield / Protect'),
|
||||
('fa-inbox', 'Inbox / Receiving'),
|
||||
('fa-archive', 'Archive / Storage'),
|
||||
('fa-truck', 'Truck / Ship'),
|
||||
('fa-paper-plane', 'Paper-Plane / Send'),
|
||||
('fa-link', 'Link / Chain'),
|
||||
('fa-scissors', 'Scissors / Cut'),
|
||||
('fa-server', 'Server / Stack'),
|
||||
('fa-tachometer', 'Tachometer / Gauge'),
|
||||
('fa-file-text-o', 'Document / Form'),
|
||||
('fa-plus-circle', 'Plus / Add'),
|
||||
],
|
||||
string='Icon',
|
||||
default='fa-cog',
|
||||
@@ -151,6 +166,32 @@ class FpProcessNode(models.Model):
|
||||
default=0,
|
||||
)
|
||||
|
||||
# ---- Reference images / instruction screenshots -------------------------
|
||||
# Recipe authors attach photos and screenshots here so operators see
|
||||
# them on the shop floor when running the step. Anything from a
|
||||
# process diagram, masking-line photo, or annotated screenshot of the
|
||||
# WI document. Many2many — supports zero, one, or many images.
|
||||
instruction_attachment_ids = fields.Many2many(
|
||||
'ir.attachment',
|
||||
'fp_node_instruction_attachment_rel',
|
||||
'node_id', 'attachment_id',
|
||||
string='Instruction Images',
|
||||
domain=[('mimetype', 'ilike', 'image/')],
|
||||
help='Reference photos and screenshots that operators see at '
|
||||
'runtime. Anything visual that helps them execute the step '
|
||||
'correctly — fixture orientation, masking pattern, gauge '
|
||||
'reading. Supports multiple images per step.',
|
||||
)
|
||||
instruction_attachment_count = fields.Integer(
|
||||
string='Instruction Image Count',
|
||||
compute='_compute_instruction_attachment_count',
|
||||
)
|
||||
|
||||
@api.depends('instruction_attachment_ids')
|
||||
def _compute_instruction_attachment_count(self):
|
||||
for rec in self:
|
||||
rec.instruction_attachment_count = len(rec.instruction_attachment_ids)
|
||||
|
||||
# ---- Timing --------------------------------------------------------------
|
||||
|
||||
estimated_duration = fields.Float(
|
||||
@@ -722,11 +763,16 @@ class FpProcessNodeInput(models.Model):
|
||||
)
|
||||
target_min = fields.Float(
|
||||
string='Target Min',
|
||||
help='Lower bound of the acceptable range, expressed in Target Unit.',
|
||||
digits=(16, 6),
|
||||
help='Lower bound of the acceptable range, expressed in Target Unit. '
|
||||
'Stored to 6 decimal places to support plating thicknesses '
|
||||
'(e.g. 0.000050 in / 50 micro-inches).',
|
||||
)
|
||||
target_max = fields.Float(
|
||||
string='Target Max',
|
||||
help='Upper bound of the acceptable range, expressed in Target Unit.',
|
||||
digits=(16, 6),
|
||||
help='Upper bound of the acceptable range, expressed in Target Unit. '
|
||||
'Stored to 6 decimal places.',
|
||||
)
|
||||
target_unit = fields.Selection(
|
||||
FP_UOM_SELECTION,
|
||||
|
||||
@@ -690,6 +690,86 @@ export class FpSimpleRecipeEditor extends Component {
|
||||
this._fpResetStepEdit();
|
||||
}
|
||||
|
||||
// -------------------- Instruction images -------------------------------
|
||||
//
|
||||
// Recipe authors drop reference photos / screenshots into this list
|
||||
// while editing a step. Operators see the gallery at runtime in the
|
||||
// Record Inputs dialog and the step quick-look modal. Backed by
|
||||
// /fp/simple_recipe/step/image/{add,remove}; mirrors the upload
|
||||
// affordance available on the tree-editor side.
|
||||
|
||||
async onUploadStepImages(stepId, ev) {
|
||||
const files = Array.from(ev.target.files || []);
|
||||
if (!files.length) return;
|
||||
for (const file of files) {
|
||||
if (!file.type.startsWith("image/")) {
|
||||
this.notification.add(
|
||||
_t("%s isn't an image — skipped.").replace("%s", file.name),
|
||||
{ type: "warning" },
|
||||
);
|
||||
continue;
|
||||
}
|
||||
// Read as base64 (strip the "data:...;base64," prefix).
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const datas = await new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
const result = reader.result || "";
|
||||
resolve(String(result).split(",")[1] || "");
|
||||
};
|
||||
reader.onerror = () => reject(reader.error);
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const result = await rpc("/fp/simple_recipe/step/image/add", {
|
||||
node_id: stepId,
|
||||
filename: file.name,
|
||||
datas: datas,
|
||||
mimetype: file.type,
|
||||
});
|
||||
if (!result.ok) {
|
||||
this.notification.add(
|
||||
_t("Could not upload %s.").replace("%s", file.name),
|
||||
{ type: "danger" },
|
||||
);
|
||||
continue;
|
||||
}
|
||||
// Append directly to the in-memory step so the gallery
|
||||
// updates without re-loading the whole recipe tree.
|
||||
const step = this.state.steps.find((s) => s.id === stepId);
|
||||
if (step) {
|
||||
step.instruction_images = [
|
||||
...(step.instruction_images || []),
|
||||
result.image,
|
||||
];
|
||||
}
|
||||
}
|
||||
// Clear the file input so the same file can be uploaded again
|
||||
// after a remove + re-add cycle.
|
||||
ev.target.value = "";
|
||||
this.notification.add(_t("Image(s) attached"), { type: "success" });
|
||||
}
|
||||
|
||||
async onRemoveStepImage(stepId, attachmentId) {
|
||||
const result = await rpc("/fp/simple_recipe/step/image/remove", {
|
||||
node_id: stepId,
|
||||
attachment_id: attachmentId,
|
||||
});
|
||||
if (!result.ok) {
|
||||
this.notification.add(
|
||||
_t("Could not remove image."),
|
||||
{ type: "danger" },
|
||||
);
|
||||
return;
|
||||
}
|
||||
const step = this.state.steps.find((s) => s.id === stepId);
|
||||
if (step) {
|
||||
step.instruction_images = (step.instruction_images || []).filter(
|
||||
(img) => img.id !== attachmentId,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------- Sub 12d — measurements config --------------------
|
||||
|
||||
async onToggleStepCollect(stepId, collect) {
|
||||
|
||||
@@ -54,17 +54,61 @@ $fp-se-drop: var(--fp-drop-bg, #{$_fp_se_drop_hex});
|
||||
.o_fp_simple_editor_meta {
|
||||
background: $fp-se-card;
|
||||
border: 1px solid $fp-se-border;
|
||||
border-radius: 4px;
|
||||
padding: 1rem;
|
||||
border-radius: 6px;
|
||||
padding: 1rem 1.25rem;
|
||||
margin-bottom: 1rem;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, .04);
|
||||
|
||||
.o_fp_import_row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: .75rem;
|
||||
|
||||
label { font-weight: 500; margin: 0; min-width: 14rem; }
|
||||
select { flex: 1; max-width: 30rem; }
|
||||
.o_fp_import_label {
|
||||
margin: 0;
|
||||
font-weight: 600;
|
||||
color: $fp-se-accent;
|
||||
white-space: nowrap;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
|
||||
.fa {
|
||||
color: $fp-se-accent;
|
||||
opacity: .8;
|
||||
}
|
||||
}
|
||||
|
||||
// Bootstrap's form-select gives us the chevron + base styling;
|
||||
// we just tighten the colours to the card tokens so the field
|
||||
// sits flush in our themed panel instead of fighting it.
|
||||
.o_fp_import_select {
|
||||
flex: 1;
|
||||
max-width: 32rem;
|
||||
min-height: 2.25rem;
|
||||
background-color: $fp-se-card;
|
||||
color: inherit;
|
||||
border-color: $fp-se-border;
|
||||
transition: border-color .15s ease, box-shadow .15s ease;
|
||||
|
||||
&:hover:not(:focus):not(:disabled) {
|
||||
border-color: $fp-se-accent;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
border-color: $fp-se-accent;
|
||||
box-shadow: 0 0 0 .15rem rgba(46, 125, 107, .18);
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
.o_fp_import_btn {
|
||||
white-space: nowrap;
|
||||
min-height: 2.25rem;
|
||||
|
||||
.fa {
|
||||
opacity: .9;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -492,3 +536,109 @@ $fp-se-drop: var(--fp-drop-bg, #{$_fp_se_drop_hex});
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
|
||||
// =============================================================================
|
||||
// Instruction images gallery — recipe-author upload + thumbnail strip in
|
||||
// the Simple Editor's inline step edit panel. Mirrors what the Record
|
||||
// Inputs dialog renders at runtime so authors can preview the same way
|
||||
// the operator will see it.
|
||||
// =============================================================================
|
||||
|
||||
.o_fp_step_images {
|
||||
.o_fp_step_images_gallery {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: .5rem;
|
||||
margin: .5rem 0;
|
||||
}
|
||||
|
||||
.o_fp_step_image_card {
|
||||
position: relative;
|
||||
width: 110px;
|
||||
background: $fp-se-card;
|
||||
border: 1px solid $fp-se-border;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
transition: border-color .12s ease, box-shadow .12s ease;
|
||||
|
||||
&:hover {
|
||||
border-color: $fp-se-accent;
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, .12);
|
||||
}
|
||||
|
||||
a {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 90px;
|
||||
cursor: zoom-in;
|
||||
}
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.o_fp_step_image_remove {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
right: 4px;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
padding: 0;
|
||||
border-radius: 50%;
|
||||
background: rgba(0, 0, 0, .55);
|
||||
color: #fff;
|
||||
border: 0;
|
||||
cursor: pointer;
|
||||
opacity: 0;
|
||||
transition: opacity .12s ease, background-color .12s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: .75rem;
|
||||
}
|
||||
|
||||
.o_fp_step_image_card:hover .o_fp_step_image_remove,
|
||||
.o_fp_step_image_remove:focus {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.o_fp_step_image_remove:hover {
|
||||
background: #c0392b;
|
||||
}
|
||||
|
||||
.o_fp_step_image_caption {
|
||||
font-size: .7rem;
|
||||
padding: 4px 6px;
|
||||
color: $fp-se-muted;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
border-top: 1px solid $fp-se-border;
|
||||
}
|
||||
|
||||
.o_fp_step_image_uploader {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 8px 14px;
|
||||
margin-top: .25rem;
|
||||
background: $fp-se-card;
|
||||
border: 1px dashed $fp-se-border;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
color: $fp-se-accent;
|
||||
font-weight: 500;
|
||||
transition: border-color .12s ease, background-color .12s ease;
|
||||
|
||||
&:hover {
|
||||
border-color: $fp-se-accent;
|
||||
border-style: solid;
|
||||
background: rgba(46, 125, 107, .06);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -23,8 +23,13 @@
|
||||
|
||||
<div class="o_fp_simple_editor_meta" t-if="state.recipe">
|
||||
<div class="o_fp_import_row">
|
||||
<label>Import starter from template:</label>
|
||||
<select t-model="state.selectedTemplate">
|
||||
<label class="o_fp_import_label" for="fp_import_template_select">
|
||||
<i class="fa fa-download me-2"/>
|
||||
Import starter from template
|
||||
</label>
|
||||
<select id="fp_import_template_select"
|
||||
class="form-select o_fp_import_select"
|
||||
t-model="state.selectedTemplate">
|
||||
<option value="">— Select template —</option>
|
||||
<t t-foreach="state.templateOptions" t-as="tpl" t-key="tpl.id">
|
||||
<option t-att-value="tpl.id">
|
||||
@@ -32,8 +37,10 @@
|
||||
</option>
|
||||
</t>
|
||||
</select>
|
||||
<button class="btn btn-primary" t-on-click="onImportTemplate"
|
||||
<button class="btn btn-primary o_fp_import_btn"
|
||||
t-on-click="onImportTemplate"
|
||||
t-att-disabled="!state.selectedTemplate">
|
||||
<i class="fa fa-plus me-1"/>
|
||||
Import
|
||||
</button>
|
||||
</div>
|
||||
@@ -193,6 +200,56 @@
|
||||
</small>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Instruction images — recipe author drops
|
||||
photos / screenshots / diagrams here.
|
||||
Operators see the gallery at runtime in
|
||||
the Record Inputs dialog and the step
|
||||
quick-look modal. -->
|
||||
<div class="o_fp_edit_field o_fp_step_images">
|
||||
<label>
|
||||
<i class="fa fa-camera me-1"/>
|
||||
<strong>Instruction Images</strong>
|
||||
</label>
|
||||
<p class="o_fp_edit_hint">
|
||||
Reference photos / screenshots / diagrams shown
|
||||
to operators while running this step. Drop
|
||||
multiple images for masking patterns, fixture
|
||||
orientation, gauge readings, etc.
|
||||
</p>
|
||||
<div class="o_fp_step_images_gallery"
|
||||
t-if="step.instruction_images and step.instruction_images.length">
|
||||
<t t-foreach="step.instruction_images"
|
||||
t-as="img" t-key="img.id">
|
||||
<div class="o_fp_step_image_card">
|
||||
<a t-att-href="img.url"
|
||||
target="_blank"
|
||||
t-att-title="img.name">
|
||||
<img t-att-src="img.url"
|
||||
t-att-alt="img.name"/>
|
||||
</a>
|
||||
<button type="button"
|
||||
class="o_fp_step_image_remove"
|
||||
title="Remove image"
|
||||
t-on-click="() => this.onRemoveStepImage(step.id, img.id)">
|
||||
<i class="fa fa-times"/>
|
||||
</button>
|
||||
<div class="o_fp_step_image_caption"
|
||||
t-esc="img.name"/>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
<label class="o_fp_step_image_uploader">
|
||||
<i class="fa fa-plus me-1"/>
|
||||
Upload images
|
||||
<input type="file"
|
||||
accept="image/*"
|
||||
multiple="multiple"
|
||||
hidden="hidden"
|
||||
t-on-change="(ev) => this.onUploadStepImages(step.id, ev)"/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Sub 12d — Measurements config -->
|
||||
<div class="o_fp_edit_field o_fp_measurements_config">
|
||||
<label>
|
||||
|
||||
@@ -136,6 +136,22 @@
|
||||
<page string="Description" name="description">
|
||||
<field name="description" widget="html"/>
|
||||
</page>
|
||||
<page string="Instruction Images"
|
||||
name="instruction_images">
|
||||
<p class="text-muted">
|
||||
Photos and screenshots operators see while
|
||||
running this step — masking patterns,
|
||||
fixture orientation, annotated diagrams,
|
||||
gauge readings. Drop one or many images
|
||||
here; they appear on the shop-floor
|
||||
step-detail panel and the Record Inputs
|
||||
dialog.
|
||||
</p>
|
||||
<field name="instruction_attachment_ids"
|
||||
widget="many2many_binary"
|
||||
options="{'accepted_file_extensions': 'image/*'}"
|
||||
nolabel="1"/>
|
||||
</page>
|
||||
<page string="Operator Inputs" name="inputs">
|
||||
<group>
|
||||
<field name="collect_measurements"
|
||||
|
||||
Reference in New Issue
Block a user