fix(jobs): qty gate false-positive on paperwork / first steps
The qty gate I added refused Finish on steps where qty_at_step > 0, to force operators to move parts forward first. But the first-step seed in _compute_qty_at_step gives the earliest non-terminal step a notional qty = job.qty — a UI hint, not actual parked parts. Paperwork steps (Contract Review, Inspection-by-paperwork, etc.) sit on that seed, and the gate was blocking Finish with a misleading error. Fixes: - button_finish gate now checks for REAL incoming moves before refusing. Seed-only qty (no incoming_move_ids filtered to non- self-loop) is exempt. - _fp_record_one_piece_auto_move detects seed-only qty and bulk- moves ALL parts in one shot to the downstream step. Correct for paperwork / first steps where parts don't physically wait per-piece — one click finishes the paperwork and pushes the whole batch forward. For steps with REAL incoming moves (parts actually moved here via a Move record), the original gate semantics still apply: qty == 1 auto-moves one part; qty > 1 raises with the "use Complete 1 → Next or Move…" message. Verified on entech: Contract Review with seed qty=6 now finishes cleanly, bulk-moving all 6 parts to the next step in one move. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
{
|
{
|
||||||
'name': 'Fusion Plating',
|
'name': 'Fusion Plating',
|
||||||
'version': '19.0.18.15.3',
|
'version': '19.0.18.15.4',
|
||||||
'category': 'Manufacturing/Plating',
|
'category': 'Manufacturing/Plating',
|
||||||
'summary': 'Core plating / metal finishing ERP: facilities, processes, tanks, baths, jobs, operators.',
|
'summary': 'Core plating / metal finishing ERP: facilities, processes, tanks, baths, jobs, operators.',
|
||||||
'description': """
|
'description': """
|
||||||
|
|||||||
@@ -393,12 +393,24 @@ class FpJobStep(models.Model):
|
|||||||
# a downstream step to move them into. Last runnable step
|
# a downstream step to move them into. Last runnable step
|
||||||
# is exempt — parts finishing there complete in place
|
# is exempt — parts finishing there complete in place
|
||||||
# (qty_done reconciliation at job close is the catch-net).
|
# (qty_done reconciliation at job close is the catch-net).
|
||||||
|
#
|
||||||
|
# Seed-only exemption: the first-step seed in
|
||||||
|
# _compute_qty_at_step gives the earliest non-terminal step
|
||||||
|
# a notional qty = job.qty. That's a UI hint, not a real
|
||||||
|
# parked batch — no incoming move record backs it. Paperwork
|
||||||
|
# steps (Contract Review, Inspection, etc.) sit on that seed.
|
||||||
|
# If the step has no REAL incoming moves, skip the gate.
|
||||||
if not skip_qty_gate and step.qty_at_step > 0:
|
if not skip_qty_gate and step.qty_at_step > 0:
|
||||||
has_downstream = step.job_id.step_ids.filtered(
|
has_downstream = step.job_id.step_ids.filtered(
|
||||||
lambda s: s.sequence > step.sequence
|
lambda s: s.sequence > step.sequence
|
||||||
and s.state in ('pending', 'ready')
|
and s.state in ('pending', 'ready')
|
||||||
)
|
)
|
||||||
if has_downstream:
|
has_real_incoming = bool(
|
||||||
|
step.incoming_move_ids.filtered(
|
||||||
|
lambda m: m.from_step_id != step
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if has_downstream and has_real_incoming:
|
||||||
raise UserError(_(
|
raise UserError(_(
|
||||||
"Step '%(name)s' still has %(n)d part(s) "
|
"Step '%(name)s' still has %(n)d part(s) "
|
||||||
"parked — move them to the next step before "
|
"parked — move them to the next step before "
|
||||||
|
|||||||
@@ -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.8.20.5',
|
'version': '19.0.8.20.6',
|
||||||
'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.',
|
||||||
|
|||||||
@@ -1112,16 +1112,18 @@ class FpJobStep(models.Model):
|
|||||||
|
|
||||||
|
|
||||||
def _fp_record_one_piece_auto_move(self):
|
def _fp_record_one_piece_auto_move(self):
|
||||||
"""Decide whether to silently record a move(qty=1) before
|
"""Decide whether to silently record a move before the step
|
||||||
the step finishes. Five cases:
|
finishes. Six cases:
|
||||||
- qty_at_step == 0: nothing to do (parts already moved).
|
- qty_at_step == 0: nothing to do (parts already moved).
|
||||||
- last runnable step: parts complete in place; no move.
|
- last runnable step: parts complete in place; no move.
|
||||||
- qty_at_step == 1 + downstream: record move(1).
|
- SEED-ONLY qty + downstream: bulk-move all parts to next
|
||||||
- qty_at_step > 1 + downstream: raise.
|
step in one move. Paperwork / first steps don't physically
|
||||||
- qty_at_step > 1 + last step: allow (parts complete in
|
hold parts per-piece.
|
||||||
place; qty_done auto-tick is Phase 2).
|
- real qty == 1 + downstream: record move(1).
|
||||||
Called from action_finish_and_advance just before
|
- real qty > 1 + downstream: raise — operator must use
|
||||||
button_finish.
|
Complete 1 → Next (streaming) or Move… (batched).
|
||||||
|
- real qty > 1 + last step: allow (qty_done auto-tick Phase 2).
|
||||||
|
Called from action_finish_and_advance just before button_finish.
|
||||||
"""
|
"""
|
||||||
self.ensure_one()
|
self.ensure_one()
|
||||||
qty = self.qty_at_step
|
qty = self.qty_at_step
|
||||||
@@ -1133,6 +1135,23 @@ class FpJobStep(models.Model):
|
|||||||
).sorted('sequence')[:1]
|
).sorted('sequence')[:1]
|
||||||
if not next_step:
|
if not next_step:
|
||||||
return False
|
return False
|
||||||
|
# Seed-only qty: no real incoming moves backing it. Paperwork
|
||||||
|
# step or first-step seed — bulk-move all parts in one shot.
|
||||||
|
has_real_incoming = bool(
|
||||||
|
self.incoming_move_ids.filtered(
|
||||||
|
lambda m: m.from_step_id != self
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if not has_real_incoming:
|
||||||
|
self.env['fp.job.step.move'].create({
|
||||||
|
'job_id': self.job_id.id,
|
||||||
|
'from_step_id': self.id,
|
||||||
|
'to_step_id': next_step.id,
|
||||||
|
'transfer_type': 'step',
|
||||||
|
'qty_moved': qty,
|
||||||
|
'moved_by_user_id': self.env.user.id,
|
||||||
|
})
|
||||||
|
return True
|
||||||
if qty > 1:
|
if qty > 1:
|
||||||
raise UserError(_(
|
raise UserError(_(
|
||||||
"Step '%s' still has %d parts here — use the row's "
|
"Step '%s' still has %d parts here — use the row's "
|
||||||
@@ -1140,6 +1159,7 @@ class FpJobStep(models.Model):
|
|||||||
"or the 'Move…' wizard (for batched flow) to drain "
|
"or the 'Move…' wizard (for batched flow) to drain "
|
||||||
"the step before finishing."
|
"the step before finishing."
|
||||||
) % (self.name, qty))
|
) % (self.name, qty))
|
||||||
|
# qty == 1 + real incoming → record single move.
|
||||||
self.env['fp.job.step.move'].create({
|
self.env['fp.job.step.move'].create({
|
||||||
'job_id': self.job_id.id,
|
'job_id': self.job_id.id,
|
||||||
'from_step_id': self.id,
|
'from_step_id': self.id,
|
||||||
|
|||||||
Reference in New Issue
Block a user