# Walk part creation + 4 process variants step by step. # Personas: # Bob (Estimator) — owns the part catalog, designs process variants # Sarah (CSR) — picks a variant on order entry # # Goal: prove that # 1) Bob can create a part # 2) Bob can attach 4 distinct process variants via the Composer flow # 3) One is flagged default; switching default works # 4) Sarah opens a Direct Order, picks the part — variant dropdown lists ALL FOUR # 5) Sarah picks a non-default variant; the SO + job actually use it from odoo import fields from odoo.addons.fusion_plating_configurator.controllers.fp_part_composer_controller \ import _list_variants, _clone_subtree P = env['res.partner'] Part = env['fp.part.catalog'] Coating = env['fp.coating.config'] Treat = env['fp.treatment'] Node = env['fusion.plating.process.node'] Tpl = Node # template recipes are also fp.process.node records # ====================================================================== STEP 2 print('='*72) print('STEP 2 — Bob creates a brand-new part') print('='*72) target_partner = P.browse(2529) # 2CM INNOVATIVE default_coating = Coating.search([], limit=1) default_treats = Treat.search([], limit=2) part = Part.create({ 'partner_id': target_partner.id, 'part_number': 'E2E-VAR-' + fields.Datetime.now().strftime('%H%M%S'), 'revision': 'A', 'name': 'E2E variant test bracket', 'substrate_material': 'aluminium', 'surface_area': 12.5, 'surface_area_uom': 'sq_in', 'weight': 0.45, 'complexity': 'simple', 'masking_zones': 1, 'x_fc_default_coating_config_id': default_coating.id, 'x_fc_default_treatment_ids': [(6, 0, default_treats.ids)], }) print(f'[Bob] Created part: {part.display_name} (id={part.id})') print(f' default coating: {part.x_fc_default_coating_config_id.name}') print(f' default treatments: {default_treats.mapped("name")}') print(f' process_variant_count (BEFORE adding any): {part.process_variant_count}') # Find a shared template recipe to clone from. Templates = fp.process.node # records with node_type='recipe', parent_id=False, part_catalog_id=False. template = Node.search([ ('node_type', '=', 'recipe'), ('parent_id', '=', False), ('part_catalog_id', '=', False), ], limit=1) if not template: print(' ❌ No shared template recipes available — cannot continue!') raise SystemExit print(f'[Bob] Will clone from shared template: {template.name} ({len(template.child_ids)} root children)') # ====================================================================== STEP 3 print() print('='*72) print('STEP 3 — Bob adds variant #1: Standard Production') print('='*72) v1 = _clone_subtree(env, template, part, parent=False) v1.variant_label = 'Standard Production' v1.is_default_variant = True part.default_process_id = v1.id print(f'[Bob] Created variant: {v1.variant_label} (root node id={v1.id}, name="{v1.name}")') print(f' is_default: {v1.is_default_variant}') print(f' child nodes cloned: {len(v1.child_ids)}') # ====================================================================== STEP 4 print() print('='*72) print('STEP 4 — Bob adds variant #2: Aerospace Cert (AS9100)') print('='*72) v2 = _clone_subtree(env, template, part, parent=False) v2.variant_label = 'Aerospace Cert (AS9100)' print(f'[Bob] Created variant: {v2.variant_label} (root id={v2.id})') print(f' is_default: {v2.is_default_variant} (correct — first one stays default)') # ====================================================================== STEP 5 print() print('='*72) print('STEP 5 — Bob adds variant #3: Quick-turn (no bake)') print('='*72) v3 = _clone_subtree(env, template, part, parent=False) v3.variant_label = 'Quick-turn (no bake)' print(f'[Bob] Created variant: {v3.variant_label} (root id={v3.id})') # ====================================================================== STEP 6 print() print('='*72) print('STEP 6 — Bob adds variant #4: Heavy build (wear)') print('='*72) v4 = _clone_subtree(env, template, part, parent=False) v4.variant_label = 'Heavy build (wear)' print(f'[Bob] Created variant: {v4.variant_label} (root id={v4.id})') # Refresh the part and inspect what the form would show. part.invalidate_recordset() print() print(f'[Bob] After 4 adds — part {part.display_name}:') print(f' process_variant_count: {part.process_variant_count}') print(f' default_process_id: {part.default_process_id.name if part.default_process_id else None}') print(f' Variants list (per Composer endpoint /fp/part/composer/state):') for entry in _list_variants(part): flag = '★ default' if entry['is_default'] else ' ' print(f' {flag} id={entry["id"]:>5} "{entry["label"]}" — {entry["node_count"]} nodes') # ====================================================================== STEP 7 print() print('='*72) print('STEP 7 — Sarah enters a Direct Order, picks the part, picks a variant') print('='*72) W = env['fp.direct.order.wizard'] Line = env['fp.direct.order.line'] w = W.create({ 'partner_id': target_partner.id, 'po_pending': True, 'po_number': 'PO-VARTEST-001', 'invoice_strategy': 'net_terms', }) w._onchange_partner_id() # Sarah adds a line, picks the part. Onchange should pre-fill default coating. ln = Line.new({'wizard_id': w.id}) ln.part_catalog_id = part ln._onchange_part_clears_variant() print(f'[Sarah] Picked part {part.part_number}.') print(f' Pre-filled coating: {ln.coating_config_id.name if ln.coating_config_id else "(none)"}') print(f' Pre-filled treatments: {ln.treatment_ids.mapped("name") if ln.treatment_ids else "(none)"}') # What variants would the dropdown show? Inspect process_variant_id field domain. print() print(f'[Sarah] Looking at the Variant dropdown on the line:') # Domain on x_fc_process_variant_id (defined on sale.order.line) is part-scoped. # For the wizard line it's process_variant_id with the same domain. visible_variants = part.process_variant_ids print(f' Domain: part_scoped (id, child_of, ...). Visible variants: {len(visible_variants)}') for v in visible_variants: flag = '★' if v.is_default_variant else ' ' print(f' {flag} {v.variant_label or v.name} (id={v.id})') # Sarah picks variant #3 (Quick-turn). ln.process_variant_id = v3 print() print(f'[Sarah] Picked variant: {ln.process_variant_id.variant_label}') # Persist via Line.create with the chosen variant. new_line = Line.create({ 'wizard_id': w.id, 'part_catalog_id': part.id, 'coating_config_id': default_coating.id, 'process_variant_id': v3.id, 'quantity': 5, 'unit_price': 25.0, }) print(f' Saved line: process_variant_id={new_line.process_variant_id.variant_label}') # ====================================================================== STEP 8 print() print('='*72) print('STEP 8 — Confirm SO; verify the JOB uses variant #3, not the default') print('='*72) result = w.action_create_order() so = env['sale.order'].browse(result['res_id']) print(f'[Sarah] SO created: {so.name}') # Inspect the SO line's variant. sol = so.order_line[:1] print(f' SO line process_variant_id: {sol.x_fc_process_variant_id.variant_label if sol.x_fc_process_variant_id else "(none)"}') # Confirm the SO. so.action_confirm() job = env['fp.job'].search([('sale_order_id', '=', so.id)], limit=1) print(f' Job created: {job.name}') print(f' Job recipe_id: {job.recipe_id.name if job.recipe_id else "(none)"}') print(f' EXPECTED: recipe_id should match variant #3 root (id={v3.id}, name="{v3.name}")') print(f' ACTUAL: recipe_id={job.recipe_id.id} (name="{job.recipe_id.name}")') if job.recipe_id.id == v3.id: print(f' ✓ Job correctly inherited the picked variant') else: print(f' ❌ Job did NOT use the picked variant! Recipe is {job.recipe_id.name}, expected {v3.name}') env.cr.commit() print() print('== Walk complete ==')