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:
@@ -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.'
|
||||
))
|
||||
|
||||
|
||||
@@ -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"/>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"/>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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.'
|
||||
|
||||
@@ -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 > 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}"
|
||||
|
||||
Reference in New Issue
Block a user