feat(configurator): Sub 3 Phase B — part-scoped Process Composer client action + part form Compose button
- Add fp_part_composer_controller with 3 JSON-RPC endpoints:
/fp/part/composer/state, /fp/part/composer/templates,
/fp/part/composer/load_template (deep-clones a shared template
into a part-owned tree inside a cr.savepoint, sets
fp.part.catalog.default_process_id atomically)
- _clone_subtree copies name/sequence/opt_in_out/treatment_uom plus
description/notes/icon/color/timing/behaviour/work_center/process_type
and stamps part_catalog_id + cloned_from_id on every node
- Add fp_part_process_composer OWL client action (JS + XML + SCSS):
picks template from dropdown, clones, hands off to existing
fp_recipe_tree_editor via context={recipe_id, part_id}
- Add Process tab on part form with readonly default_process_id
field and Compose button calling action_open_part_composer
- Register new assets in web.assets_backend, bump configurator
version to 19.0.11.0.0
This commit is contained in:
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
{
|
{
|
||||||
'name': 'Fusion Plating — Configurator',
|
'name': 'Fusion Plating — Configurator',
|
||||||
'version': '19.0.10.0.0',
|
'version': '19.0.11.0.0',
|
||||||
'category': 'Manufacturing/Plating',
|
'category': 'Manufacturing/Plating',
|
||||||
'summary': 'Quotation configurator with part catalog, coating configs, and formula-based pricing engine.',
|
'summary': 'Quotation configurator with part catalog, coating configs, and formula-based pricing engine.',
|
||||||
'description': """
|
'description': """
|
||||||
@@ -64,6 +64,10 @@ Provides:
|
|||||||
'fusion_plating_configurator/static/src/js/fp_drawing_preview.js',
|
'fusion_plating_configurator/static/src/js/fp_drawing_preview.js',
|
||||||
'fusion_plating_configurator/static/src/xml/fp_pdf_inline_preview.xml',
|
'fusion_plating_configurator/static/src/xml/fp_pdf_inline_preview.xml',
|
||||||
'fusion_plating_configurator/static/src/js/fp_pdf_inline_preview.js',
|
'fusion_plating_configurator/static/src/js/fp_pdf_inline_preview.js',
|
||||||
|
# Sub 3 — part-scoped Process Composer
|
||||||
|
'fusion_plating_configurator/static/src/scss/fp_part_process_composer.scss',
|
||||||
|
'fusion_plating_configurator/static/src/xml/fp_part_process_composer.xml',
|
||||||
|
'fusion_plating_configurator/static/src/js/fp_part_process_composer.js',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
'installable': True,
|
'installable': True,
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
from . import configurator_controller
|
from . import configurator_controller
|
||||||
|
from . import fp_part_composer_controller
|
||||||
|
|||||||
@@ -0,0 +1,196 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright 2026 Nexa Systems Inc.
|
||||||
|
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||||
|
# Part of the Fusion Plating product family.
|
||||||
|
#
|
||||||
|
# Sub 3 — part-scoped Process Composer RPC.
|
||||||
|
#
|
||||||
|
# Endpoints:
|
||||||
|
# POST /fp/part/composer/state — part info + current tree status
|
||||||
|
# POST /fp/part/composer/templates — list shared-template recipes
|
||||||
|
# POST /fp/part/composer/load_template — clone a shared template into a part
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from odoo import http
|
||||||
|
from odoo.http import request
|
||||||
|
|
||||||
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
# Node fields we defensively copy from source → cloned node.
|
||||||
|
# Only include fields that definitely exist on fusion.plating.process.node.
|
||||||
|
# Verified against fusion_plating/models/fp_process_node.py.
|
||||||
|
_CLONABLE_FIELDS = (
|
||||||
|
'description',
|
||||||
|
'notes',
|
||||||
|
'icon',
|
||||||
|
'color',
|
||||||
|
'estimated_duration',
|
||||||
|
'auto_complete',
|
||||||
|
'customer_visible',
|
||||||
|
'is_manual',
|
||||||
|
'requires_signoff',
|
||||||
|
'active',
|
||||||
|
'process_type_id',
|
||||||
|
'work_center_id',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _clone_subtree(env, source, part, parent):
|
||||||
|
"""Recursively clone a process node subtree for a specific part.
|
||||||
|
|
||||||
|
Creates a new node mirroring ``source`` with part ownership set,
|
||||||
|
links it to ``cloned_from_id``, then recurses into child nodes.
|
||||||
|
|
||||||
|
:param env: active Odoo environment.
|
||||||
|
:param source: source fusion.plating.process.node (shared template or otherwise).
|
||||||
|
:param part: fp.part.catalog record receiving the clone.
|
||||||
|
:param parent: parent fusion.plating.process.node for the new node, or False for root.
|
||||||
|
:return: newly created root node (recordset).
|
||||||
|
"""
|
||||||
|
Node = env['fusion.plating.process.node']
|
||||||
|
|
||||||
|
vals = {
|
||||||
|
'name': source.name,
|
||||||
|
'code': False, # codes must be globally unique; don't carry over
|
||||||
|
'node_type': source.node_type,
|
||||||
|
'sequence': source.sequence,
|
||||||
|
'opt_in_out': source.opt_in_out,
|
||||||
|
'treatment_uom': source.treatment_uom,
|
||||||
|
'part_catalog_id': part.id,
|
||||||
|
'cloned_from_id': source.id,
|
||||||
|
'parent_id': parent.id if parent else False,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Copy additional fields defensively — skip anything missing on the
|
||||||
|
# model (future-safe for field removals).
|
||||||
|
for fname in _CLONABLE_FIELDS:
|
||||||
|
if fname in source._fields:
|
||||||
|
try:
|
||||||
|
value = source[fname]
|
||||||
|
# Many2one → extract id; everything else passes through.
|
||||||
|
if source._fields[fname].type == 'many2one':
|
||||||
|
vals[fname] = value.id if value else False
|
||||||
|
else:
|
||||||
|
vals[fname] = value
|
||||||
|
except Exception:
|
||||||
|
# Field exists but read failed — ignore and move on.
|
||||||
|
pass
|
||||||
|
|
||||||
|
new_node = Node.create(vals)
|
||||||
|
|
||||||
|
# Recurse into children in deterministic sequence order.
|
||||||
|
for child in source.child_ids.sorted('sequence'):
|
||||||
|
_clone_subtree(env, child, part, new_node)
|
||||||
|
|
||||||
|
return new_node
|
||||||
|
|
||||||
|
|
||||||
|
class FpPartComposerController(http.Controller):
|
||||||
|
"""JSON-RPC endpoints for the part-scoped Process Composer."""
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Read — current part + tree status
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
@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)."""
|
||||||
|
part = request.env['fp.part.catalog'].browse(int(part_id)).exists()
|
||||||
|
if not part:
|
||||||
|
return {'ok': False, 'error': 'Part not found'}
|
||||||
|
root = part.default_process_id
|
||||||
|
return {
|
||||||
|
'ok': True,
|
||||||
|
'part': {
|
||||||
|
'id': part.id,
|
||||||
|
'part_number': part.part_number or '',
|
||||||
|
'revision': part.revision or '',
|
||||||
|
'name': part.name or '',
|
||||||
|
'display': part.display_name or '',
|
||||||
|
'customer': part.partner_id.display_name or '',
|
||||||
|
},
|
||||||
|
'has_tree': bool(root),
|
||||||
|
'root_id': root.id if root else False,
|
||||||
|
}
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# List — shared-template recipes
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
@http.route('/fp/part/composer/templates', type='jsonrpc', auth='user')
|
||||||
|
def templates(self):
|
||||||
|
"""Return shared-template recipes (part_catalog_id IS NULL, node_type='recipe')."""
|
||||||
|
Node = request.env['fusion.plating.process.node']
|
||||||
|
templates = Node.search([
|
||||||
|
('part_catalog_id', '=', False),
|
||||||
|
('node_type', '=', 'recipe'),
|
||||||
|
('active', '=', True),
|
||||||
|
], order='name asc')
|
||||||
|
return {
|
||||||
|
'ok': True,
|
||||||
|
'templates': [
|
||||||
|
{'id': t.id, 'name': t.name or '', 'code': t.code or ''}
|
||||||
|
for t in templates
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Write — clone a template into the part
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
@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.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
The whole operation runs inside a savepoint — if anything fails
|
||||||
|
partway through, the part is left in its previous state.
|
||||||
|
"""
|
||||||
|
part = request.env['fp.part.catalog'].browse(int(part_id)).exists()
|
||||||
|
tpl = request.env['fusion.plating.process.node'].browse(int(template_id)).exists()
|
||||||
|
if not part:
|
||||||
|
return {'ok': False, 'error': 'Part not found'}
|
||||||
|
if not tpl:
|
||||||
|
return {'ok': False, 'error': 'Template not found'}
|
||||||
|
if tpl.part_catalog_id:
|
||||||
|
return {'ok': False, 'error': 'Invalid template (must be a shared-template recipe)'}
|
||||||
|
if tpl.node_type != 'recipe':
|
||||||
|
return {'ok': False, 'error': 'Template must be a recipe-type node'}
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
||||||
|
# 2. Deep-clone the template subtree with part ownership.
|
||||||
|
new_root = _clone_subtree(request.env, tpl, part, parent=False)
|
||||||
|
|
||||||
|
# 3. Pin part.default_process_id to the new root.
|
||||||
|
part.default_process_id = new_root.id
|
||||||
|
|
||||||
|
node_count = request.env['fusion.plating.process.node'].search_count([
|
||||||
|
('part_catalog_id', '=', part.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,
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
'ok': True,
|
||||||
|
'root_id': new_root.id,
|
||||||
|
'node_count': node_count,
|
||||||
|
}
|
||||||
|
except Exception as exc:
|
||||||
|
_logger.exception('Part Composer load_template failed')
|
||||||
|
return {'ok': False, 'error': str(exc)}
|
||||||
@@ -0,0 +1,147 @@
|
|||||||
|
/** @odoo-module **/
|
||||||
|
// =============================================================================
|
||||||
|
// Fusion Plating — Part-Scoped Process Composer (OWL client action)
|
||||||
|
// Copyright 2026 Nexa Systems Inc.
|
||||||
|
// License OPL-1 (Odoo Proprietary License v1.0)
|
||||||
|
//
|
||||||
|
// Thin wrapper around the existing recipe tree editor. Gives a part
|
||||||
|
// its own composed process tree by cloning a shared template, then
|
||||||
|
// hands off to the fp_recipe_tree_editor action for edits.
|
||||||
|
//
|
||||||
|
// Odoo 19 conventions:
|
||||||
|
// * Backend OWL: static template + static props = ["*"]
|
||||||
|
// * RPC: standalone rpc() from @web/core/network/rpc
|
||||||
|
// * Registered under registry.category("actions") → "fp_part_process_composer"
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
import { Component, useState, onMounted } from "@odoo/owl";
|
||||||
|
import { registry } from "@web/core/registry";
|
||||||
|
import { rpc } from "@web/core/network/rpc";
|
||||||
|
import { useService } from "@web/core/utils/hooks";
|
||||||
|
|
||||||
|
export class FpPartProcessComposer extends Component {
|
||||||
|
static template = "fusion_plating_configurator.FpPartProcessComposer";
|
||||||
|
static props = ["*"];
|
||||||
|
|
||||||
|
setup() {
|
||||||
|
this.action = useService("action");
|
||||||
|
this.notification = useService("notification");
|
||||||
|
|
||||||
|
// Pull part_id out of the client action's params (set by
|
||||||
|
// fp.part.catalog.action_open_part_composer on the server).
|
||||||
|
const params = (this.props.action && this.props.action.params) || {};
|
||||||
|
this.partId = params.part_id || null;
|
||||||
|
|
||||||
|
this.state = useState({
|
||||||
|
loading: true,
|
||||||
|
error: null,
|
||||||
|
part: null,
|
||||||
|
hasTree: false,
|
||||||
|
rootId: null,
|
||||||
|
templates: [],
|
||||||
|
selectedTemplateId: null,
|
||||||
|
loadingTemplate: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
onMounted(() => this.refresh());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Data loading -------------------------------------------------------
|
||||||
|
|
||||||
|
async refresh() {
|
||||||
|
if (!this.partId) {
|
||||||
|
this.state.error = "No part specified.";
|
||||||
|
this.state.loading = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.state.loading = true;
|
||||||
|
this.state.error = null;
|
||||||
|
try {
|
||||||
|
const [stateRes, tplRes] = await Promise.all([
|
||||||
|
rpc("/fp/part/composer/state", { part_id: this.partId }),
|
||||||
|
rpc("/fp/part/composer/templates", {}),
|
||||||
|
]);
|
||||||
|
if (!stateRes.ok) throw new Error(stateRes.error || "Failed to load part state.");
|
||||||
|
if (!tplRes.ok) throw new Error(tplRes.error || "Failed to load templates.");
|
||||||
|
|
||||||
|
this.state.part = stateRes.part;
|
||||||
|
this.state.hasTree = stateRes.has_tree;
|
||||||
|
this.state.rootId = stateRes.root_id || null;
|
||||||
|
this.state.templates = tplRes.templates || [];
|
||||||
|
|
||||||
|
// Default the dropdown selection to the first template so the
|
||||||
|
// user can click Load immediately.
|
||||||
|
if (this.state.templates.length > 0 && !this.state.selectedTemplateId) {
|
||||||
|
this.state.selectedTemplateId = this.state.templates[0].id;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
this.state.error = err.message || String(err);
|
||||||
|
} finally {
|
||||||
|
this.state.loading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Handlers -----------------------------------------------------------
|
||||||
|
|
||||||
|
onSelectTemplate(ev) {
|
||||||
|
this.state.selectedTemplateId = parseInt(ev.target.value, 10) || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async onLoadTemplate() {
|
||||||
|
if (!this.state.selectedTemplateId) return;
|
||||||
|
const confirmReplace = this.state.hasTree
|
||||||
|
? window.confirm("This will replace the current process tree for this part. Continue?")
|
||||||
|
: true;
|
||||||
|
if (!confirmReplace) return;
|
||||||
|
|
||||||
|
this.state.loadingTemplate = true;
|
||||||
|
try {
|
||||||
|
const res = await rpc("/fp/part/composer/load_template", {
|
||||||
|
part_id: this.partId,
|
||||||
|
template_id: this.state.selectedTemplateId,
|
||||||
|
});
|
||||||
|
if (!res.ok) throw new Error(res.error || "Load failed.");
|
||||||
|
this.notification.add(
|
||||||
|
`Template loaded — ${res.node_count} nodes cloned into this part's tree.`,
|
||||||
|
{ type: "success" }
|
||||||
|
);
|
||||||
|
await this.refresh();
|
||||||
|
// Hand off directly to the tree editor so the user can
|
||||||
|
// immediately start customising.
|
||||||
|
this.openRecipeEditor(res.root_id);
|
||||||
|
} catch (err) {
|
||||||
|
this.notification.add(
|
||||||
|
`Load failed: ${err.message || err}`,
|
||||||
|
{ type: "danger" }
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
this.state.loadingTemplate = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
openRecipeEditor(rootId) {
|
||||||
|
const id = rootId || this.state.rootId;
|
||||||
|
if (!id) return;
|
||||||
|
// The existing fp_recipe_tree_editor reads recipe_id from
|
||||||
|
// this.props.action?.context — pass it via `context`, not `params`.
|
||||||
|
this.action.doAction({
|
||||||
|
type: "ir.actions.client",
|
||||||
|
tag: "fp_recipe_tree_editor",
|
||||||
|
name: `Process Composer — ${(this.state.part && this.state.part.display) || ""}`,
|
||||||
|
context: { recipe_id: id, part_id: this.partId },
|
||||||
|
target: "current",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
backToPart() {
|
||||||
|
this.action.doAction({
|
||||||
|
type: "ir.actions.act_window",
|
||||||
|
res_model: "fp.part.catalog",
|
||||||
|
res_id: this.partId,
|
||||||
|
views: [[false, "form"]],
|
||||||
|
target: "current",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
registry.category("actions").add("fp_part_process_composer", FpPartProcessComposer);
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
// Copyright 2026 Nexa Systems Inc.
|
||||||
|
// License OPL-1 (Odoo Proprietary License v1.0)
|
||||||
|
// Part of the Fusion Plating product family.
|
||||||
|
//
|
||||||
|
// Sub 3 — Process Composer styles.
|
||||||
|
|
||||||
|
.o_fp_part_composer {
|
||||||
|
padding: 16px;
|
||||||
|
max-width: 900px;
|
||||||
|
margin: 0 auto;
|
||||||
|
|
||||||
|
&_state {
|
||||||
|
padding: 32px;
|
||||||
|
text-align: center;
|
||||||
|
color: var(--bs-secondary-color, #666);
|
||||||
|
|
||||||
|
.fa {
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&_error {
|
||||||
|
color: var(--bs-danger, #c00);
|
||||||
|
}
|
||||||
|
|
||||||
|
&_header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
padding-bottom: 16px;
|
||||||
|
border-bottom: 1px solid var(--bs-border-color, #e0e0e0);
|
||||||
|
}
|
||||||
|
|
||||||
|
&_title {
|
||||||
|
h2 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&_loader {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
padding: 16px;
|
||||||
|
background: var(--bs-tertiary-bg, #f5f5f5);
|
||||||
|
border-radius: 8px;
|
||||||
|
|
||||||
|
label {
|
||||||
|
margin: 0;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
flex: 1;
|
||||||
|
max-width: 400px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&_tree {
|
||||||
|
min-height: 300px;
|
||||||
|
padding: 24px;
|
||||||
|
background: var(--bs-body-bg, #ffffff);
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid var(--bs-border-color, #d8dadd);
|
||||||
|
}
|
||||||
|
|
||||||
|
&_hint,
|
||||||
|
&_empty {
|
||||||
|
text-align: center;
|
||||||
|
padding: 48px 16px;
|
||||||
|
|
||||||
|
.fa {
|
||||||
|
color: var(--bs-secondary-color, #999);
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
color: var(--bs-secondary-color, #666);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!--
|
||||||
|
Copyright 2026 Nexa Systems Inc.
|
||||||
|
License OPL-1 (Odoo Proprietary License v1.0)
|
||||||
|
Part of the Fusion Plating product family.
|
||||||
|
|
||||||
|
OWL template for the part-scoped Process Composer client action.
|
||||||
|
-->
|
||||||
|
<templates xml:space="preserve">
|
||||||
|
|
||||||
|
<t t-name="fusion_plating_configurator.FpPartProcessComposer">
|
||||||
|
<div class="o_fp_part_composer">
|
||||||
|
<t t-if="state.loading">
|
||||||
|
<div class="o_fp_part_composer_state">
|
||||||
|
<i class="fa fa-spinner fa-spin"/>
|
||||||
|
<span> Loading…</span>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
<t t-elif="state.error">
|
||||||
|
<div class="o_fp_part_composer_state o_fp_part_composer_error">
|
||||||
|
<i class="fa fa-exclamation-triangle"/>
|
||||||
|
<span t-esc="state.error"/>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
<t t-elif="state.part">
|
||||||
|
<div class="o_fp_part_composer_header">
|
||||||
|
<button class="btn btn-secondary" t-on-click="backToPart">
|
||||||
|
<i class="fa fa-arrow-left"/>
|
||||||
|
<span> Back to Part</span>
|
||||||
|
</button>
|
||||||
|
<div class="o_fp_part_composer_title">
|
||||||
|
<h2>Process Composer — <t t-esc="state.part.display"/></h2>
|
||||||
|
<small class="text-muted" t-if="state.part.customer">
|
||||||
|
Customer: <t t-esc="state.part.customer"/>
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="o_fp_part_composer_loader">
|
||||||
|
<label>Load Existing Process:</label>
|
||||||
|
<select class="form-select" t-on-change="onSelectTemplate">
|
||||||
|
<t t-foreach="state.templates" t-as="tpl" t-key="tpl.id">
|
||||||
|
<option t-att-value="tpl.id"
|
||||||
|
t-att-selected="tpl.id == state.selectedTemplateId">
|
||||||
|
<t t-esc="tpl.name"/>
|
||||||
|
</option>
|
||||||
|
</t>
|
||||||
|
</select>
|
||||||
|
<button class="btn btn-primary"
|
||||||
|
t-on-click="onLoadTemplate"
|
||||||
|
t-att-disabled="state.loadingTemplate or !state.selectedTemplateId">
|
||||||
|
<t t-if="state.loadingTemplate">
|
||||||
|
<i class="fa fa-spinner fa-spin"/>
|
||||||
|
<span> Loading…</span>
|
||||||
|
</t>
|
||||||
|
<t t-else="">
|
||||||
|
<t t-if="state.hasTree">Replace with Selected</t>
|
||||||
|
<t t-else="">Load</t>
|
||||||
|
</t>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="o_fp_part_composer_tree">
|
||||||
|
<t t-if="state.hasTree">
|
||||||
|
<div class="o_fp_part_composer_hint">
|
||||||
|
<p>This part has a composed process tree. Click below to open the
|
||||||
|
full tree editor where you can add, remove, reorder, and configure
|
||||||
|
the process nodes.</p>
|
||||||
|
<button class="btn btn-primary"
|
||||||
|
t-on-click="() => this.openRecipeEditor()">
|
||||||
|
<i class="fa fa-edit"/>
|
||||||
|
<span> Open Tree Editor</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
<t t-else="">
|
||||||
|
<div class="o_fp_part_composer_empty">
|
||||||
|
<i class="fa fa-cogs fa-3x"/>
|
||||||
|
<p>No process composed yet.</p>
|
||||||
|
<p class="text-muted">
|
||||||
|
Pick a template above and click <strong>Load</strong> to get started.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
|
||||||
|
</templates>
|
||||||
@@ -144,6 +144,24 @@
|
|||||||
</div>
|
</div>
|
||||||
</group>
|
</group>
|
||||||
<notebook>
|
<notebook>
|
||||||
|
<page string="Process" name="process">
|
||||||
|
<group>
|
||||||
|
<field name="default_process_id" readonly="1"
|
||||||
|
help="Use the Compose button to set up this part's process tree."/>
|
||||||
|
</group>
|
||||||
|
<div class="mt-2">
|
||||||
|
<button name="action_open_part_composer" type="object"
|
||||||
|
string="Compose"
|
||||||
|
icon="fa-wrench"
|
||||||
|
class="btn-primary"
|
||||||
|
help="Open the Process Composer to load a template and edit this part's tree."/>
|
||||||
|
</div>
|
||||||
|
<p class="text-muted mt-3">
|
||||||
|
The <strong>Compose</strong> button opens the Process Composer where you can
|
||||||
|
load a shared template and customise it for this part. When a job runs for
|
||||||
|
this part, work orders are generated from the composed tree.
|
||||||
|
</p>
|
||||||
|
</page>
|
||||||
<page string="Dimensions & Complexity" name="dimensions">
|
<page string="Dimensions & Complexity" name="dimensions">
|
||||||
<group>
|
<group>
|
||||||
<group>
|
<group>
|
||||||
|
|||||||
Reference in New Issue
Block a user