fix(configurator): programmatic SO-line create fallback for x_fc_internal_description

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>
This commit is contained in:
gsinghpal
2026-04-21 23:34:32 -04:00
parent 733998dc95
commit afd8bae514
2 changed files with 174 additions and 0 deletions

View File

@@ -67,6 +67,36 @@ class SaleOrderLine(models.Model):
'preserved for audit. Useful when a part is cancelled mid-order.',
)
@api.model_create_multi
def create(self, vals_list):
"""Default `x_fc_internal_description` from `name` when a caller
creates a line programmatically without supplying the internal
description.
Sub 2 made `x_fc_internal_description` required. The UI-side
onchange fills it when the user picks a description template,
but programmatic creators (sale_mrp bridge, migration scripts,
external integrations, demo seeders) may not know about this
field. Instead of forcing every call site to update, fall back
to `name` — same rule the upgrade migration used when it
back-filled historical lines.
"""
Product = self.env['product.product']
for vals in vals_list:
if vals.get('x_fc_internal_description'):
continue
# Try the explicit `name` first. If the caller didn't pass
# one (sale_mrp + some Odoo internals don't — they let the
# name compute from product_id later), fall back to the
# product's display_name so we have SOMETHING non-empty.
fallback = vals.get('name')
if not fallback and vals.get('product_id'):
prod = Product.browse(vals['product_id']).exists()
if prod:
fallback = prod.display_name or prod.name
vals['x_fc_internal_description'] = fallback or ''
return super().create(vals_list)
@api.onchange('x_fc_description_template_id')
def _onchange_description_template(self):
"""When estimator picks a template, auto-fill both descriptions.