440 lines
22 KiB
Python
440 lines
22 KiB
Python
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
|
|
|
from dateutil.relativedelta import relativedelta
|
|
|
|
from odoo import api, fields, models, Command
|
|
from odoo.exceptions import UserError
|
|
from odoo.tools import format_date
|
|
|
|
|
|
class SignSendRequest(models.TransientModel):
|
|
_name = 'sign.send.request'
|
|
_description = 'Sign send request'
|
|
|
|
def _selection_target_model(self):
|
|
return [(model.model, model.name) for model in self.env['ir.model'].sudo().search(
|
|
[
|
|
('model', '!=', 'sign.request'),
|
|
('is_mail_thread', '=', 'True'),
|
|
]
|
|
)]
|
|
|
|
activity_id = fields.Many2one('mail.activity', 'Linked Activity', readonly=True)
|
|
reference_doc = fields.Reference(string="Linked to", selection='_selection_target_model', readonly=True)
|
|
has_default_template = fields.Boolean()
|
|
available_template_ids = fields.Many2many(comodel_name='sign.template', compute='_compute_available_template_ids')
|
|
template_id = fields.Many2one('sign.template', string="Sign Template", ondelete='cascade')
|
|
|
|
signer_ids = fields.One2many('sign.send.request.signer', 'sign_send_request_id', string="Signers", compute='_compute_signer_ids', store=True)
|
|
set_sign_order = fields.Boolean(string="Signing Order",
|
|
help="""Specify the order for each signer. The signature request only gets sent to \
|
|
the next signers in the sequence when all signers from the previous level have \
|
|
signed the document.
|
|
""")
|
|
signer_id = fields.Many2one('res.partner', string="Send To")
|
|
signers_count = fields.Integer(compute='_compute_signers_count')
|
|
cc_partner_ids = fields.Many2many('res.partner', string="Copy to", help="Contacts in copy will be notified by email once the document is either fully signed or refused.")
|
|
is_user_signer = fields.Boolean(compute='_compute_is_user_signer')
|
|
|
|
subject = fields.Char(string="Subject", compute='_compute_subject', store=True, readonly=False)
|
|
body = fields.Html('body', compute='_compute_mail_message_body', readonly=False, help="Message to be sent to signers of the specified document", store=True)
|
|
message_cc = fields.Html("CC Message", help="Message to be sent to contacts in copy of the signed document")
|
|
attachment_ids = fields.Many2many('ir.attachment', string='Attachments', bypass_search_access=True)
|
|
filename = fields.Char("Filename", compute='_compute_filename', store=True)
|
|
|
|
validity = fields.Date(compute='_compute_validity', store=True, readonly=False,
|
|
string='Valid Until', help="Leave empty for requests without expiration.")
|
|
reminder_enabled = fields.Boolean(default=False)
|
|
reminder = fields.Integer(string='Reminder', default=7)
|
|
certificate_reference = fields.Boolean(string="Certificate Reference", default=False, help="If checked, the unique certificate reference will be added on the final signed document.")
|
|
model = fields.Char('Related Document Model')
|
|
res_ids = fields.Text('Related Document IDs')
|
|
scheduled_date = fields.Char('Scheduled Date')
|
|
|
|
only_autofill_readonly = fields.Boolean(
|
|
string='Only Autofill',
|
|
compute='_compute_only_autofill_readonly',
|
|
)
|
|
display_download_button = fields.Boolean(
|
|
string="Display Download Button",
|
|
compute='_compute_display_download_button',
|
|
store=False
|
|
)
|
|
|
|
@api.constrains('validity')
|
|
def _check_validity(self):
|
|
if self.validity and self.validity < fields.Date.today():
|
|
raise UserError(self.env._('Request expiration date must be set in the future.'))
|
|
|
|
@api.onchange('reminder')
|
|
def _onchange_reminder(self):
|
|
if self.reminder > 365:
|
|
self.reminder = 365
|
|
|
|
def _get_default_signer(self):
|
|
"""Helper method to define default signer (see hr_recruitment_sign/wizard/sign_send_request.py).
|
|
"""
|
|
if self.reference_doc or self.env.context.get('default_reference_doc'):
|
|
ref = self.reference_doc
|
|
# If the reference document has a direct partner, such as in the contacts module.
|
|
if ref._name == 'res.partner':
|
|
return ref.id
|
|
|
|
# return the partner_id of the reference document
|
|
partner = 'partner_id' in ref and ref.partner_id
|
|
if partner:
|
|
return partner.id
|
|
|
|
# If the reference document has an user_id, some modules like MRP etc.
|
|
user = 'user_id' in ref and ref.user_id
|
|
if user and user.partner_id:
|
|
return user.partner_id.id
|
|
|
|
# If the reference document has an employee_id, some modules like hr_recruitment, expense, etc.
|
|
employee = 'employee_id' in ref and ref.employee_id
|
|
if employee and employee.work_contact_id:
|
|
return employee.work_contact_id.id
|
|
|
|
return self.env.context.get("default_signer_id", self.env.user.partner_id.id)
|
|
|
|
@api.depends('template_id', 'set_sign_order', 'template_id.sign_item_ids')
|
|
def _compute_signer_ids(self):
|
|
default_signer = self._get_default_signer()
|
|
for wiz in self:
|
|
template = wiz.template_id
|
|
roles = template.sign_item_ids.responsible_id.sorted()
|
|
signer_ids = []
|
|
if (self.signer_ids and len(self.signer_ids) == len(roles)):
|
|
# Check if all signers currently have the default mail_sent_order = 1
|
|
all_default = all(signer.mail_sent_order == 1 for signer in self.signer_ids)
|
|
for default_signing_order, signer in enumerate(self.signer_ids):
|
|
if wiz.set_sign_order:
|
|
# If signing order is enabled, assign based on order only if all signers are default.
|
|
# Otherwise, keep their existing mail_sent_order.
|
|
mail_sent_order = default_signing_order + 1 if all_default else signer.mail_sent_order
|
|
else:
|
|
mail_sent_order = 1
|
|
signer_ids.append((
|
|
0, 0, {
|
|
'role_id': signer.role_id.id,
|
|
'partner_id': signer.partner_id.id,
|
|
'mail_sent_order': mail_sent_order,
|
|
}
|
|
))
|
|
else:
|
|
for default_signing_order, role in enumerate(roles):
|
|
# First signer logic
|
|
if default_signing_order == 0:
|
|
# If role has assign_to, use it; else use default_signer
|
|
partner_id = role.assign_to.id if role.assign_to else default_signer
|
|
else:
|
|
# For other roles, check assign_to
|
|
partner_id = role.assign_to.id if role.assign_to else False
|
|
signer_vals = {
|
|
'role_id': role.id,
|
|
'partner_id': partner_id,
|
|
'mail_sent_order': default_signing_order + 1 if wiz.set_sign_order else 1,
|
|
}
|
|
signer_ids.append((0, 0, signer_vals))
|
|
wiz.signer_ids = [(5, 0, 0)] + signer_ids
|
|
|
|
@api.depends('signer_ids')
|
|
def _compute_signers_count(self):
|
|
for wiz in self:
|
|
wiz.signers_count = len(wiz.signer_ids)
|
|
|
|
@api.depends('template_id', 'reference_doc')
|
|
def _compute_display_name(self):
|
|
for wiz in self:
|
|
display_name = self.env._("Sign Request ")
|
|
if wiz.reference_doc:
|
|
display_name = self.env._("Sign Request - %(template_name)s - %(res_name)s", template_name=wiz.template_id.display_name, res_name=wiz.reference_doc.display_name or '')
|
|
wiz.display_name = display_name
|
|
|
|
@api.depends('template_id', 'reference_doc')
|
|
def _compute_subject(self):
|
|
for wiz in self:
|
|
subject = self.env._("Signature Request")
|
|
if wiz.reference_doc:
|
|
subject = self.env._("Signature Request - %(template_name)s - %(res_name)s", template_name=wiz.template_id.display_name, res_name=wiz.reference_doc.display_name or '')
|
|
elif wiz.template_id:
|
|
subject = self.env._("Signature Request - %(file_name)s", file_name=wiz.template_id.name)
|
|
wiz.subject = subject
|
|
|
|
@api.depends('template_id', 'reference_doc')
|
|
def _compute_filename(self):
|
|
for wiz in self:
|
|
filename = self.env._("Sign Request")
|
|
if wiz.reference_doc:
|
|
filename = self.env._("Sign Request - %(template_name)s - %(res_name)s", template_name=wiz.template_id.display_name, res_name=wiz.reference_doc.display_name or '')
|
|
elif wiz.template_id:
|
|
filename = wiz.template_id.display_name
|
|
wiz.filename = filename
|
|
|
|
@api.depends('template_id')
|
|
def _compute_validity(self):
|
|
for wiz in self:
|
|
if wiz.template_id:
|
|
if wiz.template_id.signature_request_validity:
|
|
wiz.validity = fields.Date.today() + relativedelta(days=wiz.template_id.signature_request_validity)
|
|
else:
|
|
wiz.validity = None
|
|
|
|
@api.depends('signer_ids.partner_id', 'signer_id', 'signers_count')
|
|
def _compute_is_user_signer(self):
|
|
if self.signers_count and self.env.user.partner_id in self.signer_ids.mapped('partner_id'):
|
|
self.is_user_signer = True
|
|
elif not self.signers_count and self.env.user.partner_id == self.signer_id:
|
|
self.is_user_signer = True
|
|
else:
|
|
self.is_user_signer = False
|
|
|
|
@api.depends('reference_doc')
|
|
def _compute_available_template_ids(self):
|
|
non_specific_templates = self.env['sign.template'].search([('model_id', '=', False)])._filtered_access('read')
|
|
template_by_model = {}
|
|
if self.reference_doc:
|
|
model_names = [ref._name for ref in self.reference_doc if self.reference_doc]
|
|
# unprivilegied user don't have access to ir.model
|
|
model_ids = self.env['ir.model'].sudo().search([('model', 'in', model_names)]).ids
|
|
res = self.env['sign.template']._read_group(
|
|
[('model_id', 'in', model_ids)],
|
|
groupby=['model_id'],
|
|
aggregates=['id:recordset'],
|
|
)
|
|
for model, template in res:
|
|
template_by_model[model.id] = template
|
|
for wiz in self:
|
|
all_template_ids = non_specific_templates.ids
|
|
if wiz.reference_doc:
|
|
model = self.env['ir.model'].sudo()._get(wiz.reference_doc._name)
|
|
other_templates = template_by_model.get(model.id, self.env['sign.template'])
|
|
all_template_ids += other_templates.ids
|
|
wiz.available_template_ids = [Command.set(all_template_ids)]
|
|
|
|
@api.depends('template_id')
|
|
def _compute_mail_message_body(self):
|
|
for wiz in self:
|
|
# Compute mail message
|
|
if wiz.template_id:
|
|
wiz.body = wiz.template_id.message
|
|
|
|
# ==== Business methods ====
|
|
|
|
def _activity_done(self):
|
|
signatories = self.signer_id.name or self.signer_ids.partner_id.mapped('name')
|
|
feedback = self.env._('Signature requested for template: %(template)s\nSignatories: %(signatories)s', template=self.template_id.name, signatories=signatories)
|
|
self.activity_id._action_done(feedback=feedback)
|
|
|
|
def create_request(self):
|
|
send_channel = self.env.context.get('send_channel', 'email')
|
|
template_id = self.template_id.id
|
|
if self.signers_count:
|
|
signers = [{'partner_id': signer.partner_id.id, 'role_id': signer.role_id.id, 'mail_sent_order': signer.mail_sent_order} for signer in self.signer_ids]
|
|
else:
|
|
signers = [{'partner_id': self.signer_id.id, 'role_id': self.env.ref('sign.sign_item_role_default').id, 'mail_sent_order': self.signer_ids.mail_sent_order}]
|
|
cc_partner_ids = self.cc_partner_ids.ids
|
|
reference = self.filename or self.template_id.name
|
|
subject = self.subject
|
|
message = self.body
|
|
message_cc = self.message_cc
|
|
attachment_ids = self.attachment_ids
|
|
scheduled_date = self.scheduled_date or False
|
|
reference_doc = None
|
|
if self.reference_doc:
|
|
reference_doc = f"{self.reference_doc._name},{self.reference_doc.id}"
|
|
sign_request = self.env['sign.request'].with_context(scheduled_date=scheduled_date).create({
|
|
'template_id': template_id,
|
|
'request_item_ids': [Command.create({
|
|
'partner_id': signer['partner_id'],
|
|
'role_id': signer['role_id'],
|
|
'mail_sent_order': signer['mail_sent_order'],
|
|
}) for signer in signers],
|
|
'reference': reference,
|
|
'subject': subject,
|
|
'message': message,
|
|
'message_cc': message_cc,
|
|
'attachment_ids': [Command.set(attachment_ids.ids)],
|
|
'validity': self.validity,
|
|
'reminder': self.reminder,
|
|
'reminder_enabled': self.reminder_enabled,
|
|
'reference_doc': reference_doc,
|
|
'certificate_reference': self.certificate_reference,
|
|
'send_channel': send_channel,
|
|
})
|
|
sign_request.message_subscribe(partner_ids=cc_partner_ids)
|
|
return sign_request
|
|
|
|
def _create_log_and_close(self, request):
|
|
self._create_request_log_note(request)
|
|
if self.activity_id:
|
|
self._activity_done()
|
|
|
|
if not self.reference_doc:
|
|
return self.env['ir.actions.actions']._for_xml_id('sign.sign_request_action')
|
|
|
|
# redirect to the record linked to reference_doc
|
|
next_action = request.get_close_values().get('action')
|
|
|
|
return {
|
|
'type': 'ir.actions.client',
|
|
'tag': 'display_notification',
|
|
'params': {
|
|
'type': 'success',
|
|
'message': self.env._("Request sent successfully"),
|
|
'next': next_action or {'type': 'ir.actions.client', 'tag': 'soft_reload'},
|
|
},
|
|
}
|
|
|
|
def send_request(self):
|
|
self.ensure_one()
|
|
request = self.create_request()
|
|
return self._create_log_and_close(request)
|
|
|
|
def _create_request_log_note(self, request):
|
|
if request.reference_doc:
|
|
model = request.reference_doc and self.env['ir.model']._get(request.reference_doc._name)
|
|
if model.is_mail_thread:
|
|
body = self.env._("A signature request has been linked to this document: %s", request._get_html_link())
|
|
request.reference_doc.message_post(body=body)
|
|
body = self.env._("%s has been linked to this sign request.", request.reference_doc._get_html_link())
|
|
request.message_post(body=body)
|
|
|
|
def sign_directly(self):
|
|
self.ensure_one()
|
|
request = self.create_request()
|
|
if self.activity_id:
|
|
self._activity_done()
|
|
if self.env.context.get('sign_all'):
|
|
# Go back to document if it exists
|
|
return request.go_to_signable_document(request.request_item_ids)
|
|
return request.go_to_signable_document()
|
|
|
|
def _get_user_signature(self, user, signature_type):
|
|
"""
|
|
This function returns the signature or initials of a user (in this case the first). Returns False if signature or initials are not present.
|
|
|
|
:param signature_type: can be 'sign_signature' or 'sign_initials' and inticates what we want to obtain.
|
|
:return: returns the signature/initials if present or False if not or if signature_type is an invalid value.
|
|
"""
|
|
if all(self.mapped('is_user_signer')) and signature_type in ['sign_signature', 'sign_initials']:
|
|
return user[signature_type]
|
|
return False
|
|
|
|
@api.depends("template_id", "signer_ids")
|
|
def _compute_only_autofill_readonly(self):
|
|
"""
|
|
Computes the flag indicating if all the fields in a request are autofillable/constant(readonly).
|
|
|
|
:return: nothing, sets the value in the only_autofill_readonly field.
|
|
"""
|
|
# TODO master: clean this as it is only needed for download button (invisible for more than one signer)
|
|
item_type_to_field = {
|
|
'signature': 'sign_signature',
|
|
'initial': 'sign_initials',
|
|
}
|
|
for request in self:
|
|
role_to_user_map = {signer.role_id.id: signer.partner_id.main_user_id for signer in self.signer_ids}
|
|
item_auto_fill_values = []
|
|
for item in request.template_id.sign_item_ids:
|
|
user = role_to_user_map.get(item.responsible_id.id)
|
|
res = False
|
|
constant_item = item.constant or item.type_id.sudo().auto_field or item.type_id.name == 'Date'
|
|
if constant_item:
|
|
res = True
|
|
elif item.type_id.item_type in item_type_to_field and user == self.env.user:
|
|
sign_field = item_type_to_field[item.type_id.item_type]
|
|
res = bool(user[sign_field])
|
|
item_auto_fill_values.append(res)
|
|
request.only_autofill_readonly = all(item_auto_fill_values)
|
|
|
|
@api.depends("only_autofill_readonly", "signers_count", "is_user_signer")
|
|
def _compute_display_download_button(self):
|
|
"""
|
|
Computes the flag indicating if the download button should be shown in the wizard.
|
|
|
|
:return: nothing, sets the value in the display_download_button field.
|
|
"""
|
|
for wiz in self:
|
|
wiz.display_download_button = (
|
|
wiz.only_autofill_readonly
|
|
and (wiz.signers_count == 1)
|
|
and wiz.is_user_signer
|
|
)
|
|
|
|
def quick_sign(self):
|
|
"""
|
|
Is triggered by the Download button, implements the quick-sign flow by retrieving the values needed to fill the fields, filling them, creating the finished document, downloading it and redirecting to the view of the model the user was coming from (sign requests can be made from other apps too).
|
|
|
|
:return: triggers an action which downloads the completed document and redirects to the correct view.
|
|
"""
|
|
# Create the sign request
|
|
request = self.create_request()
|
|
|
|
# Build a dictionary matching the fields that exist in the document with their properties.
|
|
# The keys in the dictionary are the stringified version of the ids because that's how the
|
|
# _fill function expects them later.
|
|
sign_values = {}
|
|
for item in self.template_id.sign_item_ids:
|
|
sign_values[str(item.id)] = {
|
|
"name": item.name,
|
|
"type_id": item.type_id.id,
|
|
"auto_field": item.type_id.sudo().auto_field,
|
|
"type_name": item.name,
|
|
}
|
|
|
|
# Build the dictionary with the information taken from the user profile as the _fill function expects it
|
|
sign_request_item = request.request_item_ids
|
|
corrected_dict = sign_values.copy()
|
|
frames = {}
|
|
for key, value in sign_values.items():
|
|
corrected_dict[key] = value["name"]
|
|
# All autofillable fields except for Signature, Initials and Date have an auto_field property set which can be used to get the value from the profile
|
|
if value.get("auto_field"):
|
|
corrected_dict[key] = sign_request_item._get_auto_field_value({
|
|
"id": value.get("type_id"),
|
|
"auto_field": value.get("auto_field")
|
|
})
|
|
# Signatures and Initials need a different procedure because of the fact that the contents are images and they might need frames and hashes
|
|
elif value.get("type_name") in ["Signature", "Initials"]:
|
|
signature_field_name = 'sign_signature' if value.get("type_name") == "Signature" else 'sign_initials'
|
|
user_signature = sign_request_item._get_user_signature(signature_field_name)
|
|
user_signature_frame = sign_request_item.sudo()._get_user_signature_frame(signature_field_name + '_frame')
|
|
corrected_dict[key] = 'data:image/png;base64,%s' % user_signature.decode() if user_signature else False
|
|
frames[key] = {
|
|
'frameValue': 'data:image/png;base64,%s' % user_signature_frame.decode() if user_signature_frame else False,
|
|
'frameHash': False,
|
|
}
|
|
# The Date field can be autofilled but doesn't have a auto_field because it's always filled with today's date
|
|
elif value.get("type_name") == "Date":
|
|
corrected_dict[key] = format_date(self.env, fields.Date.today())
|
|
|
|
# Fill the fields with the values contained in corrected_dict, set the request state as signed and generate the document.
|
|
sign_request_item.sudo()._sign(corrected_dict, frame=frames)
|
|
|
|
# Trigger the download and the redirection with a single custom action
|
|
# The reference_doc tells us if the request has been made from somewhere outside of Sign (other apps)
|
|
url = f"/sign/download/{request.id}/{request.access_token}/completed"
|
|
if self.reference_doc:
|
|
view_record = self.reference_doc
|
|
action = {
|
|
'type': 'ir.actions.client',
|
|
'tag': 'sign_download_document_and_return',
|
|
'params': {
|
|
'url': url,
|
|
'res_model': view_record._name,
|
|
'res_id': view_record.id,
|
|
'view_mode': 'form'
|
|
}
|
|
}
|
|
else:
|
|
action = {
|
|
'type': 'ir.actions.client',
|
|
'tag': 'sign_download_document_and_return',
|
|
'params': {
|
|
'url': url,
|
|
'redirect_to': 'sign.sign_request_action'
|
|
}
|
|
}
|
|
return action
|