feat(jobs): step sequences are 1, 2, 3, ... not 10, 20, 30, ...

User feedback: operators kept asking why their work order said "Step 10"
for the first row. The 10-spacing was originally there to allow midpoint
inserts (insert sequence 15 between 10 and 20 without renumbering).
Tradeoff is operator confusion, and recipe authors rarely insert in the
middle anyway. Switching to 1-based contiguous sequences.

Files changed (every step-sequence allocation in the codebase):

fusion_plating_jobs/models/fp_job.py
  _generate_steps_from_recipe — seq_counter starts at 1, increments by 1.
  This is the path that builds fp.job.step records, so new jobs now show
  Step 1, 2, 3, ... in the work order.

fusion_plating_bridge_mrp/models/mrp_production.py
  Same change for the legacy MRP bridge so customers still on
  mrp.production also get 1-based numbering.

fusion_plating/controllers/recipe_controller.py
  - create_node: max_seq + 1
  - reorder_nodes: idx + 1
  - swap renumber: i (was i * 10)
  - paste-import renumber: i (was i * 10)
  - move_node: max_seq + 1
  - _copy_subtree (recipe duplicate/import): i (was i * 10)

fusion_plating/controllers/simple_recipe_controller.py
  - _sequence_for_position rewritten — always renumbers siblings to
    keep them contiguous. Returns pos + 1 for the inserted node.
    Old code used midpoint-with-fallback-to-renumber (10/20/30 spacing).
  - step_reorder: i (was i * 10)
  - library_input_add + step_add_input: existing_max + 1

What this DOESN'T do
  Existing fp.job.step records keep their old sequences (10, 20, ...).
  Re-confirm the SO to spawn a fresh job if you want the clean 1-based
  numbering on a current test job. No data migration — we're in dev
  and the user explicitly said test data is disposable.

What this DOES do
  Every NEW job created from this commit forward shows Step 1, 2, 3, ...
  Every NEW recipe step inserted via the simple editor / tree editor
  also gets sequence 1, 2, 3, ...

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsinghpal
2026-05-03 22:58:21 -04:00
parent e37eab9f23
commit 32d48ea44d
7 changed files with 39 additions and 33 deletions

View File

@@ -5,7 +5,7 @@
{ {
'name': 'Fusion Plating', 'name': 'Fusion Plating',
'version': '19.0.18.12.0', 'version': '19.0.18.12.1',
'category': 'Manufacturing/Plating', 'category': 'Manufacturing/Plating',
'summary': 'Core plating / metal finishing ERP: facilities, processes, tanks, baths, jobs, operators.', 'summary': 'Core plating / metal finishing ERP: facilities, processes, tanks, baths, jobs, operators.',
'description': """ 'description': """

View File

@@ -53,7 +53,7 @@ class FpRecipeController(http.Controller):
'name': name, 'name': name,
'node_type': node_type, 'node_type': node_type,
'parent_id': parent.id, 'parent_id': parent.id,
'sequence': max_seq + 10, 'sequence': max_seq + 1,
} }
if vals: if vals:
data.update(vals) data.update(vals)
@@ -132,7 +132,7 @@ class FpRecipeController(http.Controller):
Node = request.env['fusion.plating.process.node'] Node = request.env['fusion.plating.process.node']
try: try:
for idx, nid in enumerate(node_ids): for idx, nid in enumerate(node_ids):
Node.browse(int(nid)).write({'sequence': (idx + 1) * 10}) Node.browse(int(nid)).write({'sequence': idx + 1})
return {'ok': True} return {'ok': True}
except Exception as exc: except Exception as exc:
_logger.exception('Recipe reorder failed') _logger.exception('Recipe reorder failed')
@@ -195,7 +195,7 @@ class FpRecipeController(http.Controller):
if a_seq == b_seq: if a_seq == b_seq:
# Sequences collided — renumber everyone cleanly, then swap # Sequences collided — renumber everyone cleanly, then swap
for i, s in enumerate(siblings, 1): for i, s in enumerate(siblings, 1):
s.sequence = i * 10 s.sequence = i
a_seq, b_seq = node.sequence, other.sequence a_seq, b_seq = node.sequence, other.sequence
node.sequence, other.sequence = b_seq, a_seq node.sequence, other.sequence = b_seq, a_seq
return {'ok': True} return {'ok': True}
@@ -260,7 +260,7 @@ class FpRecipeController(http.Controller):
vals['sequence'] = base_seq vals['sequence'] = base_seq
new_node = Node.create(vals) new_node = Node.create(vals)
for i, child in enumerate(src_node.child_ids.sorted('sequence'), 1): for i, child in enumerate(src_node.child_ids.sorted('sequence'), 1):
_copy_subtree(child, new_node, i * 10) _copy_subtree(child, new_node, i)
return new_node return new_node
# Phase 1 — create every copied top-level child, tracking their # Phase 1 — create every copied top-level child, tracking their
@@ -308,8 +308,8 @@ class FpRecipeController(http.Controller):
+ existing_top[anchor_idx:] + existing_top[anchor_idx:]
) )
for i, node in enumerate(final_order, 1): for i, node in enumerate(final_order, 1):
if node.sequence != i * 10: if node.sequence != i:
node.sequence = i * 10 node.sequence = i
return { return {
'ok': True, 'ok': True,
@@ -341,7 +341,7 @@ class FpRecipeController(http.Controller):
max_seq = max((c.sequence for c in parent.child_ids), default=0) max_seq = max((c.sequence for c in parent.child_ids), default=0)
node.write({ node.write({
'parent_id': parent.id, 'parent_id': parent.id,
'sequence': max_seq + 10, 'sequence': max_seq + 1,
}) })
return {'ok': True} return {'ok': True}
except Exception as exc: except Exception as exc:

View File

@@ -271,7 +271,7 @@ class SimpleRecipeController(http.Controller):
'template_id': tpl.id, 'template_id': tpl.id,
'name': (payload or {}).get('name') or 'New Prompt', 'name': (payload or {}).get('name') or 'New Prompt',
'input_type': (payload or {}).get('input_type') or 'text', 'input_type': (payload or {}).get('input_type') or 'text',
'sequence': existing_max + 10, 'sequence': existing_max + 1,
'required': bool((payload or {}).get('required')), 'required': bool((payload or {}).get('required')),
}) })
return {'ok': True, 'input_id': rec.id, return {'ok': True, 'input_id': rec.id,
@@ -356,25 +356,26 @@ class SimpleRecipeController(http.Controller):
return {'id': new_node.id, 'sequence': new_node.sequence} return {'id': new_node.id, 'sequence': new_node.sequence}
def _sequence_for_position(self, recipe, position): def _sequence_for_position(self, recipe, position):
"""Return the sequence value for a NEW step inserted at
`position` among the recipe's existing children.
Always renumbers existing siblings so the result is contiguous
1, 2, 3, ... matching what the operator sees on the work order.
(Pre-Sub 13c we used 10-spacing to allow midpoint inserts —
operators kept asking why their first step said "Step 10".)
"""
siblings = recipe.child_ids.sorted('sequence') siblings = recipe.child_ids.sorted('sequence')
if not siblings: if not siblings:
return 10 return 1
if position >= len(siblings): pos = max(0, min(position, len(siblings)))
return siblings[-1].sequence + 10 # Make room: siblings before `pos` keep their 1-based index;
if position <= 0: # siblings at or after `pos` shift up by one so the new step
return max(1, siblings[0].sequence - 10) # lands at sequence (pos + 1).
before = siblings[position - 1].sequence
after = siblings[position].sequence
if after - before > 1:
return (before + after) // 2
# Sequences are tightly packed (gap == 1 → midpoint == after,
# which collides). Renumber siblings to 10/20/30… first, then
# the new step lands cleanly between renumbered neighbours.
for idx, sib in enumerate(siblings): for idx, sib in enumerate(siblings):
new_seq = (idx + 1) * 10 target = idx + 1 if idx < pos else idx + 2
if sib.sequence != new_seq: if sib.sequence != target:
sib.sequence = new_seq sib.sequence = target
return position * 10 + 5 return pos + 1
def _copy_inputs_from_template(self, tpl, new_node): def _copy_inputs_from_template(self, tpl, new_node):
NodeInput = request.env['fusion.plating.process.node.input'] NodeInput = request.env['fusion.plating.process.node.input']
@@ -412,7 +413,7 @@ class SimpleRecipeController(http.Controller):
def step_reorder(self, node_ids): def step_reorder(self, node_ids):
Node = request.env['fusion.plating.process.node'] Node = request.env['fusion.plating.process.node']
for i, nid in enumerate(node_ids, start=1): for i, nid in enumerate(node_ids, start=1):
Node.browse(nid).write({'sequence': i * 10}) Node.browse(nid).write({'sequence': i})
return {'ok': True} return {'ok': True}
# -------------------------------------------------------------- template # -------------------------------------------------------------- template
@@ -521,7 +522,7 @@ class SimpleRecipeController(http.Controller):
'input_type': (payload or {}).get('input_type') or 'text', 'input_type': (payload or {}).get('input_type') or 'text',
'kind': 'step_input', 'kind': 'step_input',
'collect': True, 'collect': True,
'sequence': existing_max + 10, 'sequence': existing_max + 1,
'required': bool((payload or {}).get('required')), 'required': bool((payload or {}).get('required')),
}) })
return {'ok': True, 'input_id': rec.id} return {'ok': True, 'input_id': rec.id}

View File

@@ -5,7 +5,7 @@
{ {
"name": "Fusion Plating — MRP Bridge", "name": "Fusion Plating — MRP Bridge",
'version': '19.0.13.0.0', 'version': '19.0.13.0.1',
'category': 'Manufacturing/Plating', 'category': 'Manufacturing/Plating',
'summary': 'Bridge Fusion Plating facilities, baths and tanks to Odoo MRP work orders.', 'summary': 'Bridge Fusion Plating facilities, baths and tanks to Odoo MRP work orders.',
'description': """ 'description': """

View File

@@ -572,7 +572,7 @@ class MrpProduction(models.Model):
# Walk tree and collect operation WO values # Walk tree and collect operation WO values
wo_vals_list = [] wo_vals_list = []
wo_steps = {} # {sequence: instruction text} — posted to WO chatter after create wo_steps = {} # {sequence: instruction text} — posted to WO chatter after create
seq_counter = [10] # mutable for closure, increments by 10 seq_counter = [1] # mutable for closure, increments by 1
def _is_node_included(node): def _is_node_included(node):
"""Determine if a node should be included based on opt-in/out """Determine if a node should be included based on opt-in/out
@@ -695,7 +695,7 @@ class MrpProduction(models.Model):
wo_vals_list.append(vals) wo_vals_list.append(vals)
if steps: if steps:
wo_steps[seq_counter[0]] = '\n'.join(steps) wo_steps[seq_counter[0]] = '\n'.join(steps)
seq_counter[0] += 10 seq_counter[0] += 1
elif node.node_type in ('recipe', 'sub_process'): elif node.node_type in ('recipe', 'sub_process'):
# Container nodes — recurse into children # Container nodes — recurse into children

View File

@@ -3,7 +3,7 @@
# License OPL-1 (Odoo Proprietary License v1.0) # License OPL-1 (Odoo Proprietary License v1.0)
{ {
'name': 'Fusion Plating — Native Jobs', 'name': 'Fusion Plating — Native Jobs',
'version': '19.0.8.17.3', 'version': '19.0.8.17.4',
'category': 'Manufacturing/Plating', 'category': 'Manufacturing/Plating',
'summary': 'Native plating job model — replaces mrp.production / mrp.workorder bridge.', 'summary': 'Native plating job model — replaces mrp.production / mrp.workorder bridge.',
'author': 'Nexa Systems Inc.', 'author': 'Nexa Systems Inc.',

View File

@@ -606,7 +606,12 @@ class FpJob(models.Model):
step_vals_list = [] step_vals_list = []
wo_steps = {} # {sequence: instruction text} wo_steps = {} # {sequence: instruction text}
seq_counter = [10] # Sequences increment by 1 (operator-friendly: Step 1, 2, 3,
# ...) instead of the legacy 10/20/30 spacing. The 10-spacing
# was originally there to allow midpoint inserts, but
# operators kept asking why their work order said "Step 10"
# for the first row.
seq_counter = [1]
def _is_node_included(node): def _is_node_included(node):
"""Determine if a node should be included based on """Determine if a node should be included based on
@@ -759,7 +764,7 @@ class FpJob(models.Model):
step_vals_list.append(vals) step_vals_list.append(vals)
if instructions: if instructions:
wo_steps[seq_counter[0]] = '\n'.join(instructions) wo_steps[seq_counter[0]] = '\n'.join(instructions)
seq_counter[0] += 10 seq_counter[0] += 1
elif node.node_type in ('recipe', 'sub_process'): elif node.node_type in ('recipe', 'sub_process'):
for child in node.child_ids.sorted('sequence'): for child in node.child_ids.sorted('sequence'):