fix(configurator): part-level saved descriptions (not generic)

The earlier description templates were global — same 8 generic texts
applied to any part. That's useless when a customer has 3,500 parts
and each part has 3–5 canned variants (standard, masked threads,
masked bore, rework, rush packaging). That's ~17,500 rows total, and
the variants ONLY make sense in the context of a specific part number.

Restructured so descriptions live on each part:

Model changes:
  fp.sale.description.template.part_catalog_id (new M2O, indexed,
    ondelete cascade) — the primary scoping field
  fp.sale.description.template.partner_id — now a related store=True
    field pulled from the part, so customer-level search still works
  fp.part.catalog.description_template_ids (new O2M inverse) — the
    5–10 canned descriptions attached to this specific part
  fp.part.catalog.description_template_count (computed)

UI changes:
  Part Catalog form: new "Descriptions" notebook page with inline
    editable list (sequence + name + tag + description + usage_count).
    5 variants take 30 seconds to enter.
  Part Catalog form: new smart button "Descriptions" showing the count,
    jumps to the full list filtered by this part.
  Template list view: part_catalog_id column added, list ordered by
    part first. Search view adds Part filter + Part-Specific /
    Generic (No Part) filters + Group By Part.

Wizard changes:
  description_template_id domain now prioritises part-specific, falls
    through to partner, coating, or generic on a single dynamic domain.
  _onchange_suggest_template priority: part → customer → coating →
    none. No longer auto-picks a random global template when a part
    has its own.

Smoke-tested on VS-HSA201-B (Amphenol):
  5 canned variants seeded on the part form
  Wizard with this part auto-suggested the lowest-sequence one
  The part's Descriptions smart button shows "5"

Bulk data entry path for the client's 3,500 parts: either use the
inline list on each part form, or import via CSV with the new
part_catalog_id column (external_id or DB id).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsinghpal
2026-04-18 17:46:53 -04:00
parent f340c87b6a
commit a660f1f05d
5 changed files with 144 additions and 38 deletions

View File

@@ -94,12 +94,20 @@ class FpDirectOrderWizard(models.TransientModel):
notes = fields.Text(string='Internal Notes')
# Description template picker (searchable / ajax-like via Odoo Many2one)
# Description template picker — the domain is dynamically narrowed to
# this part's canned descriptions first. When no part is chosen it
# falls through to generic templates.
description_template_id = fields.Many2one(
'fp.sale.description.template',
string='Description Template',
help='Pick a saved boilerplate description and tweak it below. '
'The final text lands on the sale order line.',
domain="[('active','=',True), "
" '|', '|', '|', "
" ('part_catalog_id','=',part_catalog_id), "
" ('part_catalog_id','=',False), "
" ('partner_id','=',partner_id), "
" ('coating_config_id','=',coating_config_id)]",
help='Pick a saved description and tweak it below. Part-specific '
'descriptions appear first, then customer / coating / generic.',
)
line_description = fields.Text(
string='Line Description',
@@ -126,28 +134,55 @@ class FpDirectOrderWizard(models.TransientModel):
if self.description_template_id:
self.line_description = self.description_template_id.description
@api.onchange('coating_config_id', 'partner_id')
@api.onchange('part_catalog_id', 'coating_config_id', 'partner_id')
def _onchange_suggest_template(self):
"""Offer a sensible default template based on coating + customer."""
"""Offer a sensible default template — part-specific wins.
Priority (first non-empty result wins):
1. This part's lowest-sequence active template
2. This customer's templates (no part)
3. This coating's templates (no part)
4. Don't auto-pick — user has to choose
"""
if self.description_template_id or self.line_description:
return # respect user's choice
if not self.coating_config_id:
return
Template = self.env['fp.sale.description.template']
# Prefer customer+coating match, then coating-only
match = Template.search([
('active', '=', True),
('partner_id', '=', self.partner_id.id if self.partner_id else False),
('coating_config_id', '=', self.coating_config_id.id),
], limit=1)
if not match:
# 1. Part-specific
if self.part_catalog_id:
match = Template.search([
('active', '=', True),
('part_catalog_id', '=', self.part_catalog_id.id),
], order='sequence', limit=1)
if match:
self.description_template_id = match.id
self.line_description = match.description
return
# 2. Customer (no part)
if self.partner_id:
match = Template.search([
('active', '=', True),
('part_catalog_id', '=', False),
('partner_id', '=', self.partner_id.id),
], order='sequence', limit=1)
if match:
self.description_template_id = match.id
self.line_description = match.description
return
# 3. Coating (no part, no customer restriction)
if self.coating_config_id:
match = Template.search([
('active', '=', True),
('part_catalog_id', '=', False),
('partner_id', '=', False),
('coating_config_id', '=', self.coating_config_id.id),
], limit=1)
if match:
self.description_template_id = match.id
self.line_description = match.description
], order='sequence', limit=1)
if match:
self.description_template_id = match.id
self.line_description = match.description
return
@api.onchange('coating_config_id', 'quantity', 'partner_id')
def _onchange_lookup_price(self):