"""Sub 2 — end-to-end smoke. Full lifecycle with Sub 2 features. Confirms: - new parts reject without part_number / revision - description template row requires both fields - SO line requires both descriptions - cert cascade via resolver, part-level wins over partner - display_name renders part_number + revision + name """ 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} ===") # Helper — savepoint wrapper so a required-field rejection doesn't # leave the transaction in an aborted state for later tests. def expect_raise(label, callable_, keyword): env.cr.execute("SAVEPOINT smoke_test") try: callable_() env.cr.execute("RELEASE SAVEPOINT smoke_test") fail(f"{label} — no exception raised") except Exception as e: env.cr.execute("ROLLBACK TO SAVEPOINT smoke_test") msg = str(e)[:120] if keyword.lower() in msg.lower(): ok(f"{label} — rejected: {msg[:80]}") else: fail(f"{label} — wrong error: {msg}") # ---- 1. Required fields on fp.part.catalog ---- hdr("1. Required fields on fp.part.catalog") expect_raise( "part without part_number", lambda: env['fp.part.catalog'].create({ 'partner_id': env['res.partner'].search([('customer_rank', '>', 0)], limit=1).id, }), keyword='part_number', ) # ---- 2. Description template two-field requirement ---- hdr("2. Description template required fields") part = env['fp.part.catalog'].create({ 'name': 'Sub2 Smoke Part', 'part_number': 'SUB2-SMOKE-001', 'revision': 'A', 'partner_id': env['res.partner'].search([('customer_rank', '>', 0)], limit=1).id, }) expect_raise( "template without customer_facing_description", lambda: env['fp.sale.description.template'].create({ 'name': 'Broken template', 'part_catalog_id': part.id, 'internal_description': 'ops only', # customer_facing_description missing }), keyword='customer_facing', ) # ---- 3. SO line dual descriptions ---- hdr("3. SO line template picker populates both descriptions") tpl = env['fp.sale.description.template'].create({ 'name': 'Standard', 'part_catalog_id': part.id, 'internal_description': 'Racking pattern A; mask threaded holes', 'customer_facing_description': 'Electroless nickel plate per customer spec', }) product = env['product.product'].search([('active', '=', True)], limit=1) # In the UI the onchange populates x_fc_internal_description + name from # the template; for a programmatic create both must be set explicitly. # The onchange is unit-tested separately (simulate it here). so_vals = { 'partner_id': part.partner_id.id, 'order_line': [(0, 0, { 'product_id': product.id, 'product_uom_qty': 1, 'x_fc_part_catalog_id': part.id, 'x_fc_description_template_id': tpl.id, 'name': tpl.customer_facing_description, 'x_fc_internal_description': tpl.internal_description, })], } if 'x_fc_po_number' in env['sale.order']._fields: so_vals['x_fc_po_number'] = 'SUB2-SMOKE-PO' so = env['sale.order'].create(so_vals) line = so.order_line[0] if line.x_fc_internal_description != tpl.internal_description: fail(f"line.x_fc_internal_description mismatch: got {line.x_fc_internal_description!r}") if line.name != tpl.customer_facing_description: fail(f"line.name mismatch: got {line.name!r}") ok(f"SO line has both descriptions set from template") # Verify onchange method works when invoked directly (simulating UI) line2 = env['sale.order.line'].new({ 'x_fc_description_template_id': tpl.id, 'product_id': product.id, 'order_id': so.id, }) line2._onchange_description_template() if line2.name != tpl.customer_facing_description: fail(f"onchange didn't set name: {line2.name!r}") if line2.x_fc_internal_description != tpl.internal_description: fail(f"onchange didn't set internal: {line2.x_fc_internal_description!r}") ok("onchange method populates both fields correctly") # ---- 4. Cert resolver end-to-end ---- hdr("4. Cert resolver with part override wins over partner") part.certificate_requirement = 'coc_thickness' part.partner_id.x_fc_send_coc = False part.partner_id.x_fc_send_thickness_report = False so.action_confirm() mo = env['mrp.production'].create({ 'product_id': product.id, 'product_qty': 1, 'origin': so.name, }) mo.action_confirm() want_coc, want_thickness = mo._fp_resolve_cert_requirement() if (want_coc, want_thickness) != (True, True): fail(f"resolver returned ({want_coc}, {want_thickness}); expected (True, True)") ok("part-level coc_thickness override wins over partner=off") # ---- 5. display_name compute ---- hdr("5. display_name includes part_number + revision") part.invalidate_recordset() if 'SUB2-SMOKE-001' not in part.display_name: fail(f"display_name missing part_number: {part.display_name!r}") if 'Rev A' not in part.display_name: fail(f"display_name missing revision: {part.display_name!r}") ok(f"display_name: {part.display_name}") # ---- 6. certificate_requirement defaults to 'inherit' ---- hdr("6. New parts default certificate_requirement to 'inherit'") p2 = env['fp.part.catalog'].create({ 'name': 'Default Inherit Test', 'part_number': 'DEF-TEST-001', 'revision': 'A', 'partner_id': env['res.partner'].search([('customer_rank', '>', 0)], limit=1).id, }) if p2.certificate_requirement != 'inherit': fail(f"expected 'inherit', got {p2.certificate_requirement!r}") ok(f"certificate_requirement default = {p2.certificate_requirement}") hdr("SUB 2 SMOKE COMPLETE")