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:
@@ -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.',
|
||||||
|
|||||||
@@ -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')
|
||||||
|
|||||||
@@ -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 & Next" icon="fa-check-circle"
|
title="Finish & 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
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
Reference in New Issue
Block a user