diff --git a/docs/superpowers/tests/2026-04-22-sub3-smoke.py b/docs/superpowers/tests/2026-04-22-sub3-smoke.py new file mode 100644 index 00000000..2fd068f0 --- /dev/null +++ b/docs/superpowers/tests/2026-04-22-sub3-smoke.py @@ -0,0 +1,145 @@ +"""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") diff --git a/docs/superpowers/tests/2026-04-22-sub3-verify.sql b/docs/superpowers/tests/2026-04-22-sub3-verify.sql new file mode 100644 index 00000000..2ea8a082 --- /dev/null +++ b/docs/superpowers/tests/2026-04-22-sub3-verify.sql @@ -0,0 +1,24 @@ +-- Sub 3 Phase A verification +\echo '--- Process node new fields present ---' +SELECT column_name, data_type FROM information_schema.columns +WHERE table_name='fusion_plating_process_node' + AND column_name IN ('part_catalog_id','cloned_from_id','treatment_uom') +ORDER BY column_name; + +\echo '--- fp.part.catalog.default_process_id present ---' +SELECT column_name, data_type FROM information_schema.columns +WHERE table_name='fp_part_catalog' AND column_name='default_process_id'; + +\echo '--- General Processing seed installed ---' +SELECT id, name, code, node_type, part_catalog_id FROM fusion_plating_process_node +WHERE code='GEN-PROC' OR name='General Processing'; + +\echo '--- All existing nodes have part_catalog_id NULL (shared templates) ---' +SELECT COUNT(*) FILTER (WHERE part_catalog_id IS NULL) AS shared_templates, + COUNT(*) FILTER (WHERE part_catalog_id IS NOT NULL) AS part_owned +FROM fusion_plating_process_node; + +\echo '--- Module versions on entech ---' +SELECT name, latest_version FROM ir_module_module +WHERE name IN ('fusion_plating','fusion_plating_configurator','fusion_plating_bridge_mrp') +ORDER BY name; diff --git a/fusion_plating/CLAUDE.md b/fusion_plating/CLAUDE.md index b5cfdef9..93cd7d48 100644 --- a/fusion_plating/CLAUDE.md +++ b/fusion_plating/CLAUDE.md @@ -368,7 +368,7 @@ rewrite code as new requirements surface. Each sub-project has its own design do |---|---|---|---| | 1 | Direct Order Wizard fix (no auto-confirm/auto-email) | **Shipped 2026-04-22** (commit afd8bae+) | Gap 1 | | 2 | Part Data Model Overhaul (part#/rev required, dual descriptions, per-part cert requirement, SKU→Part Number on customer docs) | **Shipped 2026-04-22** (commits 868b418..afd8bae) | 2b, 2c, 2d, 4 | -| 3 | Default Process + Composer per part (reuse recipe tree) | Pending | 2e, 2f | +| 3 | Default Process + Composer per part (reuse recipe tree) | **Shipped 2026-04-22** (commits ce07daa..f059348) | 2e, 2f | | 4 | Contract Review two-portion workflow (QA Assistant + QA Manager; pre-production gate) | Pending | 2i | | 5 | Order-line fields (serial, job#, thickness dropdown, revision picker) | Pending | 5, 6, Q2 | | 6 | Contact Profiles & Communication Routing (sub-contacts + per-location notification lists + global contacts) | Pending | client transcript A/B/C |