fix(audit-trail): 3 production bugs found via end-to-end Anodize battle test
Battle-tested complete workflow on entech: ABC Manufacturing + Anodize recipe (id=136) cloned to part-variant (id=1775) → SO S00276 confirmed → fp.job 1234 with 17 steps → recorded 56 measurement values exercising all 13 input types (incl. all 4 new types) → CoC chronological report renders 69KB with all values incl. photo thumbnails. Bugs found and fixed: 1. fp.process.node.input_ids missing copy=True — when a master recipe was cloned per-part (the standard variant pattern), the operator prompts on each step did NOT get copied to the variant. Result: jobs built from variants ran with zero prompts even though the master had them. Fixed: input_ids now copy=True so cloning auto-duplicates. 2. CoC chronological template read dest.input_ids where dest is fp.job.step. Steps don't carry input_ids — that field lives on the recipe node. Result: AttributeError aborted the entire CoC render. Fixed: walk via dest.recipe_node_id.input_ids; preserves the existing collect=True filter. 3. CoC chronological template used hasattr() in a t-value expression. QWeb's expression engine doesn't expose Python builtins, raised KeyError: 'hasattr'. Fixed: use 'collect' in i._fields instead. Also enhanced photo rendering in CoC: was just "[Attachment]" placeholder; now renders an actual <img> thumbnail (max 80px tall) plus the filename. Battle-test script saved to fusion_plating/scripts/bt_e2e_anodize_v2.py for re-runs / regression testing. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -321,6 +321,83 @@ export class FpSimpleRecipeEditor extends Component {
|
||||
this.state.editInstructions = "";
|
||||
}
|
||||
|
||||
// -------------------- Sub 12d — measurements config --------------------
|
||||
|
||||
async onToggleStepCollect(stepId, collect) {
|
||||
await rpc("/fp/simple_recipe/step/toggle_collect", {
|
||||
node_id: stepId, collect,
|
||||
});
|
||||
await this.loadAll();
|
||||
}
|
||||
|
||||
async onToggleInputCollect(inputId, collect) {
|
||||
await rpc("/fp/simple_recipe/step/edit_input", {
|
||||
input_id: inputId,
|
||||
payload: { collect },
|
||||
});
|
||||
await this.loadAll();
|
||||
}
|
||||
|
||||
async onEditInputField(inputId, field, value) {
|
||||
const payload = {};
|
||||
payload[field] = value;
|
||||
await rpc("/fp/simple_recipe/step/edit_input", {
|
||||
input_id: inputId,
|
||||
payload,
|
||||
});
|
||||
await this.loadAll();
|
||||
}
|
||||
|
||||
async onAddCustomInput(stepId) {
|
||||
await rpc("/fp/simple_recipe/step/add_input", {
|
||||
node_id: stepId,
|
||||
payload: { name: _t("New Prompt"), input_type: "text" },
|
||||
});
|
||||
await this.loadAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* Auto-save an input field on blur or change. Skips empty names so
|
||||
* accidental Tab-out doesn't blank the prompt.
|
||||
*/
|
||||
async onInputBlur(inputId, field, ev) {
|
||||
const value = ev.target.value;
|
||||
if (field === "name" && !value.trim()) return;
|
||||
await this.onEditInputField(inputId, field, value);
|
||||
}
|
||||
|
||||
async onRemoveCustomInput(inputId) {
|
||||
const result = await rpc("/fp/simple_recipe/step/remove_input", {
|
||||
input_id: inputId,
|
||||
});
|
||||
if (result.error === "library_sourced") {
|
||||
this.notification.add(
|
||||
_t("Library prompts can't be deleted — toggle Collect off instead."),
|
||||
{ type: "warning" }
|
||||
);
|
||||
return;
|
||||
}
|
||||
await this.loadAll();
|
||||
}
|
||||
|
||||
async onResetToLibrary(stepId) {
|
||||
const result = await rpc("/fp/simple_recipe/step/reset_to_library", {
|
||||
node_id: stepId,
|
||||
});
|
||||
if (!result.ok) {
|
||||
this.notification.add(
|
||||
_t("This step has no linked library template."),
|
||||
{ type: "warning" }
|
||||
);
|
||||
return;
|
||||
}
|
||||
await this.loadAll();
|
||||
this.notification.add(
|
||||
_t("Reset to library defaults — custom prompts preserved"),
|
||||
{ type: "success" }
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render stored HTML as plain text for the textarea. Strips tags,
|
||||
* collapses block elements to newlines. Good enough for the simple
|
||||
|
||||
@@ -233,6 +233,27 @@ $fp-se-drop: var(--fp-drop-bg, #{$_fp_se_drop_hex});
|
||||
color: $fp-se-muted;
|
||||
}
|
||||
|
||||
.o_fp_measurements_config {
|
||||
margin-top: .75rem;
|
||||
padding-top: .75rem;
|
||||
border-top: 1px solid #d8dadd;
|
||||
|
||||
.o_fp_inputs_table_wrap {
|
||||
margin-top: .5rem;
|
||||
}
|
||||
.o_fp_inputs_table {
|
||||
font-size: .85rem;
|
||||
margin-bottom: .25rem;
|
||||
th { font-weight: 500; }
|
||||
td { vertical-align: middle; }
|
||||
}
|
||||
.o_fp_inputs_actions {
|
||||
display: flex;
|
||||
gap: .5rem;
|
||||
margin-top: .25rem;
|
||||
}
|
||||
}
|
||||
|
||||
.o_fp_edit_actions {
|
||||
display: flex;
|
||||
gap: .5rem;
|
||||
|
||||
@@ -71,6 +71,13 @@
|
||||
t-if="step.tank_ids and step.tank_ids.length">
|
||||
<t t-esc="step.tank_ids.length"/> stations
|
||||
</span>
|
||||
<span class="badge ms-1"
|
||||
t-att-class="step.measurements_badge_class"
|
||||
t-if="step.measurements_badge_text"
|
||||
title="Measurement collection state">
|
||||
<i class="fa fa-clipboard"/>
|
||||
<t t-esc="step.measurements_badge_text"/>
|
||||
</span>
|
||||
<button class="o_fp_step_edit"
|
||||
title="Edit name & instructions"
|
||||
t-on-click="() => this.onToggleEdit(step.id)">
|
||||
@@ -102,6 +109,113 @@
|
||||
Shown to operators when running this step at the tank. Use line breaks for separate points.
|
||||
</p>
|
||||
</div>
|
||||
<!-- Sub 12d — Measurements config -->
|
||||
<div class="o_fp_edit_field o_fp_measurements_config">
|
||||
<label>
|
||||
<input type="checkbox"
|
||||
t-att-checked="step.collect_measurements"
|
||||
t-on-change="(ev) => this.onToggleStepCollect(step.id, ev.target.checked)"/>
|
||||
<strong> Collect measurements at this step</strong>
|
||||
</label>
|
||||
<p class="o_fp_edit_hint">
|
||||
Master switch. When off, the operator wizard skips this step entirely
|
||||
(no input prompts shown at runtime).
|
||||
</p>
|
||||
|
||||
<div t-if="step.collect_measurements"
|
||||
class="o_fp_inputs_table_wrap">
|
||||
<table class="table table-sm o_fp_inputs_table"
|
||||
t-if="step.inputs and step.inputs.length">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:60px;">Collect</th>
|
||||
<th>Prompt</th>
|
||||
<th style="width:160px;">Type</th>
|
||||
<th style="width:90px;">Min</th>
|
||||
<th style="width:90px;">Max</th>
|
||||
<th style="width:60px;">Req</th>
|
||||
<th style="width:36px;"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr t-foreach="step.inputs" t-as="inp" t-key="inp.id">
|
||||
<td>
|
||||
<input type="checkbox"
|
||||
t-att-checked="inp.collect"
|
||||
t-on-change="(ev) => this.onToggleInputCollect(inp.id, ev.target.checked)"/>
|
||||
</td>
|
||||
<td>
|
||||
<input type="text" class="form-control form-control-sm"
|
||||
t-att-value="inp.name"
|
||||
t-on-blur="(ev) => this.onInputBlur(inp.id, 'name', ev)"/>
|
||||
<small t-if="inp.from_library"
|
||||
class="text-muted"
|
||||
title="From library template — toggle Collect off instead of deleting">
|
||||
from library
|
||||
</small>
|
||||
</td>
|
||||
<td>
|
||||
<select class="form-select form-select-sm"
|
||||
t-on-change="(ev) => this.onEditInputField(inp.id, 'input_type', ev.target.value)">
|
||||
<option value="text" t-att-selected="inp.input_type === 'text'">Text</option>
|
||||
<option value="number" t-att-selected="inp.input_type === 'number'">Number</option>
|
||||
<option value="boolean" t-att-selected="inp.input_type === 'boolean'">Yes/No</option>
|
||||
<option value="selection" t-att-selected="inp.input_type === 'selection'">Selection</option>
|
||||
<option value="date" t-att-selected="inp.input_type === 'date'">Date / Time</option>
|
||||
<option value="signature" t-att-selected="inp.input_type === 'signature'">Signature</option>
|
||||
<option value="time_hms" t-att-selected="inp.input_type === 'time_hms'">Time (HH:MM:SS)</option>
|
||||
<option value="time_seconds" t-att-selected="inp.input_type === 'time_seconds'">Time (sec)</option>
|
||||
<option value="temperature" t-att-selected="inp.input_type === 'temperature'">Temperature</option>
|
||||
<option value="thickness" t-att-selected="inp.input_type === 'thickness'">Thickness</option>
|
||||
<option value="pass_fail" t-att-selected="inp.input_type === 'pass_fail'">Pass / Fail</option>
|
||||
<option value="photo" t-att-selected="inp.input_type === 'photo'">Photo</option>
|
||||
<option value="multi_point_thickness" t-att-selected="inp.input_type === 'multi_point_thickness'">Multi-Point Thickness</option>
|
||||
<option value="bath_chemistry_panel" t-att-selected="inp.input_type === 'bath_chemistry_panel'">Bath Chemistry Panel</option>
|
||||
<option value="ph" t-att-selected="inp.input_type === 'ph'">pH</option>
|
||||
</select>
|
||||
</td>
|
||||
<td>
|
||||
<input type="number" step="any" class="form-control form-control-sm"
|
||||
t-att-value="inp.target_min"
|
||||
t-on-blur="(ev) => this.onInputBlur(inp.id, 'target_min', ev)"/>
|
||||
</td>
|
||||
<td>
|
||||
<input type="number" step="any" class="form-control form-control-sm"
|
||||
t-att-value="inp.target_max"
|
||||
t-on-blur="(ev) => this.onInputBlur(inp.id, 'target_max', ev)"/>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<input type="checkbox"
|
||||
t-att-checked="inp.required"
|
||||
t-on-change="(ev) => this.onEditInputField(inp.id, 'required', ev.target.checked)"/>
|
||||
</td>
|
||||
<td>
|
||||
<button t-if="!inp.from_library"
|
||||
class="btn btn-link btn-sm text-danger p-0"
|
||||
title="Remove custom prompt"
|
||||
t-on-click="() => this.onRemoveCustomInput(inp.id)">×</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p t-if="!step.inputs or !step.inputs.length"
|
||||
class="text-muted">
|
||||
No measurement prompts on this step yet.
|
||||
</p>
|
||||
<div class="o_fp_inputs_actions">
|
||||
<button class="btn btn-link btn-sm"
|
||||
t-on-click="() => this.onAddCustomInput(step.id)">
|
||||
<i class="fa fa-plus"/> Add custom prompt
|
||||
</button>
|
||||
<button t-if="step.source_template_id"
|
||||
class="btn btn-link btn-sm"
|
||||
t-on-click="() => this.onResetToLibrary(step.id)">
|
||||
<i class="fa fa-refresh"/> Reset to library defaults
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="o_fp_edit_actions">
|
||||
<button class="btn btn-primary btn-sm"
|
||||
t-on-click="() => this.onSaveStep()">
|
||||
|
||||
Reference in New Issue
Block a user