feat(jobs): Sub 13 sequential step enforcement + Sub 12e v3 wizard

Two coherent feature drops shipping together because their fp_job_step
edits overlap. Both target operator workflow correctness.

## Sub 13 — Sequential step enforcement (recipe + per-step)

Background:
  Investigation on WH/JOB/00339 showed operators starting Incoming
  Inspection while Contract Review was still in_progress. Audit:
  98.7% of recipe operations system-wide had requires_predecessor_done
  = false (the legacy per-step opt-in defaults off, recipe authors
  rarely tick the box).

Architecture:
  Recipe-level toggle + per-step opt-out (Option A from /investigate).
  * fusion.plating.process.node.enforce_sequential — Boolean on the
    recipe root. Default True. When True, every operation under this
    recipe waits for earlier-sequence steps to finish before it can
    start.
  * fusion.plating.process.node.parallel_start — Boolean on operation
    nodes. When True, this step bypasses the sequential gate (e.g.
    paperwork or QA review that runs alongside production).
  * Mirrored on fp.step.template (parallel_start) so library steps
    carry the flag into snapshots.
  * fp.job.enforce_sequential — related from recipe_id. Snapshotted
    at job creation so a recipe author flipping the recipe's flag
    AFTER job generation does NOT change behaviour mid-run.
  * fp.job.step.parallel_start — related from recipe_node_id.
  * Decision matrix (encapsulated in
    fp.job.step._fp_should_block_predecessors):
        recipe.enforce_sequential | step.parallel_start | step.req_pred_done | block?
        --------------------------|---------------------|--------------------|------
                 True             |       False         |        any         |  YES
                 True             |       True          |        any         |   no
                 False            |        any          |       True         |  YES
                 False            |        any          |       False        |   no
  * Manager bypass via context fp_skip_predecessor_check=True (existing).

Runtime gates:
  * fp.job.step.button_start — calls _fp_should_block_predecessors;
    raises UserError naming the blocking earlier step(s).
  * fp.job.step.can_start — computed Boolean for view-side disable.
  * Move wizard predecessor check
    (fusion_plating_shopfloor/controllers/move_controller.py) — uses
    the same helper so tablet + backend behave identically.

UI surface:
  * Recipe form (fp_process_node_views.xml) — enforce_sequential
    toggle on recipe root, parallel_start checkbox on operations.
  * Step template form — parallel_start checkbox.
  * Simple Recipe Editor (inline library form) — Parallel Start
    checkbox + legacy flag demoted with muted styling + supervisor
    group gate.
  * Recipe Tree Editor (properties panel) — both flags exposed,
    only-show on the right node_type.
  * Controllers updated to allowlist + payload the new fields.

Migration:
  fusion_plating/migrations/19.0.18.12.0/post-migrate.py — sets
  enforce_sequential = TRUE on every existing recipe-root node.
  Idempotent. User confirmed dev-stage data, so retroactive flip
  is safe (no production jobs to disrupt).

Tests:
  TestSequentialEnforcement (10 tests) covering:
    * sequential mode blocks out-of-order start
    * first step always startable
    * predecessor finish/skip unlocks next
    * parallel_start opts out of gate
    * free-flow mode bypasses gate
    * legacy requires_predecessor_done still honoured in free-flow
    * manager bypass via context
    * can_start compute reflects state correctly
    * library template parallel_start snapshots into recipe-node

## Sub 12e — Record Inputs Wizard v3 (card layout, dark-mode aware)

Background:
  v2 wizard was a 17-column wide editable table. Operators got lost
  finding which value column applied to their row's type, horizontal
  scroll required on tablets, composite types crammed into one row.

New layout:
  * Each measurement renders as a stacked card (CSS Grid + display
    transformation on the existing list widget — preserves inline
    editing, no JS rewrite).
  * Card header: prompt name (large, bold) + type/unit pills.
  * Card body: ONLY the value widget for this row's type
    (number / boolean / date / text / photo / multi-point / panel).
  * Composite types (multi-point thickness 5x reading + avg, bath
    panel 4 fields) get inline sub-grid inside the card.
  * Empty state ("no measurement prompts") with friendly CTA.

Dark mode:
  * SCSS branches at compile time on $o-webclient-color-scheme
    (per fusion-plating/CLAUDE.md note).
  * Tokens: 7 surface colours + 4 ink levels with light/dark hex
    pairs, all behind var(--fp-*) custom properties for per-deploy
    override.
  * Registered in BOTH web.assets_backend AND web.assets_web_dark
    so each bundle compiles its own palette.

Tablet polish:
  @media (max-width: 900px) — collapse meta below prompt + bump
  numeric input min-height to 56px.

Defensive:
  * v2 view kept in the XML file (instant rollback by changing one
    view_id ref).
  * `:has(.o_invisible_modifier)` rule drops empty cells out of the
    grid so Odoo's invisible="..." doesn't punch holes in layout.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsinghpal
2026-05-03 21:24:12 -04:00
parent ee80673579
commit 9794a98de9
20 changed files with 1109 additions and 34 deletions

View File

@@ -135,8 +135,26 @@ class FpJobStep(models.Model):
requires_predecessor_done = fields.Boolean(
related='recipe_node_id.requires_predecessor_done',
store=True,
help='If True, button_start blocks until every earlier-sequence '
'step in this job is done/skipped/cancelled.',
help='LEGACY: per-step opt-in for predecessor enforcement. '
'Still honoured when the parent recipe has '
'enforce_sequential=False (free-flow recipe with one '
'specific step that needs to wait).',
)
# Sub 13 — sequential enforcement (recipe + per-step). New default
# behaviour is "every step waits for predecessors", with two escape
# hatches: enforce_sequential=False on the recipe (free-flow), or
# parallel_start=True on this specific step (explicit parallelism).
# The per-step parallel_start field is on this CORE model because
# it just mirrors a core field (recipe_node_id.parallel_start).
# The runtime gate logic (can_start, _fp_should_block_predecessors)
# lives in fusion_plating_jobs because it reads the recipe-level
# enforce_sequential which only exists when that bridge is loaded.
parallel_start = fields.Boolean(
related='recipe_node_id.parallel_start',
store=True,
help='If True, this step can start while earlier-sequence '
'steps are still in progress. Only meaningful when the '
'parent recipe has enforce_sequential=True.',
)
# ===== Sub 12b — chain-of-custody + rack awareness =====================

View File

@@ -181,14 +181,41 @@ class FpProcessNode(models.Model):
help='Quality hold point — requires operator sign-off.',
)
requires_predecessor_done = fields.Boolean(
string='Requires Predecessor Done',
string='Requires Predecessor Done (legacy)',
default=False,
help='If checked, this step cannot start until ALL earlier-'
'sequence steps in the job are done / skipped / cancelled. '
'Use for serial-required operations (e.g. Plating must '
'follow Acid Etch with no time gap — passivation layer '
'forms in seconds). Leaving unchecked allows parallel '
'work across tanks (the default).',
help='LEGACY per-step opt-in for predecessor enforcement. As of '
'19.0.X, recipes default to enforce_sequential=True so every '
'step naturally waits for its predecessors. This flag still '
'works on recipes whose enforce_sequential is False — turn '
'it on to make a single step block in an otherwise free-flow '
'recipe.',
)
# ===== Sub 13 — sequential step enforcement (recipe + per-step) ==========
# Replaces the unused per-step requires_predecessor_done as the primary
# enforcement vector. Two layers:
# 1. enforce_sequential (recipe root) — entire recipe is sequential
# by default. Author can disable for free-flow recipes.
# 2. parallel_start (operation step) — escape hatch within a
# sequential recipe, for steps that legitimately run in parallel
# (e.g. paperwork that doesn't need previous step done).
enforce_sequential = fields.Boolean(
string='Enforce Sequential Order',
default=True,
help='Only meaningful on the recipe root node. When True (the '
'default), every operation under this recipe waits for all '
'earlier-sequence steps to be done/skipped/cancelled before '
'it can start. Mark a specific step as Parallel Start to '
'opt it out. Disable on the recipe to fall back to the '
'legacy per-step Requires Predecessor Done flag.',
)
parallel_start = fields.Boolean(
string='Parallel Start',
default=False,
help='Only meaningful on operation nodes inside a recipe with '
'Enforce Sequential Order = True. When checked, this step '
'can be started while earlier-sequence steps are still in '
'progress (e.g. paperwork or QA review that runs alongside '
'production).',
)
opt_in_out = fields.Selection(
[
@@ -538,6 +565,10 @@ class FpProcessNode(models.Model):
'customer_visible': self.customer_visible,
'is_manual': self.is_manual,
'requires_signoff': self.requires_signoff,
# Sub 13 — sequential enforcement
'enforce_sequential': self.enforce_sequential,
'parallel_start': self.parallel_start,
'requires_predecessor_done': self.requires_predecessor_done,
'version': self.version,
'child_count': len(children),
'opt_in_out': self.opt_in_out or 'disabled',

View File

@@ -66,9 +66,19 @@ class FpStepTemplate(models.Model):
viscosity_target = fields.Float(string='Viscosity Target')
requires_signoff = fields.Boolean(string='Require QA Sign-off')
requires_predecessor_done = fields.Boolean(string='Require Predecessor Done',
help='S14 lock — operator cannot start this step until earlier '
'sequenced steps are done.')
requires_predecessor_done = fields.Boolean(
string='Require Predecessor Done (legacy)',
help='Legacy per-step opt-in for predecessor enforcement. Recipes '
'now default to Enforce Sequential Order — use Parallel '
'Start instead when you want a step to run alongside others.',
)
parallel_start = fields.Boolean(
string='Parallel Start',
help='Sub 13. When this template lands inside a sequential '
'recipe, the resulting step can be started while '
'earlier-sequence steps are still in progress (e.g. '
'paperwork that runs alongside production).',
)
requires_rack_assignment = fields.Boolean(string='Requires Rack Assignment',
help='Triggers Rack Parts sub-dialog at runtime (Sub 12b).')
requires_transition_form = fields.Boolean(string='Requires Transition Form',