feat(promote-customer-spec): Phase B — two-picker SO line UX

Spec-side picker (x_fc_customer_spec_id / customer_spec_id) added on:
- sale.order.line (via quality inherit — onchange autofill, create()
  fallback to part default, _prepare_invoice_line carry)
- account.move.line (via quality inherit — invoice rendering)
- fp.part.catalog (via quality inherit — x_fc_default_customer_spec_id)
- fp.direct.order.line (via quality inherit — wizard picker + autofill)
- fp.direct.order.wizard (action_create_order post-creates spec on SO line)

Thickness picker switched to fp.recipe.thickness (replaces coating-scoped):
- sale.order.line.x_fc_thickness_id comodel + domain rewired to recipe
- account.move.line + fp.delivery same
- fp.direct.order.line.thickness_id same

View inherits in quality add Specification picker next to legacy
Primary Treatment column on:
- SO form line tree
- part catalog Default Treatments block
- direct-order wizard line tree + drawer

Wizard files (fp.contract.review.client.email.wizard) pulled from
entech into the repo — they were ahead of the repo. Quality __init__
now imports wizards/.

Legacy x_fc_coating_config_id + treatment_ids remain visible during
transition; Phase E removes them.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsinghpal
2026-05-15 01:16:25 -04:00
parent c96f27b96c
commit 7cafab1b9f
23 changed files with 486 additions and 29 deletions

View File

@@ -10,6 +10,9 @@ from . import fp_calibration_event
from . import fp_avl
from . import fp_customer_spec
from . import fp_process_node_inherit
from . import sale_order_line_inherit
from . import account_move_line_inherit
from . import fp_direct_order_line_inherit
from . import fp_audit
from . import fp_fair
from . import fp_doc_control

View File

@@ -0,0 +1,23 @@
# -*- coding: utf-8 -*-
# Copyright 2026 Nexa Systems Inc.
# License OPL-1 (Odoo Proprietary License v1.0)
# Part of the Fusion Plating product family.
from odoo import fields, models
class AccountMoveLine(models.Model):
"""Add the Specification reference to the invoice line.
Lives here (not in configurator) because fusion.plating.customer.spec
lives in fusion_plating_quality and configurator can't reference it
without a circular dep.
"""
_inherit = 'account.move.line'
x_fc_customer_spec_id = fields.Many2one(
'fusion.plating.customer.spec',
string='Specification',
help='Carried from the SO line so the invoice PDF can render the '
'spec reference next to the part number.',
)

View File

@@ -0,0 +1,58 @@
# -*- coding: utf-8 -*-
# Copyright 2026 Nexa Systems Inc.
# License OPL-1 (Odoo Proprietary License v1.0)
# Part of the Fusion Plating product family.
from odoo import api, fields, models
class FpDirectOrderLine(models.Model):
"""Add the Specification picker to the direct-order wizard line.
Lives in fusion_plating_quality because fusion.plating.customer.spec
lives here.
"""
_inherit = 'fp.direct.order.line'
customer_spec_id = fields.Many2one(
'fusion.plating.customer.spec',
string='Specification',
help='Customer / industry specification the work ships to. '
'Carried onto the SO line at order creation.',
)
@api.onchange('part_catalog_id')
def _onchange_part_default_spec(self):
"""Pre-fill the line's specification from the part's default."""
for rec in self:
if (rec.part_catalog_id
and rec.part_catalog_id.x_fc_default_customer_spec_id
and not rec.customer_spec_id):
rec.customer_spec_id = (
rec.part_catalog_id.x_fc_default_customer_spec_id
)
class FpDirectOrderWizard(models.Model):
_inherit = 'fp.direct.order.wizard'
def action_create_order(self):
"""Carry customer_spec_id from each wizard line to its SO line.
The base method (in configurator) builds the SO with all the
coating/treatment/process fields. We can't insert spec into the
vals dict from here without a circular dep, so post-create we
pair wizard lines to SO lines by sequence and patch.
"""
result = super().action_create_order()
if self.sale_order_id:
wiz_lines = self.line_ids.sorted(
key=lambda r: (r.sequence, r.id)
)
so_lines = self.sale_order_id.order_line.sorted(
key=lambda r: (r.sequence, r.id)
)
for wiz_line, so_line in zip(wiz_lines, so_lines):
if wiz_line.customer_spec_id and not so_line.x_fc_customer_spec_id:
so_line.x_fc_customer_spec_id = wiz_line.customer_spec_id.id
return result

View File

@@ -14,6 +14,12 @@ _logger = logging.getLogger(__name__)
class FpPartCatalog(models.Model):
_inherit = 'fp.part.catalog'
x_fc_default_customer_spec_id = fields.Many2one(
'fusion.plating.customer.spec',
string='Default Specification',
help='Default specification applied when this part is dropped on '
'a direct order line. Operator can override per order.',
)
x_fc_contract_review_id = fields.Many2one(
'fp.contract.review',
string='Contract Review',

View File

@@ -0,0 +1,61 @@
# -*- coding: utf-8 -*-
# Copyright 2026 Nexa Systems Inc.
# License OPL-1 (Odoo Proprietary License v1.0)
# Part of the Fusion Plating product family.
from odoo import api, fields, models
class SaleOrderLine(models.Model):
"""Add the Specification picker to the SO line.
Lives in fusion_plating_quality because fusion.plating.customer.spec
lives here. Configurator can't reference it directly without a
circular dep (quality depends on configurator).
"""
_inherit = 'sale.order.line'
x_fc_customer_spec_id = fields.Many2one(
'fusion.plating.customer.spec',
string='Specification',
help='Customer / industry specification the work is being shipped '
'to (e.g. AMS 2404 Rev D, BAC 5680 Rev E). Drives certificate '
'auto-fill and FAI / Nadcap routing.',
)
@api.onchange('x_fc_part_catalog_id')
def _onchange_part_default_spec(self):
"""Pre-fill the line's specification from the part's default."""
for line in self:
if (line.x_fc_part_catalog_id
and line.x_fc_part_catalog_id.x_fc_default_customer_spec_id
and not line.x_fc_customer_spec_id):
line.x_fc_customer_spec_id = (
line.x_fc_part_catalog_id.x_fc_default_customer_spec_id
)
@api.model_create_multi
def create(self, vals_list):
"""Fall back to the part's default spec when none is supplied.
Catches programmatic creation paths (wizard, import, sale_mrp
bridge) where the onchange doesn't fire. Explicit spec wins;
only fills when blank AND the part has a default.
"""
Part = self.env['fp.part.catalog']
for vals in vals_list:
if (not vals.get('x_fc_customer_spec_id')
and vals.get('x_fc_part_catalog_id')):
part = Part.browse(vals['x_fc_part_catalog_id']).exists()
if part and part.x_fc_default_customer_spec_id:
vals['x_fc_customer_spec_id'] = (
part.x_fc_default_customer_spec_id.id
)
return super().create(vals_list)
def _prepare_invoice_line(self, **optional_values):
"""Carry x_fc_customer_spec_id to the invoice line."""
vals = super()._prepare_invoice_line(**optional_values)
if self.x_fc_customer_spec_id:
vals['x_fc_customer_spec_id'] = self.x_fc_customer_spec_id.id
return vals