566 lines
30 KiB
Python
566 lines
30 KiB
Python
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
|
|
|
import uuid
|
|
|
|
from datetime import timedelta
|
|
from dateutil.relativedelta import relativedelta
|
|
from hashlib import sha256
|
|
from markupsafe import Markup
|
|
from random import randint
|
|
from werkzeug.urls import url_quote, url_encode
|
|
|
|
from odoo import _, api, fields, models
|
|
from odoo.exceptions import UserError, ValidationError
|
|
from odoo.tools import consteq, email_normalize, formataddr, groupby, get_lang, html2plaintext, is_html_empty
|
|
from odoo.tools.misc import hmac
|
|
from odoo.tools.urls import urljoin as url_join
|
|
|
|
|
|
class SignRequestItem(models.Model):
|
|
_name = 'sign.request.item'
|
|
_description = "Signature Request Item"
|
|
_inherit = ['portal.mixin']
|
|
_rec_name = 'partner_id'
|
|
|
|
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))
|
|
|
|
# this display_name (with sudo) is used for many2many_tags especially the partner_id is private
|
|
display_name = fields.Char(compute_sudo=True)
|
|
|
|
partner_id = fields.Many2one('res.partner', string="Signer", ondelete='restrict')
|
|
sign_request_id = fields.Many2one('sign.request', string="Signature Request", ondelete='cascade', required=True, copy=False, index=True)
|
|
sign_item_value_ids = fields.One2many('sign.request.item.value', 'sign_request_item_id', string="Value")
|
|
reference = fields.Char(related='sign_request_id.reference', string="Document Name")
|
|
mail_sent_order = fields.Integer(default=1)
|
|
communication_company_id = fields.Many2one(related='sign_request_id.communication_company_id')
|
|
|
|
access_token = fields.Char(required=True, default=_default_access_token, readonly=True, copy=False, groups="base.group_system")
|
|
access_via_link = fields.Boolean('Accessed Through Token', copy=False)
|
|
role_id = fields.Many2one('sign.item.role', string="Role", required=True, readonly=True)
|
|
sms_number = fields.Char(related='partner_id.phone', readonly=False, depends=(['partner_id']), store=True, copy=False)
|
|
sms_token = fields.Char('SMS Token', readonly=True, copy=False)
|
|
signed_without_extra_auth = fields.Boolean('Signed Without Extra Authentication', default=False, readonly=True, copy=False)
|
|
|
|
signature = fields.Binary(attachment=True, copy=False)
|
|
frame_hash = fields.Char(size=256, compute='_compute_frame_hash')
|
|
signing_date = fields.Date('Signed on', readonly=True, copy=False)
|
|
|
|
state = fields.Selection([
|
|
("sent", "To Sign"),
|
|
("completed", "Signed"),
|
|
("canceled", "Cancelled"),
|
|
], readonly=True, default="sent", copy=False, index=True)
|
|
color = fields.Integer(compute='_compute_color')
|
|
|
|
signer_email = fields.Char(string='Email', compute="_compute_email", store=True)
|
|
is_mail_sent = fields.Boolean(readonly=True, copy=False, help="The signature mail has been sent.")
|
|
change_authorized = fields.Boolean(related='role_id.change_authorized')
|
|
|
|
latitude = fields.Float(digits=(10, 7), copy=False)
|
|
longitude = fields.Float(digits=(10, 7), copy=False)
|
|
|
|
@api.constrains('signer_email')
|
|
def _check_signer_email_validity(self):
|
|
if any(sri.partner_id and not sri.signer_email for sri in self):
|
|
raise ValidationError(_("All signers must have valid email addresses"))
|
|
|
|
@api.constrains('sign_request_id', 'partner_id', 'role_id')
|
|
def _check_signers_validity(self):
|
|
# this check allows one signer to be False, which is used to "share" a sign template
|
|
self.sign_request_id._check_signers_roles_validity()
|
|
self.sign_request_id._check_signers_partners_validity()
|
|
|
|
@api.depends('signer_email')
|
|
def _compute_frame_hash(self):
|
|
db_uuid = self.env['ir.config_parameter'].sudo().get_param('database.uuid')
|
|
for sri in self:
|
|
if sri.partner_id:
|
|
sri.frame_hash = sha256((sri.signer_email + db_uuid).encode()).hexdigest()
|
|
else:
|
|
sri.frame_hash = ''
|
|
|
|
@api.depends('partner_id.name')
|
|
def _compute_display_name(self):
|
|
for sri in self:
|
|
sri.display_name = sri.partner_id.display_name if sri.partner_id else _('Public User')
|
|
|
|
def write(self, vals):
|
|
if vals.get('partner_id') is False:
|
|
raise UserError(_("You need to define a signatory"))
|
|
request_items_reassigned = self.env['sign.request.item']
|
|
if vals.get('partner_id'):
|
|
request_items_reassigned |= self.filtered(lambda sri: sri.partner_id and sri.partner_id.id != vals['partner_id'])
|
|
if any(sri.state != 'sent'
|
|
or sri.sign_request_id.state != 'sent'
|
|
or (sri.partner_id and not sri.role_id.change_authorized)
|
|
or sri.sign_request_id.state == 'shared'
|
|
for sri in request_items_reassigned):
|
|
raise UserError(_("You cannot reassign this signatory"))
|
|
new_sign_partner = self.env['res.partner'].browse(vals.get('partner_id'))
|
|
for request_item in request_items_reassigned:
|
|
sign_request = request_item.sign_request_id
|
|
old_sign_user = request_item.partner_id.user_ids[:1]
|
|
# remove old activities for internal users if they are no longer one of the unsigned signers of their sign requests
|
|
if old_sign_user and old_sign_user.has_group('sign.group_sign_user') and \
|
|
not sign_request.request_item_ids.filtered(
|
|
lambda sri: sri.partner_id == request_item.partner_id and sri.state == 'sent' and sri not in request_items_reassigned):
|
|
sign_request.activity_unlink(['sign.mail_activity_data_signature_request'], user_id=old_sign_user.id)
|
|
# create logs
|
|
sign_request.message_post(
|
|
body=_('The contact of %(role)s has been changed from %(old_partner)s to %(new_partner)s.',
|
|
role=request_item.role_id.name, old_partner=request_item.partner_id.name, new_partner=new_sign_partner.name))
|
|
|
|
# add new followers
|
|
request_items_reassigned.sign_request_id.message_subscribe(partner_ids=[vals.get('partner_id')])
|
|
# add new activities for internal users
|
|
new_sign_user = self.env['res.users'].search([
|
|
('partner_id', '=', vals.get('partner_id')),
|
|
('all_group_ids', 'in', [self.env.ref('sign.group_sign_user').id])
|
|
], limit=1)
|
|
if new_sign_user:
|
|
activity_ids = set(request_items_reassigned.sign_request_id.activity_search(['sign.mail_activity_data_signature_request'], user_id=new_sign_user.id).mapped('res_id'))
|
|
request_items_reassigned.sign_request_id.filtered(lambda sr: sr.id not in activity_ids)._schedule_activity(new_sign_user)
|
|
|
|
if vals.get('signer_email') and not self.env.user.has_group('sign.group_sign_manager') and self.env.user != self.create_uid:
|
|
raise UserError(_("You cannot change the email of a signatory"))
|
|
|
|
res = super().write(vals)
|
|
|
|
# change access token
|
|
for request_item in request_items_reassigned.filtered(lambda sri: sri.is_mail_sent):
|
|
request_item.sudo().update({'access_token': self._default_access_token()})
|
|
request_item.is_mail_sent = False
|
|
return res
|
|
|
|
def _cancel(self, no_access=True):
|
|
""" Cancel a SignRequestItem. It can only be used in the SignRequest.cancel or SignRequest._refuse
|
|
:param bool no_access: Whether the sign request item cannot be accessed by the previous link in the email
|
|
"""
|
|
for request_item in self:
|
|
request_item.write({
|
|
'state': 'canceled' if request_item.state == 'sent' else request_item.state,
|
|
'signing_date': fields.Date.context_today(self) if request_item.state == 'sent' else request_item.signing_date,
|
|
'is_mail_sent': False if no_access else request_item.is_mail_sent,
|
|
})
|
|
request_item.sudo().write({'access_token': self._default_access_token() if no_access else request_item.access_token})
|
|
|
|
def _refuse(self, request_state, refusal_reason, refusal_name="", refusal_email=""):
|
|
""" Refuse a sign request item with 'sent' or 'shared' states. """
|
|
self.ensure_one()
|
|
if not self.env.su:
|
|
raise UserError(_("This function can only be called with sudo."))
|
|
|
|
# Get the post message according to the logged user.
|
|
refuse_user = self.partner_id.user_ids[:1]
|
|
# If refusing with a signed user, we use the user as the refuser.
|
|
if not refuse_user and self.env.user and not self.env.user.is_public:
|
|
refuse_user = self.env.user
|
|
message_post = ""
|
|
if refuse_user:
|
|
message_post = _(
|
|
"The signature has been refused by %(partner)s (%(role)s), refusal reason:",
|
|
partner=self.partner_id.name,
|
|
role=self.role_id.name
|
|
)
|
|
else:
|
|
message_post = _(
|
|
"The signature has been refused by %(name)s with email (%(email)s), refusal reason:",
|
|
name=refusal_name,
|
|
email=refusal_email,
|
|
)
|
|
refusal_reason = _("No specified reason") if not refusal_reason or refusal_reason.isspace() else refusal_reason
|
|
message_post = Markup('{}<p style="white-space: pre">{}</p>').format(message_post, refusal_reason)
|
|
|
|
if self.state == 'sent' and request_state == 'sent':
|
|
self._refuse_sent(refuse_user, message_post, refusal_reason)
|
|
elif self.state == 'sent' and request_state == 'shared':
|
|
self._refuse_shared(refuse_user, message_post, refusal_name, refusal_email)
|
|
else:
|
|
raise UserError(_("This sign request item cannot be refused"))
|
|
|
|
def _refuse_sent(self, refuse_user, message_post, refusal_reason):
|
|
""" Refuse requests that were sent directly to the signers ('sent' state).
|
|
The cancelling flow happens directly in the refused sign request. """
|
|
self.env['sign.log'].create({'sign_request_item_id': self.id, 'action': 'refuse'})
|
|
self.write({'signing_date': fields.Date.context_today(self), 'state': 'canceled'})
|
|
|
|
# Mark the activity as done for the refuser.
|
|
if refuse_user and refuse_user.has_group('sign.group_sign_user'):
|
|
self.sign_request_id.activity_feedback(['mail.mail_activity_data_todo'], user_id=refuse_user.id)
|
|
|
|
self.sign_request_id.message_post(body=message_post)
|
|
self.sign_request_id._refuse(self.partner_id, refusal_reason)
|
|
|
|
def _refuse_shared(self, refuse_user, message_post, refusal_name="", refusal_email=""):
|
|
""" Refuse requests that were shared by a shared link ('shared' state).
|
|
The request is duplicated and its copy is cancelled, allowing other users to also refuse it.
|
|
Public users can also refuse the request by disclosing their identity (name and email). """
|
|
user_identified = bool(refusal_name and refusal_email)
|
|
if not refuse_user and not user_identified:
|
|
raise UserError(_("The public user must identify itself for the refusal."))
|
|
|
|
# Duplicate the sign request, allowing other users to also refuse it.
|
|
request_copy_values = self.sign_request_id.copy_data()[0]
|
|
request_copy_values['state'] = 'shared'
|
|
new_request = self.env['sign.request'].create(request_copy_values)
|
|
new_sign_request_item = new_request.request_item_ids
|
|
|
|
# Do the refuse action in the request item and set the copied request as cancelled.
|
|
self.env['sign.log'].create({'sign_request_item_id': new_sign_request_item.id, 'action': 'refuse'})
|
|
new_sign_request_item.write({'signing_date': fields.Date.context_today(new_sign_request_item), 'state': 'canceled'})
|
|
new_request.state = "canceled"
|
|
|
|
# Mark the activity as done for the refuser.
|
|
if refuse_user and refuse_user.has_group('sign.group_sign_user'):
|
|
new_sign_request_item.sign_request_id.activity_feedback(['mail.mail_activity_data_todo'], user_id=refuse_user.id)
|
|
new_sign_request_item.sign_request_id.message_post(body=message_post)
|
|
|
|
# Link the refusal partner to the cancelled request, it is used for easying the identification.
|
|
refusal_partner = refuse_user.partner_id or self.env['res.partner'].search([('email', '=', refusal_email)], limit=1)
|
|
if not refusal_partner:
|
|
refusal_partner = self.env['res.partner'].create({'name': refusal_name, 'email': refusal_email})
|
|
new_sign_request_item.partner_id = refusal_partner
|
|
|
|
def _get_url_parameters(self, signer, expiry_link_timestamp):
|
|
return url_encode({
|
|
'timestamp': expiry_link_timestamp,
|
|
'exp': signer._generate_expiry_signature(signer.id, expiry_link_timestamp)
|
|
})
|
|
|
|
def _get_access_token(self, signer):
|
|
return signer.sudo().access_token
|
|
|
|
def _get_sign_and_cancel_links(self, signer):
|
|
expiry_link_timestamp = signer._generate_expiry_link_timestamp()
|
|
url_params = self._get_url_parameters(signer, expiry_link_timestamp)
|
|
partial_url = "sign/document/mail/%(request_id)s/%(access_token)s?%(url_params)s" % {
|
|
'request_id': signer.sign_request_id.id,
|
|
'access_token': self._get_access_token(signer),
|
|
'url_params': url_params
|
|
}
|
|
link_sign = url_join(signer.get_base_url(), partial_url)
|
|
company = self.communication_company_id
|
|
if 'website_id' in company and company.website_id.domain:
|
|
link_sign = url_join(company.website_id.domain, partial_url)
|
|
link_cancel = link_sign + '&refuseDocument=1'
|
|
|
|
return link_sign, link_cancel
|
|
|
|
def _send_signature_access_message(self):
|
|
for signer in self:
|
|
signer_email_normalized = email_normalize(signer.signer_email or '')
|
|
signer_lang = get_lang(self.env, lang_code=signer.partner_id.lang).code
|
|
# We hide the validity information if it is the default (6 month from the create_date)
|
|
has_default_validity = signer.sign_request_id.validity and signer.sign_request_id.validity - relativedelta(months=6) == signer.sign_request_id.create_date.date()
|
|
link_sign, link_cancel = self._get_sign_and_cancel_links(signer)
|
|
body = self.env['ir.qweb']._render('sign.sign_template_mail_request', {
|
|
'record': signer,
|
|
'link': link_sign,
|
|
'link_cancel': link_cancel,
|
|
'subject': signer.sign_request_id.subject,
|
|
'body': signer.sign_request_id.message if not is_html_empty(signer.sign_request_id.message) else False,
|
|
'use_sign_terms': self.env['ir.config_parameter'].sudo().get_param('sign.use_sign_terms'),
|
|
'user_signature': signer.create_uid.signature,
|
|
'show_validity': signer.sign_request_id.validity and not has_default_validity,
|
|
}, lang=signer_lang, minimal_qcontext=True)
|
|
|
|
attachment_ids = signer.sign_request_id.attachment_ids.ids
|
|
self.env['sign.request'].with_context(lang=signer.partner_id.lang or self.env.lang)._message_send_mail(
|
|
body,
|
|
record_name=signer.sign_request_id.reference,
|
|
notif_values={
|
|
'model_description': _('Signature'),
|
|
'company': signer.communication_company_id or signer.sign_request_id.create_uid.company_id,
|
|
'partner': signer.partner_id,
|
|
},
|
|
mail_values={
|
|
'author_id': signer.create_uid.partner_id.id,
|
|
'email_from': signer.create_uid.email_formatted,
|
|
'email_to': formataddr((signer.partner_id.name, signer_email_normalized)),
|
|
'subject': signer.sign_request_id.subject,
|
|
'attachment_ids': attachment_ids,
|
|
},
|
|
force_send=self.env.context.get('force_send', True), # only force_send if not from cron
|
|
)
|
|
signer.is_mail_sent = True
|
|
|
|
def sign(self, signature, **kwargs):
|
|
""" Sign sign request items at once.
|
|
:param signature: dictionary containing signature values and corresponding ids
|
|
"""
|
|
self.ensure_one()
|
|
if not self.env.su:
|
|
raise UserError(_("This function can only be called with sudo."))
|
|
elif self.state != 'sent' or self.sign_request_id.state != 'sent':
|
|
raise UserError(_("This sign request item cannot be signed"))
|
|
elif self.sign_request_id.validity and self.sign_request_id.validity < fields.Date.today():
|
|
raise UserError(_('This sign request is not valid anymore'))
|
|
|
|
self._sign(signature, **kwargs)
|
|
|
|
def _sign(self, signature, **kwargs):
|
|
""" Stores the sign request item values.
|
|
:param signature: dictionary containing signature values and corresponding ids / signature image
|
|
:param validation_required: boolean indicating whether the sign request item will after a further validation process or now
|
|
"""
|
|
self.ensure_one()
|
|
if not self.env.su:
|
|
raise UserError(_("This function can only be called with sudo."))
|
|
elif self.state != 'sent' or self.sign_request_id.state != 'sent':
|
|
raise UserError(_("This sign request item cannot be signed"))
|
|
elif self.sign_request_id.validity and self.sign_request_id.validity < fields.Date.today():
|
|
raise UserError(_('This sign request is not valid anymore'))
|
|
|
|
# Constant items are populated automatically and cannot be modified by the signer,
|
|
# so they are excluded from required field validation.
|
|
required_ids = set(self.sign_request_id.template_id.sign_item_ids.filtered(
|
|
lambda r: r.responsible_id.id == self.role_id.id and r.required and not r.constant).ids)
|
|
signature_ids = {int(k) for k in signature} if isinstance(signature, dict) else set()
|
|
if not (required_ids <= signature_ids): # Security check
|
|
raise UserError(_("Some required items are not filled"))
|
|
if self.state != 'sent' or self.sign_request_id.state != 'sent':
|
|
raise UserError(_("This sign request item cannot be filled"))
|
|
if not self.env.su:
|
|
raise UserError(_("This function can only be called with sudo."))
|
|
self._fill(signature, **kwargs)
|
|
if not kwargs.get('validation_required', False):
|
|
self._post_fill_request_item()
|
|
|
|
def _send_no_credits_email(self):
|
|
partner_lang = get_lang(self.env, lang_code=self.create_uid.partner_id.lang).code
|
|
body = self.env['ir.qweb']._render('sign.sign_template_mail_not_enough_credits', {
|
|
'record': self,
|
|
'recipient_name': self.create_uid.name,
|
|
'subject': '%s signed' % self.reference,
|
|
'signer': self.partner_id,
|
|
'auth_method': dict(self.role_id._fields['auth_method']._description_selection(self.env))[self.role_id.auth_method]
|
|
}, lang=partner_lang, minimal_qcontext=True)
|
|
|
|
self.env['sign.request'].with_context(lang=self.create_uid.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': self.create_uid.partner_id,
|
|
},
|
|
mail_values={
|
|
'author_id': self.create_uid.partner_id.id,
|
|
'email_from': self.create_uid.email_formatted,
|
|
'email_to': self.create_uid.email_formatted,
|
|
'subject': _('%s: missing credits for extra-authentication', self.reference)
|
|
},
|
|
force_send=True,
|
|
)
|
|
|
|
def _post_fill_request_item(self):
|
|
self.env['sign.log'].create({'sign_request_item_id': self.id, 'action': 'sign'})
|
|
self.write({'signing_date': fields.Date.context_today(self), 'state': 'completed'})
|
|
if self.signed_without_extra_auth:
|
|
self._send_no_credits_email()
|
|
|
|
# mark signature as done in next activity
|
|
if not self.sign_request_id.request_item_ids.filtered(lambda sri: sri.partner_id == self.partner_id and sri.state == 'sent'):
|
|
sign_user = self.partner_id.user_ids[:1]
|
|
if sign_user and sign_user.has_group('sign.group_sign_user'):
|
|
self.sign_request_id.activity_feedback(['sign.mail_activity_data_signature_request'], user_id=sign_user.id)
|
|
sign_request = self.sign_request_id
|
|
if all(sri.state == 'completed' for sri in sign_request.request_item_ids):
|
|
sign_request._sign()
|
|
elif all(sri.state == 'completed' for sri in sign_request.request_item_ids.filtered(lambda sri: sri.mail_sent_order == self.mail_sent_order)):
|
|
sign_request.send_signature_accesses()
|
|
|
|
def _fill(self, signature, **kwargs):
|
|
""" Stores the sign request item values. (Can be used to pre-fill the document as a hack)
|
|
:param signature: dictionary containing signature values and corresponding ids / signature image
|
|
"""
|
|
self.ensure_one()
|
|
authorised_ids = set(self.sign_request_id.template_id.sign_item_ids.filtered(lambda r: r.responsible_id.id == self.role_id.id).ids)
|
|
signature_ids = {int(k) for k in signature} if isinstance(signature, dict) else set()
|
|
if not (signature_ids <= authorised_ids):
|
|
raise UserError(_("Some unauthorised items are filled"))
|
|
|
|
if not isinstance(signature, dict):
|
|
self.signature = signature
|
|
else:
|
|
SignItemValue = self.env['sign.request.item.value']
|
|
sign_request = self.sign_request_id
|
|
new_item_values_list = []
|
|
item_values_dict = {str(sign_item_value.sign_item_id.id): sign_item_value for sign_item_value in self.sign_item_value_ids}
|
|
signature_item_ids = set(sign_request.template_id.sign_item_ids.filtered(lambda r: r.type_id.item_type == 'signature').ids)
|
|
for itemId in signature:
|
|
frame = kwargs.get('frame', False)
|
|
if frame and itemId in frame:
|
|
frame_value = frame[itemId].get('frameValue', False)
|
|
frame_has_hash = bool(frame[itemId].get('frameHash', False))
|
|
else:
|
|
frame_value = False
|
|
frame_has_hash = False
|
|
if itemId not in item_values_dict:
|
|
new_item_values_list.append({'sign_item_id': int(itemId), 'sign_request_id': sign_request.id,
|
|
'value': signature[itemId], 'frame_value': frame_value,
|
|
'frame_has_hash': frame_has_hash, 'sign_request_item_id': self.id})
|
|
else:
|
|
item_values_dict[itemId].write({
|
|
'value': signature[itemId], 'frame_value': frame_value, 'frame_has_hash': frame_has_hash
|
|
})
|
|
if int(itemId) in signature_item_ids:
|
|
self.signature = signature[itemId][signature[itemId].find(',') + 1:]
|
|
SignItemValue.create(new_item_values_list)
|
|
|
|
def send_signature_accesses(self):
|
|
self.sign_request_id._check_senders_validity()
|
|
users = self.partner_id.user_ids
|
|
user_ids = set(users.sudo().search([('all_group_ids', 'in', self.env.ref('sign.group_sign_user').id), ('id', 'in', users.ids)]).ids)
|
|
for sign_request, sign_request_items_list in groupby(self, lambda sri: sri.sign_request_id):
|
|
notified_users = [sri.partner_id.user_ids[:1]
|
|
for sri in sign_request_items_list
|
|
if not sri.is_mail_sent and sri.state == 'sent' and sri.partner_id.user_ids[:1].id in user_ids]
|
|
sign_request._schedule_activity(notified_users)
|
|
body = _("The signature mail has been sent to: ")
|
|
receiver_names = ["%s(%s)" % (sri.partner_id.name, sri.role_id.name) for sri in sign_request_items_list]
|
|
body += ', '.join(receiver_names)
|
|
if not is_html_empty(sign_request.message):
|
|
body += sign_request.message
|
|
if not sign_request.communication_company_id:
|
|
sign_request.communication_company_id = self.env.company
|
|
sign_request.message_post(body=body)
|
|
self._send_signature_access_message()
|
|
|
|
def _get_user_signature(self, signature_type='sign_signature'):
|
|
""" Gets the user's stored sign_signature/sign_initials (needs sudo permission)
|
|
:param str signature_type: 'sign_signature' or 'sign_initials'
|
|
:returns bytes or False
|
|
"""
|
|
self.ensure_one()
|
|
sign_user = self.partner_id.user_ids[:1]
|
|
current_user_sign = False
|
|
if self.env.user._is_public() or sign_user and sign_user == self.env.user:
|
|
current_user_sign = True
|
|
if current_user_sign and signature_type in ['sign_signature', 'sign_initials']:
|
|
return sign_user[signature_type]
|
|
return False
|
|
|
|
def _get_user_signature_frame(self, signature_type='sign_signature_frame'):
|
|
""" Gets the user's stored sign_signature/sign_initials (needs sudo permission)
|
|
:param str signature_type: 'sign_signature' or 'sign_initials'
|
|
:returns bytes or False
|
|
"""
|
|
self.ensure_one()
|
|
sign_user = self.partner_id.user_ids[:1]
|
|
if sign_user and signature_type in ['sign_signature_frame', 'sign_initials_frame']:
|
|
return sign_user[signature_type]
|
|
return False
|
|
|
|
def _reset_sms_token(self):
|
|
for record in self:
|
|
record.sms_token = randint(100000, 999999)
|
|
|
|
def _send_sms(self):
|
|
self._reset_sms_token()
|
|
sms_values = [{'body': _('Your confirmation code is %s', rec.sms_token), 'number': rec.sms_number} for rec in self]
|
|
self.env['sms.sms'].sudo().create(sms_values).send()
|
|
|
|
def _compute_access_url(self):
|
|
super()._compute_access_url()
|
|
for signature_request in self:
|
|
signature_request.access_url = '/my/signature/%s' % signature_request.id
|
|
|
|
@api.model
|
|
def _generate_expiry_link_timestamp(self):
|
|
duration = int(self.env['ir.config_parameter'].sudo().get_param('sign.link_expiry_duration', 360))
|
|
expiry_date = fields.Datetime.now() + timedelta(hours=duration)
|
|
return int(expiry_date.timestamp())
|
|
|
|
@api.model
|
|
def _generate_expiry_signature(self, sign_request_item_id, timestamp):
|
|
return hmac(self.env(su=True), "sign_expiration", (timestamp, sign_request_item_id))
|
|
|
|
def _validate_expiry(self, exp_timestamp, exp_hash):
|
|
""" Validates if the expiry code is still valid
|
|
:param float exp_timestamp: a timestamp provided by the user in the URL params
|
|
:param str exp_hash: code provided in the URL to be checked
|
|
"""
|
|
self.ensure_one()
|
|
if not (exp_timestamp and exp_hash):
|
|
return False
|
|
exp_timestamp = int(exp_timestamp)
|
|
now = fields.Datetime.now().timestamp()
|
|
if now > exp_timestamp:
|
|
return False
|
|
return consteq(exp_hash, self._generate_expiry_signature(self.id, exp_timestamp))
|
|
|
|
@api.depends('state')
|
|
def _compute_color(self):
|
|
color_map = {"canceled": 0,
|
|
"sent": 0,
|
|
"completed": 10}
|
|
for sign_request_item in self:
|
|
sign_request_item.color = color_map[sign_request_item.state]
|
|
|
|
@api.depends('partner_id.email')
|
|
def _compute_email(self):
|
|
for sign_request_item in self.filtered(lambda sri: sri.state == "sent" or not sri.signer_email):
|
|
sign_request_item.signer_email = sign_request_item.partner_id.email_normalized
|
|
|
|
def _get_auto_field_value(self, item_type):
|
|
""" Return the automatic value of a sign item based on the linked model and partner access
|
|
:return: str: auto_value
|
|
"""
|
|
self.ensure_one()
|
|
item_type_sudo = self.env['sign.item.type'].sudo().browse(item_type['id'])
|
|
record = None
|
|
field_record_sudo = None
|
|
if item_type_sudo.model_id.model == 'res.partner':
|
|
record = self.partner_id
|
|
else:
|
|
if item_type_sudo.item_type == "stamp":
|
|
return self._get_stamp_value()
|
|
|
|
linked_record = self.sign_request_id.reference_doc
|
|
model = linked_record and self.env['ir.model']._get(linked_record._name)
|
|
if not model or not model.is_mail_thread or linked_record._name != item_type_sudo.model_id.model:
|
|
return ''
|
|
record = linked_record
|
|
try:
|
|
if record:
|
|
field_record_sudo = self.env['ir.model.fields'].sudo()._get(record._name, item_type_sudo.auto_field)
|
|
auto_field = record.mapped(item_type['auto_field'])
|
|
auto_value = auto_field[0] if auto_field and not isinstance(auto_field, models.BaseModel) else ''
|
|
if field_record_sudo and field_record_sudo.ttype == 'html':
|
|
auto_value = html2plaintext(auto_value)
|
|
except (KeyError, TypeError):
|
|
auto_value = ""
|
|
return auto_value
|
|
|
|
def _get_stamp_value(self):
|
|
"""
|
|
Return the formatted stamp value (company name, address, phone) for the partner.
|
|
|
|
:returns: str Company name and phone separated by a newline, or an empty string if not found.
|
|
"""
|
|
partner = self.with_context(show_address=1).partner_id
|
|
company_address = None
|
|
|
|
if partner:
|
|
if partner.parent_id:
|
|
# Partner has a parent → use parent company
|
|
company_address = partner.parent_id
|
|
elif partner.is_company:
|
|
# Partner is a standalone company → use itself (commercial partner)
|
|
company_address = partner.commercial_partner_id
|
|
elif partner.user_ids and partner.user_ids[0].company_id:
|
|
# Partner is an individual user → use their company partner
|
|
company_address = partner.user_ids[0].company_id.partner_id
|
|
|
|
if not company_address:
|
|
return ''
|
|
|
|
return self.env._(
|
|
"%(company_address)s\n%(phone)s", company_address=company_address.display_name, phone=company_address.phone or ""
|
|
)
|