feat(promote-customer-spec): Phase E — final removal of coating + treatment

DELETED entirely (model + view + ACL + data file + menu):
- fp.coating.config (configurator)
- fp.treatment (configurator + seeded data)
- fp.coating.thickness (configurator) — replaced by fp.recipe.thickness in Phase A
- fp.customer.price.list (configurator) — coating-keyed, no replacement

Field deletions:
- sale.order.x_fc_coating_config_id
- sale.order.line.x_fc_coating_config_id + x_fc_treatment_ids
- account.move.line.x_fc_coating_config_id
- fp.part.catalog.x_fc_default_coating_config_id + x_fc_default_treatment_ids
- fp.job.coating_config_id
- fp.pricing.rule.coating_config_id
- fp.quality.point.coating_config_ids
- fp.direct.order.line.coating_config_id + treatment_ids
- fp.sale.description.template.coating_config_id

Refactored:
- fp.quote.configurator.coating_config_id → recipe_id (now points at
  fusion.plating.process.node, the actual recipe). All compute, onchange,
  and matcher logic updated to use recipe directly. Quality inherit
  extends matcher with spec-tier scoring.
- fp.job._fp_create_certificates now reads spec from job.customer_spec_id
  and formats spec_reference as "code Rev rev". Same for thickness
  source — bake fields read from recipe_root (Phase A).
- fp.job.step.button_finish bake-window auto-spawn reads bake settings
  from recipe_root instead of coating.
- fp.certificate auto-fill spec_min_mils/max_mils from recipe (Phase A
  thickness fields) instead of coating.
- jobs/sale_order.py: job creation reads x_fc_customer_spec_id from
  line, drops coating refs and the legacy header-coating fallback.
- Wizards drop coating + treatment fields and refs.
- Configurator views drop x_fc_coating_config_id + x_fc_treatment_ids
  fields entirely. Quality inherits re-anchor on stable fields
  (x_fc_part_catalog_id, x_fc_internal_description, default_process_id,
  process_variant_id, substrate_material) so they keep working.
- Reports drop coating fallback elifs; print recipe / spec.
- Tablet payload drops coating_config_id from job.read fields.

Skipped (deferred to backlog):
- fusion_plating_bridge_mrp — module is uninstalled per Sub 11; source
  files retain coating refs but no runtime impact.
- fusion_plating_portal — circular dep (portal → quality → certs →
  portal). Customer-facing portal coating picker stays for now;
  promote-spec polish is a separate sub-project.

Verification: grep for "coating_config_id|fp.coating.config|
fp.treatment|fp.coating.thickness" in live (non-bridge_mrp,
non-portal, non-script, non-test) Python/XML/CSV returns 3 hits,
all in module / class docstrings explaining Phase E history.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsinghpal
2026-05-15 02:00:41 -04:00
parent e0eacc2530
commit d891002c84
54 changed files with 233 additions and 1283 deletions

View File

@@ -43,14 +43,14 @@ class FpAddFromQuoteWizard(models.TransientModel):
wizard = self.direct_order_wizard_id
copied = 0
for q in self.quote_ids:
if not q.part_catalog_id or not q.coating_config_id:
if not q.part_catalog_id or not q.recipe_id:
continue
Line._create_from_quote(q, wizard)
copied += 1
if not copied:
raise UserError(_(
'The selected quotes do not have both part and coating set, '
'The selected quotes do not have both part and recipe set, '
'so nothing could be copied.'
))

View File

@@ -22,7 +22,7 @@
<list>
<field name="name"/>
<field name="part_catalog_id"/>
<field name="coating_config_id"/>
<field name="recipe_id"/>
<field name="quantity"/>
<field name="calculated_price" widget="monetary"/>
<field name="estimator_override_price" widget="monetary"/>

View File

@@ -53,14 +53,12 @@ class FpAddFromSoWizard(models.TransientModel):
wizard = self.direct_order_wizard_id
copied = 0
for src in self.source_line_ids:
if not src.x_fc_part_catalog_id or not src.x_fc_coating_config_id:
# Skip SO lines that predate the plating fields
if not src.x_fc_part_catalog_id:
# Skip non-plating SO lines
continue
Line.create({
'wizard_id': wizard.id,
'part_catalog_id': src.x_fc_part_catalog_id.id,
'coating_config_id': src.x_fc_coating_config_id.id,
'treatment_ids': [(6, 0, src.x_fc_treatment_ids.ids)],
'quantity': int(src.product_uom_qty) or 1,
'unit_price': src.price_unit or 0.0,
'part_deadline': src.x_fc_part_deadline,

View File

@@ -27,7 +27,7 @@
<list>
<field name="name"/>
<field name="x_fc_part_catalog_id"/>
<field name="x_fc_coating_config_id"/>
<field name="x_fc_part_deadline" optional="hide"/>
<field name="product_uom_qty"/>
<field name="price_unit"/>
<field name="x_fc_part_deadline"/>

View File

@@ -51,22 +51,9 @@ class FpDirectOrderLine(models.Model):
new_drawing_filename = fields.Char(string='Filename')
revision_note = fields.Char(string='Revision Note')
# ---- Treatments ----
coating_config_id = fields.Many2one(
'fp.coating.config',
string='Primary Treatment',
help='Optional. Some orders are non-coating work (re-inspection, '
'rework, masking-only, etc.) and the operator picks the '
'workflow downstream — leaving this blank lets that path '
'through.',
)
# customer_spec_id is added by fusion_plating_quality (where
# fusion.plating.customer.spec lives).
treatment_ids = fields.Many2many(
'fp.treatment',
string='Additional Treatments',
help='Extra pre/post treatments applied to this line.',
)
# Specification picker (customer_spec_id) added by
# fusion_plating_quality. Legacy coating_config_id +
# treatment_ids removed.
# Sub 9 (polished 2026-04-28) — process variant per line. The picker
# now lets the estimator pick ANY root recipe in the system: the
# part's own variants, another customer's variants, or a template
@@ -107,8 +94,7 @@ class FpDirectOrderLine(models.Model):
)
@api.depends('process_variant_id',
'part_catalog_id.default_process_id',
'coating_config_id.recipe_id')
'part_catalog_id.default_process_id')
def _compute_effective_process(self):
for rec in self:
if rec.process_variant_id:
@@ -122,12 +108,6 @@ class FpDirectOrderLine(models.Model):
rec.effective_process_id = part_proc
rec.effective_process_source = 'Part default'
continue
cc_proc = (rec.coating_config_id.recipe_id
if rec.coating_config_id else False)
if cc_proc:
rec.effective_process_id = cc_proc
rec.effective_process_source = 'Coating default'
continue
rec.effective_process_id = False
rec.effective_process_source = False
@@ -168,35 +148,26 @@ class FpDirectOrderLine(models.Model):
if not rec.part_catalog_id:
continue
part = rec.part_catalog_id
has_default_coating = bool(getattr(
part, 'x_fc_default_coating_config_id', False))
has_default_treatments = bool(getattr(
part, 'x_fc_default_treatment_ids', False))
# Pre-fill default coating if the line is empty.
if not rec.coating_config_id and has_default_coating:
rec.coating_config_id = part.x_fc_default_coating_config_id
# Pre-fill default treatments if any are configured.
if not rec.treatment_ids and has_default_treatments:
rec.treatment_ids = [(6, 0, part.x_fc_default_treatment_ids.ids)]
# Default-spec auto-fill is implemented by an inherit in
# fusion_plating_quality (where customer_spec_id field lives).
# New-part auto-suggest: if neither default exists, this is
has_default_spec = bool(getattr(
part, 'x_fc_default_customer_spec_id', False))
# New-part auto-suggest: if no default spec exists, this is
# likely a first-time use of the part. Auto-tick the
# push_to_defaults toggle so whatever Sarah picks becomes
# the saved default — surface a warning popup so she knows.
# `is_one_off` always wins (operator opted out of catalog
# persistence), so don't auto-tick in that case.
if (not has_default_coating
and not has_default_treatments
if (not has_default_spec
and not rec.is_one_off
and not rec.push_to_defaults):
rec.push_to_defaults = True
warning = {
'title': _('First-Time Part — Defaults Will Be Saved'),
'message': _(
'%(part)s has no saved coating / treatments. '
'The coating + treatments you pick on this line '
'will be saved as the part\'s defaults so the '
'%(part)s has no saved specification. '
'The specification you pick on this line will '
'be saved as the part\'s default so the '
'next order auto-fills them. Untick "Save as '
'Default" on the line if you don\'t want this.'
) % {'part': part.display_name or part.part_number or '(part)'},
@@ -269,11 +240,11 @@ class FpDirectOrderLine(models.Model):
start_at_node_id = fields.Many2one(
'fusion.plating.process.node',
string='Start at Node',
domain="[('id', 'child_of', coating_config_id and coating_config_id.recipe_id.id or 0)]",
domain="[('id', 'child_of', process_variant_id and process_variant_id.id or 0)]",
help='For re-work jobs: pick the recipe step where this job should '
'begin. Pick a coating first — nodes are scoped to its '
'recipe tree. Skips earlier steps in the generated WO but '
'keeps later siblings and sub-processes.',
'begin. Pick a recipe first — nodes are scoped to it. Skips '
'earlier steps in the generated WO but keeps later siblings '
'and sub-processes.',
)
is_one_off = fields.Boolean(
string='One-off Part',
@@ -436,12 +407,11 @@ class FpDirectOrderLine(models.Model):
for rec in self:
rec.line_subtotal = (rec.quantity or 0) * (rec.unit_price or 0.0)
@api.depends('part_catalog_id', 'coating_config_id', 'unit_price', 'quantity')
@api.depends('part_catalog_id', 'unit_price', 'quantity')
def _compute_is_missing_info(self):
for rec in self:
rec.is_missing_info = not (
rec.part_catalog_id
and rec.coating_config_id
and rec.unit_price
and rec.quantity
)
@@ -499,14 +469,16 @@ class FpDirectOrderLine(models.Model):
# ---- Onchange ----
@api.onchange('quote_id')
def _onchange_quote_id(self):
"""Auto-fill part, coating, and unit price from the linked quote."""
"""Auto-fill part and unit price from the linked quote.
Spec carry-over from quote → wizard line is handled by an
inherit in fusion_plating_quality.
"""
if not self.quote_id:
return
q = self.quote_id
if q.part_catalog_id and not self.part_catalog_id:
self.part_catalog_id = q.part_catalog_id
if q.coating_config_id and not self.coating_config_id:
self.coating_config_id = q.coating_config_id
if not self.unit_price:
final = q.estimator_override_price or q.calculated_price
if final and q.quantity:
@@ -514,13 +486,13 @@ class FpDirectOrderLine(models.Model):
@api.onchange('part_catalog_id')
def _onchange_part_defaults(self):
"""When a part is picked, seed coating + treatments from its catalog defaults."""
"""Seed defaults when a part is picked.
Spec auto-fill is handled by an inherit in fusion_plating_quality
(the customer_spec_id field lives there).
"""
if not self.part_catalog_id:
return
if not self.coating_config_id and self.part_catalog_id.x_fc_default_coating_config_id:
self.coating_config_id = self.part_catalog_id.x_fc_default_coating_config_id
if not self.treatment_ids and self.part_catalog_id.x_fc_default_treatment_ids:
self.treatment_ids = self.part_catalog_id.x_fc_default_treatment_ids
# Seed default taxes from the FP-SERVICE product, fiscal-position
# mapped from the customer. Only fills when the user hasn't set
# taxes manually.
@@ -543,21 +515,10 @@ class FpDirectOrderLine(models.Model):
if taxes:
self.tax_ids = [(6, 0, taxes.ids)]
@api.onchange('coating_config_id', 'quantity', 'part_catalog_id')
def _onchange_lookup_price(self):
"""Auto-fill unit_price from customer price list when available."""
if self.unit_price:
return
partner = self.wizard_id.partner_id
if not (partner and self.coating_config_id):
return
price = self.env['fp.customer.price.list']._find_price(
partner.id,
self.coating_config_id.id,
quantity=self.quantity or 1,
)
if price:
self.unit_price = price.unit_price
# Auto-fill unit_price from a customer price list — extended in
# fusion_plating_quality (the spec field lives there). The base
# configurator wizard no longer triggers price lookup since
# coating_config_id is gone.
@api.onchange('description_template_id')
def _onchange_description_template(self):
@@ -575,15 +536,14 @@ class FpDirectOrderLine(models.Model):
if tpl.internal_description:
self.internal_description = tpl.internal_description
@api.onchange('part_catalog_id', 'coating_config_id')
@api.onchange('part_catalog_id')
def _onchange_suggest_template(self):
"""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
3. Don't auto-pick — user has to choose
"""
if self.description_template_id or self.line_description:
return
@@ -616,16 +576,6 @@ class FpDirectOrderLine(models.Model):
_apply(match)
return
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),
], order='sequence', limit=1)
if match:
_apply(match)
# ---- Helpers ----
@api.model
def _create_from_quote(self, quote, wizard):
@@ -635,16 +585,17 @@ class FpDirectOrderLine(models.Model):
the bulk "Add From Quotes" sub-wizard — keeps the field mapping
in one place so the two flows can never drift.
"""
if not quote.part_catalog_id or not quote.coating_config_id:
if not quote.part_catalog_id:
raise UserError(_(
'Quote %s has no part or coating set; cannot seed a line.'
'Quote %s has no part set; cannot seed a line.'
) % (quote.name or quote.id))
final = quote.estimator_override_price or quote.calculated_price
unit = (final / quote.quantity) if (final and quote.quantity) else 0.0
# Spec carry-over from quote → wizard line is handled by an
# inherit in fusion_plating_quality (customer_spec_id field).
return self.create({
'wizard_id': wizard.id,
'part_catalog_id': quote.part_catalog_id.id,
'coating_config_id': quote.coating_config_id.id,
'quantity': int(quote.quantity) or 1,
'unit_price': unit,
'quote_id': quote.id,

View File

@@ -550,12 +550,13 @@ class FpDirectOrderWizard(models.Model):
for line in self.line_ids:
part = line._get_or_bump_revision()
resolved_parts[line.id] = part
# Build the line header. Primary treatment is optional now;
# when missing, drop it from the header rather than printing
# Build the line header. Specification is optional; when
# missing, drop it from the header rather than printing
# "False - PartName Rev A".
treatment_label = line.coating_config_id.name or _('No coating')
spec = getattr(line, 'customer_spec_id', False)
spec_label = (spec.display_name if spec else '') or _('No spec')
header = '%s - %s Rev %s (x%d)' % (
treatment_label,
spec_label,
part.name,
part.revision,
line.quantity,
@@ -573,10 +574,9 @@ class FpDirectOrderWizard(models.Model):
'x_fc_part_catalog_id': part.id,
'x_fc_description_template_id': line.description_template_id.id or False,
'x_fc_internal_description': line.internal_description or False,
'x_fc_coating_config_id': line.coating_config_id.id,
'x_fc_treatment_ids': [(6, 0, line.treatment_ids.ids)],
# x_fc_customer_spec_id is added to vals by an extension
# of this method in fusion_plating_quality.
# x_fc_customer_spec_id is set on the resulting SO line
# by an extension in fusion_plating_quality (post-create
# patch — see fp_direct_order_line_inherit.py).
'x_fc_part_deadline': line.part_deadline,
'x_fc_part_deadline_offset_days': line.part_deadline_offset_days,
'x_fc_rush_order': line.rush_order,
@@ -630,19 +630,9 @@ class FpDirectOrderWizard(models.Model):
'Quote won — promoted onto Direct Order %(doo)s, SO %(so)s.'
) % {'doo': self.name, 'so': so.name})
# 6. Push-to-defaults (C4) — uses the resolved part cached
# during the build loop so rev-bumped lines write defaults to
# the NEW revision, not the pre-bump one.
for line in self.line_ids:
if not line.push_to_defaults or line.is_one_off:
continue
part = resolved_parts.get(line.id) or line.part_catalog_id
if not part:
continue
part.write({
'x_fc_default_coating_config_id': line.coating_config_id.id or False,
'x_fc_default_treatment_ids': [(6, 0, line.treatment_ids.ids)],
})
# 6. Push-to-defaults — Specification carry-over to the part's
# x_fc_default_customer_spec_id is handled by an inherit in
# fusion_plating_quality (the field lives there).
so.message_post(body=_(
'Quotation created from PO %s with %d line(s). '
'Review and confirm manually when ready.'

View File

@@ -154,8 +154,6 @@
optional="hide"/>
<field name="internal_description"
optional="hide"/>
<field name="coating_config_id"
optional="show"/>
<field name="process_variant_id"
string="Process / Recipe"
options="{'no_quick_create': True}"
@@ -194,9 +192,6 @@
class="btn-link"
invisible="not part_catalog_id or serial_count &gt; 0"/>
<field name="job_number" optional="hide"/>
<field name="treatment_ids"
widget="many2many_tags"
invisible="1"/>
<field name="quantity"
optional="show"/>
<field name="unit_price"
@@ -239,9 +234,6 @@
invisible="not part_catalog_id"/>
<field name="part_revision"
invisible="not part_catalog_id"/>
<field name="coating_config_id"/>
<field name="treatment_ids"
widget="many2many_tags"/>
<field name="process_variant_id"
string="Process / Recipe"
options="{'no_quick_create': True}"