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:
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user