736 lines
33 KiB
Python
736 lines
33 KiB
Python
# -*- coding: utf-8 -*-
|
|
|
|
import base64
|
|
import io
|
|
import os
|
|
import time
|
|
import uuid
|
|
|
|
from PyPDF2 import PdfFileReader, PdfFileWriter
|
|
from reportlab.lib.utils import ImageReader
|
|
from reportlab.pdfbase import pdfmetrics
|
|
from reportlab.pdfbase.ttfonts import TTFont
|
|
from reportlab.rl_config import TTFSearchPath
|
|
from reportlab.pdfgen import canvas
|
|
from reportlab.platypus import Paragraph
|
|
from reportlab.lib.styles import getSampleStyleSheet
|
|
from reportlab.pdfbase.pdfmetrics import stringWidth
|
|
from werkzeug.urls import url_join
|
|
from random import randint
|
|
|
|
from odoo import api, fields, models, http, _
|
|
from odoo.tools import DEFAULT_SERVER_DATE_FORMAT, formataddr, config, get_lang
|
|
from odoo.exceptions import UserError
|
|
|
|
TTFSearchPath.append(os.path.join(config["root_path"], "..", "addons", "web", "static", "src", "fonts", "sign"))
|
|
|
|
|
|
def _fix_image_transparency(image):
|
|
""" Modify image transparency to minimize issue of grey bar artefact.
|
|
|
|
When an image has a transparent pixel zone next to white pixel zone on a
|
|
white background, this may cause on some renderer grey line artefacts at
|
|
the edge between white and transparent.
|
|
|
|
This method sets transparent pixel to white transparent pixel which solves
|
|
the issue for the most probable case. With this the issue happen for a
|
|
black zone on black background but this is less likely to happen.
|
|
"""
|
|
pixels = image.load()
|
|
for x in range(image.size[0]):
|
|
for y in range(image.size[1]):
|
|
if pixels[x, y] == (0, 0, 0, 0):
|
|
pixels[x, y] = (255, 255, 255, 0)
|
|
|
|
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 _expand_states(self, states, domain, order):
|
|
return [key for key, val in type(self).state.selection]
|
|
|
|
template_id = fields.Many2one('sign.template', string="Template", required=True)
|
|
reference = fields.Char(required=True, string="Document Name", help="This is how the document will be named in the mail")
|
|
|
|
access_token = fields.Char('Security Token', required=True, default=_default_access_token, readonly=True)
|
|
|
|
request_item_ids = fields.One2many('sign.request.item', 'sign_request_id', string="Signers")
|
|
state = fields.Selection([
|
|
("sent", "Sent"),
|
|
("signed", "Fully Signed"),
|
|
("canceled", "Canceled")
|
|
], default='sent', tracking=True, group_expand='_expand_states')
|
|
|
|
completed_document = fields.Binary(readonly=True, string="Completed Document", attachment=True)
|
|
|
|
nb_wait = fields.Integer(string="Sent Requests", compute="_compute_count", store=True)
|
|
nb_closed = fields.Integer(string="Completed Signatures", compute="_compute_count", store=True)
|
|
nb_total = fields.Integer(string="Requested Signatures", compute="_compute_count", store=True)
|
|
progress = fields.Char(string="Progress", compute="_compute_count", compute_sudo=True)
|
|
start_sign = fields.Boolean(string="Signature Started", help="At least one signer has signed the document.", compute="_compute_count", 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")
|
|
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_count", compute_sudo=True)
|
|
|
|
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='Template Tags', related='template_id.tag_ids')
|
|
|
|
@api.depends('request_item_ids.state')
|
|
def _compute_count(self):
|
|
for rec in self:
|
|
wait, closed = 0, 0
|
|
for s in rec.request_item_ids:
|
|
if s.state == "sent":
|
|
wait += 1
|
|
if s.state == "completed":
|
|
closed += 1
|
|
rec.nb_wait = wait
|
|
rec.nb_closed = closed
|
|
rec.nb_total = wait + closed
|
|
rec.start_sign = bool(closed)
|
|
rec.progress = "{} / {}".format(closed, wait + closed)
|
|
if closed:
|
|
rec.start_sign = True
|
|
signed_requests = rec.request_item_ids.filtered('signing_date')
|
|
if wait == 0 and closed and signed_requests:
|
|
last_completed_request = signed_requests.sorted(key=lambda i: i.signing_date, reverse=True)[0]
|
|
rec.completion_date = last_completed_request.signing_date
|
|
else:
|
|
rec.completion_date = 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.partner_id.name or _('Public User'),
|
|
'state': item.state,
|
|
'signing_date': item.signing_date or ''
|
|
} for item in request.request_item_ids]
|
|
|
|
def _check_after_compute(self):
|
|
for rec in self:
|
|
if rec.state == 'sent' and rec.nb_closed == len(rec.request_item_ids) and len(rec.request_item_ids) > 0: # All signed
|
|
rec.action_signed()
|
|
|
|
def _get_final_recipients(self):
|
|
self.ensure_one()
|
|
all_recipients = set(self.request_item_ids.mapped('signer_email'))
|
|
all_recipients |= set(self.mapped('message_follower_ids.partner_id.email'))
|
|
# Remove False from all_recipients to avoid crashing later
|
|
all_recipients.discard(False)
|
|
return all_recipients
|
|
|
|
def button_send(self):
|
|
self.action_sent()
|
|
|
|
def go_to_document(self):
|
|
self.ensure_one()
|
|
request_item = self.request_item_ids.filtered(lambda r: r.partner_id and r.partner_id.id == self.env.user.partner_id.id)[:1]
|
|
return {
|
|
'name': self.reference,
|
|
'type': 'ir.actions.client',
|
|
'tag': 'odex25_sign.Document',
|
|
'context': {
|
|
'id': self.id,
|
|
'token': self.access_token,
|
|
'sign_token': request_item.access_token if request_item and request_item.state == "sent" else None,
|
|
'create_uid': self.create_uid.id,
|
|
'state': self.state,
|
|
},
|
|
}
|
|
|
|
def open_request(self):
|
|
self.ensure_one()
|
|
return {
|
|
"type": "ir.actions.act_window",
|
|
"res_model": "sign.request",
|
|
"views": [[False, "form"]],
|
|
"res_id": self.id,
|
|
}
|
|
|
|
def get_completed_document(self):
|
|
self.ensure_one()
|
|
if not self.completed_document:
|
|
self.generate_completed_document()
|
|
|
|
return {
|
|
'name': 'Signed Document',
|
|
'type': 'ir.actions.act_url',
|
|
'url': '/odex25_sign/download/%(request_id)s/%(access_token)s/completed' % {'request_id': self.id, 'access_token': self.access_token},
|
|
}
|
|
|
|
def open_logs(self):
|
|
self.ensure_one()
|
|
return {
|
|
"name": _("Access History"),
|
|
"type": "ir.actions.act_window",
|
|
"res_model": "sign.log",
|
|
'view_mode': 'tree,form',
|
|
'domain': [('sign_request_id', '=', self.id)],
|
|
}
|
|
|
|
@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 action_resend(self):
|
|
self.action_draft()
|
|
subject = _("Signature Request - %s") % (self.template_id.attachment_id.name)
|
|
self.action_sent(subject=subject)
|
|
|
|
def action_draft(self):
|
|
self.write({'completed_document': None, 'access_token': self._default_access_token()})
|
|
|
|
def action_sent_without_mail(self):
|
|
self.write({'state': 'sent'})
|
|
for sign_request in self:
|
|
for sign_request_item in sign_request.request_item_ids:
|
|
sign_request_item.write({'state':'sent'})
|
|
Log = http.request.env['sign.log'].sudo()
|
|
vals = Log._prepare_vals_from_request(sign_request)
|
|
vals['action'] = 'create'
|
|
vals = Log._update_vals_with_http_request(vals)
|
|
Log.create(vals)
|
|
|
|
def action_sent(self, subject=None, message=None):
|
|
# Send accesses by email
|
|
self.write({'state': 'sent'})
|
|
for sign_request in self:
|
|
ignored_partners = []
|
|
for request_item in sign_request.request_item_ids:
|
|
if request_item.state != 'draft':
|
|
ignored_partners.append(request_item.partner_id.id)
|
|
included_request_items = sign_request.request_item_ids.filtered(lambda r: not r.partner_id or r.partner_id.id not in ignored_partners)
|
|
|
|
if sign_request.send_signature_accesses(subject, message, ignored_partners=ignored_partners):
|
|
Log = http.request.env['sign.log'].sudo()
|
|
vals = Log._prepare_vals_from_request(sign_request)
|
|
vals['action'] = 'create'
|
|
vals = Log._update_vals_with_http_request(vals)
|
|
Log.create(vals)
|
|
followers = sign_request.message_follower_ids.mapped('partner_id')
|
|
followers -= sign_request.create_uid.partner_id
|
|
followers -= sign_request.request_item_ids.mapped('partner_id')
|
|
if followers:
|
|
sign_request.send_follower_accesses(followers, subject, message)
|
|
included_request_items.action_sent()
|
|
else:
|
|
sign_request.action_draft()
|
|
|
|
def action_signed(self):
|
|
self.write({'state': 'signed'})
|
|
self.env.cr.commit()
|
|
if not self.check_is_encrypted():
|
|
# if the file is encrypted, we must wait that the document is decrypted
|
|
self.send_completed_document()
|
|
|
|
def check_is_encrypted(self):
|
|
self.ensure_one()
|
|
if not self.template_id.sign_item_ids:
|
|
return False
|
|
|
|
old_pdf = PdfFileReader(io.BytesIO(base64.b64decode(self.template_id.attachment_id.datas)), strict=False, overwriteWarnings=False)
|
|
return old_pdf.isEncrypted
|
|
|
|
def action_canceled(self):
|
|
self.write({'completed_document': None, 'access_token': self._default_access_token(), 'state': 'canceled'})
|
|
for request_item in self.mapped('request_item_ids'):
|
|
request_item.action_draft()
|
|
|
|
def set_signers(self, signers):
|
|
SignRequestItem = self.env['sign.request.item']
|
|
|
|
for rec in self:
|
|
rec.request_item_ids.filtered(lambda r: not r.partner_id or not r.role_id).unlink()
|
|
ids_to_remove = []
|
|
for request_item in rec.request_item_ids:
|
|
for i in range(0, len(signers)):
|
|
if signers[i]['partner_id'] == request_item.partner_id.id and signers[i]['role'] == request_item.role_id.id:
|
|
signers.pop(i)
|
|
break
|
|
else:
|
|
ids_to_remove.append(request_item.id)
|
|
|
|
SignRequestItem.browse(ids_to_remove).unlink()
|
|
for signer in signers:
|
|
SignRequestItem.create({
|
|
'partner_id': signer['partner_id'],
|
|
'sign_request_id': rec.id,
|
|
'role_id': signer['role'],
|
|
})
|
|
|
|
def send_signature_accesses(self, subject=None, message=None, ignored_partners=[]):
|
|
self.ensure_one()
|
|
if len(self.request_item_ids) <= 0 or (set(self.request_item_ids.mapped('role_id')) != set(self.template_id.sign_item_ids.mapped('responsible_id'))):
|
|
return False
|
|
|
|
self.request_item_ids.filtered(lambda r: not r.partner_id or r.partner_id.id not in ignored_partners).send_signature_accesses(subject, message)
|
|
return True
|
|
|
|
def send_follower_accesses(self, followers, subject=None, message=None):
|
|
self.ensure_one()
|
|
base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url')
|
|
tpl = self.env.ref('odex25_sign.sign_template_mail_follower')
|
|
for follower in followers:
|
|
if not follower.email:
|
|
continue
|
|
if not self.create_uid.email:
|
|
raise UserError(_("Please configure the sender's email address"))
|
|
tpl_follower = tpl.with_context(lang=get_lang(self.env, lang_code=follower.lang).code)
|
|
body = tpl_follower._render({
|
|
'record': self,
|
|
'link': url_join(base_url, 'odex25_sign/document/%s/%s' % (self.id, self.access_token)),
|
|
'subject': subject,
|
|
'body': message,
|
|
}, engine='ir.qweb', minimal_qcontext=True)
|
|
self.env['sign.request']._message_send_mail(
|
|
body, 'mail.mail_notification_light',
|
|
{'record_name': self.reference},
|
|
{'model_description': 'signature', 'company': self.create_uid.company_id},
|
|
{'email_from': self.create_uid.email_formatted,
|
|
'author_id': self.create_uid.partner_id.id,
|
|
'email_to': follower.email_formatted,
|
|
'subject': subject or _('%s : Signature request', self.reference)},
|
|
lang=follower.lang,
|
|
)
|
|
self.message_subscribe(partner_ids=follower.ids)
|
|
|
|
def send_completed_document(self):
|
|
self.ensure_one()
|
|
if len(self.request_item_ids) <= 0 or self.state != 'signed':
|
|
return False
|
|
|
|
if not self.completed_document:
|
|
self.generate_completed_document()
|
|
|
|
base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url')
|
|
attachment = self.env['ir.attachment'].create({
|
|
'name': "%s.pdf" % self.reference if self.reference.split('.')[-1] != 'pdf' else self.reference,
|
|
'datas': self.completed_document,
|
|
'type': 'binary',
|
|
'res_model': self._name,
|
|
'res_id': self.id,
|
|
})
|
|
report_action = self.env.ref('odex25_sign.action_sign_request_print_logs')
|
|
# 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, __ = report_action.with_user(public_user).sudo()._render_qweb_pdf(self.id)
|
|
attachment_log = self.env['ir.attachment'].create({
|
|
'name': "Certificate of completion - %s.pdf" % time.strftime('%Y-%m-%d - %H:%M:%S'),
|
|
'datas': base64.b64encode(pdf_content),
|
|
'type': 'binary',
|
|
'res_model': self._name,
|
|
'res_id': self.id,
|
|
})
|
|
tpl = self.env.ref('odex25_sign.sign_template_mail_completed')
|
|
for signer in self.request_item_ids:
|
|
if not signer.signer_email:
|
|
continue
|
|
signer_lang = get_lang(self.env, lang_code=signer.partner_id.lang).code
|
|
tpl = tpl.with_context(lang=signer_lang)
|
|
body = tpl._render({
|
|
'record': self,
|
|
'link': url_join(base_url, 'odex25_sign/document/%s/%s' % (self.id, signer.access_token)),
|
|
'subject': '%s signed' % self.reference,
|
|
'body': False,
|
|
}, engine='ir.qweb', minimal_qcontext=True)
|
|
|
|
if not self.create_uid.email:
|
|
raise UserError(_("Please configure the sender's email address"))
|
|
if not signer.signer_email:
|
|
raise UserError(_("Please configure the signer's email address"))
|
|
|
|
self.env['sign.request']._message_send_mail(
|
|
body, 'mail.mail_notification_light',
|
|
{'record_name': self.reference},
|
|
{'model_description': 'signature', 'company': self.create_uid.company_id},
|
|
{'email_from': self.create_uid.email_formatted,
|
|
'author_id': self.create_uid.partner_id.id,
|
|
'email_to': signer.partner_id.email_formatted,
|
|
'subject': _('%s has been signed', self.reference),
|
|
'attachment_ids': [(4, attachment.id), (4, attachment_log.id)]},
|
|
force_send=True,
|
|
lang=signer_lang,
|
|
)
|
|
|
|
tpl = self.env.ref('odex25_sign.sign_template_mail_completed')
|
|
for follower in self.mapped('message_follower_ids.partner_id') - self.request_item_ids.mapped('partner_id'):
|
|
if not follower.email:
|
|
continue
|
|
if not self.create_uid.email:
|
|
raise UserError(_("Please configure the sender's email address"))
|
|
|
|
tpl_follower = tpl.with_context(lang=get_lang(self.env, lang_code=follower.lang).code)
|
|
body = tpl._render({
|
|
'record': self,
|
|
'link': url_join(base_url, 'odex25_sign/document/%s/%s' % (self.id, self.access_token)),
|
|
'subject': '%s signed' % self.reference,
|
|
'body': '',
|
|
}, engine='ir.qweb', minimal_qcontext=True)
|
|
self.env['sign.request']._message_send_mail(
|
|
body, 'mail.mail_notification_light',
|
|
{'record_name': self.reference},
|
|
{'model_description': 'signature', 'company': self.create_uid.company_id},
|
|
{'email_from': self.create_uid.email_formatted,
|
|
'author_id': self.create_uid.partner_id.id,
|
|
'email_to': follower.email_formatted,
|
|
'subject': _('%s has been signed', self.reference)},
|
|
lang=follower.lang,
|
|
)
|
|
|
|
return True
|
|
|
|
def _get_font(self):
|
|
custom_font = self.env["ir.config_parameter"].sudo().get_param("odex25_sign.use_custom_font")
|
|
# The font must be a TTF font. The tool 'otf2ttf' may be useful for conversion.
|
|
if custom_font:
|
|
pdfmetrics.registerFont(TTFont(custom_font, custom_font + ".ttf"))
|
|
return custom_font
|
|
return "Helvetica"
|
|
|
|
def _get_normal_font_size(self):
|
|
return 0.015
|
|
|
|
def generate_completed_document(self, password=""):
|
|
self.ensure_one()
|
|
if not self.template_id.sign_item_ids:
|
|
self.completed_document = self.template_id.attachment_id.datas
|
|
return
|
|
|
|
old_pdf = PdfFileReader(io.BytesIO(base64.b64decode(self.template_id.attachment_id.datas)), strict=False, overwriteWarnings=False)
|
|
|
|
isEncrypted = old_pdf.isEncrypted
|
|
if isEncrypted and not old_pdf.decrypt(password):
|
|
# password is not correct
|
|
return
|
|
|
|
font = self._get_font()
|
|
normalFontSize = self._get_normal_font_size()
|
|
|
|
packet = io.BytesIO()
|
|
can = canvas.Canvas(packet)
|
|
itemsByPage = self.template_id.sign_item_ids.getByPage()
|
|
SignItemValue = self.env['sign.request.item.value']
|
|
for p in range(0, old_pdf.getNumPages()):
|
|
page = old_pdf.getPage(p)
|
|
# Absolute values are taken as it depends on the MediaBox template PDF metadata, they may be negative
|
|
width = float(abs(page.mediaBox.getWidth()))
|
|
height = float(abs(page.mediaBox.getHeight()))
|
|
|
|
# Set page orientation (either 0, 90, 180 or 270)
|
|
rotation = page.get('/Rotate')
|
|
if rotation:
|
|
can.rotate(rotation)
|
|
# Translate system so that elements are placed correctly
|
|
# despite of the orientation
|
|
if rotation == 90:
|
|
width, height = height, width
|
|
can.translate(0, -height)
|
|
elif rotation == 180:
|
|
can.translate(-width, -height)
|
|
elif rotation == 270:
|
|
width, height = height, width
|
|
can.translate(-width, 0)
|
|
|
|
items = itemsByPage[p + 1] if p + 1 in itemsByPage else []
|
|
for item in items:
|
|
value = SignItemValue.search([('sign_item_id', '=', item.id), ('sign_request_id', '=', self.id)], limit=1)
|
|
if not value or not value.value:
|
|
continue
|
|
|
|
value = value.value
|
|
|
|
if item.type_id.item_type == "text":
|
|
can.setFont(font, height*item.height*0.8)
|
|
can.drawString(width*item.posX, height*(1-item.posY-item.height*0.9), value)
|
|
|
|
elif item.type_id.item_type == "selection":
|
|
content = []
|
|
for option in item.option_ids:
|
|
if option.id != int(value):
|
|
content.append("<strike>%s</strike>" % (option.value))
|
|
else:
|
|
content.append(option.value)
|
|
font_size = height * normalFontSize * 0.8
|
|
can.setFont(font, font_size)
|
|
text = " / ".join(content)
|
|
string_width = stringWidth(text.replace("<strike>", "").replace("</strike>", ""), font, font_size)
|
|
p = Paragraph(text, getSampleStyleSheet()["Normal"])
|
|
w, h = p.wrap(width, height)
|
|
posX = width * (item.posX + item.width * 0.5) - string_width // 2
|
|
posY = height * (1 - item.posY - item.height * 0.5) - h // 2
|
|
p.drawOn(can, posX, posY)
|
|
|
|
elif item.type_id.item_type == "textarea":
|
|
can.setFont(font, height*normalFontSize*0.8)
|
|
lines = value.split('\n')
|
|
y = (1-item.posY)
|
|
for line in lines:
|
|
y -= normalFontSize*0.9
|
|
can.drawString(width*item.posX, height*y, line)
|
|
y -= normalFontSize*0.1
|
|
|
|
elif item.type_id.item_type == "checkbox":
|
|
can.setFont(font, height*item.height*0.8)
|
|
value = 'X' if value == 'on' else ''
|
|
can.drawString(width*item.posX, height*(1-item.posY-item.height*0.9), value)
|
|
|
|
elif item.type_id.item_type == "signature" or item.type_id.item_type == "initial":
|
|
image_reader = ImageReader(io.BytesIO(base64.b64decode(value[value.find(',')+1:])))
|
|
_fix_image_transparency(image_reader._image)
|
|
can.drawImage(image_reader, width*item.posX, height*(1-item.posY-item.height), width*item.width, height*item.height, 'auto', True)
|
|
|
|
can.showPage()
|
|
|
|
can.save()
|
|
|
|
item_pdf = PdfFileReader(packet, overwriteWarnings=False)
|
|
new_pdf = PdfFileWriter()
|
|
|
|
for p in range(0, old_pdf.getNumPages()):
|
|
page = old_pdf.getPage(p)
|
|
page.mergePage(item_pdf.getPage(p))
|
|
new_pdf.addPage(page)
|
|
|
|
if isEncrypted:
|
|
new_pdf.encrypt(password)
|
|
|
|
output = io.BytesIO()
|
|
new_pdf.write(output)
|
|
self.completed_document = base64.b64encode(output.getvalue())
|
|
output.close()
|
|
|
|
@api.model
|
|
def _message_send_mail(self, body, notif_template_xmlid, message_values, notif_values, mail_values, force_send=False, **kwargs):
|
|
""" Shortcut to send an email. """
|
|
default_lang = get_lang(self.env, lang_code=kwargs.get('lang')).code
|
|
lang = kwargs.get('lang', default_lang)
|
|
sign_request = self.with_context(lang=lang)
|
|
|
|
# the notif layout wrapping expects a mail.message record, but we don't want
|
|
# to actually create the record
|
|
# See @tde-banana-odoo for details
|
|
msg = sign_request.env['mail.message'].sudo().new(dict(body=body, **message_values))
|
|
notif_layout = sign_request.env.ref(notif_template_xmlid)
|
|
body_html = notif_layout._render(dict(message=msg, **notif_values), engine='ir.qweb', minimal_qcontext=True)
|
|
body_html = sign_request.env['mail.render.mixin']._replace_local_links(body_html)
|
|
|
|
mail = sign_request.env['mail.mail'].sudo().create(dict(body_html=body_html, state='outgoing', **mail_values))
|
|
if force_send:
|
|
mail.send()
|
|
return mail
|
|
|
|
@api.model
|
|
def initialize_new(self, id, signers, followers, reference, subject, message, send=True, without_mail=False):
|
|
sign_users = self.env['res.users'].search([('partner_id', 'in', [signer['partner_id'] for signer in signers])]).filtered(lambda u: u.has_group('odex25_sign.group_sign_employee'))
|
|
sign_request = self.create({'template_id': id, 'reference': reference})
|
|
sign_request.message_subscribe(partner_ids=followers)
|
|
sign_request.activity_update(sign_users)
|
|
sign_request.set_signers(signers)
|
|
if send:
|
|
sign_request.action_sent(subject, message)
|
|
if without_mail:
|
|
sign_request.action_sent_without_mail()
|
|
return {
|
|
'id': sign_request.id,
|
|
'token': sign_request.access_token,
|
|
'sign_token': sign_request.request_item_ids.filtered(lambda r: r.partner_id == self.env.user.partner_id)[:1].access_token,
|
|
}
|
|
|
|
@api.model
|
|
def add_followers(self, id, followers):
|
|
sign_request = self.browse(id)
|
|
old_followers = set(sign_request.message_follower_ids.mapped('partner_id.id'))
|
|
followers = list(set(followers) - old_followers)
|
|
if followers:
|
|
sign_request.message_subscribe(partner_ids=followers)
|
|
sign_request.send_follower_accesses(self.env['res.partner'].browse(followers))
|
|
return sign_request.id
|
|
|
|
@api.model
|
|
def activity_update(self, sign_users):
|
|
for user in sign_users:
|
|
self.with_context(mail_activity_quick_update=True).activity_schedule(
|
|
'mail.mail_activity_data_todo',
|
|
user_id=user.id
|
|
)
|
|
|
|
|
|
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())
|
|
|
|
partner_id = fields.Many2one('res.partner', string="Contact", ondelete='cascade')
|
|
sign_request_id = fields.Many2one('sign.request', string="Signature Request", ondelete='cascade', required=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")
|
|
|
|
access_token = fields.Char(required=True, default=_default_access_token, readonly=True)
|
|
access_via_link = fields.Boolean('Accessed Through Token')
|
|
role_id = fields.Many2one('sign.item.role', string="Role")
|
|
sms_number = fields.Char(related='partner_id.mobile', readonly=False, depends=(['partner_id']), store=True)
|
|
sms_token = fields.Char('SMS Token', readonly=True)
|
|
|
|
signature = fields.Binary(attachment=True)
|
|
signing_date = fields.Date('Signed on', readonly=True)
|
|
state = fields.Selection([
|
|
("draft", "Draft"),
|
|
("sent", "To Sign"),
|
|
("completed", "Completed")
|
|
], readonly=True, default="draft")
|
|
|
|
signer_email = fields.Char(related='partner_id.email', readonly=False, depends=(['partner_id']), store=True)
|
|
|
|
latitude = fields.Float(digits=(10, 7))
|
|
longitude = fields.Float(digits=(10, 7))
|
|
|
|
def action_draft(self):
|
|
self.write({
|
|
'signature': None,
|
|
'signing_date': None,
|
|
'access_token': self._default_access_token(),
|
|
'state': 'draft',
|
|
})
|
|
for request_item in self:
|
|
itemsToClean = request_item.sign_request_id.template_id.sign_item_ids.filtered(lambda r: r.responsible_id == request_item.role_id or not r.responsible_id)
|
|
self.env['sign.request.item.value'].search([('sign_item_id', 'in', itemsToClean.mapped('id')), ('sign_request_id', '=', request_item.sign_request_id.id)]).unlink()
|
|
self.mapped('sign_request_id')._check_after_compute()
|
|
|
|
def action_sent(self):
|
|
self.write({'state': 'sent'})
|
|
self.mapped('sign_request_id')._check_after_compute()
|
|
|
|
def action_completed(self):
|
|
date = fields.Date.context_today(self).strftime(DEFAULT_SERVER_DATE_FORMAT)
|
|
self.write({'signing_date': date, 'state': 'completed'})
|
|
self.mapped('sign_request_id')._check_after_compute()
|
|
|
|
def send_signature_accesses(self, subject=None, message=None):
|
|
base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url')
|
|
tpl = self.env.ref('odex25_sign.sign_template_mail_request')
|
|
for signer in self:
|
|
if not signer.partner_id or not signer.partner_id.email:
|
|
continue
|
|
if not signer.create_uid.email:
|
|
continue
|
|
signer_lang = get_lang(self.env, lang_code=signer.partner_id.lang).code
|
|
tpl = tpl.with_context(lang=signer_lang)
|
|
body = tpl._render({
|
|
'record': signer,
|
|
'link': url_join(base_url, "odex25_sign/document/mail/%(request_id)s/%(access_token)s" % {'request_id': signer.sign_request_id.id, 'access_token': signer.access_token}),
|
|
'subject': subject,
|
|
'body': message if message != '<p><br></p>' else False,
|
|
}, engine='ir.qweb', minimal_qcontext=True)
|
|
|
|
if not signer.signer_email:
|
|
raise UserError(_("Please configure the signer's email address"))
|
|
self.env['sign.request']._message_send_mail(
|
|
body, 'mail.mail_notification_light',
|
|
{'record_name': signer.sign_request_id.reference},
|
|
{'model_description': 'signature', 'company': signer.create_uid.company_id},
|
|
{'email_from': signer.create_uid.email_formatted,
|
|
'author_id': signer.create_uid.partner_id.id,
|
|
'email_to': signer.partner_id.email_formatted,
|
|
'subject': subject},
|
|
force_send=True,
|
|
lang=signer_lang,
|
|
)
|
|
|
|
def sign(self, signature):
|
|
self.ensure_one()
|
|
if not isinstance(signature, dict):
|
|
self.signature = signature
|
|
else:
|
|
SignItemValue = self.env['sign.request.item.value']
|
|
request = self.sign_request_id
|
|
|
|
signerItems = request.template_id.sign_item_ids.filtered(lambda r: not r.responsible_id or r.responsible_id.id == self.role_id.id)
|
|
autorizedIDs = set(signerItems.mapped('id'))
|
|
requiredIDs = set(signerItems.filtered('required').mapped('id'))
|
|
|
|
itemIDs = {int(k) for k in signature}
|
|
if not (itemIDs <= autorizedIDs and requiredIDs <= itemIDs): # Security check
|
|
return False
|
|
|
|
user = self.env['res.users'].search([('partner_id', '=', self.partner_id.id)], limit=1).sudo()
|
|
for itemId in signature:
|
|
item_value = SignItemValue.search([('sign_item_id', '=', int(itemId)), ('sign_request_id', '=', request.id)])
|
|
if not item_value:
|
|
item_value = SignItemValue.create({'sign_item_id': int(itemId), 'sign_request_id': request.id,
|
|
'value': signature[itemId], 'sign_request_item_id': self.id})
|
|
else:
|
|
item_value.write({'value': signature[itemId]})
|
|
if item_value.sign_item_id.type_id.item_type == 'signature':
|
|
self.signature = signature[itemId][signature[itemId].find(',')+1:]
|
|
if user:
|
|
user.sign_signature = self.signature
|
|
if item_value.sign_item_id.type_id.item_type == 'initial' and user:
|
|
user.sign_initials = signature[itemId][signature[itemId].find(',')+1:]
|
|
|
|
return True
|
|
|
|
@api.model
|
|
def resend_access(self, id):
|
|
sign_request_item = self.browse(id)
|
|
subject = _("Signature Request - %s") % (sign_request_item.sign_request_id.template_id.attachment_id.name)
|
|
self.browse(id).send_signature_accesses(subject=subject)
|
|
|
|
def _reset_sms_token(self):
|
|
for record in self:
|
|
record.sms_token = randint(100000, 999999)
|
|
|
|
def _send_sms(self):
|
|
for rec in self:
|
|
rec._reset_sms_token()
|
|
self.env['sms.api']._send_sms([rec.sms_number], _('Your confirmation code is %s', rec.sms_token))
|
|
|
|
def _compute_access_url(self):
|
|
super(SignRequestItem, self)._compute_access_url()
|
|
for signature_request in self:
|
|
signature_request.access_url = '/my/signature/%s' % signature_request.id
|
|
|
|
|
|
class SignRequestItemValue(models.Model):
|
|
_name = "sign.request.item.value"
|
|
_description = "Signature Item Value"
|
|
_rec_name = 'sign_request_id'
|
|
|
|
sign_request_item_id = fields.Many2one('sign.request.item', string="Signature Request item", required=True,
|
|
ondelete='cascade')
|
|
sign_item_id = fields.Many2one('sign.item', string="Signature Item", required=True, ondelete='cascade')
|
|
sign_request_id = fields.Many2one(string="Signature Request", required=True, ondelete='cascade', related='sign_request_item_id.sign_request_id')
|
|
|
|
value = fields.Text()
|