Files
enterprise-addons/sign/models/sign_request.py
T

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
)