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