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:
@@ -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 3–5 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 {
|
||||
|
||||
@@ -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 3–5 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'),
|
||||
|
||||
Reference in New Issue
Block a user