feat(sub12b): fp.job.step + fp.job — rack/move/traveller fields

fp.job.step:
  + requires_rack_assignment (related from recipe_node_id)
  + requires_transition_form (related)
  + move_ids (O2M from_step_id), incoming_move_ids (O2M to_step_id)
  + is_racked (compute, stored, depends rack_id) — drives tablet
    rack-vs-parts greyed-button guard
  + qty_at_step_start, qty_at_step_finish (advanced by move commits)

  NOTE: existing 'rack_id' field is reused as the 'current rack' pointer
  (already there on line 95). Adding requires_rack_assignment as a
  related from recipe_node_id for runtime gate evaluation.

fp.job:
  + qty_received, qty_visual_inspection_rejects, qty_rework
  + special_requirements (Text — paper traveller header)
  + active_timer_ids (filtered O2M, depends on Task 7's state field)
  + move_ids (O2M to fp.job.step.move)

All additive. No removed fields. Existing battle tests unaffected.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsinghpal
2026-04-27 21:07:19 -04:00
parent 0794f7e3c9
commit dcd6df71c0
2 changed files with 69 additions and 0 deletions

View File

@@ -186,6 +186,40 @@ class FpJob(models.Model):
'job_id',
string='Steps',
)
# ===== Sub 12b — traveller header + active timer ========================
# Header counters mirror the paper traveller's "Qty Rec." / "VIS INSP."
# / "Rework" columns (screens 16-18). Sub 12c's traveller report pulls
# these into the printed header.
qty_received = fields.Integer(
string='Qty Received',
help='Paper traveller "Qty Rec." column.',
)
qty_visual_inspection_rejects = fields.Integer(
string='Visual Insp Rejects',
help='Paper traveller "VIS INSP." column.',
)
qty_rework = fields.Integer(
string='Qty Sent to Rework',
help='Paper traveller "Rework" column.',
)
special_requirements = fields.Text(
string='Special Requirements',
help='Long free-form spec text from customer; printed on the '
'traveller header (Sub 12c).',
)
active_timer_ids = fields.One2many(
'fp.job.step.timelog',
'job_id',
string='Active Timers',
domain=[('state', 'in', ('running', 'paused'))],
help='Sub 12b — used by tablet for live timer badges. Filtered '
'on state by Task 7\'s state field.',
)
move_ids = fields.One2many(
'fp.job.step.move', 'job_id',
string='Move Log',
)
# step_count + step_done_count are stored (drive list views / stat
# buttons in Task 1.8). step_progress_pct stays non-stored — it's a
# cheap derivative. Odoo flags as inconsistent when stored and

View File

@@ -139,6 +139,41 @@ class FpJobStep(models.Model):
'step in this job is done/skipped/cancelled.',
)
# ===== Sub 12b — chain-of-custody + rack awareness =====================
# Note: rack_id (line 95 above) already exists — reused as the "current
# rack on this step" pointer. Sub 12b builds the runtime guards on top.
requires_rack_assignment = fields.Boolean(
related='recipe_node_id.requires_rack_assignment',
store=True,
help='If True, the Move Parts dialog requires a rack to be '
'assigned to the parts before the move commits. Snapshot '
'from the recipe step at job creation.',
)
requires_transition_form = fields.Boolean(
related='recipe_node_id.requires_transition_form',
store=True,
)
move_ids = fields.One2many(
'fp.job.step.move', 'from_step_id',
string='Outgoing Moves',
)
incoming_move_ids = fields.One2many(
'fp.job.step.move', 'to_step_id',
string='Incoming Moves',
)
is_racked = fields.Boolean(
string='Racked', compute='_compute_is_racked', store=True,
help='True when rack_id is set — drives the tablet rack-vs-parts '
'button-state guard (Move Parts greys out).',
)
qty_at_step_start = fields.Integer(string='Qty at Step Start')
qty_at_step_finish = fields.Integer(string='Qty at Step Finish')
@api.depends('rack_id')
def _compute_is_racked(self):
for rec in self:
rec.is_racked = bool(rec.rack_id)
# ------------------------------------------------------------------
# Cost rollup (Task 1.6)
# cost_per_hour comes from fp.work.centre (Task 1.2 added it there).