293 lines
12 KiB
Python
293 lines
12 KiB
Python
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
|
|
|
import base64
|
|
import io
|
|
import json
|
|
import uuid
|
|
from werkzeug.urls import url_join
|
|
|
|
from odoo import _, http
|
|
from odoo.tools import consteq
|
|
from odoo.http import request
|
|
from odoo.exceptions import ValidationError
|
|
from odoo.tools.pdf import PdfFileReader, PdfReadError
|
|
|
|
from odoo.addons.iap import jsonrpc
|
|
from odoo.addons.sign.controllers.main import Sign as SignController
|
|
from odoo.addons.sign_emsigner.utils import compress_pdf_base64, decompress_pdf_base64
|
|
from odoo.addons.sign_emsigner.const import IAP_DEFAULT_DOMAIN
|
|
|
|
IAP_SERVICE_NAME = 'emsigner_proxy'
|
|
|
|
|
|
class SignEmsigner(SignController):
|
|
|
|
def get_document_qweb_context(self, sign_request_id, token, **post):
|
|
"""
|
|
Override to add show_emsigner_thank_you_dialog and error_message to the context
|
|
"""
|
|
res = super().get_document_qweb_context(sign_request_id, token, **post)
|
|
if res.get('rendering_context'):
|
|
# show_thank_you_dialog and error_message come from IAP sign_emsigner redirect
|
|
res['rendering_context']['show_emsigner_thank_you_dialog'] = post.get('show_emsigner_thank_you_dialog')
|
|
res['rendering_context']['error_message'] = post.get('error_message')
|
|
return res
|
|
|
|
def _get_signature_fields_position(self, document_id, request_item_sudo):
|
|
""" Compute the positions of all signature fields on the PDF for a given document and request item.
|
|
|
|
This method retrieves all signature items associated with the given request item (typically
|
|
based on role), and calculates their absolute positions on the PDF page.
|
|
|
|
:param document_id: sign.document record
|
|
:param request_item_sudo: sign.request.item record
|
|
:return: dict of signature field positions
|
|
"""
|
|
try:
|
|
pdf_file_data = io.BytesIO(document_id.attachment_id.raw)
|
|
pdf_file_reader = PdfFileReader(pdf_file_data, strict=False)
|
|
pdf_file_reader.getNumPages()
|
|
except (ValueError, PdfReadError) as e:
|
|
raise ValidationError(self.env._("ERROR: Invalid PDF file! %s") % str(e))
|
|
|
|
page_size = document_id._get_page_size(pdf_file_reader)
|
|
pdf_width = float(page_size[0])
|
|
pdf_height = float(page_size[1])
|
|
|
|
# Get all signature items for the role
|
|
signature_item_ids = request_item_sudo._get_current_signature_sign_items()
|
|
fields_position = []
|
|
|
|
for item in signature_item_ids:
|
|
fields_position.append({
|
|
'page': item.page,
|
|
'left': pdf_width * item.posX,
|
|
'top': pdf_height * (1 - item.posY),
|
|
'width': (pdf_width * item.posX) + (pdf_width * item.width),
|
|
'height': pdf_height * (1 - item.posY - item.height),
|
|
})
|
|
return fields_position
|
|
|
|
def _is_file_large(self, file_size):
|
|
""" Check if the file size exceeds the 10 MB limit.
|
|
:param file_size: size of the file in bytes
|
|
:return: bool
|
|
"""
|
|
return file_size >= (1024 * 1024 * 9) # 9 MB limit for compression
|
|
|
|
def _get_emsigner_params(self, request_item_sudo, **kwargs):
|
|
""" create the payload to send data to the emsigner portal
|
|
|
|
:params request_item_sudo: sign.request.item record
|
|
:return: dict
|
|
"""
|
|
sign_request = request_item_sudo.sign_request_id
|
|
document_id = sign_request.template_id.document_ids
|
|
|
|
if len(document_id) != 1:
|
|
raise ValidationError(
|
|
self.env._("Emsigner only supports signing a single document at a time.")
|
|
)
|
|
|
|
signed_values = {}
|
|
# Read the values of the sign request item values related to the sign request
|
|
values_dict = self.env['sign.request.item.value'].sudo()._read_group(
|
|
[('sign_request_id', '=', sign_request.id)],
|
|
groupby=['sign_item_id'],
|
|
aggregates=['value:array_agg', 'frame_value:array_agg', 'frame_has_hash:array_agg']
|
|
)
|
|
|
|
# Add current signer's values to the signed_values dict
|
|
for sign_item, values, frame_values, frame_has_hashes in values_dict:
|
|
signed_values[sign_item.id] = {
|
|
'value': values[0],
|
|
'frame': frame_values[0],
|
|
'frame_has_hash': frame_has_hashes[0],
|
|
}
|
|
|
|
signature_info = kwargs.get('signatureInfo', {})
|
|
frame_info = kwargs.get('frame', {})
|
|
|
|
for key_str, value in signature_info.items():
|
|
key = int(key_str) # convert string key to int
|
|
frame_data = frame_info.get(key_str, {})
|
|
signed_values[key] = {
|
|
'value': value,
|
|
'frame': frame_data.get('frameValue'),
|
|
'frame_has_hash': bool(frame_data.get('frameHash'))
|
|
}
|
|
|
|
final_log_hash = sign_request._get_final_signature_log_hash()
|
|
output = document_id.render_document_with_items(
|
|
signed_values=signed_values,
|
|
values_dict=True,
|
|
final_log_hash=final_log_hash
|
|
)
|
|
to_sign_data = output.getvalue()
|
|
output.close()
|
|
|
|
# Generate a unique reference number
|
|
reference_number = uuid.uuid4().hex
|
|
|
|
# Get the signature position on the pdf page
|
|
signature_position = self._get_signature_fields_position(document_id, request_item_sudo)
|
|
page_numbers = ",".join(str(item['page']) for item in signature_position)
|
|
# Offset (+8 left, -35 height) to prevent emSigner certificate from overlapping Odoo signature by placing it slightly below.
|
|
page_coordinates_with_page = ";".join([
|
|
f"{item['page']},{item['left'] + 8},{item['top'] - (item['top'] - item['height'])},{item['width']},{item['height'] - 35}"
|
|
for item in signature_position
|
|
])
|
|
|
|
# Check if the file is large and needs compression
|
|
is_file_large = self._is_file_large(file_size=document_id.attachment_id.file_size)
|
|
if is_file_large:
|
|
to_sign_data = compress_pdf_base64(to_sign_data)
|
|
|
|
# Prepare the dynamic content for the emsigner
|
|
dyamic_content = [
|
|
{
|
|
"Key": "eSigned using Aadhaar by",
|
|
"Value": request_item_sudo.partner_id.email
|
|
},
|
|
{
|
|
"Key": "Date",
|
|
"Value": "DateTime"
|
|
}
|
|
]
|
|
dyamic_content_bytes = json.dumps(dyamic_content).encode('utf-8')
|
|
dyamic_content_data = base64.b64encode(dyamic_content_bytes)
|
|
|
|
return {
|
|
"Name": request_item_sudo.partner_id.name,
|
|
"FileType": "PDF",
|
|
"SignatureMode": "12", # 12 for Aadhaar based eSign
|
|
"SelectPage": "PAGE LEVEL",
|
|
"File": base64.b64encode(to_sign_data).decode('utf-8'),
|
|
"PageNumber": page_numbers,
|
|
"PreviewRequired": False,
|
|
"PagelevelCoordinates": page_coordinates_with_page,
|
|
"ReferenceNumber": reference_number,
|
|
"EnableUploadSignature": False,
|
|
"EnableFontSignature": False,
|
|
"EnableDrawSignature": False,
|
|
"EnableESignaturePad": False,
|
|
"IsCompressed": is_file_large,
|
|
"IsCosign": False, # Make the value True if there are multiple signers
|
|
"StoreToDB": True,
|
|
"IsGSTN": False,
|
|
"IsGSTN3B": False,
|
|
"AuthenticationMode": 1,
|
|
"Reason": f"Digitally Signed by {request_item_sudo.partner_id.email}",
|
|
"DynamicContent": dyamic_content_data.decode('utf-8'),
|
|
'eMudhraV2SignatureType': 1,
|
|
}
|
|
|
|
def _validate_auth_method(self, request_item_sudo, **kwargs):
|
|
if request_item_sudo.role_id.auth_method == 'emsigner':
|
|
account = request.env['iap.account'].sudo().get(IAP_SERVICE_NAME)
|
|
if not account.sudo().account_token:
|
|
return {
|
|
'success': False,
|
|
'message': _("emSigner IAP service could not be found.")
|
|
}
|
|
endpoint = request.env['ir.config_parameter'].sudo().get_param(
|
|
'sign_emsigner.iap_endpoint', IAP_DEFAULT_DOMAIN
|
|
)
|
|
emsigner_credits = request.env['iap.account'].sudo().get_credits(IAP_SERVICE_NAME)
|
|
# If no credits are available, still allow signing without emsigner auth method
|
|
if emsigner_credits < 1:
|
|
request_item_sudo.signed_without_extra_auth = True
|
|
return {
|
|
'success': True
|
|
}
|
|
params = {
|
|
'signature_data': self._get_emsigner_params(request_item_sudo, **kwargs),
|
|
'account_token': account.sudo().account_token, # FOR IAP CREDIT
|
|
'emsigner_state': '%s.%s' % (request_item_sudo.sign_request_id.id, request_item_sudo.access_token),
|
|
'db_uuid': self.env['ir.config_parameter'].sudo().get_param('database.uuid'),
|
|
'url': account.get_base_url(),
|
|
}
|
|
response = jsonrpc(url_join(endpoint, '/api/emsigner/1/sign_identity_request'), params=params)
|
|
return response
|
|
return super()._validate_auth_method(request_item_sudo, **kwargs)
|
|
|
|
# -------------
|
|
# HTTP Routes
|
|
# -------------
|
|
|
|
@http.route('/emsigner_sign/emsigner_successful', type='jsonrpc', auth='public', csrf='false')
|
|
def sign_emsigner_complete(
|
|
self, emsigner_state, reference_number, transaction_number, return_status, error_message, decrypted_data
|
|
):
|
|
""" After successful signing, from emsigner portal, this method is called to complete the signing process for odoo side.
|
|
:param emsigner_state: The state from emsigner, which contains sign_request_id and access_token
|
|
:param reference_number: Unique reference from eMSigner
|
|
:param transaction_number: The transaction number from emsigner
|
|
:param return_status: The status of the signing process
|
|
:param error_message: Any error message returned from emsigner
|
|
:param decrypted_data: The signed document data in base64 format
|
|
:return: dict indicating success or failure of the signing process
|
|
"""
|
|
if not emsigner_state or error_message:
|
|
return {
|
|
'success': False,
|
|
}
|
|
sign_request_id, token = emsigner_state.split(".")
|
|
items_sudo = request.env['sign.request.item'].sudo().search([
|
|
('sign_request_id', '=', int(sign_request_id)),
|
|
('state', '=', 'sent')
|
|
])
|
|
|
|
request_item = next(
|
|
(item for item in items_sudo if consteq(item.access_token, token)),
|
|
None
|
|
)
|
|
|
|
if not (request_item and request_item.role_id.auth_method == 'emsigner'):
|
|
return {
|
|
'success': False
|
|
}
|
|
|
|
sign_user_sudo = request.env['res.users'].sudo().search(
|
|
[('partner_id', '=', request_item.partner_id.id)],
|
|
limit=1
|
|
)
|
|
if sign_user_sudo:
|
|
# sign as a known user
|
|
context = {}
|
|
if request.env.user != sign_user_sudo and not request.env.user._is_public():
|
|
context.update(logged_user_id=request.env.user.id)
|
|
request_item = request_item.with_context(context).with_user(sign_user_sudo).sudo()
|
|
|
|
sign_request = request_item.sign_request_id
|
|
|
|
document_id = sign_request.template_id.document_ids
|
|
if len(document_id) != 1:
|
|
raise ValidationError(
|
|
self.env._("Emsigner only supports signing a single document at a time.")
|
|
)
|
|
|
|
# Check if file is large and needs decompression
|
|
file_size = document_id.attachment_id.file_size
|
|
if self._is_file_large(file_size=file_size):
|
|
decrypted_data = decompress_pdf_base64(decrypted_data)
|
|
|
|
sign_request.completed_document_ids = request.env['sign.completed.document'].sudo().create({
|
|
'sign_request_id': sign_request.id,
|
|
'file': decrypted_data.encode() or sign_request.template_id.document_ids.datas,
|
|
'document_id': sign_request.template_id.document_ids.id,
|
|
})
|
|
|
|
request_item._write_emsigner_data(reference_number, transaction_number, return_status)
|
|
request_item._post_fill_request_item()
|
|
|
|
return {
|
|
'success': True
|
|
}
|
|
|
|
# CREDITS
|
|
|
|
@http.route(['/emsigner/has_emsigner_credits'], type="jsonrpc", auth="public")
|
|
def has_emsigner_credits(self):
|
|
return request.env['iap.account'].sudo().get_credits(IAP_SERVICE_NAME) >= 1
|