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:
@@ -5,7 +5,7 @@
|
||||
|
||||
{
|
||||
'name': 'Fusion Plating — Configurator',
|
||||
'version': '19.0.18.10.4',
|
||||
'version': '19.0.19.0.0',
|
||||
'category': 'Manufacturing/Plating',
|
||||
'summary': 'Quotation configurator with part catalog, coating configs, and formula-based pricing engine.',
|
||||
'description': """
|
||||
|
||||
@@ -66,10 +66,12 @@ class AccountMoveLine(models.Model):
|
||||
help='Copied from sale.order.line.',
|
||||
)
|
||||
x_fc_thickness_id = fields.Many2one(
|
||||
'fp.coating.thickness',
|
||||
'fp.recipe.thickness',
|
||||
string='Thickness',
|
||||
help='Copied from sale.order.line for customer-facing invoice PDFs.',
|
||||
)
|
||||
# x_fc_customer_spec_id is added by fusion_plating_quality (where
|
||||
# fusion.plating.customer.spec lives).
|
||||
x_fc_revision_snapshot = fields.Char(
|
||||
string='Revision (snapshot)',
|
||||
help='Revision letter from the source SO line.',
|
||||
|
||||
@@ -283,6 +283,8 @@ class FpPartCatalog(models.Model):
|
||||
help='Default coating applied when this part is dropped onto a '
|
||||
'direct order line. Updated when "Save as Default" is ticked.',
|
||||
)
|
||||
# x_fc_default_customer_spec_id is added by fusion_plating_quality
|
||||
# (where fusion.plating.customer.spec lives).
|
||||
x_fc_default_treatment_ids = fields.Many2many(
|
||||
'fp.treatment',
|
||||
relation='fp_part_catalog_default_treatment_rel',
|
||||
|
||||
@@ -62,6 +62,9 @@ class SaleOrderLine(models.Model):
|
||||
x_fc_coating_config_id = fields.Many2one(
|
||||
'fp.coating.config', string='Primary Treatment',
|
||||
)
|
||||
# x_fc_customer_spec_id is added by fusion_plating_quality (where
|
||||
# fusion.plating.customer.spec lives). Configurator can't reference
|
||||
# it directly without a circular dep.
|
||||
x_fc_treatment_ids = fields.Many2many(
|
||||
'fp.treatment', string='Additional Treatments',
|
||||
)
|
||||
@@ -308,12 +311,11 @@ class SaleOrderLine(models.Model):
|
||||
'order confirmation; editable. Blank is allowed.',
|
||||
)
|
||||
x_fc_thickness_id = fields.Many2one(
|
||||
'fp.coating.thickness',
|
||||
'fp.recipe.thickness',
|
||||
string='Thickness',
|
||||
ondelete='set null',
|
||||
domain="[('coating_config_id', '=', x_fc_coating_config_id)]",
|
||||
help="Target coating thickness. Options come from the line's "
|
||||
'coating configuration.',
|
||||
domain="[('recipe_id', '=', x_fc_process_variant_id)]",
|
||||
help="Target thickness. Options come from the line's recipe.",
|
||||
)
|
||||
x_fc_revision_snapshot = fields.Char(
|
||||
string='Revision (snapshot)',
|
||||
@@ -481,6 +483,8 @@ class SaleOrderLine(models.Model):
|
||||
vals['x_fc_thickness_id'] = self.x_fc_thickness_id.id
|
||||
if self.x_fc_revision_snapshot:
|
||||
vals['x_fc_revision_snapshot'] = self.x_fc_revision_snapshot
|
||||
# x_fc_customer_spec_id carry-over is handled by an
|
||||
# extension in fusion_plating_quality (the field lives there).
|
||||
return vals
|
||||
|
||||
@api.onchange('x_fc_part_catalog_id')
|
||||
@@ -498,6 +502,9 @@ class SaleOrderLine(models.Model):
|
||||
if line.x_fc_part_catalog_id and line.x_fc_part_catalog_id.default_process_id:
|
||||
line.x_fc_process_variant_id = line.x_fc_part_catalog_id.default_process_id
|
||||
|
||||
# Spec auto-fill onchange lives in fusion_plating_quality
|
||||
# (the customer.spec model lives there, so the inherit must too).
|
||||
|
||||
def _fp_clone_recipe_to_part(self):
|
||||
"""Deep-copy the picked recipe onto this line's part if it isn't
|
||||
already scoped there. Returns the cloned (or unchanged) variant.
|
||||
@@ -575,17 +582,17 @@ class SaleOrderLine(models.Model):
|
||||
'target': 'current',
|
||||
}
|
||||
|
||||
@api.onchange('x_fc_coating_config_id')
|
||||
def _onchange_coating_clears_thickness(self):
|
||||
"""Clear the thickness picker when coating config changes.
|
||||
@api.onchange('x_fc_process_variant_id')
|
||||
def _onchange_recipe_clears_thickness(self):
|
||||
"""Clear the thickness picker when recipe changes.
|
||||
|
||||
The thickness options are scoped to the coating config; a value
|
||||
carried over from a previous coating would fail its domain.
|
||||
Thickness options are scoped to the recipe; a value carried over
|
||||
from a previous recipe would fail its domain.
|
||||
"""
|
||||
for line in self:
|
||||
if (line.x_fc_thickness_id
|
||||
and line.x_fc_thickness_id.coating_config_id
|
||||
!= line.x_fc_coating_config_id):
|
||||
and line.x_fc_thickness_id.recipe_id
|
||||
!= line.x_fc_process_variant_id):
|
||||
line.x_fc_thickness_id = False
|
||||
|
||||
def action_generate_serial(self):
|
||||
|
||||
@@ -264,9 +264,9 @@
|
||||
optional="hide"/>
|
||||
<field name="x_fc_thickness_id"
|
||||
options="{'no_quick_create': True}"
|
||||
context="{'default_coating_config_id': x_fc_coating_config_id}"
|
||||
domain="[('coating_config_id', '=', x_fc_coating_config_id)]"
|
||||
invisible="not x_fc_coating_config_id"
|
||||
context="{'default_recipe_id': x_fc_process_variant_id}"
|
||||
domain="[('recipe_id', '=', x_fc_process_variant_id)]"
|
||||
invisible="not x_fc_process_variant_id"
|
||||
optional="show"/>
|
||||
<field name="x_fc_serial_ids"
|
||||
widget="many2many_tags"
|
||||
@@ -290,7 +290,7 @@
|
||||
<field name="x_fc_revision_snapshot"
|
||||
readonly="1"
|
||||
optional="hide"/>
|
||||
<field name="x_fc_treatment_ids" widget="many2many_tags" optional="hide"/>
|
||||
<field name="x_fc_treatment_ids" widget="many2many_tags" invisible="1"/>
|
||||
<field name="x_fc_part_deadline" string="Part Deadline Override" optional="hide"/>
|
||||
<field name="x_fc_part_deadline_offset_days" string="Days Offset" optional="hide"/>
|
||||
<field name="x_fc_effective_part_deadline" string="Effective Deadline"
|
||||
|
||||
@@ -60,6 +60,8 @@ class FpDirectOrderLine(models.Model):
|
||||
'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',
|
||||
@@ -176,6 +178,8 @@ class FpDirectOrderLine(models.Model):
|
||||
# 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
|
||||
# likely a first-time use of the part. Auto-tick the
|
||||
# push_to_defaults toggle so whatever Sarah picks becomes
|
||||
@@ -420,9 +424,9 @@ class FpDirectOrderLine(models.Model):
|
||||
rec.serial_ids = [(4, rec.serial_id.id)]
|
||||
job_number = fields.Char(string='Job #')
|
||||
thickness_id = fields.Many2one(
|
||||
'fp.coating.thickness',
|
||||
'fp.recipe.thickness',
|
||||
string='Thickness',
|
||||
domain="[('coating_config_id', '=', coating_config_id)]",
|
||||
domain="[('recipe_id', '=', process_variant_id)]",
|
||||
ondelete='set null',
|
||||
)
|
||||
|
||||
@@ -442,11 +446,11 @@ class FpDirectOrderLine(models.Model):
|
||||
and rec.quantity
|
||||
)
|
||||
|
||||
@api.onchange('coating_config_id')
|
||||
def _onchange_coating_clears_thickness(self):
|
||||
@api.onchange('process_variant_id')
|
||||
def _onchange_recipe_clears_thickness(self):
|
||||
for rec in self:
|
||||
if (rec.thickness_id
|
||||
and rec.thickness_id.coating_config_id != rec.coating_config_id):
|
||||
and rec.thickness_id.recipe_id != rec.process_variant_id):
|
||||
rec.thickness_id = False
|
||||
|
||||
def action_generate_serial(self):
|
||||
|
||||
@@ -575,6 +575,8 @@ class FpDirectOrderWizard(models.Model):
|
||||
'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_part_deadline': line.part_deadline,
|
||||
'x_fc_part_deadline_offset_days': line.part_deadline_offset_days,
|
||||
'x_fc_rush_order': line.rush_order,
|
||||
|
||||
@@ -176,9 +176,9 @@
|
||||
optional="hide"/>
|
||||
<field name="thickness_id"
|
||||
options="{'no_quick_create': True}"
|
||||
context="{'default_coating_config_id': coating_config_id}"
|
||||
domain="[('coating_config_id', '=', coating_config_id)]"
|
||||
invisible="not coating_config_id"
|
||||
context="{'default_recipe_id': process_variant_id}"
|
||||
domain="[('recipe_id', '=', process_variant_id)]"
|
||||
invisible="not process_variant_id"
|
||||
optional="show"/>
|
||||
<field name="serial_ids"
|
||||
widget="many2many_tags"
|
||||
@@ -196,7 +196,7 @@
|
||||
<field name="job_number" optional="hide"/>
|
||||
<field name="treatment_ids"
|
||||
widget="many2many_tags"
|
||||
optional="hide"/>
|
||||
invisible="1"/>
|
||||
<field name="quantity"
|
||||
optional="show"/>
|
||||
<field name="unit_price"
|
||||
|
||||
Reference in New Issue
Block a user