feat(plating): session 2026-05-23 deploys — F1/F7/S22/S23 + UI fixes

Consolidated commit of session work already deployed to entech and
verified via the deep audit + the persona walk:

S22 — Signoff gate (fp.job.step.requires_signoff was 100% unenforced,
42/42 done steps had NULL signoff_user_id). Three-piece fix:
_fp_autosign_if_required (captures finisher on button_finish),
_fp_check_signoff_complete (raises UserError if NULL after autosign),
action_signoff (explicit supervisor pre-sign). Bypass:
fp_skip_signoff_gate=True.

S23 — Transition-form gate (same dormant-field shape as S22, caught
preventively before recipe authors flipped requires_transition_form
on). Model helpers on fp.job.step.move + controller gate in
move_controller (parts commit) + pre-reject in rack commit.

F7 — Chatter standardization: _fp_create_qc_check_if_needed,
_fp_fire_notification, _fp_create_delivery silent failures now also
post to job chatter instead of only logging to file.

UI fixes:
- Critical Rule 20 documented + applied: OWL templates only expose
  Math as a global. Calling String(d) inside t-on-click throws
  'v2 is not a function'. Fixed pin_pad.xml (string array instead of
  number array with String() coercion). Also swept parseInt/
  parseFloat in recipe_tree_editor + simple_recipe_editor.
- Notes panel HTML escape fix: chatter messages off /fp/workspace/load
  were rendered via t-out, escaping the HTML. Wrap with markup() in
  job_workspace.js refresh() before assigning to state.

Versions:
  fusion_plating         19.0.20.8.0 → 19.0.20.9.0
  fusion_plating_jobs    19.0.10.20.0 → 19.0.10.23.0
  fusion_plating_shopfloor 19.0.30.2.0 → 19.0.30.5.0

All deployed to entech (LXC 111) and verified live.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsinghpal
2026-05-23 20:37:17 -04:00
parent d6ebcb6233
commit 1a3ca8704e
15 changed files with 597 additions and 142 deletions

View File

@@ -203,6 +203,13 @@ class FpTabletMoveController(http.Controller):
for prompt_id, value in (prompt_values or {}).items():
self._capture_prompt_value(move, int(prompt_id), value)
# S23 — required transition-input gate. Runs AFTER value capture
# so the operator gets credit for whatever they filled in. Raises
# UserError if to_step.requires_transition_form=True and any
# required transition_input prompt has no value. Rollback unwinds
# the move + value rows. Manager bypass: fp_skip_transition_form.
move._fp_check_transition_inputs_complete()
# Advance qty_at_step counters
to_step.qty_at_step_start = (to_step.qty_at_step_start or 0) + qty
from_step.qty_at_step_finish = (from_step.qty_at_step_finish or 0) + qty
@@ -298,6 +305,42 @@ class FpTabletMoveController(http.Controller):
rack = Rack.browse(rack_id)
to_step = Step.browse(to_step_id)
# S23 — pre-check: rack moves don't capture transition prompts
# (no per-move dialog), so if to_step.requires_transition_form
# we must reject up-front and force the operator through Move
# Parts (which has the form UI). Without this check, rack moves
# silently bypass the audit gate that Move Parts enforces.
if (to_step.requires_transition_form
and not request.env.context.get('fp_skip_transition_form')):
# Use the same model helper for consistency — build a dummy
# in-memory move to compute "missing" set, then surface a
# clear message that points operators at the right tool.
recipe_node = to_step.recipe_node_id
required_prompts = recipe_node.input_ids if recipe_node else (
request.env['fusion.plating.process.node.input']
)
if 'kind' in required_prompts._fields:
required_prompts = required_prompts.filtered(
lambda i: i.kind == 'transition_input')
required_prompts = required_prompts.filtered(
lambda i: i.required)
if required_prompts:
names = ', '.join(
'"%s"' % (p.name or '').strip()
for p in required_prompts
)
raise UserError(_(
'Step "%(step)s" requires a transition form '
'(%(n)s required prompt(s): %(names)s). '
'Use Move Parts for one batch at a time so the form '
'can be filled in, or have a manager override with '
'context flag fp_skip_transition_form=True.'
) % {
'step': to_step.name,
'n': len(required_prompts),
'names': names,
})
moves = []
for batch in Step.search([('rack_id', '=', rack.id)]):
qty = (batch.qty_done or 0) - (batch.qty_scrapped or 0)