diff --git a/fusion_plating/fusion_plating_jobs/__manifest__.py b/fusion_plating/fusion_plating_jobs/__manifest__.py
index d272418a..98fbf528 100644
--- a/fusion_plating/fusion_plating_jobs/__manifest__.py
+++ b/fusion_plating/fusion_plating_jobs/__manifest__.py
@@ -3,7 +3,7 @@
# License OPL-1 (Odoo Proprietary License v1.0)
{
'name': 'Fusion Plating - Native Jobs',
- 'version': '19.0.12.5.0',
+ 'version': '19.0.12.6.0',
'category': 'Manufacturing/Plating',
'summary': 'Native plating job model - replaces mrp.production / mrp.workorder bridge.',
'author': 'Nexa Systems Inc.',
diff --git a/fusion_plating/fusion_plating_jobs/models/fp_job.py b/fusion_plating/fusion_plating_jobs/models/fp_job.py
index f2f522a9..f5fc0494 100644
--- a/fusion_plating/fusion_plating_jobs/models/fp_job.py
+++ b/fusion_plating/fusion_plating_jobs/models/fp_job.py
@@ -1385,6 +1385,70 @@ class FpJob(models.Model):
# - Drops work_role_id (not on fp.job.step yet - Task 2.6+)
# - Drops _fp_autofill_default_equipment (not yet on step)
# ------------------------------------------------------------------
+ def _fp_resync_recipe_from_so(self):
+ """Re-resolve the recipe from this job's SO line(s) and build its
+ steps. Heals work orders created before the recipe was set on the
+ SO line (new parts, or the copy-from-quote path): once the
+ estimator sets the process variant on the line, the WO can pull it
+ in. Acts only on jobs with NO steps yet and not terminal, so
+ in-progress work is never disturbed. Idempotent. Returns True when
+ steps were generated.
+ """
+ self.ensure_one()
+ if self.step_ids or self.state in ('done', 'cancelled'):
+ return False
+ so = self.sale_order_id
+ if not so:
+ return False
+ recipe = False
+ for line in self.sale_order_line_ids:
+ recipe = so._fp_resolve_recipe_for_line(line)
+ if recipe:
+ break
+ if not recipe:
+ return False
+ if self.recipe_id != recipe:
+ self.recipe_id = recipe.id
+ # Mirror action_confirm's post-recipe sequence so a healed WO
+ # matches a normally-confirmed one (steps, ready, express text).
+ self._generate_steps_from_recipe()
+ pending = self.step_ids.filtered(lambda s: s.state == 'pending')
+ if pending:
+ pending.write({'state': 'ready'})
+ if (self.recipe_id and self.step_ids
+ and 'x_fc_masking_enabled' in self.env['sale.order.line']._fields):
+ for sol in self.sale_order_line_ids:
+ if hasattr(sol, '_fp_apply_express_overrides_to_job'):
+ sol._fp_apply_express_overrides_to_job(self)
+ self.message_post(body=_(
+ 'Recipe re-synced from the sale order (%(recipe)s); %(n)d '
+ 'step(s) generated.'
+ ) % {'recipe': recipe.display_name, 'n': len(self.step_ids)})
+ return True
+
+ def action_fp_resync_recipe_from_so(self):
+ """Header button: re-pull the recipe from the SO and build steps
+ for work orders that came out empty because the recipe was set on
+ the SO line after the WO was created. Safe and idempotent.
+ """
+ healed = self.env['fp.job']
+ for job in self:
+ if job._fp_resync_recipe_from_so():
+ healed |= job
+ if not healed:
+ return {
+ 'type': 'ir.actions.client',
+ 'tag': 'display_notification',
+ 'params': {
+ 'type': 'warning',
+ 'message': _('Nothing to re-sync: no resolvable recipe '
+ 'on the sale order, or the job already has '
+ 'steps.'),
+ 'sticky': False,
+ },
+ }
+ return True
+
def _generate_steps_from_recipe(self):
"""Generate fp.job.step records from the assigned recipe.
diff --git a/fusion_plating/fusion_plating_jobs/models/sale_order_line.py b/fusion_plating/fusion_plating_jobs/models/sale_order_line.py
index 09104d8d..cb73ec06 100644
--- a/fusion_plating/fusion_plating_jobs/models/sale_order_line.py
+++ b/fusion_plating/fusion_plating_jobs/models/sale_order_line.py
@@ -25,6 +25,18 @@ class SaleOrderLine(models.Model):
for line in self:
old_qty_by_id[line.id] = line.product_uom_qty
result = super().write(vals)
+ # Recipe set/changed late on the line -> heal the linked WO that
+ # was created empty before the estimator picked the process. Only
+ # not-yet-started jobs (no steps) are touched.
+ if 'x_fc_process_variant_id' in vals:
+ Job = self.env['fp.job']
+ for line in self:
+ jobs = Job.search([
+ ('sale_order_line_ids', 'in', line.id),
+ ('state', 'not in', ('done', 'cancelled')),
+ ])
+ for job in jobs.filtered(lambda j: not j.step_ids):
+ job.sudo()._fp_resync_recipe_from_so()
if 'product_uom_qty' not in vals:
return result
Job = self.env['fp.job']
diff --git a/fusion_plating/fusion_plating_jobs/views/fp_job_form_inherit.xml b/fusion_plating/fusion_plating_jobs/views/fp_job_form_inherit.xml
index f2d59558..9630dbb0 100644
--- a/fusion_plating/fusion_plating_jobs/views/fp_job_form_inherit.xml
+++ b/fusion_plating/fusion_plating_jobs/views/fp_job_form_inherit.xml
@@ -20,6 +20,14 @@
+
+