"""Sub 3 — end-to-end smoke.""" import sys env = self.env def ok(msg): print(f" [OK] {msg}") def fail(msg): print(f" [FAIL] {msg}"); sys.exit(1) def hdr(t): print(f"\n=== {t} ===") hdr("1. Schema presence") Node = env['fusion.plating.process.node'] for f in ('part_catalog_id', 'cloned_from_id', 'treatment_uom'): if f not in Node._fields: fail(f"Node missing {f}") ok("all 3 new fields present on fusion.plating.process.node") Part = env['fp.part.catalog'] if 'default_process_id' not in Part._fields: fail("fp.part.catalog missing default_process_id") ok("default_process_id present on fp.part.catalog") hdr("2. opt_in_out label rename") sel = dict(Node._fields['opt_in_out'].selection) expected = { 'disabled': 'Always Included', 'opt_out': 'Included by Default', 'opt_in': 'Excluded by Default', } for k, v in expected.items(): if sel.get(k) != v: fail(f"opt_in_out[{k}] = {sel.get(k)!r}, expected {v!r}") ok("opt_in_out Selection labels renamed correctly, keys unchanged") hdr("3. General Processing seed is a shared template") gp = env['fusion.plating.process.node'].search([ ('code', '=', 'GENERAL_PROCESSING'), ], limit=1) if not gp: fail("General Processing template not found") if gp.part_catalog_id: fail(f"General Processing should be shared (part_catalog_id NULL), got {gp.part_catalog_id}") if gp.node_type != 'recipe': fail(f"expected recipe, got {gp.node_type}") ok(f"General Processing seed: {gp.name} (id={gp.id}, shared template)") hdr("4. Controller endpoints registered") from odoo.addons.fusion_plating_configurator.controllers.fp_part_composer_controller import FpPartComposerController routes = [] for m in ('state', 'templates', 'load_template'): meth = getattr(FpPartComposerController, m, None) if meth is None: fail(f"method {m} missing") route_attr = getattr(meth, 'original_routing', None) or getattr(meth, 'routing', None) if not route_attr: fail(f"method {m} has no routing metadata") routes.append((m, route_attr.get('routes'))) ok(f"3 routes registered: {routes}") hdr("5. Clone subtree end-to-end") from odoo.addons.fusion_plating_configurator.controllers.fp_part_composer_controller import _clone_subtree part = env['fp.part.catalog'].search([('part_number', '!=', False)], limit=1) if not part: fail("no part with part_number to test") # Clean any prior test clones for this part env['fusion.plating.process.node'].search([ ('part_catalog_id', '=', part.id), ]).unlink() template_nodes_before = env['fusion.plating.process.node'].search_count([ ('id', 'child_of', gp.id), ]) ok(f"testing with part {part.display_name}, template has {template_nodes_before} nodes") new_root = _clone_subtree(env, gp, part, parent=False) cloned = env['fusion.plating.process.node'].search_count([ ('part_catalog_id', '=', part.id), ]) if cloned != template_nodes_before: fail(f"clone count mismatch: template={template_nodes_before}, cloned={cloned}") if new_root.part_catalog_id.id != part.id: fail(f"new root part_catalog_id={new_root.part_catalog_id.id}, expected {part.id}") if new_root.cloned_from_id.id != gp.id: fail(f"cloned_from_id={new_root.cloned_from_id.id}, expected {gp.id}") if new_root.node_type != 'recipe': fail(f"cloned root should be recipe, got {new_root.node_type}") ok(f"cloned {cloned} nodes, root={new_root.name}, cloned_from_id points at template") part.default_process_id = new_root.id env.cr.commit() part.invalidate_recordset() if part.default_process_id.id != new_root.id: fail("default_process_id not set on part") ok(f"part.default_process_id = {part.default_process_id.name}") hdr("6. Walker resolution via _resolve_mo_process_tree") # Build a fresh MO linked to this part via SO Product = env['product.product'] product = Product.search([('active', '=', True)], limit=1) so_vals = { 'partner_id': part.partner_id.id, 'order_line': [(0, 0, { 'product_id': product.id, 'product_uom_qty': 1, 'price_unit': 50.0, 'x_fc_part_catalog_id': part.id, 'name': 'Sub3 smoke line', 'x_fc_internal_description': 'internal', })], } if 'x_fc_po_number' in env['sale.order']._fields: so_vals['x_fc_po_number'] = 'SUB3-SMOKE' so = env['sale.order'].create(so_vals) mo = env['mrp.production'].create({ 'product_id': product.id, 'product_qty': 1, 'origin': so.name, }) root = mo._resolve_mo_process_tree() if not root: fail("resolver returned no root") if root.id != new_root.id: fail(f"resolver returned {root.id}, expected part's clone {new_root.id}") ok(f"resolver correctly returned part's cloned tree (root={root.name})") hdr("7. Fallback path — MO with no linked part") orphan = env['mrp.production'].create({ 'product_id': product.id, 'product_qty': 1, }) legacy_root = orphan._resolve_mo_process_tree() # Should fall back to x_fc_recipe_id (which is likely NULL for a bare MO) # We just need it to not crash and return False/None rather than erroring if legacy_root and legacy_root.part_catalog_id: fail(f"orphan MO should not get a part-owned tree, got {legacy_root.name}") ok("resolver falls through cleanly for orphan MO") hdr("8. Templates query returns only shared recipes") tpl_recipes = env['fusion.plating.process.node'].search([ ('part_catalog_id', '=', False), ('node_type', '=', 'recipe'), ('active', '=', True), ]) part_owned = env['fusion.plating.process.node'].search([ ('part_catalog_id', '=', part.id), ]) if not tpl_recipes: fail("no shared templates found") if any(n.part_catalog_id for n in tpl_recipes): fail("shared-template query returned part-owned nodes") ok(f"shared templates: {len(tpl_recipes)}, part-owned for test part: {len(part_owned)}") hdr("SUB 3 SMOKE COMPLETE")