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

@@ -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.',

View File

@@ -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',

View File

@@ -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):