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

@@ -190,6 +190,17 @@ class FpPartCatalog(models.Model):
revision_count = fields.Integer(
string='Revisions', compute='_compute_revision_count',
)
description_template_ids = fields.One2many(
'fp.sale.description.template', 'part_catalog_id',
string='Saved Descriptions',
help='Canned descriptions for this specific part. When an order is '
'created for this part, these show up first in the picker. '
'Typically 35 variants per part covering different masking, '
'packaging, or spec callouts.',
)
description_template_count = fields.Integer(
compute='_compute_description_template_count',
)
_sql_constraints = [
('fp_part_catalog_partner_partnum_uniq', 'unique(partner_id, part_number)',
@@ -301,6 +312,10 @@ class FpPartCatalog(models.Model):
'|', ('id', '=', root.id), ('parent_part_id', '=', root.id),
])
def _compute_description_template_count(self):
for part in self:
part.description_template_count = len(part.description_template_ids)
def action_view_customer(self):
self.ensure_one()
return {

View File

@@ -7,21 +7,28 @@ from odoo import fields, models
class FpSaleDescriptionTemplate(models.Model):
"""Reusable boilerplate descriptions for sale.order.line items.
"""Saved description snippets — most often attached to a specific part.
Plating shops run the same customer part over and over with small
variations (masking rules, special handling, packaging). Instead of
retyping or half-remembering — the description every time, the
estimator picks a named template here, tweaks it, and the tweaked
text lands on the SO line as its description.
Real-world usage: a plating shop keeps 35 canned descriptions PER
PART because the same customer part runs with different masking,
packaging, or spec-callout variations. With 3,500 parts and 5
variants each, that's ~17,500 rows — so descriptions are scoped
primarily by part, with optional fallback to customer / coating /
global.
When a user creates a new order:
1. If a part is picked, show templates for that part first.
2. Else show templates for the customer.
3. Else show templates for the coating.
4. Else show global (generic) templates.
"""
_name = 'fp.sale.description.template'
_description = 'Fusion Plating — Sale Order Line Description Template'
_order = 'sequence, name'
_order = 'part_catalog_id, sequence, name'
name = fields.Char(
string='Template Name', required=True,
help='Short name shown in the picker (e.g. "ENP — Standard Aluminium").',
help='Short name shown in the picker (e.g. "Standard masking", "With threaded holes masked").',
)
description = fields.Text(
string='Description', required=True,
@@ -29,16 +36,25 @@ class FpSaleDescriptionTemplate(models.Model):
'it lands on the order line.',
)
sequence = fields.Integer(default=10)
part_catalog_id = fields.Many2one(
'fp.part.catalog', string='Part',
ondelete='cascade', index=True,
help='If set, this description belongs to one specific customer '
'part — it only appears in the picker when this part is on '
'the order. Leave blank for generic fallback templates.',
)
# Related fields — surface the part's partner/coating for search &
# grouping without writing them twice.
partner_id = fields.Many2one(
'res.partner', string='Customer',
related='part_catalog_id.partner_id', store=True, readonly=True,
)
# Keep the explicit coating slot for global templates that aren't
# part-specific but are still coating-specific.
coating_config_id = fields.Many2one(
'fp.coating.config', string='Associated Coating',
ondelete='set null',
help='If set, this template is offered first when this coating is '
'chosen on the order.',
)
partner_id = fields.Many2one(
'res.partner', string='Customer (optional)',
ondelete='set null',
help='If set, restrict this template to a specific customer.',
help='For generic (no-part) templates, restrict to one coating.',
)
tag = fields.Selection(
[('standard', 'Standard'),