changes
This commit is contained in:
@@ -37,6 +37,28 @@ _CLONABLE_FIELDS = (
|
||||
)
|
||||
|
||||
|
||||
def _list_variants(part):
|
||||
"""Return a list of {id, label, is_default, node_count} for a part's variants."""
|
||||
Node = part.env['fusion.plating.process.node']
|
||||
variants = part.process_variant_ids.sorted(
|
||||
lambda v: (not v.is_default_variant, v.variant_label or v.name or '')
|
||||
)
|
||||
out = []
|
||||
for v in variants:
|
||||
node_count = Node.search_count([
|
||||
('part_catalog_id', '=', part.id),
|
||||
('id', 'child_of', v.id),
|
||||
])
|
||||
out.append({
|
||||
'id': v.id,
|
||||
'label': v.variant_label or v.name or '(unnamed)',
|
||||
'name': v.name or '',
|
||||
'is_default': bool(v.is_default_variant),
|
||||
'node_count': node_count,
|
||||
})
|
||||
return out
|
||||
|
||||
|
||||
def _clone_subtree(env, source, part, parent):
|
||||
"""Recursively clone a process node subtree for a specific part.
|
||||
|
||||
@@ -117,7 +139,7 @@ class FpPartComposerController(http.Controller):
|
||||
# ------------------------------------------------------------------
|
||||
@http.route('/fp/part/composer/state', type='jsonrpc', auth='user')
|
||||
def state(self, part_id):
|
||||
"""Return part info plus the current default_process_id tree (or None)."""
|
||||
"""Return part info, current default tree, and full variant list."""
|
||||
part = request.env['fp.part.catalog'].browse(int(part_id)).exists()
|
||||
if not part:
|
||||
return {'ok': False, 'error': 'Part not found'}
|
||||
@@ -134,6 +156,7 @@ class FpPartComposerController(http.Controller):
|
||||
},
|
||||
'has_tree': bool(root),
|
||||
'root_id': root.id if root else False,
|
||||
'variants': _list_variants(part),
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
@@ -157,18 +180,20 @@ class FpPartComposerController(http.Controller):
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Write — clone a template into the part
|
||||
# Write — create a new variant by cloning a template OR another variant
|
||||
# ------------------------------------------------------------------
|
||||
@http.route('/fp/part/composer/load_template', type='jsonrpc', auth='user')
|
||||
def load_template(self, part_id, template_id):
|
||||
"""Clone a shared template into a part-scoped tree.
|
||||
def load_template(self, part_id, template_id, variant_label=None,
|
||||
make_default=None):
|
||||
"""Clone a shared template into a NEW variant on this part.
|
||||
|
||||
Deletes any existing part-owned tree for this part first, then
|
||||
deep-clones the template subtree with part ownership set. Finally
|
||||
pins ``part.default_process_id`` to the new root.
|
||||
Unlike the previous behaviour (wipe & replace), this now adds a
|
||||
variant alongside any existing ones. The first variant created
|
||||
becomes the default; subsequent variants only become default if
|
||||
``make_default`` is true.
|
||||
|
||||
The whole operation runs inside a savepoint — if anything fails
|
||||
partway through, the part is left in its previous state.
|
||||
If ``variant_label`` is omitted, the controller uses the
|
||||
template's name as the label.
|
||||
"""
|
||||
part = request.env['fp.part.catalog'].browse(int(part_id)).exists()
|
||||
tpl = request.env['fusion.plating.process.node'].browse(int(template_id)).exists()
|
||||
@@ -181,38 +206,119 @@ class FpPartComposerController(http.Controller):
|
||||
if tpl.node_type != 'recipe':
|
||||
return {'ok': False, 'error': 'Template must be a recipe-type node'}
|
||||
|
||||
label = (variant_label or tpl.name or 'Variant').strip()
|
||||
|
||||
try:
|
||||
with request.env.cr.savepoint():
|
||||
# 1. Delete any prior part-owned tree for this part.
|
||||
# parent_id has ondelete='cascade', so deleting root(s)
|
||||
# wipes their descendants. Use search so we don't assume
|
||||
# only default_process_id's tree exists.
|
||||
prior = request.env['fusion.plating.process.node'].search([
|
||||
('part_catalog_id', '=', part.id),
|
||||
])
|
||||
if prior:
|
||||
prior.unlink()
|
||||
# First variant on this part is always the default.
|
||||
is_first = not part.process_variant_ids
|
||||
make_default_flag = bool(make_default) or is_first
|
||||
|
||||
# 2. Deep-clone the template subtree with part ownership.
|
||||
new_root = _clone_subtree(request.env, tpl, part, parent=False)
|
||||
new_root.variant_label = label
|
||||
new_root.is_default_variant = make_default_flag
|
||||
|
||||
# 3. Pin part.default_process_id to the new root.
|
||||
part.default_process_id = new_root.id
|
||||
if make_default_flag:
|
||||
# Clear flag from any other variants and pin default_process_id.
|
||||
others = part.process_variant_ids.filtered(
|
||||
lambda v: v.id != new_root.id and v.is_default_variant
|
||||
)
|
||||
if others:
|
||||
others.write({'is_default_variant': False})
|
||||
part.default_process_id = new_root.id
|
||||
|
||||
node_count = request.env['fusion.plating.process.node'].search_count([
|
||||
('part_catalog_id', '=', part.id),
|
||||
('id', 'child_of', new_root.id),
|
||||
])
|
||||
|
||||
_logger.info(
|
||||
'Part Composer: cloned template %s (%s) → part %s (%s), %s nodes, by uid %s',
|
||||
tpl.id, tpl.name, part.id, part.display_name,
|
||||
node_count, request.env.uid,
|
||||
'Part Composer: variant "%s" cloned from template %s onto part %s (default=%s, %s nodes), uid %s',
|
||||
label, tpl.id, part.id, make_default_flag, node_count, request.env.uid,
|
||||
)
|
||||
return {
|
||||
'ok': True,
|
||||
'root_id': new_root.id,
|
||||
'node_count': node_count,
|
||||
'variants': _list_variants(part),
|
||||
}
|
||||
except Exception as exc:
|
||||
_logger.exception('Part Composer load_template failed')
|
||||
return {'ok': False, 'error': str(exc)}
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Variant CRUD
|
||||
# ------------------------------------------------------------------
|
||||
@http.route('/fp/part/composer/duplicate_variant', type='jsonrpc', auth='user')
|
||||
def duplicate_variant(self, part_id, source_variant_id, variant_label=None):
|
||||
"""Deep-copy an existing variant into a new variant on the same part."""
|
||||
part = request.env['fp.part.catalog'].browse(int(part_id)).exists()
|
||||
src = request.env['fusion.plating.process.node'].browse(int(source_variant_id)).exists()
|
||||
if not part:
|
||||
return {'ok': False, 'error': 'Part not found'}
|
||||
if not src or src.part_catalog_id.id != part.id or src.parent_id:
|
||||
return {'ok': False, 'error': 'Invalid source variant'}
|
||||
|
||||
label = (variant_label or ((src.variant_label or src.name or 'Variant') + ' (copy)')).strip()
|
||||
try:
|
||||
with request.env.cr.savepoint():
|
||||
new_root = _clone_subtree(request.env, src, part, parent=False)
|
||||
new_root.variant_label = label
|
||||
new_root.is_default_variant = False # never auto-default a duplicate
|
||||
node_count = request.env['fusion.plating.process.node'].search_count([
|
||||
('id', 'child_of', new_root.id),
|
||||
])
|
||||
return {
|
||||
'ok': True,
|
||||
'root_id': new_root.id,
|
||||
'node_count': node_count,
|
||||
'variants': _list_variants(part),
|
||||
}
|
||||
except Exception as exc:
|
||||
_logger.exception('Part Composer duplicate_variant failed')
|
||||
return {'ok': False, 'error': str(exc)}
|
||||
|
||||
@http.route('/fp/part/composer/rename_variant', type='jsonrpc', auth='user')
|
||||
def rename_variant(self, part_id, variant_id, variant_label):
|
||||
part = request.env['fp.part.catalog'].browse(int(part_id)).exists()
|
||||
v = request.env['fusion.plating.process.node'].browse(int(variant_id)).exists()
|
||||
if not part:
|
||||
return {'ok': False, 'error': 'Part not found'}
|
||||
if not v or v.part_catalog_id.id != part.id or v.parent_id:
|
||||
return {'ok': False, 'error': 'Invalid variant'}
|
||||
label = (variant_label or '').strip()
|
||||
if not label:
|
||||
return {'ok': False, 'error': 'Label cannot be empty'}
|
||||
v.variant_label = label
|
||||
return {'ok': True, 'variants': _list_variants(part)}
|
||||
|
||||
@http.route('/fp/part/composer/set_default_variant', type='jsonrpc', auth='user')
|
||||
def set_default_variant(self, part_id, variant_id):
|
||||
part = request.env['fp.part.catalog'].browse(int(part_id)).exists()
|
||||
if not part:
|
||||
return {'ok': False, 'error': 'Part not found'}
|
||||
ok = part.action_set_default_variant(int(variant_id))
|
||||
if not ok:
|
||||
return {'ok': False, 'error': 'Variant does not belong to this part'}
|
||||
return {'ok': True, 'variants': _list_variants(part)}
|
||||
|
||||
@http.route('/fp/part/composer/delete_variant', type='jsonrpc', auth='user')
|
||||
def delete_variant(self, part_id, variant_id):
|
||||
part = request.env['fp.part.catalog'].browse(int(part_id)).exists()
|
||||
v = request.env['fusion.plating.process.node'].browse(int(variant_id)).exists()
|
||||
if not part:
|
||||
return {'ok': False, 'error': 'Part not found'}
|
||||
if not v or v.part_catalog_id.id != part.id or v.parent_id:
|
||||
return {'ok': False, 'error': 'Invalid variant'}
|
||||
if v.is_default_variant and len(part.process_variant_ids) > 1:
|
||||
return {'ok': False,
|
||||
'error': 'Cannot delete the default variant. Set another variant as default first.'}
|
||||
try:
|
||||
with request.env.cr.savepoint():
|
||||
if part.default_process_id.id == v.id:
|
||||
part.default_process_id = False
|
||||
# ondelete=cascade on parent_id wipes descendants.
|
||||
v.unlink()
|
||||
return {'ok': True, 'variants': _list_variants(part)}
|
||||
except Exception as exc:
|
||||
_logger.exception('Part Composer delete_variant failed')
|
||||
return {'ok': False, 'error': str(exc)}
|
||||
|
||||
Reference in New Issue
Block a user