diff --git a/fusion_plating/fusion_plating_jobs/__manifest__.py b/fusion_plating/fusion_plating_jobs/__manifest__.py
index c3240cc4..ce61ab41 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.1.7',
+ 'version': '19.0.12.4.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_step.py b/fusion_plating/fusion_plating_jobs/models/fp_job_step.py
index 04f26e57..89425bfc 100644
--- a/fusion_plating/fusion_plating_jobs/models/fp_job_step.py
+++ b/fusion_plating/fusion_plating_jobs/models/fp_job_step.py
@@ -486,6 +486,45 @@ class FpJobStep(models.Model):
step.state = 'cancelled'
return True
+ def button_reset(self):
+ """Reset a step back to 'ready' so it can be redone — operator
+ self-serve for a mistake, an accidental skip, or a customer redo
+ request. Clears the finish + sign-off stamps and closes any open
+ timelog so the redo re-captures them; KEEPS the first-start audit
+ (date_started / started_by) and the move / CoC history intact.
+ Posts a chatter audit on the parent job. No-op on a step already
+ ready/pending (nothing to undo).
+ """
+ now = fields.Datetime.now()
+ for step in self:
+ if step.state in ('ready', 'pending'):
+ continue
+ prev_label = dict(
+ step._fields['state'].selection
+ ).get(step.state, step.state)
+ open_logs = step.time_log_ids.filtered(
+ lambda l: not l.date_finished)
+ if open_logs:
+ open_logs.write({'date_finished': now, 'state': 'stopped'})
+ vals = {'state': 'ready'}
+ if 'date_finished' in step._fields:
+ vals['date_finished'] = False
+ if 'finished_by_user_id' in step._fields:
+ vals['finished_by_user_id'] = False
+ if 'signoff_user_id' in step._fields:
+ vals['signoff_user_id'] = False
+ step.write(vals)
+ if step.job_id:
+ step.job_id.message_post(body=_(
+ 'Step "%(s)s" reset to Ready (was %(p)s) by %(u)s '
+ 'for redo.'
+ ) % {
+ 's': step.name or '?',
+ 'p': prev_label,
+ 'u': self.env.user.display_name,
+ })
+ return True
+
def write(self, vals):
"""Post a chatter trail on the parent JOB whenever an active
step gets reassigned. The step itself already tracks
@@ -846,6 +885,16 @@ class FpJobStep(models.Model):
Prompt = self.env['fusion.plating.process.node.input']
if not node:
return Prompt
+ # Master switch (Sub 12d): when the recipe node opts OUT of
+ # measurement collection, the Record-Inputs wizard returns ZERO
+ # rows (fp_job_step_input_wizard.default_get). The finish gate MUST
+ # agree — otherwise required prompts are demanded with no way to
+ # enter them and the step is permanently stuck (bake nodes with
+ # collect_measurements=False but required prompts — WO-30098 + 63
+ # others on entech). Honour the switch here so gate <=> wizard.
+ if ('collect_measurements' in node._fields
+ and not node.collect_measurements):
+ return Prompt
prompts = node.input_ids
if 'kind' in prompts._fields:
prompts = prompts.filtered(lambda i: i.kind == 'step_input')
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 9a1b88f9..9aaab80f 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
@@ -100,8 +100,8 @@
-
-
+
+
@@ -118,7 +118,7 @@
-
+
+