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

@@ -3,7 +3,10 @@
# License OPL-1 (Odoo Proprietary License v1.0)
# Part of the Fusion Plating product family.
from odoo import api, fields, models
from markupsafe import Markup
from odoo import _, api, fields, models
from odoo.exceptions import UserError
class FpJobStepMove(models.Model):
@@ -74,6 +77,92 @@ class FpJobStepMove(models.Model):
string='Transition Input Values',
)
# ------------------------------------------------------------------
# S23 — required transition-input gate
# ------------------------------------------------------------------
# When the destination step has requires_transition_form=True, the
# recipe author wants chain-of-custody attestations captured on the
# move (location, photo, customer WO #, etc.). Same dormant-field
# shape as S22's signoff bug — the field existed but nothing enforced
# it. Callers (tablet controllers, future backend wizards) MUST call
# _fp_check_transition_inputs_complete() after writing values to
# transition_input_value_ids.
#
# We can't gate on create() because values are written in a separate
# call after the move row. Model-level enforcement would require
# either a deferred-commit pattern or a write hook; explicit caller
# invocation is the simplest contract.
def _fp_missing_required_transition_inputs(self):
"""Return the recordset of required transition_input prompts on
the to_step's recipe node that have NO captured value on this
move. Centralised helper — used by the gate below and by future
diagnostics."""
self.ensure_one()
Prompt = self.env['fusion.plating.process.node.input']
to_step = self.to_step_id
if not to_step or not to_step.recipe_node_id:
return Prompt
if not to_step.requires_transition_form:
return Prompt
prompts = to_step.recipe_node_id.input_ids
if 'kind' in prompts._fields:
prompts = prompts.filtered(
lambda i: i.kind == 'transition_input')
if 'collect' in prompts._fields:
prompts = prompts.filtered(lambda i: i.collect)
required_prompts = prompts.filtered(lambda i: i.required)
if not required_prompts:
return Prompt
recorded_input_ids = set(
self.transition_input_value_ids.mapped('node_input_id.id')
)
return required_prompts.filtered(
lambda p: p.id not in recorded_input_ids
)
def _fp_check_transition_inputs_complete(self):
"""Raise UserError when the destination step has
requires_transition_form=True and required transition_input
prompts haven't been recorded on this move. Audit gate — same
shape as fp.job.step._fp_check_step_inputs_complete (S21) and
._fp_check_signoff_complete (S22).
Manager bypass via context fp_skip_transition_form=True
(consistent with the existing audit-trail flag on the tablet
controllers). Bypasses are posted to chatter on the move
record naming the user.
"""
if self.env.context.get('fp_skip_transition_form'):
for move in self:
if not move.to_step_id.requires_transition_form:
continue
move.message_post(body=Markup(_(
'Transition-form gate bypassed by %s. '
'Documented deviation — required prompts not '
'recorded on this move.'
)) % self.env.user.name)
return
for move in self:
missing = move._fp_missing_required_transition_inputs()
if not missing:
continue
names = ', '.join(
'"%s"' % (p.name or '').strip() for p in missing
)
raise UserError(_(
'Move to step "%(step)s" cannot be committed — '
'%(n)s required transition prompt(s) not recorded: '
'%(names)s. Fill them in the Move dialog before '
'committing. Managers can override via context flag '
'fp_skip_transition_form=True for documented '
'deviations.'
) % {
'step': move.to_step_id.name,
'n': len(missing),
'names': names,
})
class FpJobStepMoveInputValue(models.Model):
"""Captured value for one transition-input prompt.