test(sub3): Sub 3 smoke + verify SQL; mark shipped in CLAUDE.md roadmap
Sub 3 (Default Process + Composer per Part) complete: - Phase A: schema additions (part_catalog_id, cloned_from_id, treatment_uom on process_node; default_process_id on part_catalog), opt_in_out label rename, General Processing seed flipped to noupdate=1 - Phase B: part-scoped Process Composer client action (fp_part_process_composer) with 3 RPC endpoints + OWL wrapper + Process tab on part form with Compose button - Phase C: tree node MO-state palette (green=completed, blue=active, red=error-only) All 8 Sub 3 smoke checks green. Phase 1-3 QC smoke + E2E still green. Sub 2 features untouched. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
145
docs/superpowers/tests/2026-04-22-sub3-smoke.py
Normal file
145
docs/superpowers/tests/2026-04-22-sub3-smoke.py
Normal file
@@ -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")
|
||||||
24
docs/superpowers/tests/2026-04-22-sub3-verify.sql
Normal file
24
docs/superpowers/tests/2026-04-22-sub3-verify.sql
Normal file
@@ -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;
|
||||||
@@ -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 |
|
| 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 |
|
| 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 |
|
| 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 |
|
| 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 |
|
| 6 | Contact Profiles & Communication Routing (sub-contacts + per-location notification lists + global contacts) | Pending | client transcript A/B/C |
|
||||||
|
|||||||
Reference in New Issue
Block a user