When Sub 2 Task 26 flipped x_fc_internal_description to required=True, any programmatic sale.order.line creation that doesn't set the field fails at the Postgres NOT NULL constraint. Callers include: - sale_mrp stock-move line creation (doesn't set name either) - demo seeders - external integrations - test scripts The UI-side onchange populates the field when the user picks a description template; this hook mirrors that for programmatic callers. Fallback chain: explicit vals['x_fc_internal_description'] → vals['name'] → product_id.display_name → '—'. Matches the migration's backfill rule. Also adds Sub 2 end-to-end smoke test (6 cases, all green): 1. Required-field rejection on part creation 2. Required-field rejection on template creation 3. Template picker populates both SO-line descriptions 4. Cert resolver: part-level override wins over partner 5. display_name renders part_number + revision + name 6. certificate_requirement defaults to 'inherit' QC Phase 1-3 regression suite remains green after the fix. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
145 lines
5.5 KiB
Python
145 lines
5.5 KiB
Python
"""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")
|