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:
@@ -5,7 +5,7 @@
|
||||
|
||||
{
|
||||
'name': 'Fusion Plating',
|
||||
'version': '19.0.18.12.0',
|
||||
'version': '19.0.18.12.1',
|
||||
'category': 'Manufacturing/Plating',
|
||||
'summary': 'Core plating / metal finishing ERP: facilities, processes, tanks, baths, jobs, operators.',
|
||||
'description': """
|
||||
|
||||
@@ -53,7 +53,7 @@ class FpRecipeController(http.Controller):
|
||||
'name': name,
|
||||
'node_type': node_type,
|
||||
'parent_id': parent.id,
|
||||
'sequence': max_seq + 10,
|
||||
'sequence': max_seq + 1,
|
||||
}
|
||||
if vals:
|
||||
data.update(vals)
|
||||
@@ -132,7 +132,7 @@ class FpRecipeController(http.Controller):
|
||||
Node = request.env['fusion.plating.process.node']
|
||||
try:
|
||||
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}
|
||||
except Exception as exc:
|
||||
_logger.exception('Recipe reorder failed')
|
||||
@@ -195,7 +195,7 @@ class FpRecipeController(http.Controller):
|
||||
if a_seq == b_seq:
|
||||
# Sequences collided — renumber everyone cleanly, then swap
|
||||
for i, s in enumerate(siblings, 1):
|
||||
s.sequence = i * 10
|
||||
s.sequence = i
|
||||
a_seq, b_seq = node.sequence, other.sequence
|
||||
node.sequence, other.sequence = b_seq, a_seq
|
||||
return {'ok': True}
|
||||
@@ -260,7 +260,7 @@ class FpRecipeController(http.Controller):
|
||||
vals['sequence'] = base_seq
|
||||
new_node = Node.create(vals)
|
||||
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
|
||||
|
||||
# Phase 1 — create every copied top-level child, tracking their
|
||||
@@ -308,8 +308,8 @@ class FpRecipeController(http.Controller):
|
||||
+ existing_top[anchor_idx:]
|
||||
)
|
||||
for i, node in enumerate(final_order, 1):
|
||||
if node.sequence != i * 10:
|
||||
node.sequence = i * 10
|
||||
if node.sequence != i:
|
||||
node.sequence = i
|
||||
|
||||
return {
|
||||
'ok': True,
|
||||
@@ -341,7 +341,7 @@ class FpRecipeController(http.Controller):
|
||||
max_seq = max((c.sequence for c in parent.child_ids), default=0)
|
||||
node.write({
|
||||
'parent_id': parent.id,
|
||||
'sequence': max_seq + 10,
|
||||
'sequence': max_seq + 1,
|
||||
})
|
||||
return {'ok': True}
|
||||
except Exception as exc:
|
||||
|
||||
@@ -271,7 +271,7 @@ class SimpleRecipeController(http.Controller):
|
||||
'template_id': tpl.id,
|
||||
'name': (payload or {}).get('name') or 'New Prompt',
|
||||
'input_type': (payload or {}).get('input_type') or 'text',
|
||||
'sequence': existing_max + 10,
|
||||
'sequence': existing_max + 1,
|
||||
'required': bool((payload or {}).get('required')),
|
||||
})
|
||||
return {'ok': True, 'input_id': rec.id,
|
||||
@@ -356,25 +356,26 @@ class SimpleRecipeController(http.Controller):
|
||||
return {'id': new_node.id, 'sequence': new_node.sequence}
|
||||
|
||||
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')
|
||||
if not siblings:
|
||||
return 10
|
||||
if position >= len(siblings):
|
||||
return siblings[-1].sequence + 10
|
||||
if position <= 0:
|
||||
return max(1, siblings[0].sequence - 10)
|
||||
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.
|
||||
return 1
|
||||
pos = max(0, min(position, len(siblings)))
|
||||
# Make room: siblings before `pos` keep their 1-based index;
|
||||
# siblings at or after `pos` shift up by one so the new step
|
||||
# lands at sequence (pos + 1).
|
||||
for idx, sib in enumerate(siblings):
|
||||
new_seq = (idx + 1) * 10
|
||||
if sib.sequence != new_seq:
|
||||
sib.sequence = new_seq
|
||||
return position * 10 + 5
|
||||
target = idx + 1 if idx < pos else idx + 2
|
||||
if sib.sequence != target:
|
||||
sib.sequence = target
|
||||
return pos + 1
|
||||
|
||||
def _copy_inputs_from_template(self, tpl, new_node):
|
||||
NodeInput = request.env['fusion.plating.process.node.input']
|
||||
@@ -412,7 +413,7 @@ class SimpleRecipeController(http.Controller):
|
||||
def step_reorder(self, node_ids):
|
||||
Node = request.env['fusion.plating.process.node']
|
||||
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}
|
||||
|
||||
# -------------------------------------------------------------- template
|
||||
@@ -521,7 +522,7 @@ class SimpleRecipeController(http.Controller):
|
||||
'input_type': (payload or {}).get('input_type') or 'text',
|
||||
'kind': 'step_input',
|
||||
'collect': True,
|
||||
'sequence': existing_max + 10,
|
||||
'sequence': existing_max + 1,
|
||||
'required': bool((payload or {}).get('required')),
|
||||
})
|
||||
return {'ok': True, 'input_id': rec.id}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
{
|
||||
"name": "Fusion Plating — MRP Bridge",
|
||||
'version': '19.0.13.0.0',
|
||||
'version': '19.0.13.0.1',
|
||||
'category': 'Manufacturing/Plating',
|
||||
'summary': 'Bridge Fusion Plating facilities, baths and tanks to Odoo MRP work orders.',
|
||||
'description': """
|
||||
|
||||
@@ -572,7 +572,7 @@ class MrpProduction(models.Model):
|
||||
# Walk tree and collect operation WO values
|
||||
wo_vals_list = []
|
||||
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):
|
||||
"""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)
|
||||
if 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'):
|
||||
# Container nodes — recurse into children
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
{
|
||||
'name': 'Fusion Plating — Native Jobs',
|
||||
'version': '19.0.8.17.3',
|
||||
'version': '19.0.8.17.4',
|
||||
'category': 'Manufacturing/Plating',
|
||||
'summary': 'Native plating job model — replaces mrp.production / mrp.workorder bridge.',
|
||||
'author': 'Nexa Systems Inc.',
|
||||
|
||||
@@ -606,7 +606,12 @@ class FpJob(models.Model):
|
||||
|
||||
step_vals_list = []
|
||||
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):
|
||||
"""Determine if a node should be included based on
|
||||
@@ -759,7 +764,7 @@ class FpJob(models.Model):
|
||||
step_vals_list.append(vals)
|
||||
if 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'):
|
||||
for child in node.child_ids.sorted('sequence'):
|
||||
|
||||
Reference in New Issue
Block a user