737 lines
36 KiB
Python
737 lines
36 KiB
Python
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
|
|
|
from collections import defaultdict
|
|
import time
|
|
import uuid
|
|
|
|
from werkzeug.urls import url_quote
|
|
from markupsafe import Markup
|
|
|
|
from odoo import _, api, fields, models, Command
|
|
from odoo.exceptions import UserError, ValidationError
|
|
from odoo.tools import get_lang, is_html_empty, format_date, formataddr
|
|
from odoo.tools.urls import urljoin as url_join
|
|
|
|
|
|
class SignRequest(models.Model):
|
|
_name = 'sign.request'
|
|
_description = "Signature Request"
|
|
_rec_name = 'reference'
|
|
_inherit = ['mail.thread', 'mail.activity.mixin']
|
|
|
|
def _default_access_token(self):
|
|
return str(uuid.uuid4())
|
|
|
|
def _get_mail_link(self, email, subject):
|
|
return "mailto:%s?subject=%s" % (url_quote(email), url_quote(subject))
|
|
|
|
@api.model
|
|
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')])]
|
|
|
|
template_id = fields.Many2one('sign.template', string="Template", required=True, index=True)
|
|
subject = fields.Char(string="Email Subject")
|
|
reference = fields.Char(required=True, string="Document Name", help="This is how the document will be named in the mail")
|
|
reference_doc = fields.Reference(string="Linked To", selection='_selection_target_model', index='btree_not_null')
|
|
|
|
access_token = fields.Char('Security Token', required=True, default=_default_access_token, readonly=True, copy=False)
|
|
share_link = fields.Char(string="Share Link", compute='_compute_share_link', readonly=False)
|
|
is_shared = fields.Boolean(string="Share Request Button", compute='_compute_is_shared', inverse='_inverse_is_shared')
|
|
|
|
request_item_ids = fields.One2many('sign.request.item', 'sign_request_id', string="Signers", copy=True)
|
|
state = fields.Selection([
|
|
("shared", "Shared"),
|
|
("sent", "To Sign"),
|
|
("signed", "Signed"),
|
|
("canceled", "Cancelled"),
|
|
("expired", "Expired"),
|
|
], default='sent', tracking=True, group_expand=True, copy=False, index=True)
|
|
|
|
template_document_ids = fields.Many2many('sign.document', string="Documents", compute='_compute_template_document_ids')
|
|
completed_document_ids = fields.One2many('sign.completed.document', 'sign_request_id', string="Completed Documents Binaries", copy=False)
|
|
nb_wait = fields.Integer(string="Sent Requests", compute="_compute_stats", store=True)
|
|
nb_closed = fields.Integer(string="Completed Signatures", compute="_compute_stats", store=True)
|
|
nb_total = fields.Integer(string="Requested Signatures", compute="_compute_stats", store=True)
|
|
progress = fields.Char(string="Progress", compute="_compute_progress", compute_sudo=True)
|
|
start_sign = fields.Boolean(string="Signature Started", help="At least one signer has signed the document.", compute="_compute_progress", compute_sudo=True)
|
|
integrity = fields.Boolean(string="Integrity of the Sign request", compute='_compute_hashes', compute_sudo=True)
|
|
|
|
active = fields.Boolean(default=True, string="Active", copy=False)
|
|
favorited_ids = fields.Many2many('res.users', string="Favorite of")
|
|
|
|
color = fields.Integer()
|
|
request_item_infos = fields.Binary(compute="_compute_request_item_infos")
|
|
last_action_date = fields.Datetime(related="message_ids.create_date", readonly=True, string="Last Action Date")
|
|
completion_date = fields.Date(string="Completion Date", compute="_compute_completion_date", compute_sudo=True, store=True)
|
|
communication_company_id = fields.Many2one('res.company', string="Company used for communication", default=lambda self: self.env.company)
|
|
|
|
sign_log_ids = fields.One2many('sign.log', 'sign_request_id', string="Logs", help="Activity logs linked to this request")
|
|
template_tags = fields.Many2many('sign.template.tag', string='Tags')
|
|
cc_partner_ids = fields.Many2many('res.partner', string='Copy to', compute='_compute_cc_partners')
|
|
message = fields.Html('sign.message')
|
|
message_cc = fields.Html('sign.message_cc')
|
|
attachment_ids = fields.Many2many('ir.attachment', string='Attachments', readonly=True, copy=False, ondelete="restrict", bypass_search_access=True)
|
|
completed_document_attachment_ids = fields.Many2many('ir.attachment', 'sign_request_completed_document_rel', string='Completed Documents', readonly=True, copy=False, ondelete="restrict", bypass_search_access=True)
|
|
|
|
need_my_signature = fields.Boolean(compute='_compute_need_my_signature', search='_search_need_my_signature')
|
|
|
|
validity = fields.Date(string='Valid Until')
|
|
reminder_enabled = fields.Boolean(default=False)
|
|
reminder = fields.Integer(string='Reminder', default=7)
|
|
last_reminder = fields.Date(string='Last reminder', default=lambda self: fields.Date.today())
|
|
certificate_reference = fields.Boolean(string="Certificate Reference", default=False)
|
|
|
|
send_channel = fields.Selection([
|
|
("email", "Email"),
|
|
], string="Delivery Method", default='email', required=True)
|
|
|
|
@api.depends('template_id')
|
|
def _compute_template_document_ids(self):
|
|
for sign_request in self:
|
|
sign_request.template_document_ids = sign_request.template_id.sudo().document_ids
|
|
|
|
@api.constrains('reminder_enabled', 'reminder')
|
|
def _check_reminder(self):
|
|
for request in self:
|
|
if request.reminder_enabled and request.reminder <= 0:
|
|
raise UserError(_("We can only send reminders in the future - as soon as we find a way to send reminders in the past we'll notify you.\nIn the mean time, please make sure to input a positive number of days for the reminder interval."))
|
|
|
|
@api.depends('state')
|
|
def _compute_is_shared(self):
|
|
for sign_request in self:
|
|
sign_request.is_shared = sign_request.state == 'shared'
|
|
|
|
def _inverse_is_shared(self):
|
|
for sign_request in self:
|
|
if sign_request.is_shared:
|
|
sign_request.state = 'shared'
|
|
else:
|
|
sign_request.state = 'sent'
|
|
|
|
@api.depends_context('uid')
|
|
def _compute_need_my_signature(self):
|
|
my_partner_id = self.env.user.partner_id
|
|
for sign_request in self:
|
|
sign_request.need_my_signature = any(sri.partner_id.id == my_partner_id.id and sri.state == 'sent' and sri.is_mail_sent for sri in sign_request.request_item_ids)
|
|
|
|
@api.model
|
|
def _search_need_my_signature(self, operator, value):
|
|
if operator != 'in':
|
|
return NotImplemented
|
|
my_partner_id = self.env.user.partner_id
|
|
documents_ids = self.env['sign.request.item'].search([('partner_id', '=', my_partner_id.id), ('state', '=', 'sent'), ('is_mail_sent', '=', True)]).mapped('sign_request_id').ids
|
|
return [('id', 'in', documents_ids)]
|
|
|
|
@api.depends('request_item_ids.state')
|
|
def _compute_stats(self):
|
|
for rec in self:
|
|
rec.nb_total = len(rec.request_item_ids)
|
|
rec.nb_wait = len(rec.request_item_ids.filtered(lambda sri: sri.state == 'sent'))
|
|
rec.nb_closed = rec.nb_total - rec.nb_wait
|
|
|
|
@api.depends('request_item_ids.state')
|
|
def _compute_progress(self):
|
|
for rec in self:
|
|
rec.start_sign = bool(rec.nb_closed)
|
|
rec.progress = "{} / {}".format(rec.nb_closed, rec.nb_total)
|
|
|
|
@api.depends('request_item_ids.state')
|
|
def _compute_completion_date(self):
|
|
for rec in self:
|
|
rec.completion_date = rec.request_item_ids.sorted(key="signing_date", reverse=True)[:1].signing_date if not rec.nb_wait else None
|
|
|
|
@api.depends('request_item_ids.state', 'request_item_ids.partner_id.name')
|
|
def _compute_request_item_infos(self):
|
|
for request in self:
|
|
request.request_item_infos = [{
|
|
'id': item.id,
|
|
'partner_name': item.display_name,
|
|
'state': item.state,
|
|
'signing_date': item.signing_date or ''
|
|
} for item in request.request_item_ids]
|
|
|
|
@api.depends('message_follower_ids.partner_id')
|
|
def _compute_cc_partners(self):
|
|
for sign_request in self:
|
|
sign_request.cc_partner_ids = sign_request.message_follower_ids.partner_id - sign_request.request_item_ids.partner_id
|
|
|
|
@api.depends('request_item_ids.access_token', 'state')
|
|
def _compute_share_link(self):
|
|
self.share_link = False
|
|
for sign_request in self.filtered(lambda sr: sr.state == 'shared'):
|
|
sign_request.share_link = "%s/sign/document/mail/%s/%s" % (self.get_base_url(), sign_request.id, sign_request.request_item_ids[0].sudo().access_token)
|
|
|
|
@api.model_create_multi
|
|
def create(self, vals_list):
|
|
sign_requests = super().create(vals_list)
|
|
sign_requests.template_id._check_send_ready()
|
|
for sign_request in sign_requests:
|
|
if not sign_request.request_item_ids:
|
|
raise ValidationError(_("A valid sign request needs at least one sign request item"))
|
|
sign_request.template_tags = [Command.set(sign_request.template_id.tag_ids.ids)]
|
|
sign_request.attachment_ids.write({'res_model': sign_request._name, 'res_id': sign_request.id})
|
|
sign_request.message_subscribe(partner_ids=sign_request.request_item_ids.partner_id.ids)
|
|
sign_request._populate_constant_items()
|
|
self.env['sign.log'].sudo().create({'sign_request_id': sign_request.id, 'action': 'create'})
|
|
|
|
if not self.env.context.get('no_sign_mail'):
|
|
sign_requests.send_signature_accesses()
|
|
return sign_requests
|
|
|
|
def _populate_constant_items(self):
|
|
self.ensure_one()
|
|
sign_values_by_role = defaultdict(
|
|
lambda: defaultdict(lambda: self.env['sign.item']))
|
|
for item in self.template_id.sign_item_ids:
|
|
if item.constant:
|
|
sign_values_by_role[item.responsible_id][str(item.id)] = {
|
|
# For constant strikethrough items, use "striked" instead of item name,
|
|
# since item name returns "strikethrough" but we need "striked" to set the value correctly.
|
|
"name": item.name if item.type_id.item_type != 'strikethrough' else 'striked',
|
|
"type_id": item.type_id.id,
|
|
"auto_field": item.type_id.sudo().auto_field
|
|
}
|
|
|
|
if not sign_values_by_role:
|
|
return
|
|
|
|
for sign_request_item in self.sudo().request_item_ids:
|
|
if sign_request_item.role_id in sign_values_by_role:
|
|
sign_items = sign_values_by_role[sign_request_item.role_id]
|
|
corrected_dict = sign_items.copy()
|
|
for key, value in sign_items.items():
|
|
corrected_dict[key] = value["name"]
|
|
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")
|
|
})
|
|
|
|
sign_request_item._fill(corrected_dict)
|
|
|
|
def write(self, vals):
|
|
today = fields.Date.today()
|
|
if vals.get('validity') and fields.Date.from_string(vals['validity']) < today:
|
|
vals['state'] = 'expired'
|
|
|
|
if vals.get('reference_doc'):
|
|
model, rec = vals['reference_doc'].split(',')
|
|
record = self.env[model].browse(int(rec)).exists()
|
|
if not record or not record.has_access('read'):
|
|
raise ValidationError(self.env._("You don't have access to the linked document."))
|
|
|
|
return super().write(vals)
|
|
|
|
def copy_data(self, default=None):
|
|
default = dict(default or {})
|
|
vals_list = super().copy_data(default=default)
|
|
if 'attachment_ids' not in default:
|
|
for request, vals in zip(self, vals_list):
|
|
vals['attachment_ids'] = request.attachment_ids.copy().ids
|
|
return vals_list
|
|
|
|
def copy(self, default=None):
|
|
sign_requests = super().copy(default)
|
|
for old_request, new_request in zip(self, sign_requests):
|
|
new_request.message_subscribe(partner_ids=old_request.cc_partner_ids.ids)
|
|
return sign_requests
|
|
|
|
@api.ondelete(at_uninstall=False)
|
|
def _unlink_if_not_signed(self):
|
|
"""
|
|
Raise an error if any of the records are in 'signed' state.
|
|
"""
|
|
if any(r.state == 'signed' for r in self):
|
|
raise UserError(_("Signed documents cannot be deleted for legal reasons. Please archive them instead."))
|
|
|
|
def action_archive(self):
|
|
self.filtered(lambda sr: sr.active and sr.state == 'sent').cancel()
|
|
return super().action_archive()
|
|
|
|
def _check_senders_validity(self):
|
|
invalid_senders = self.create_uid.filtered(lambda u: not u.email_formatted)
|
|
if invalid_senders:
|
|
raise ValidationError(_("Please configure senders'(%s) email addresses", ', '.join(invalid_senders.mapped('name'))))
|
|
|
|
def _check_signers_roles_validity(self):
|
|
for sign_request in self:
|
|
template_roles = sign_request.sudo().template_id.sign_item_ids.responsible_id
|
|
sign_request_items = sign_request.request_item_ids
|
|
if len(sign_request_items) != max(len(template_roles), 1) or \
|
|
set(sign_request_items.role_id.ids) != (set(template_roles.ids) if template_roles else set([self.env.ref('sign.sign_item_role_default').id])):
|
|
raise ValidationError(_("You must specify one signer for each role of your sign template"))
|
|
|
|
def _check_signers_partners_validity(self):
|
|
for sign_request in self:
|
|
sign_request_items = sign_request.request_item_ids
|
|
if sign_request.state != 'shared' and any(not sri.partner_id for sri in sign_request_items):
|
|
raise ValidationError(_("A non-shared sign request's should not have any signer with an empty partner"))
|
|
|
|
def _get_final_recipients(self):
|
|
all_recipients = set(self.request_item_ids.mapped('signer_email')) | \
|
|
set(self.cc_partner_ids.filtered(lambda p: p.email_formatted).mapped('email'))
|
|
return all_recipients
|
|
|
|
def _get_next_sign_request_items(self):
|
|
self.ensure_one()
|
|
sign_request_items_sent = self.request_item_ids.filtered(lambda sri: sri.state == 'sent')
|
|
if not sign_request_items_sent:
|
|
return self.env['sign.request.item']
|
|
smallest_order = min(sign_request_items_sent.mapped('mail_sent_order'))
|
|
next_request_items = sign_request_items_sent.filtered(lambda sri: sri.mail_sent_order == smallest_order)
|
|
return next_request_items
|
|
|
|
def go_to_document(self):
|
|
self.ensure_one()
|
|
request_items = self.request_item_ids.filtered(lambda r: not r.partner_id or (r.state == 'sent' and r.partner_id.id == self.env.user.partner_id.id))
|
|
sequenced_signature_mail = any(req.mail_sent_order > 1 for req in self.request_item_ids)
|
|
return {
|
|
'name': self.reference,
|
|
'type': 'ir.actions.client',
|
|
'tag': 'sign.Document',
|
|
'context': {
|
|
'id': self.id,
|
|
'token': self.access_token,
|
|
'need_to_sign': bool(request_items),
|
|
'create_uid': self.create_uid.id,
|
|
'state': self.state,
|
|
'request_item_states': {str(item.id): item.is_mail_sent for item in self.request_item_ids},
|
|
'sequenced_signature_mail': sequenced_signature_mail,
|
|
},
|
|
}
|
|
|
|
def go_to_signable_document(self, request_items=None):
|
|
""" go to the signable document as the signers for specified request_items or the current user"""
|
|
self.ensure_one()
|
|
if not request_items:
|
|
request_items = self.request_item_ids.filtered(lambda r: not r.partner_id or (r.state == 'sent' and r.partner_id.id == self.env.user.partner_id.id))
|
|
if not request_items:
|
|
return
|
|
return {
|
|
'name': self.reference,
|
|
'type': 'ir.actions.client',
|
|
'tag': 'sign.SignableDocument',
|
|
'context': {
|
|
'id': self.id,
|
|
'token': request_items[:1].sudo().access_token,
|
|
'create_uid': self.create_uid.id,
|
|
'state': self.state,
|
|
'request_item_states': {item.id: item.is_mail_sent for item in self.request_item_ids},
|
|
'template_editable': self.nb_closed == 0,
|
|
'token_list': request_items[1:].sudo().mapped('access_token'),
|
|
'name_list': [item.partner_id.name for item in request_items[1:]],
|
|
},
|
|
}
|
|
|
|
def get_sign_request_documents(self):
|
|
if not self:
|
|
raise UserError(_('You should select at least one document to download.'))
|
|
|
|
if len(self) == 1:
|
|
if self.state == 'signed':
|
|
return {
|
|
'name': 'Signed Document',
|
|
'type': 'ir.actions.act_url',
|
|
'url': '/sign/download/%(request_id)s/%(access_token)s/completed' % {'request_id': self.id, 'access_token': self.access_token},
|
|
}
|
|
else:
|
|
return {
|
|
'name': 'Template Document',
|
|
'type': 'ir.actions.act_url',
|
|
'url': '/sign/download/%(request_id)s/%(access_token)s/origin' % {'request_id': self.id, 'access_token': self.access_token},
|
|
}
|
|
else:
|
|
return {
|
|
'name': 'Sign Request Documents',
|
|
'type': 'ir.actions.act_url',
|
|
'url': f'/sign/download/zip/{",".join(map(str, self.ids))}',
|
|
}
|
|
|
|
def _get_linked_record_action(self, default_action=None):
|
|
"""" Return the default action for any kind of record. This method can be override for specific kind or rec
|
|
"""
|
|
self.ensure_one()
|
|
if not default_action:
|
|
default_action = {}
|
|
# user might not have access to Action Window model
|
|
action_rec_sudo = self.env['ir.actions.act_window'].sudo().sudo().search([
|
|
('res_model', '=', self.reference_doc._name),
|
|
('context', 'not ilike', 'active_id')], limit=1)
|
|
if action_rec_sudo:
|
|
action = action_rec_sudo._get_action_dict()
|
|
action.update({
|
|
"views": [(False, "form")],
|
|
"view_mode": 'form',
|
|
"res_id": self.reference_doc.id,
|
|
"target": 'current',
|
|
})
|
|
else:
|
|
action = default_action
|
|
return action
|
|
|
|
def get_close_values(self):
|
|
self.ensure_one()
|
|
# check if frontend user or backend
|
|
action = self.env["ir.actions.actions"]._for_xml_id("sign.sign_request_action")
|
|
result = {"action": action, "label": _("Close"), "custom_action": False}
|
|
if self.reference_doc and self.reference_doc.exists() and self.reference_doc.has_access('read'):
|
|
action = self._get_linked_record_action(action)
|
|
result = {"action": action, "label": _("Back to %s", self.reference_doc._description), "custom_action": True}
|
|
return result
|
|
|
|
@api.onchange("progress", "start_sign")
|
|
def _compute_hashes(self):
|
|
for document in self:
|
|
try:
|
|
document.integrity = self.sign_log_ids._check_document_integrity()
|
|
except Exception:
|
|
document.integrity = False
|
|
|
|
def toggle_favorited(self):
|
|
self.ensure_one()
|
|
self.write({'favorited_ids': [(3 if self.env.user in self.favorited_ids else 4, self.env.user.id)]})
|
|
|
|
def _refuse(self, refuser, refusal_reason):
|
|
""" Refuse a SignRequest. It can only be used in SignRequestItem._refuse
|
|
:param res.partner refuser: the refuser who refuse to sign
|
|
:param str refusal_reason: the refusal reason provided by the refuser
|
|
"""
|
|
self.ensure_one()
|
|
if self.state != 'sent':
|
|
raise UserError(_("This sign request cannot be refused"))
|
|
self._check_senders_validity()
|
|
self.cancel()
|
|
|
|
# cancel request and activities for other unsigned users
|
|
for user in self.request_item_ids.partner_id.user_ids.filtered(lambda u: u.has_group('sign.group_sign_user')):
|
|
self.activity_unlink(['sign.mail_activity_data_signature_request'], user_id=user.id)
|
|
|
|
# send emails to signers and cc_partners
|
|
for sign_request_item in self.request_item_ids:
|
|
self._send_refused_message(refuser, refusal_reason, sign_request_item.partner_id,
|
|
access_token=sign_request_item.sudo().access_token, force_send=True, sign_request_item=sign_request_item)
|
|
for partner in self.cc_partner_ids.filtered(lambda p: p.email_formatted) - self.request_item_ids.partner_id:
|
|
self._send_refused_message(refuser, refusal_reason, partner)
|
|
|
|
def _send_refused_message(self, refuser, refusal_reason, partner, access_token=None, force_send=False, sign_request_item=None):
|
|
self.ensure_one()
|
|
if access_token is None:
|
|
access_token = self.access_token
|
|
subject = _("The document %(template_name)s has been rejected by %(refuser_name)s",
|
|
template_name=self.template_id.name,
|
|
refuser_name=refuser.name,
|
|
)
|
|
base_url = self.get_base_url()
|
|
partner_lang = get_lang(self.env, lang_code=partner.lang).code
|
|
body = self.env['ir.qweb']._render('sign.sign_template_mail_refused', {
|
|
'record': self,
|
|
'recipient': partner,
|
|
'refuser': refuser,
|
|
'link': url_join(base_url, 'sign/document/%s/%s' % (self.id, access_token)),
|
|
'subject': subject,
|
|
'body': Markup('<p style="white-space: pre">{}</p>').format(refusal_reason),
|
|
}, lang=partner_lang, minimal_qcontext=True)
|
|
|
|
self.with_context(lang=partner.lang or self.env.lang)._message_send_mail(
|
|
body,
|
|
record_name=self.reference,
|
|
notif_values={
|
|
'model_description': _('Signature'),
|
|
'company': self.communication_company_id or self.create_uid.company_id,
|
|
'partner': partner,
|
|
},
|
|
mail_values={
|
|
'subject': subject,
|
|
**({'email_to': formataddr((sign_request_item.partner_id.name, sign_request_item.signer_email))} if sign_request_item else {}),
|
|
},
|
|
force_send=force_send,
|
|
)
|
|
|
|
def send_signature_accesses(self):
|
|
# Send/Resend accesses for 'sent' sign.request.items by email
|
|
allowed_request_ids = self.filtered(lambda sr: sr.state == 'sent')
|
|
allowed_request_ids._check_senders_validity()
|
|
for sign_request in allowed_request_ids:
|
|
sign_request._get_next_sign_request_items().send_signature_accesses()
|
|
sign_request.last_reminder = fields.Date.today()
|
|
|
|
@api.model
|
|
def _cron_reminder(self):
|
|
today = fields.Date.today()
|
|
# find all expired sign requests and those that need a reminder
|
|
# in one query, the code will handle them differently
|
|
# note: archived requests are not fetched.
|
|
self.flush_model()
|
|
self.env.cr.execute(f'''
|
|
SELECT id
|
|
FROM sign_request sr
|
|
WHERE sr.state = 'sent'
|
|
AND active = TRUE
|
|
AND (
|
|
sr.validity < '{today}'
|
|
OR (sr.reminder_enabled AND sr.last_reminder + sr.reminder * ('1 day'::interval) <= '{today}')
|
|
)
|
|
''')
|
|
res = self.env.cr.fetchall()
|
|
request_to_send = self.env['sign.request']
|
|
for request in self.browse(v[0] for v in res):
|
|
if request.validity and request.validity < today:
|
|
request.state = 'expired'
|
|
else:
|
|
request_to_send += request
|
|
request_to_send.with_context(force_send=False).send_signature_accesses()
|
|
|
|
def _sign(self):
|
|
""" Sign a SignRequest. It can only be used in the SignRequestItem._sign """
|
|
self.ensure_one()
|
|
if self.state != 'sent' or any(sri.state != 'completed' for sri in self.request_item_ids):
|
|
raise UserError(_("This sign request cannot be signed"))
|
|
self.write({'state': 'signed'})
|
|
self._send_completed_documents()
|
|
|
|
if self.reference_doc:
|
|
model = self.env['ir.model']._get(self.reference_doc._name)
|
|
if model.is_mail_thread:
|
|
self.reference_doc.message_post_with_source(
|
|
"sign.message_signature_link",
|
|
render_values={"request": self, "salesman": self.env.user.partner_id},
|
|
subtype_xmlid='mail.mt_note',
|
|
)
|
|
# attach a copy of the signed document to the record for easy retrieval
|
|
attachment_values = []
|
|
for doc in self.completed_document_ids:
|
|
attachment_values.append({
|
|
"name": doc.document_id.name,
|
|
"datas": doc.file,
|
|
"type": "binary",
|
|
"res_model": self.reference_doc._name,
|
|
"res_id": self.reference_doc.id
|
|
})
|
|
self.env["ir.attachment"].create(attachment_values)
|
|
|
|
def cancel(self):
|
|
# Exclude sign requests that are in 'signed' state as they mustn't be canceled.
|
|
sign_requests = self.filtered(lambda request: request.state != 'signed')
|
|
for sign_request in sign_requests:
|
|
sign_request.write({'access_token': self._default_access_token(), 'state': 'canceled'})
|
|
sign_requests.request_item_ids._cancel()
|
|
|
|
# cancel activities for signers
|
|
for user in sign_requests.request_item_ids.sudo().partner_id.user_ids.filtered(lambda u: u.has_group('sign.group_sign_user')):
|
|
sign_requests.activity_unlink(['sign.mail_activity_data_signature_request'], user_id=user.id)
|
|
|
|
self.env['sign.log'].sudo().create([{'sign_request_id': sign_request.id, 'action': 'cancel'} for sign_request in sign_requests])
|
|
|
|
def _send_completed_documents(self):
|
|
""" Send the completed document to signers and Contacts in copy with emails
|
|
"""
|
|
self.ensure_one()
|
|
if self.state != 'signed':
|
|
raise UserError(_('The sign request has not been fully signed'))
|
|
self._check_senders_validity()
|
|
|
|
if not self.completed_document_ids:
|
|
self._generate_completed_documents()
|
|
|
|
signers = [{'name': signer.partner_id.name, 'email': signer.signer_email, 'id': signer.partner_id.id} for signer in self.request_item_ids]
|
|
request_edited = any(log.action == "update" for log in self.sign_log_ids)
|
|
for sign_request_item in self.request_item_ids:
|
|
self._send_completed_documents_message(signers, request_edited, sign_request_item.partner_id,
|
|
access_token=sign_request_item.sudo().access_token, with_message_cc=False, force_send=True, sign_request_item=sign_request_item)
|
|
|
|
cc_partners_valid = self.cc_partner_ids.filtered(lambda p: p.email_formatted)
|
|
for cc_partner in cc_partners_valid:
|
|
self._send_completed_documents_message(signers, request_edited, cc_partner)
|
|
if cc_partners_valid:
|
|
body = _(
|
|
"The mail has been sent to contacts in copy: %(contacts)s",
|
|
contacts=cc_partners_valid.mapped("name"),
|
|
)
|
|
if not is_html_empty(self.message_cc):
|
|
body += self.message_cc
|
|
self.message_post(body=body, attachment_ids=self.attachment_ids.ids + self.completed_document_attachment_ids.ids)
|
|
if self.reference_doc:
|
|
record_body = _("The document %s has been fully signed.", self._get_html_link())
|
|
self.reference_doc.message_post(
|
|
body=record_body,
|
|
attachment_ids=self.completed_document_attachment_ids.ids,
|
|
partner_ids=cc_partners_valid.ids,
|
|
)
|
|
|
|
def _send_completed_documents_message(self, signers, request_edited, partner, access_token=None, with_message_cc=True, force_send=False, sign_request_item=None):
|
|
self.ensure_one()
|
|
if access_token is None:
|
|
access_token = self.access_token
|
|
partner_lang = get_lang(self.env, lang_code=partner.lang).code
|
|
base_url = self.get_base_url()
|
|
body = self.env['ir.qweb']._render('sign.sign_template_mail_completed', {
|
|
'record': self,
|
|
'link': url_join(base_url, 'sign/document/%s/%s' % (self.id, access_token)),
|
|
'subject': '%s signed' % self.reference,
|
|
'body': self.message_cc if with_message_cc and not is_html_empty(self.message_cc) else False,
|
|
'recipient_name': partner.name,
|
|
'recipient_id': partner.id,
|
|
'signers': signers,
|
|
'request_edited': request_edited,
|
|
}, lang=partner_lang, minimal_qcontext=True)
|
|
|
|
self.with_context(lang=partner.lang or self.env.lang)._message_send_mail(
|
|
body,
|
|
record_name=self.reference,
|
|
notif_values={
|
|
'model_description': _('Signature'),
|
|
'company': self.communication_company_id or self.create_uid.company_id,
|
|
'partner': partner,
|
|
},
|
|
mail_values={
|
|
'attachment_ids': self.attachment_ids.ids + self.completed_document_attachment_ids.ids,
|
|
'subject': _('%s has been edited and signed', self.reference) if request_edited else _('%s has been signed', self.reference),
|
|
**({'email_to': formataddr((sign_request_item.partner_id.name, sign_request_item.signer_email))} if sign_request_item else {}),
|
|
},
|
|
force_send=force_send,
|
|
)
|
|
|
|
@api.autovacuum
|
|
def _gc_expired_sr(self):
|
|
"""
|
|
Deletes all the shared sign requests which have an expired validity date
|
|
"""
|
|
sign_request = self.env["sign.request"].search([("state", "=", "shared"), ("validity", "<", fields.Date.today())])
|
|
sign_request.unlink()
|
|
|
|
##################
|
|
# PDF Rendering #
|
|
##################
|
|
|
|
def _get_user_formatted_datetime(self, datetime_val):
|
|
"""
|
|
Get the user's preferred datetime format based on their language settings.
|
|
"""
|
|
lang = self.env['res.lang']._lang_get(self.create_uid.lang)
|
|
user_date_format, user_time_format = lang.date_format, lang.time_format
|
|
return datetime_val.strftime(f"{user_date_format} {user_time_format}")
|
|
|
|
def _get_final_signature_log_hash(self):
|
|
"""
|
|
Fetch the log_hash of the final signature from the sign.log table.
|
|
"""
|
|
self.ensure_one()
|
|
if not self.certificate_reference:
|
|
return False
|
|
|
|
final_log = self.env['sign.log'].search([
|
|
('sign_request_id', '=', self.id),
|
|
('action', 'in', ['sign', 'create']),
|
|
], order='id DESC', limit=1)
|
|
|
|
return final_log.log_hash if final_log else False
|
|
|
|
def _generate_completed_documents(self):
|
|
if self.state != 'signed':
|
|
raise UserError(_("The completed document cannot be created because the sign request is not fully signed"))
|
|
|
|
for record in self:
|
|
if not record.completed_document_ids:
|
|
self.env['sign.completed.document'].create([{
|
|
'sign_request_id': record.id,
|
|
'document_id': document.id,
|
|
} for document in record.template_id.document_ids])
|
|
record.completed_document_ids._generate_completed_document()
|
|
attachment_ids = self.env['ir.attachment'].create([{
|
|
'name': document.document_id.name,
|
|
'datas': document.file,
|
|
'type': 'binary',
|
|
'res_model': self._name,
|
|
'res_id': record.id,
|
|
} for document in record.completed_document_ids])
|
|
|
|
# print the report with the public user in a sudoed env
|
|
# public user because we don't want groups to pollute the result
|
|
# (e.g. if the current user has the group Sign Manager,
|
|
# some private information will be sent to *all* signers)
|
|
# sudoed env because we have checked access higher up the stack
|
|
public_user = self.env.ref('base.public_user', raise_if_not_found=False)
|
|
if not public_user:
|
|
# public user was deleted, fallback to avoid crash (info may leak)
|
|
public_user = self.env.user
|
|
pdf_content, __ = self.env["ir.actions.report"].with_user(public_user).sudo()._render_qweb_pdf(
|
|
'sign.action_sign_request_print_logs',
|
|
record.id,
|
|
data={'format_date': format_date, 'company_id': record.communication_company_id}
|
|
)
|
|
attachment_log = self.env['ir.attachment'].create({
|
|
'name': self.env._("Certificate of completion - %s.pdf", time.strftime('%Y-%m-%d - %H:%M:%S')),
|
|
'raw': pdf_content,
|
|
'type': 'binary',
|
|
'res_model': self._name,
|
|
'res_id': record.id,
|
|
})
|
|
self.completed_document_attachment_ids = [Command.link(attachment_log.id)] + [Command.link(att.id) for att in attachment_ids]
|
|
|
|
def _get_signing_field_name(self) -> str:
|
|
"""Generates a name for the signing field of the pdf document
|
|
|
|
Returns:
|
|
str: the name of the signature field
|
|
"""
|
|
return self.env.company.name
|
|
|
|
##################
|
|
# Mail overrides #
|
|
##################
|
|
|
|
def _message_send_mail(self, body, notif_values=None, mail_values=None, record_name=False, force_send=False):
|
|
""" Shortcut to sent a notification or an email. """
|
|
notif_values = notif_values or {}
|
|
company = notif_values.get('company')
|
|
model_description = notif_values.get('model_description')
|
|
partner = notif_values.get('partner')
|
|
notification_layout_xmlid = notif_values.get('notification_layout_xmlid', 'sign.sign_mail_notification_light')
|
|
scheduled_date = self.env.context.get('scheduled_date', False)
|
|
|
|
mail_values = mail_values or {}
|
|
if 'author_id' not in mail_values:
|
|
mail_values['author_id'] = self.create_uid.partner_id.id
|
|
mail_values['email_from'] = self.create_uid.email_formatted
|
|
if 'email_to' not in mail_values:
|
|
mail_values['email_to'] = partner.email_formatted
|
|
|
|
if partner and len(partner.user_ids) == 1 and partner.user_ids.notification_type == "inbox":
|
|
self.message_notify(
|
|
attachment_ids=mail_values.get("attachment_ids"),
|
|
author_id=self.create_uid.partner_id.id,
|
|
body=body,
|
|
email_from=mail_values.get("email_from"),
|
|
force_record_name=record_name,
|
|
force_send=force_send,
|
|
mail_auto_delete=False,
|
|
model_description=model_description,
|
|
partner_ids=partner.ids,
|
|
subject=mail_values.get("subject"),
|
|
)
|
|
|
|
mail_values['body_html'] = self.env['mail.render.mixin']._render_encapsulate(
|
|
notification_layout_xmlid, body,
|
|
context_record=self,
|
|
add_context={
|
|
'company': company,
|
|
'model_description': model_description,
|
|
'record_name': record_name,
|
|
},
|
|
)
|
|
mail_values['reply_to'] = mail_values.get('email_from')
|
|
if scheduled_date:
|
|
mail_values['scheduled_date'] = scheduled_date
|
|
mail = self.env['mail.mail'].sudo().create(mail_values)
|
|
if force_send and not scheduled_date:
|
|
mail.send_after_commit()
|
|
return mail
|
|
|
|
def _schedule_activity(self, sign_users):
|
|
for user in sign_users:
|
|
self.with_context(mail_activity_quick_update=True).activity_schedule(
|
|
'sign.mail_activity_data_signature_request',
|
|
user_id=user.id
|
|
)
|