Shop-floor sign-off currently makes operators redraw a signature every time, and the drawing is discarded (reports read x_fc_signature_image). Spec: use the saved Plating Signature (one-tap confirm-with-preview); draw once when absent and persist it to x_fc_signature_image so future sign-offs + reports reuse it. Tablet-workspace scope; no model/migration. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
11 KiB
Shop-Floor Sign-Off: Reuse the Saved Plating Signature
Date: 2026-06-04
Module(s): fusion_plating_shopfloor (frontend + controller), reads res.users.x_fc_signature_image (defined in fusion_plating_jobs)
Author: Gurpreet (Nexa Systems Inc.)
Status: Draft — pending user review of this spec
Summary
On the shop-floor Job Workspace, finishing any recipe step with
requires_signoff=True pops a draw-pad and makes the operator draw a
signature from scratch every time. Worse, that per-step drawing is
saved as an ir.attachment on the step and then never used — the WO
Detail / CoC reports render the signer's Plating Signature
(res.users.x_fc_signature_image, per CLAUDE.md rule 14b), not the step
attachment.
This change makes sign-off reuse the operator's saved Plating Signature: if they have one, finishing is a one-tap confirm (preview + "Sign & Finish"); if they don't, they draw once and it is persisted to their Plating Signature, so every later sign-off — and every report — uses it without redrawing.
Current behaviour (the bug)
onFinishStep(job_workspace.js:364) — whenstep.requires_signoff, always opensFpSignaturePad; on submit POSTs the drawing to/fp/workspace/sign_off./fp/workspace/sign_off(workspace_controller.py:451) — requires a non-emptysignature_data_uri, creates a per-stepir.attachmentfrom it, then callsstep.button_finish()(which setssignoff_user_idvia_fp_autosign_if_required).- Reports read
signer_user.x_fc_signature_image, not the step attachment → the drawing is wasted. x_fc_signature_image=fields.Binary(string='Plating Signature', attachment=True)onres.users(defined infusion_plating_jobs/models/res_users.py), already inSELF_READABLE_FIELDSandSELF_WRITEABLE_FIELDS(fusion_plating/models/res_users.py) — so a tablet tech can read and write their own signature with no sudo.
Locked decisions (from brainstorming, 2026-06-04)
| Q | Decision |
|---|---|
| Finish UX when the user HAS a saved signature | Quick confirm with preview — small dialog showing their saved signature + "Sign & Finish", plus a "Use a different signature" link. One tap, no drawing. |
| Finish UX when the user has NO saved signature | Existing draw-pad → on submit, persist the drawing to their Plating Signature + finish. |
| "Use a different signature" | Opens the draw-pad; the new drawing replaces their saved Plating Signature (it is their signature) and signs this step. |
Per-step signature ir.attachment |
Dropped — redundant (reports never read it). Audit of who signed when stays on signoff_user_id + the finish timestamp. |
| Scope | Tablet Job Workspace only. The backend job-form action_signoff already works off x_fc_signature_image implicitly (no draw UI) — unchanged. |
Goals / non-goals
Goals
- A user with a saved Plating Signature never redraws — one-tap confirm.
- A user without one draws exactly once; it persists to their Plating Signature.
- The signature shown on certs/WO reports is the same saved Plating Signature (already true; this guarantees it exists).
Non-goals
- Changing the backend
action_signoff/ job-form flow. - Per-signoff historical signature snapshots (reports already read the live
x_fc_signature_image; not changing that). - Touching the signoff gate logic (
requires_signoff,_fp_autosign_if_required,_fp_check_signoff_complete) — unchanged. - QC-checklist or any non-workspace signature surface (none use
FpSignaturePad).
Architecture
1. Workspace load payload — expose the saved signature
In the /fp/workspace/load payload builder (workspace_controller.py),
add two keys derived from the current user (request.env.user, already
the per-tech session):
user = request.env.user
sig = user.x_fc_signature_image # base64 or False (SELF_READABLE)
payload['user_has_plating_signature'] = bool(sig)
payload['user_plating_signature'] = (
('data:image/png;base64,%s' % sig.decode()) if sig else ''
)
(x_fc_signature_image is a small PNG; one data URI per load is fine. If
it ever grows, switch to a /web/image/res.users/<uid>/x_fc_signature_image
URL — deferred.)
2. Frontend — confirm-vs-draw in onFinishStep
job_workspace.js, onFinishStep(step) — replace the unconditional
FpSignaturePad branch with:
if (step.requires_signoff) {
if (this.state.data.user_has_plating_signature) {
this.dialog.add(FpSignatureConfirm, {
title: `Sign to finish ${step.name}`,
contextLabel: `${this.state.data.job.display_wo_name} · Step ${step.sequence_display}: ${step.name}`,
signatureUrl: this.state.data.user_plating_signature,
onConfirm: () => this._commitSignOff(step, null), // no drawing -> use saved
onRedraw: () => this._openSignaturePad(step), // draw -> replaces saved
});
} else {
this._openSignaturePad(step); // first time -> draw + persist
}
return;
}
await this._callFinishStep(step, false); // plain finish (unchanged)
New helpers:
_openSignaturePad(step)— opens the existingFpSignaturePad; itsonSubmit(dataUri)callsthis._commitSignOff(step, dataUri)._commitSignOff(step, dataUri)— POSTs{ step_id, signature_data_uri: dataUri /* may be null */ }to/fp/workspace/sign_off, handles ok/error notifications +refresh()(the existing logic, factored out of the current inlineonSubmit).
3. New OWL component — FpSignatureConfirm
fusion_plating_shopfloor/static/src/js/components/signature_confirm.js
(+ signature_confirm.xml, reuse _signature_pad.scss tokens or add a
small _signature_confirm.scss). A Dialog showing:
- the saved signature image (
<img t-att-src="props.signatureUrl"/>), - the context label,
- Sign & Finish →
props.onConfirm(); props.close(); - Use a different signature →
props.onRedraw(); props.close(); - Cancel →
props.close();
Props: close, title?, contextLabel?, signatureUrl, onConfirm, onRedraw.
Mirrors FpSignaturePad's shape. Register it in JobWorkspace.components
and the manifest assets.
4. Backend — /fp/workspace/sign_off persists, drops the attachment
workspace_controller.py, sign_off(self, step_id, signature_data_uri=None):
env = request.env
step = env['fp.job.step'].browse(int(step_id))
if not step.exists():
return {'ok': False, 'error': f'Step {step_id} not found'}
sig = (signature_data_uri or '').strip()
user = env.user
if sig:
# A drawing was supplied (first-time, or "use a different signature").
if ',' in sig and sig.startswith('data:'):
sig = sig.split(',', 1)[1]
try:
user.write({'x_fc_signature_image': sig}) # SELF_WRITEABLE; own record
except Exception:
_logger.exception("sign_off: persisting Plating Signature failed for uid %s", env.uid)
return {'ok': False, 'error': 'Failed to save your signature.'}
elif not user.x_fc_signature_image:
# No drawing AND no saved signature — nothing to sign with.
return {'ok': False, 'error': 'A signature is required. Draw one to continue.'}
try:
step.button_finish() # sets signoff_user_id + gates
except Exception as exc:
_logger.exception("sign_off: button_finish failed")
return {'ok': False, 'error': str(exc)}
return {'ok': True, 'step_id': step.id, 'state': step.state}
signature_data_uriis now optional (defaultsNone).- No
ir.attachmentis created (the dropped per-step artifact). - The signature persists to the user's own
x_fc_signature_image(direct write — the field is inSELF_WRITEABLE_FIELDS).
Files touched
| # | File | Change |
|---|---|---|
| 1 | fusion_plating_shopfloor/controllers/workspace_controller.py |
sign_off: optional signature_data_uri, persist to x_fc_signature_image, drop attachment; add user_has_plating_signature + user_plating_signature to the load payload. |
| 2 | fusion_plating_shopfloor/static/src/js/components/signature_confirm.js |
NEW confirm dialog. |
| 3 | fusion_plating_shopfloor/static/src/xml/components/signature_confirm.xml |
NEW template. |
| 4 | fusion_plating_shopfloor/static/src/scss/components/_signature_confirm.scss |
NEW (small). |
| 5 | fusion_plating_shopfloor/static/src/js/job_workspace.js |
onFinishStep branch; _openSignaturePad + _commitSignOff helpers; register FpSignatureConfirm. |
| 6 | fusion_plating_shopfloor/__manifest__.py |
add the 3 new asset files + version bump. |
No model, view, ACL, or migration changes. res.users.x_fc_signature_image already exists with the right SELF_* access.
Edge cases
| Case | Behaviour |
|---|---|
| Has saved sig → "Sign & Finish" | No drawing sent; button_finish() only; report uses saved sig. |
| No saved sig → draw | Drawing persists to x_fc_signature_image; future steps are one-tap. |
| Has saved sig → "Use a different signature" → draw | New drawing replaces saved sig + signs. |
| Empty draw | FpSignaturePad.onSubmit already no-ops without ink; backend also rejects empty+no-saved. |
button_finish raises a gate error (required inputs, predecessor, etc.) |
Returned as {ok:false, error} and shown as a notification — the signature has already persisted (harmless; it's their signature either way). |
| Manager/Owner with no saved sig | Same flow — draws once, persists. |
Testing
fusion_plating_shopfloor can't install on local Community; verify on an
entech clone (-u + odoo-shell), like the WO-grouping deploy.
- Unit (controller logic, runnable where the module installs):
sign_offwith a data URI writesenv.user.x_fc_signature_imageand finishes;sign_offwith no URI + an existing saved sig finishes without writing;sign_offwith no URI + no saved sig returns the "signature required" error; noir.attachmentis created in any path. - Payload:
/fp/workspace/loadreturnsuser_has_plating_signature=False+ emptyuser_plating_signaturefor a user with no sig, andTrue+ adata:image/png;base64,…URI once set. - Live smoke (entech clone): a tech with no Plating Signature draws on a sign-off step → their
x_fc_signature_imageis populated; the next sign-off shows the confirm-preview (no pad); the WO Detail report renders the saved signature.
Static-check note
node --check rejects ESM import on a .js; copy the OWL files to
/tmp/x.mjs for a syntax check, and lxml/ET-parse the .xml template
(per the project's static-check conventions).