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