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:
gsinghpal
2026-05-12 00:44:29 -04:00
parent d6bda9740f
commit 8b5472bf4e
4 changed files with 43 additions and 11 deletions

View File

@@ -5,7 +5,7 @@
{
'name': 'Fusion Plating',
'version': '19.0.18.15.3',
'version': '19.0.18.15.4',
'category': 'Manufacturing/Plating',
'summary': 'Core plating / metal finishing ERP: facilities, processes, tanks, baths, jobs, operators.',
'description': """

View File

@@ -393,12 +393,24 @@ class FpJobStep(models.Model):
# a downstream step to move them into. Last runnable step
# is exempt — parts finishing there complete in place
# (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:
has_downstream = step.job_id.step_ids.filtered(
lambda s: s.sequence > step.sequence
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(_(
"Step '%(name)s' still has %(n)d part(s) "
"parked — move them to the next step before "