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 @@ + +