Merge: self-heal WOs missing recipe/steps (fusion_plating_jobs 19.0.12.6.0; deployed to entech)

This commit is contained in:
gsinghpal
2026-06-05 01:15:18 -04:00
4 changed files with 85 additions and 1 deletions

View File

@@ -3,7 +3,7 @@
# License OPL-1 (Odoo Proprietary License v1.0) # License OPL-1 (Odoo Proprietary License v1.0)
{ {
'name': 'Fusion Plating - Native Jobs', 'name': 'Fusion Plating - Native Jobs',
'version': '19.0.12.5.0', 'version': '19.0.12.6.0',
'category': 'Manufacturing/Plating', 'category': 'Manufacturing/Plating',
'summary': 'Native plating job model - replaces mrp.production / mrp.workorder bridge.', 'summary': 'Native plating job model - replaces mrp.production / mrp.workorder bridge.',
'author': 'Nexa Systems Inc.', 'author': 'Nexa Systems Inc.',

View File

@@ -1385,6 +1385,70 @@ class FpJob(models.Model):
# - Drops work_role_id (not on fp.job.step yet - Task 2.6+) # - Drops work_role_id (not on fp.job.step yet - Task 2.6+)
# - Drops _fp_autofill_default_equipment (not yet on step) # - 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): def _generate_steps_from_recipe(self):
"""Generate fp.job.step records from the assigned recipe. """Generate fp.job.step records from the assigned recipe.

View File

@@ -25,6 +25,18 @@ class SaleOrderLine(models.Model):
for line in self: for line in self:
old_qty_by_id[line.id] = line.product_uom_qty old_qty_by_id[line.id] = line.product_uom_qty
result = super().write(vals) 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: if 'product_uom_qty' not in vals:
return result return result
Job = self.env['fp.job'] Job = self.env['fp.job']

View File

@@ -20,6 +20,14 @@
<field name="inherit_id" ref="fusion_plating.view_fp_job_form"/> <field name="inherit_id" ref="fusion_plating.view_fp_job_form"/>
<field name="arch" type="xml"> <field name="arch" type="xml">
<xpath expr="//header" position="inside"> <xpath expr="//header" position="inside">
<!-- Heal a WO that was created before the recipe was set
on the SO line: re-pull the recipe and build steps.
Hidden once steps exist or the job is terminal. -->
<button name="action_fp_resync_recipe_from_so" type="object"
string="Re-sync Recipe from SO"
class="btn-secondary"
icon="fa-refresh"
invisible="state in ('done', 'cancelled') or step_ids"/>
<!-- Phase 1 - Tablet redesign. Opens the JobWorkspace OWL <!-- Phase 1 - Tablet redesign. Opens the JobWorkspace OWL
client action focused on this WO. Primary entry point client action focused on this WO. Primary entry point
for techs before the Landing kanban (Phase 3) ships; for techs before the Landing kanban (Phase 3) ships;