Files
Odoo-Modules/fusion_plating/docs/superpowers/specs/2026-06-04-shopfloor-signoff-reuse-plating-signature-design.md
gsinghpal 5e3e6b5319 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>
2026-06-04 00:06:10 -04:00

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) — 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) — 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):

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 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 & Finishprops.onConfirm(); props.close();
  • Use a different signatureprops.onRedraw(); props.close();
  • Cancelprops.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_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).