feat(fusion_plating_shopfloor): sign_off reuses+persists Plating Signature; load exposes it
/fp/workspace/sign_off: signature_data_uri now optional; a supplied drawing persists to res.users.x_fc_signature_image (SELF_WRITEABLE) and the wasted per-step ir.attachment is dropped; no drawing + a saved signature just finishes. /fp/workspace/load exposes user_has_plating_signature + user_plating_signature. Merged 3 new tests into the existing TestWorkspaceSignOff. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -240,6 +240,11 @@ class FpWorkspaceController(http.Controller):
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
'ok': True,
|
'ok': True,
|
||||||
|
'user_has_plating_signature': bool(env.user.x_fc_signature_image),
|
||||||
|
'user_plating_signature': (
|
||||||
|
('data:image/png;base64,%s' % env.user.x_fc_signature_image.decode())
|
||||||
|
if env.user.x_fc_signature_image else ''
|
||||||
|
),
|
||||||
'job': {
|
'job': {
|
||||||
'id': job.id,
|
'id': job.id,
|
||||||
'name': job.name,
|
'name': job.name,
|
||||||
@@ -448,37 +453,35 @@ class FpWorkspaceController(http.Controller):
|
|||||||
# /fp/workspace/sign_off — capture signature + finish step atomically
|
# /fp/workspace/sign_off — capture signature + finish step atomically
|
||||||
# ======================================================================
|
# ======================================================================
|
||||||
@http.route('/fp/workspace/sign_off', type='jsonrpc', auth='user')
|
@http.route('/fp/workspace/sign_off', type='jsonrpc', auth='user')
|
||||||
def sign_off(self, step_id, signature_data_uri):
|
def sign_off(self, step_id, signature_data_uri=None):
|
||||||
env = request.env
|
env = request.env
|
||||||
sig = (signature_data_uri or '').strip()
|
|
||||||
if not sig:
|
|
||||||
_logger.warning("workspace/sign_off: empty signature for step %s", step_id)
|
|
||||||
return {
|
|
||||||
'ok': False,
|
|
||||||
'error': 'A signature is required to finish this step.',
|
|
||||||
}
|
|
||||||
|
|
||||||
step = env['fp.job.step'].browse(int(step_id))
|
step = env['fp.job.step'].browse(int(step_id))
|
||||||
if not step.exists():
|
if not step.exists():
|
||||||
return {'ok': False, 'error': f'Step {step_id} not found'}
|
return {'ok': False, 'error': f'Step {step_id} not found'}
|
||||||
|
|
||||||
# Strip "data:...;base64," prefix if present (canvas.toDataURL adds it)
|
sig = (signature_data_uri or '').strip()
|
||||||
if ',' in sig and sig.startswith('data:'):
|
user = env.user
|
||||||
sig = sig.split(',', 1)[1]
|
if sig:
|
||||||
|
# A drawing was supplied (first-time, or "use a different
|
||||||
try:
|
# signature"). Persist it as the user's Plating Signature so
|
||||||
env['ir.attachment'].create({
|
# every future sign-off + report reuses it. x_fc_signature_image
|
||||||
'name': f'signature_{step.id}.png',
|
# is in SELF_WRITEABLE_FIELDS, so writing one's own is allowed.
|
||||||
'datas': sig,
|
if ',' in sig and sig.startswith('data:'):
|
||||||
'res_model': 'fp.job.step',
|
sig = sig.split(',', 1)[1]
|
||||||
'res_id': step.id,
|
try:
|
||||||
'mimetype': 'image/png',
|
user.write({'x_fc_signature_image': sig})
|
||||||
})
|
except Exception:
|
||||||
except Exception:
|
_logger.exception(
|
||||||
_logger.exception(
|
"workspace/sign_off: persisting Plating Signature failed for uid %s",
|
||||||
"workspace/sign_off: attachment failed for step %s", step.id,
|
env.uid,
|
||||||
)
|
)
|
||||||
return {'ok': False, 'error': 'Failed to save signature.'}
|
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:
|
try:
|
||||||
step.button_finish()
|
step.button_finish()
|
||||||
@@ -487,11 +490,7 @@ class FpWorkspaceController(http.Controller):
|
|||||||
return {'ok': False, 'error': str(exc)}
|
return {'ok': False, 'error': str(exc)}
|
||||||
|
|
||||||
_logger.info("Step %s signed off by uid %s", step.id, env.uid)
|
_logger.info("Step %s signed off by uid %s", step.id, env.uid)
|
||||||
return {
|
return {'ok': True, 'step_id': step.id, 'state': step.state}
|
||||||
'ok': True,
|
|
||||||
'step_id': step.id,
|
|
||||||
'state': step.state,
|
|
||||||
}
|
|
||||||
|
|
||||||
# ======================================================================
|
# ======================================================================
|
||||||
# /fp/workspace/advance_milestone — fire next_milestone_action
|
# /fp/workspace/advance_milestone — fire next_milestone_action
|
||||||
|
|||||||
@@ -126,6 +126,8 @@ class TestWorkspaceSignOff(HttpCase):
|
|||||||
})
|
})
|
||||||
|
|
||||||
def test_sign_off_rejects_empty_signature(self):
|
def test_sign_off_rejects_empty_signature(self):
|
||||||
|
# Empty drawing AND no saved Plating Signature -> reject.
|
||||||
|
self.env.user.x_fc_signature_image = False
|
||||||
res = _rpc(
|
res = _rpc(
|
||||||
self, '/fp/workspace/sign_off',
|
self, '/fp/workspace/sign_off',
|
||||||
step_id=self.step.id, signature_data_uri='',
|
step_id=self.step.id, signature_data_uri='',
|
||||||
@@ -142,6 +144,46 @@ class TestWorkspaceSignOff(HttpCase):
|
|||||||
self.step.invalidate_recordset(['state'])
|
self.step.invalidate_recordset(['state'])
|
||||||
self.assertEqual(self.step.state, 'done')
|
self.assertEqual(self.step.state, 'done')
|
||||||
|
|
||||||
|
def test_load_exposes_plating_signature_flags(self):
|
||||||
|
self.env.user.x_fc_signature_image = False
|
||||||
|
res = _rpc(self, '/fp/workspace/load', job_id=self.job.id)
|
||||||
|
self.assertFalse(res['user_has_plating_signature'])
|
||||||
|
self.assertEqual(res['user_plating_signature'], '')
|
||||||
|
self.env.user.x_fc_signature_image = _TINY_PNG_B64
|
||||||
|
res2 = _rpc(self, '/fp/workspace/load', job_id=self.job.id)
|
||||||
|
self.assertTrue(res2['user_has_plating_signature'])
|
||||||
|
self.assertTrue(
|
||||||
|
res2['user_plating_signature'].startswith('data:image/png;base64,'))
|
||||||
|
|
||||||
|
def test_sign_off_with_drawing_persists_signature_and_drops_attachment(self):
|
||||||
|
# First-time draw: persists to the user's Plating Signature, finishes
|
||||||
|
# the (in_progress) step, and creates NO per-step signature attachment.
|
||||||
|
self.env.user.x_fc_signature_image = False
|
||||||
|
data_uri = 'data:image/png;base64,' + _TINY_PNG_B64
|
||||||
|
res = _rpc(
|
||||||
|
self, '/fp/workspace/sign_off',
|
||||||
|
step_id=self.step.id, signature_data_uri=data_uri,
|
||||||
|
)
|
||||||
|
self.assertTrue(res['ok'])
|
||||||
|
self.step.invalidate_recordset(['state'])
|
||||||
|
self.assertEqual(self.step.state, 'done')
|
||||||
|
self.env.user.invalidate_recordset(['x_fc_signature_image'])
|
||||||
|
self.assertTrue(
|
||||||
|
self.env.user.x_fc_signature_image,
|
||||||
|
'drawing persisted to the Plating Signature')
|
||||||
|
n = self.env['ir.attachment'].search_count([
|
||||||
|
('res_model', '=', 'fp.job.step'), ('res_id', '=', self.step.id)])
|
||||||
|
self.assertEqual(n, 0, 'no per-step signature attachment is created')
|
||||||
|
|
||||||
|
def test_sign_off_uses_saved_signature_without_drawing(self):
|
||||||
|
# User already has a saved signature -> finishing without a drawing
|
||||||
|
# still works (no signature_data_uri sent).
|
||||||
|
self.env.user.x_fc_signature_image = _TINY_PNG_B64
|
||||||
|
res = _rpc(self, '/fp/workspace/sign_off', step_id=self.step.id)
|
||||||
|
self.assertTrue(res['ok'])
|
||||||
|
self.step.invalidate_recordset(['state'])
|
||||||
|
self.assertEqual(self.step.state, 'done')
|
||||||
|
|
||||||
|
|
||||||
@tagged('-at_install', 'post_install', 'fp_shopfloor')
|
@tagged('-at_install', 'post_install', 'fp_shopfloor')
|
||||||
class TestWorkspaceAdvanceMilestone(HttpCase):
|
class TestWorkspaceAdvanceMilestone(HttpCase):
|
||||||
|
|||||||
Reference in New Issue
Block a user