fix(simple-editor): stop seed resurrection + add promote/demote + drag substeps
Three bugs reported on 2026-05-20:
1. RESURRECTION. User deletes a substep in the Simple Editor (e.g.
Soak Clean (S-3) under Cleaner), then on the next -u fusion_plating
the substep comes back. Root cause: the recipe XML lived in the
manifest's `data` list with `noupdate="1"`. Odoo's noupdate=1 only
blocks UPDATE of existing records — when a record's ir.model.data
row is missing, the loader treats it as "not yet created" and
re-creates from XML. Every upgrade resurrected every user-deleted
seed node.
Fix: pull the recipe XML files out of `data` and load them once
via post_init_hook → _seed_starter_recipes_once. Sentinel checks
ir.model.data for each recipe's root xmlid; if present, skip
loading entirely. Result: deletions are permanent across all
future upgrades. Existing entech recipes untouched.
Files affected: fp_recipe_enp_alum_basic, fp_recipe_enp_steel_basic,
fp_recipe_enp_sp, fp_recipe_general_processing, fp_recipe_anodize,
fp_recipe_chem_conversion.
2. PROMOTE / DEMOTE. Simple Editor had no way to turn a substep into
a top-level operation, or to tuck an operation under another as a
substep. Authors had to delete + re-create. New endpoints:
* /fp/simple_recipe/step/promote → flips node_type 'step' →
'operation', re-parents to the recipe (or sub-process) root,
places right after the old parent operation.
* /fp/simple_recipe/step/demote → flips 'operation' → 'step',
re-parents under the preceding operation (or a caller-supplied
target_op_id). Blocks demoting an operation that has its own
children, with a helpful message.
UI: each row in the editor now carries an up-arrow (promote, only
shown on substeps) and a down-arrow (demote, only shown on
operations). Confirmation dialog explains what's about to happen.
3. DRAG SUBSTEPS. Last commit (2142a66b) disabled drag on substep
rows. Operators couldn't reorder substeps within an operation.
Re-enabled drag on substeps. The step_reorder endpoint now groups
incoming node_ids by parent_id and renumbers within each parent
(10, 20, 30…). Cross-parent drag still no-ops on parent change —
Promote/Demote buttons are the way to move between parents.
Drive-by:
- Added `from odoo import _` to the controller (missing import the
new endpoints surfaced).
- Edit-panel field wiring audited: all fields visible in the screen
(Step name, Default instructions, Step Type, Triggers Workflow,
Parallel Start, QA Sign-off, Collect measurements, Instruction
Images, custom prompts) persist correctly through step_write or
dedicated endpoints. No broken wires.
Tests: 15 total in TestSimpleRecipeFlatten (was 10). 5 new cover
promote happy-path, promote reject (non-substep), demote happy-path,
demote block on has_children, and reorder parent-scoping.
Module: fusion_plating 19.0.20.4.0 → 19.0.20.5.0.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -152,6 +152,61 @@ export class FpSimpleRecipeEditor extends Component {
|
||||
await this.loadAll();
|
||||
}
|
||||
|
||||
// ---- Promote / demote -------------------------------------------------
|
||||
//
|
||||
// Substep → operation: turn a child step into a top-level operation
|
||||
// under the recipe root (or sub-process root if applicable).
|
||||
// Operation → substep: tuck a top-level operation under the
|
||||
// preceding operation as one of its substeps. Handy when the author
|
||||
// realises a "header" should actually live as part of another
|
||||
// operation's workflow.
|
||||
|
||||
async onPromoteStep(stepId) {
|
||||
const proceed = await this._confirm(
|
||||
_t(
|
||||
"Promote this substep to a top-level operation? It will be " +
|
||||
"moved out of its parent operation and placed directly under " +
|
||||
"the recipe."
|
||||
)
|
||||
);
|
||||
if (!proceed) return;
|
||||
const res = await rpc("/fp/simple_recipe/step/promote", {
|
||||
node_id: stepId,
|
||||
});
|
||||
if (!res.ok) {
|
||||
this.notification.add(
|
||||
res.message || _t("Could not promote step."),
|
||||
{ type: "warning" }
|
||||
);
|
||||
return;
|
||||
}
|
||||
await this.loadAll();
|
||||
this.notification.add(_t("Step promoted to operation."), { type: "success" });
|
||||
}
|
||||
|
||||
async onDemoteStep(stepId) {
|
||||
const proceed = await this._confirm(
|
||||
_t(
|
||||
"Demote this operation to a substep under the previous " +
|
||||
"operation? It will be tucked underneath the operation " +
|
||||
"immediately above it in the list."
|
||||
)
|
||||
);
|
||||
if (!proceed) return;
|
||||
const res = await rpc("/fp/simple_recipe/step/demote", {
|
||||
node_id: stepId,
|
||||
});
|
||||
if (!res.ok) {
|
||||
this.notification.add(
|
||||
res.message || _t("Could not demote step."),
|
||||
{ type: "warning" }
|
||||
);
|
||||
return;
|
||||
}
|
||||
await this.loadAll();
|
||||
this.notification.add(_t("Operation demoted to substep."), { type: "success" });
|
||||
}
|
||||
|
||||
async onAddInlineStep() {
|
||||
await rpc("/fp/simple_recipe/step/insert", {
|
||||
recipe_id: this._recipeId,
|
||||
|
||||
@@ -255,6 +255,21 @@ $fp-se-drop: var(--fp-drop-bg, #{$_fp_se_drop_hex});
|
||||
cursor: default;
|
||||
}
|
||||
}
|
||||
.o_fp_step_promote,
|
||||
.o_fp_step_demote {
|
||||
background: none;
|
||||
border: none;
|
||||
color: $fp-se-muted;
|
||||
padding: .2rem .4rem;
|
||||
cursor: pointer;
|
||||
font-size: .85rem;
|
||||
border-radius: 4px;
|
||||
transition: background .12s ease, color .12s ease;
|
||||
&:hover {
|
||||
background: $fp-se-page;
|
||||
color: $fp-se-accent;
|
||||
}
|
||||
}
|
||||
.o_fp_step_edit,
|
||||
.o_fp_step_remove {
|
||||
background: none;
|
||||
|
||||
@@ -69,11 +69,11 @@
|
||||
<t t-foreach="state.steps" t-as="step" t-key="step.id">
|
||||
<div class="o_fp_step_row"
|
||||
t-att-class="(state.editingStepId === step.id ? 'o_fp_step_row_editing ' : '') + (step.is_substep ? 'o_fp_substep_row' : '')"
|
||||
t-att-draggable="step.is_substep ? 'false' : 'true'"
|
||||
t-on-dragstart="(ev) => step.is_substep ? null : this.onSelectedDragStart(step.id, ev)"
|
||||
draggable="true"
|
||||
t-on-dragstart="(ev) => this.onSelectedDragStart(step.id, ev)"
|
||||
t-on-dragover="(ev) => this.onRowDragOver(step_index, ev)">
|
||||
<span class="o_fp_drag_handle" t-if="!step.is_substep">⠿</span>
|
||||
<span class="o_fp_drag_handle o_fp_substep_indent" t-if="step.is_substep">↳</span>
|
||||
<span class="o_fp_drag_handle o_fp_substep_indent" t-if="step.is_substep" title="Drag to reorder among substeps of the same operation">⠿</span>
|
||||
<span class="o_fp_step_position" t-if="!step.is_substep">
|
||||
<t t-esc="step_index + 1"/>.
|
||||
</span>
|
||||
@@ -107,6 +107,18 @@
|
||||
<i class="fa fa-clipboard"/>
|
||||
<t t-esc="step.measurements_badge_text"/>
|
||||
</span>
|
||||
<button class="o_fp_step_promote"
|
||||
t-if="step.is_substep"
|
||||
title="Promote: turn this substep into a top-level operation"
|
||||
t-on-click="() => this.onPromoteStep(step.id)">
|
||||
<i class="fa fa-arrow-up"/>
|
||||
</button>
|
||||
<button class="o_fp_step_demote"
|
||||
t-if="!step.is_substep"
|
||||
title="Demote: tuck this operation under the previous one as a substep"
|
||||
t-on-click="() => this.onDemoteStep(step.id)">
|
||||
<i class="fa fa-arrow-down"/>
|
||||
</button>
|
||||
<button class="o_fp_step_edit"
|
||||
title="Edit name & instructions"
|
||||
t-on-click="() => this.onToggleEdit(step.id)">
|
||||
|
||||
Reference in New Issue
Block a user