feat(configurator): 19.0.22.1.0 — recipe-driven orders + auto-sync to part

Four customer-feedback fixes (G1-G4):

G1 — Part cell display redundancy. fp.part.catalog.display_name was
showing 'PART (Rev X) — Name' which duplicated with my Part cell widget's
separately-rendered revision + name rows. Added @api.depends_context
('fp_express_part_picker') to _compute_display_name: when the context
flag is True, display_name returns JUST the part_number. The Express
view passes the flag on the part_catalog_id field, so the picker shows
'9876699373' and the widget's row 2/3 show the rev + name.

G2 — Material/Process Tag is now the order's RECIPE, not a free-text
shop tag. Converted material_process from Char to Many2One(fusion.
plating.process.node) with domain [('node_type','=','recipe')] on both
fp.direct.order.wizard AND sale.order. Pre-migration (19.0.22.1.0/
pre-migrate.py) drops the old VARCHAR column so Odoo recreates as
INTEGER FK. Per dev-stage policy, old tag data is dropped.

G3 — Auto-apply order recipe to every line. New onchange
_onchange_material_process_apply_to_lines on the wizard: when the
header recipe is picked / changed, propagate to every line's
process_variant_id (unless the line has an explicit per-line override
that doesn't match the previous header value).

Plus an override on fp.direct.order.line.create that seeds new lines'
process_variant_id from wizard.material_process. So a newly-added
line auto-inherits the order's recipe.

G4 — Auto-sync line edits back to the part catalog. New
_fp_sync_to_part method called from create() + write() on
fp.direct.order.line. Tracked fields:
- line_description     → part.default_specification_text
- bake_instructions    → part.default_bake_instructions
- thickness_range      → part.x_fc_default_thickness_range
- masking_enabled      → part.default_masking_enabled
- process_variant_id   → part.default_process_variant_id

Future orders for the same part will auto-pull these updated defaults
via the existing _onchange_part_default_thickness chain. Last-write-
wins semantics across concurrent edits (acceptable per dev-stage).
This commit is contained in:
gsinghpal
2026-05-26 23:20:27 -04:00
parent 4c5ee6143c
commit 48c2a4bfe1
8 changed files with 190 additions and 10 deletions

View File

@@ -583,6 +583,97 @@ class FpDirectOrderLine(models.Model):
mask_default = getattr(part, 'default_masking_enabled', True)
rec.masking_enabled = mask_default
# ---- Express Orders: auto-sync line edits to fp.part.catalog ----
# Fields tracked for write-back. When any of these change on a wizard
# line, the corresponding default on the part is updated so future
# orders pre-fill from the latest values.
_FP_PART_SYNC_FIELDS = {
'line_description': 'default_specification_text',
'bake_instructions': 'default_bake_instructions',
'thickness_range': 'x_fc_default_thickness_range',
'masking_enabled': 'default_masking_enabled',
'process_variant_id': 'default_process_variant_id',
}
def _fp_sync_to_part(self):
"""Push tracked line fields back to the linked part's defaults.
Called from create + write. Last-write-wins semantics — if two
orders simultaneously edit the same part, the later one's values
become the part's defaults. Acceptable per dev-stage policy;
the part chatter records the change either way.
"""
for rec in self:
part = rec.part_catalog_id
if not part:
continue
wb_vals = {}
for line_field, part_field in self._FP_PART_SYNC_FIELDS.items():
if part_field not in part._fields:
continue
line_val = rec[line_field]
# For M2O, normalise to id
if hasattr(line_val, 'id'):
line_val = line_val.id
# Booleans always write; text fields only when non-empty
if isinstance(line_val, bool):
pass
elif not line_val:
continue
# Only write if the part's current value differs
current = part[part_field]
if hasattr(current, 'id'):
current = current.id
if current == line_val:
continue
wb_vals[part_field] = line_val
if wb_vals:
part.sudo().with_context(
fp_skip_chatter_spam=True,
).write(wb_vals)
@api.model_create_multi
def create(self, vals_list):
"""Auto-apply order-level recipe to new lines + sync to part on create.
Recipe propagation: when estimator adds a line to an order that
already has a Material/Process recipe picked at the header, the
line inherits that recipe automatically.
Part sync: any line values typed during create immediately
write back to the linked part's defaults.
"""
# 1. Recipe propagation from wizard.material_process
Wizard = self.env['fp.direct.order.wizard']
wizard_cache = {}
for vals in vals_list:
if vals.get('process_variant_id'):
continue
wiz_id = vals.get('wizard_id') or self.env.context.get('default_wizard_id')
if not wiz_id:
continue
if wiz_id not in wizard_cache:
wiz = Wizard.browse(wiz_id).exists()
wizard_cache[wiz_id] = (wiz.material_process.id
if wiz and wiz.material_process
else False)
recipe_id = wizard_cache[wiz_id]
if recipe_id:
vals['process_variant_id'] = recipe_id
# 2. Create the lines
records = super().create(vals_list)
# 3. Push to part defaults
records._fp_sync_to_part()
return records
def write(self, vals):
"""Auto-sync line edits to the linked part's defaults."""
res = super().write(vals)
sync_keys = set(self._FP_PART_SYNC_FIELDS.keys())
if sync_keys & set(vals.keys()) or 'part_catalog_id' in vals:
self._fp_sync_to_part()
return res
def action_open_part(self):
"""Open the linked fp.part.catalog form in a modal."""
self.ensure_one()

View File

@@ -165,6 +165,33 @@ class FpDirectOrderWizard(models.Model):
"""Default pricelist = company's default. Re-resolved on partner pick."""
return self.env.company.partner_id.property_product_pricelist.id or False
# ---- Express Orders: auto-apply order recipe to all lines ----
@api.onchange('material_process')
def _onchange_material_process_apply_to_lines(self):
"""When the order-level recipe is picked / changed, propagate it to
every line's process_variant_id so the operator doesn't have to
set the recipe line-by-line.
Lines that have explicitly overridden their recipe AFTER this
onchange last fired won't be clobbered — we only update lines
whose current process_variant_id is empty OR matches the PREVIOUS
material_process value (i.e. they inherited from the header and
haven't been customised).
"""
for rec in self:
new_recipe = rec.material_process
if not new_recipe:
continue
for line in rec.line_ids:
# Skip lines that have a different recipe explicitly set
# (i.e. the operator chose a per-line override).
if (line.process_variant_id
and line.process_variant_id != new_recipe
and line.process_variant_id
!= rec._origin.material_process):
continue
line.process_variant_id = new_recipe
# ---- View switching (Express ↔ Legacy) ----
def action_switch_to_express(self):
"""Re-open this draft in the Express view."""
@@ -255,10 +282,18 @@ class FpDirectOrderWizard(models.Model):
help='Visible only to the estimator / planner / shop. Never prints.',
)
# ---- Express Orders header (2026-05-26) ----
material_process = fields.Char(
# ---- Express Orders header (2026-05-26 / 2026-05-27 redesign) ----
# Material/Process Tag IS the recipe: when set on the order header,
# every line auto-uses this recipe (unless the line explicitly
# overrides via its own process_variant_id). Was a Char tag until
# 19.0.22.1.0 — converted to Many2One per customer feedback.
material_process = fields.Many2one(
'fusion.plating.process.node',
string='Material / Process Tag',
help='Free-text shop tag (e.g. ENP-STEEL-HP-ADVANCED). Informational.',
domain="[('node_type', '=', 'recipe')]",
help='Pick a recipe — applies automatically to every line on this '
'order. Individual lines can still override via their own '
'Process / Recipe column.',
)
validity_date = fields.Date(
string='Quote Validity',