diff --git a/fusion_plating/fusion_plating_shopfloor/controllers/workspace_controller.py b/fusion_plating/fusion_plating_shopfloor/controllers/workspace_controller.py index 895914ae..9a0563e2 100644 --- a/fusion_plating/fusion_plating_shopfloor/controllers/workspace_controller.py +++ b/fusion_plating/fusion_plating_shopfloor/controllers/workspace_controller.py @@ -240,6 +240,11 @@ class FpWorkspaceController(http.Controller): return { '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': { 'id': job.id, 'name': job.name, @@ -448,37 +453,35 @@ class FpWorkspaceController(http.Controller): # /fp/workspace/sign_off — capture signature + finish step atomically # ====================================================================== @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 - 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)) if not step.exists(): return {'ok': False, 'error': f'Step {step_id} not found'} - # Strip "data:...;base64," prefix if present (canvas.toDataURL adds it) - if ',' in sig and sig.startswith('data:'): - sig = sig.split(',', 1)[1] - - try: - env['ir.attachment'].create({ - 'name': f'signature_{step.id}.png', - 'datas': sig, - 'res_model': 'fp.job.step', - 'res_id': step.id, - 'mimetype': 'image/png', - }) - except Exception: - _logger.exception( - "workspace/sign_off: attachment failed for step %s", step.id, - ) - return {'ok': False, 'error': 'Failed to save signature.'} + sig = (signature_data_uri or '').strip() + user = env.user + if sig: + # A drawing was supplied (first-time, or "use a different + # signature"). Persist it as the user's Plating Signature so + # every future sign-off + report reuses it. x_fc_signature_image + # is in SELF_WRITEABLE_FIELDS, so writing one's own is allowed. + if ',' in sig and sig.startswith('data:'): + sig = sig.split(',', 1)[1] + try: + user.write({'x_fc_signature_image': sig}) + except Exception: + _logger.exception( + "workspace/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() @@ -487,11 +490,7 @@ class FpWorkspaceController(http.Controller): return {'ok': False, 'error': str(exc)} _logger.info("Step %s signed off by uid %s", step.id, env.uid) - return { - 'ok': True, - 'step_id': step.id, - 'state': step.state, - } + return {'ok': True, 'step_id': step.id, 'state': step.state} # ====================================================================== # /fp/workspace/advance_milestone — fire next_milestone_action diff --git a/fusion_plating/fusion_plating_shopfloor/tests/test_workspace_controller.py b/fusion_plating/fusion_plating_shopfloor/tests/test_workspace_controller.py index 7da17d9e..3dfce47b 100644 --- a/fusion_plating/fusion_plating_shopfloor/tests/test_workspace_controller.py +++ b/fusion_plating/fusion_plating_shopfloor/tests/test_workspace_controller.py @@ -126,6 +126,8 @@ class TestWorkspaceSignOff(HttpCase): }) 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( self, '/fp/workspace/sign_off', step_id=self.step.id, signature_data_uri='', @@ -142,6 +144,46 @@ class TestWorkspaceSignOff(HttpCase): self.step.invalidate_recordset(['state']) 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') class TestWorkspaceAdvanceMilestone(HttpCase):