docs(fusion_plating_shopfloor): spec - reuse saved Plating Signature on sign-off

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>
This commit is contained in:
gsinghpal
2026-06-04 00:06:10 -04:00
parent 774d21863e
commit 5e3e6b5319

View File

@@ -0,0 +1,192 @@
# 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](../../../fusion_plating_shopfloor/static/src/js/job_workspace.js)) — when `step.requires_signoff`, always opens `FpSignaturePad`; on submit POSTs the drawing to `/fp/workspace/sign_off`.
- `/fp/workspace/sign_off` ([workspace_controller.py:451](../../../fusion_plating_shopfloor/controllers/workspace_controller.py)) — requires a non-empty `signature_data_uri`, creates a per-step `ir.attachment` from it, then calls `step.button_finish()` (which sets `signoff_user_id` via `_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)` on `res.users` (defined in `fusion_plating_jobs/models/res_users.py`), already in `SELF_READABLE_FIELDS` **and** `SELF_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):
```python
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:
```js
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 existing `FpSignaturePad`; its `onSubmit(dataUri)` calls `this._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 inline `onSubmit`).
### 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)`:
```python
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_uri` is now **optional** (defaults `None`).
- No `ir.attachment` is created (the dropped per-step artifact).
- The signature persists to the user's own `x_fc_signature_image` (direct write — the field is in `SELF_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_off` with a data URI writes `env.user.x_fc_signature_image` and finishes; `sign_off` with no URI + an existing saved sig finishes without writing; `sign_off` with no URI + no saved sig returns the "signature required" error; no `ir.attachment` is created in any path.
- **Payload:** `/fp/workspace/load` returns `user_has_plating_signature=False` + empty `user_plating_signature` for a user with no sig, and `True` + a `data: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_image` is 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).