feat(jobs+receiving): confirm->receive flow, lock recipe, reset step, lock steps, fix bake gate

- Confirm->Receive (A): after a single interactive SO confirm, receiving's
  action_confirm returns action_view_receiving() so the user lands straight
  on the Receive Parts screen (opt-out via fp_no_receiving_redirect context).
- Lock recipe (1): recipe_id readonly on the WO form — stick to the
  order-entry recipe.
- Hide spec (2): customer_spec_id invisible on the WO form.
- Reset step (3): new fp.job.step.button_reset (operator-usable, audited) +
  an undo button next to Start. Resets to Ready, clears finish + sign-off,
  closes open timelogs, keeps start audit + move/CoC history.
- Lock steps (4): steps list create=false delete=false (no Add a line / no
  trash) — steps come from the recipe, only skippable, never deleted.
- Bake gate fix (5): _fp_missing_required_step_inputs now honours the node's
  collect_measurements master switch, matching the Record-Inputs wizard.
  collect_measurements=False + required prompts no longer blocks finish
  (wizard shows 0 rows, so the gate must too). Unblocks WO-30098 + 63 other
  affected nodes (bake steps).

Deployed + verified on entech (-u jobs; bake finishes, reset done->ready,
recipe readonly, spec hidden, steps locked, receiving redirect target OK).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsinghpal
2026-06-04 16:55:34 -04:00
parent 197030a188
commit dcd4955bb7
4 changed files with 70 additions and 4 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.1.7', 'version': '19.0.12.4.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

@@ -486,6 +486,45 @@ class FpJobStep(models.Model):
step.state = 'cancelled' step.state = 'cancelled'
return True 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): def write(self, vals):
"""Post a chatter trail on the parent JOB whenever an active """Post a chatter trail on the parent JOB whenever an active
step gets reassigned. The step itself already tracks step gets reassigned. The step itself already tracks
@@ -846,6 +885,16 @@ class FpJobStep(models.Model):
Prompt = self.env['fusion.plating.process.node.input'] Prompt = self.env['fusion.plating.process.node.input']
if not node: if not node:
return Prompt 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 prompts = node.input_ids
if 'kind' in prompts._fields: if 'kind' in prompts._fields:
prompts = prompts.filtered(lambda i: i.kind == 'step_input') prompts = prompts.filtered(lambda i: i.kind == 'step_input')

View File

@@ -100,8 +100,8 @@
</xpath> </xpath>
<xpath expr="//field[@name='product_id']" position="after"> <xpath expr="//field[@name='product_id']" position="after">
<field name="part_catalog_id" string="Part"/> <field name="part_catalog_id" string="Part"/>
<field name="customer_spec_id" string="Specification"/> <field name="customer_spec_id" string="Specification" invisible="1"/>
<field name="recipe_id" string="Process Recipe"/> <field name="recipe_id" string="Process Recipe" readonly="1"/>
</xpath> </xpath>
<!-- Show qty completed alongside total so the partial-qty <!-- Show qty completed alongside total so the partial-qty
picture is visible at a glance without opening Move Log. --> picture is visible at a glance without opening Move Log. -->
@@ -118,7 +118,7 @@
<xpath expr="//page[@name='steps']/field[@name='step_ids']" position="replace"> <xpath expr="//page[@name='steps']/field[@name='step_ids']" position="replace">
<field name="step_ids" mode="list" <field name="step_ids" mode="list"
context="{'form_view_ref': 'fusion_plating_jobs.view_fp_job_step_quick_look_form'}"> context="{'form_view_ref': 'fusion_plating_jobs.view_fp_job_step_quick_look_form'}">
<list editable="bottom" <list editable="bottom" create="false" delete="false"
decoration-info="state in ('ready', 'in_progress')" decoration-info="state in ('ready', 'in_progress')"
decoration-success="state == 'done'" decoration-success="state == 'done'"
decoration-warning="state == 'paused'" decoration-warning="state == 'paused'"
@@ -162,6 +162,14 @@
title="Finish &amp; Next" icon="fa-check-circle" title="Finish &amp; Next" icon="fa-check-circle"
class="btn-link o_fp_finish_btn" class="btn-link o_fp_finish_btn"
invisible="state != 'in_progress'"/> invisible="state != 'in_progress'"/>
<!-- Reset / redo — back to Ready so the step can be
run again (mistake, accidental skip, customer
redo). Clears finish + sign-off stamps; keeps the
start audit + moves. Hidden on ready/pending. -->
<button name="button_reset" type="object"
title="Reset step (redo)" icon="fa-undo"
class="btn-link text-warning"
invisible="state in ('ready', 'pending')"/>
<!-- Secondary actions — small icons only. Pause is <!-- Secondary actions — small icons only. Pause is
only relevant on a running step; Record Inputs only relevant on a running step; Record Inputs

View File

@@ -74,6 +74,15 @@ class SaleOrder(models.Model):
'expected_qty': int(total_qty), 'expected_qty': int(total_qty),
'line_ids': line_vals, 'line_ids': line_vals,
}) })
# Seamless flow: after a single interactive confirm, jump straight
# to the Receive Parts screen so the dock counts parts in right away
# (idiot-proof — no hunting for the smart button). Guarded to a
# single order + an opt-out context flag so batch / programmatic
# confirms (and tests) keep the native return value.
if (len(self) == 1
and not self.env.context.get('fp_no_receiving_redirect')
and self.x_fc_receiving_ids):
return self.action_view_receiving()
return res return res
def action_view_receiving(self): def action_view_receiving(self):