[REMOVE]REMOVE digital sig
|
|
@ -1,5 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import controllers
|
||||
from . import models
|
||||
from . import wizard
|
||||
|
|
@ -1,40 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
{
|
||||
'name': 'Sign',
|
||||
'version': '1.0',
|
||||
'category':'Odex25-Sales/Odex25-Sales',
|
||||
'sequence': 105,
|
||||
'summary': "Send documents to sign online and handle filled copies",
|
||||
'description': """
|
||||
Sign and complete your documents easily. Customize your documents with text and signature fields and send them to your recipients.\n
|
||||
Let your customers follow the signature process easily.
|
||||
""",
|
||||
'author': "Expert Co. Ltd.",
|
||||
'website': "http://www.exp-sa.com",
|
||||
'depends': ['mail', 'attachment_indexation', 'portal', 'sms'],
|
||||
'data': [
|
||||
'security/security.xml',
|
||||
'security/ir.model.access.csv',
|
||||
'data/sign_data.xml',
|
||||
'views/sign_template_views_mobile.xml',
|
||||
'wizard/sign_send_request_views.xml',
|
||||
'wizard/sign_template_share_views.xml',
|
||||
'wizard/sign_request_send_copy_views.xml',
|
||||
'views/sign_request_templates.xml',
|
||||
'views/sign_template_templates.xml',
|
||||
'views/sign_request_views.xml',
|
||||
'views/sign_template_views.xml',
|
||||
'views/sign_log_views.xml',
|
||||
'views/sign_portal_templates.xml',
|
||||
'views/res_users_views.xml',
|
||||
'views/res_partner_views.xml',
|
||||
'views/sign_pdf_iframe_templates.xml',
|
||||
'report/sign_log_reports.xml',
|
||||
],
|
||||
'qweb': ['static/src/xml/*.xml'],
|
||||
'demo': [
|
||||
'data/sign_demo.xml',
|
||||
],
|
||||
'application': True,
|
||||
'installable': True,
|
||||
}
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import main
|
||||
from . import portal
|
||||
|
|
@ -1,342 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
import base64
|
||||
import io
|
||||
import logging
|
||||
import mimetypes
|
||||
import re
|
||||
import werkzeug
|
||||
|
||||
from PyPDF2 import PdfFileReader
|
||||
|
||||
from odoo import http, _
|
||||
from odoo.http import request
|
||||
from odoo.addons.web.controllers.main import content_disposition
|
||||
from odoo.addons.iap.tools import iap_tools
|
||||
|
||||
_logger = logging.getLogger()
|
||||
|
||||
|
||||
class Sign(http.Controller):
|
||||
|
||||
def get_document_qweb_context(self, id, token):
|
||||
sign_request = http.request.env['sign.request'].sudo().browse(id).exists()
|
||||
if not sign_request:
|
||||
if token:
|
||||
return http.request.render('odex25_sign.deleted_sign_request')
|
||||
else:
|
||||
return http.request.not_found()
|
||||
|
||||
current_request_item = None
|
||||
if token:
|
||||
current_request_item = sign_request.request_item_ids.filtered(lambda r: r.access_token == token)
|
||||
if not current_request_item and sign_request.access_token != token and http.request.env.user.id != sign_request.create_uid.id:
|
||||
return http.request.render('odex25_sign.deleted_sign_request')
|
||||
elif sign_request.create_uid.id != http.request.env.user.id:
|
||||
return http.request.not_found()
|
||||
|
||||
sign_item_types = http.request.env['sign.item.type'].sudo().search_read([])
|
||||
if current_request_item:
|
||||
for item_type in sign_item_types:
|
||||
if item_type['auto_field']:
|
||||
fields = item_type['auto_field'].split('.')
|
||||
auto_field = current_request_item.partner_id
|
||||
for field in fields:
|
||||
if auto_field and field in auto_field:
|
||||
auto_field = auto_field[field]
|
||||
else:
|
||||
auto_field = ""
|
||||
break
|
||||
item_type['auto_field'] = auto_field
|
||||
|
||||
if current_request_item.state != 'completed':
|
||||
""" When signer attempts to sign the request again,
|
||||
its localisation should be reset.
|
||||
We prefer having no/approximative (from geoip) information
|
||||
than having wrong old information (from geoip/browser)
|
||||
on the signer localisation.
|
||||
"""
|
||||
current_request_item.write({
|
||||
'latitude': request.session['geoip'].get('latitude') if 'geoip' in request.session else 0,
|
||||
'longitude': request.session['geoip'].get('longitude') if 'geoip' in request.session else 0,
|
||||
})
|
||||
|
||||
item_values = {}
|
||||
sr_values = http.request.env['sign.request.item.value'].sudo().search([('sign_request_id', '=', sign_request.id), '|', ('sign_request_item_id', '=', current_request_item.id), ('sign_request_item_id.state', '=', 'completed')])
|
||||
for value in sr_values:
|
||||
item_values[value.sign_item_id.id] = value.value
|
||||
|
||||
Log = request.env['sign.log'].sudo()
|
||||
vals = Log._prepare_vals_from_item(current_request_item) if current_request_item else Log._prepare_vals_from_request(sign_request)
|
||||
vals['action'] = 'open'
|
||||
vals = Log._update_vals_with_http_request(vals)
|
||||
Log.create(vals)
|
||||
|
||||
return {
|
||||
'sign_request': sign_request,
|
||||
'current_request_item': current_request_item,
|
||||
'token': token,
|
||||
'nbComments': len(sign_request.message_ids.filtered(lambda m: m.message_type == 'comment')),
|
||||
'isPDF': (sign_request.template_id.attachment_id.mimetype.find('pdf') > -1),
|
||||
'webimage': re.match('image.*(gif|jpe|jpg|png)', sign_request.template_id.attachment_id.mimetype),
|
||||
'hasItems': len(sign_request.template_id.sign_item_ids) > 0,
|
||||
'sign_items': sign_request.template_id.sign_item_ids,
|
||||
'item_values': item_values,
|
||||
'role': current_request_item.role_id.id if current_request_item else 0,
|
||||
'readonly': not (current_request_item and current_request_item.state == 'sent'),
|
||||
'sign_item_types': sign_item_types,
|
||||
'sign_item_select_options': sign_request.template_id.sign_item_ids.mapped('option_ids'),
|
||||
}
|
||||
|
||||
# -------------
|
||||
# HTTP Routes
|
||||
# -------------
|
||||
@http.route(["/odex25_sign/document/<int:id>"], type='http', auth='user')
|
||||
def sign_document_user(self, id, **post):
|
||||
return self.sign_document_public(id, None)
|
||||
|
||||
@http.route(["/odex25_sign/document/mail/<int:id>/<token>"], type='http', auth='public')
|
||||
def sign_document_from_mail(self, id, token):
|
||||
sign_request = request.env['sign.request'].sudo().browse(id)
|
||||
if not sign_request:
|
||||
return http.request.render('odex25_sign.deleted_sign_request')
|
||||
current_request_item = sign_request.request_item_ids.filtered(lambda r: r.access_token == token)
|
||||
current_request_item.access_via_link = True
|
||||
return werkzeug.redirect('/odex25_sign/document/%s/%s' % (id, token))
|
||||
|
||||
@http.route(["/odex25_sign/document/<int:id>/<token>"], type='http', auth='public')
|
||||
def sign_document_public(self, id, token, **post):
|
||||
document_context = self.get_document_qweb_context(id, token)
|
||||
document_context['portal'] = post.get('portal')
|
||||
if not isinstance(document_context, dict):
|
||||
return document_context
|
||||
|
||||
current_request_item = document_context.get('current_request_item')
|
||||
if current_request_item and current_request_item.partner_id.lang:
|
||||
http.request.env.context = dict(http.request.env.context, lang=current_request_item.partner_id.lang)
|
||||
return http.request.render('odex25_sign.doc_sign', document_context)
|
||||
|
||||
@http.route(['/odex25_sign/download/<int:id>/<token>/<download_type>'], type='http', auth='public')
|
||||
def download_document(self, id, token, download_type, **post):
|
||||
sign_request = http.request.env['sign.request'].sudo().browse(id).exists()
|
||||
if not sign_request or sign_request.access_token != token:
|
||||
return http.request.not_found()
|
||||
|
||||
document = None
|
||||
if download_type == "log":
|
||||
report_action = http.request.env.ref('odex25_sign.action_sign_request_print_logs').sudo()
|
||||
pdf_content, __ = report_action._render_qweb_pdf(sign_request.id)
|
||||
pdfhttpheaders = [
|
||||
('Content-Type', 'application/pdf'),
|
||||
('Content-Length', len(pdf_content)),
|
||||
('Content-Disposition', 'attachment; filename=' + "Certificate.pdf;")
|
||||
]
|
||||
return request.make_response(pdf_content, headers=pdfhttpheaders)
|
||||
elif download_type == "origin":
|
||||
document = sign_request.template_id.attachment_id.datas
|
||||
elif download_type == "completed":
|
||||
document = sign_request.completed_document
|
||||
if not document: # if the document is completed but the document is encrypted
|
||||
return http.redirect_with_hash('/odex25_sign/password/%(request_id)s/%(access_token)s' % {'request_id': id, 'access_token': token})
|
||||
|
||||
if not document:
|
||||
# Shouldn't it fall back on 'origin' download type?
|
||||
return http.redirect_with_hash("/odex25_sign/document/%(request_id)s/%(access_token)s" % {'request_id': id, 'access_token': token})
|
||||
|
||||
# Avoid to have file named "test file.pdf (V2)" impossible to open on Windows.
|
||||
# This line produce: test file (V2).pdf
|
||||
extension = '.' + sign_request.template_id.attachment_id.mimetype.replace('application/', '').replace(';base64', '')
|
||||
filename = sign_request.reference.replace(extension, '') + extension
|
||||
|
||||
return http.request.make_response(
|
||||
base64.b64decode(document),
|
||||
headers = [
|
||||
('Content-Type', mimetypes.guess_type(filename)[0] or 'application/octet-stream'),
|
||||
('Content-Disposition', content_disposition(filename))
|
||||
]
|
||||
)
|
||||
|
||||
@http.route(['/odex25_sign/<link>'], type='http', auth='public')
|
||||
def share_link(self, link, **post):
|
||||
template = http.request.env['sign.template'].sudo().search([('share_link', '=', link)], limit=1)
|
||||
if not template:
|
||||
return http.request.not_found()
|
||||
|
||||
sign_request = http.request.env['sign.request'].with_user(template.create_uid).create({
|
||||
'template_id': template.id,
|
||||
'reference': "%(template_name)s-public" % {'template_name': template.attachment_id.name},
|
||||
'favorited_ids': [(4, template.create_uid.id)],
|
||||
})
|
||||
|
||||
request_item = http.request.env['sign.request.item'].sudo().create({'sign_request_id': sign_request.id, 'role_id': template.sign_item_ids.mapped('responsible_id').id})
|
||||
sign_request.action_sent()
|
||||
|
||||
return http.redirect_with_hash('/odex25_sign/document/%(request_id)s/%(access_token)s' % {'request_id': sign_request.id, 'access_token': request_item.access_token})
|
||||
|
||||
@http.route(['/odex25_sign/password/<int:sign_request_id>/<token>'], type='http', auth='public')
|
||||
def check_password_page(self, sign_request_id, token, **post):
|
||||
values = http.request.params.copy()
|
||||
request_item = http.request.env['sign.request.item'].sudo().search([
|
||||
('sign_request_id', '=', sign_request_id),
|
||||
('state', '=', 'completed'),
|
||||
('sign_request_id.access_token', '=', token)], limit=1)
|
||||
if not request_item:
|
||||
return http.request.not_found()
|
||||
|
||||
if 'password' not in http.request.params:
|
||||
return http.request.render('odex25_sign.encrypted_ask_password')
|
||||
|
||||
password = http.request.params['password']
|
||||
template_id = request_item.sign_request_id.template_id
|
||||
|
||||
old_pdf = PdfFileReader(io.BytesIO(base64.b64decode(template_id.attachment_id.datas)), strict=False, overwriteWarnings=False)
|
||||
if old_pdf.isEncrypted and not old_pdf.decrypt(password):
|
||||
values['error'] = _("Wrong password")
|
||||
return http.request.render('odex25_sign.encrypted_ask_password', values)
|
||||
|
||||
request_item.sign_request_id.generate_completed_document(password)
|
||||
request_item.sign_request_id.send_completed_document()
|
||||
return http.redirect_with_hash('/odex25_sign/document/%(request_id)s/%(access_token)s' % {'request_id': sign_request_id, 'access_token': token})
|
||||
|
||||
# -------------
|
||||
# JSON Routes
|
||||
# -------------
|
||||
@http.route(["/odex25_sign/get_document/<int:id>/<token>"], type='json', auth='user')
|
||||
def get_document(self, id, token):
|
||||
return http.Response(template='odex25_sign._doc_sign', qcontext=self.get_document_qweb_context(id, token)).render()
|
||||
|
||||
@http.route(['/odex25_sign/new_partners'], type='json', auth='user')
|
||||
def new_partners(self, partners=[]):
|
||||
ResPartner = http.request.env['res.partner']
|
||||
pIDs = []
|
||||
for p in partners:
|
||||
existing = ResPartner.search([('email', '=', p[1])], limit=1)
|
||||
pIDs.append(existing.id if existing else ResPartner.create({'name': p[0], 'email': p[1]}).id)
|
||||
return pIDs
|
||||
|
||||
@http.route(['/odex25_sign/get_signature/<int:request_id>/<item_access_token>'], type='json', auth='public')
|
||||
def sign_get_user_signature(self, request_id, item_access_token, signature_type='signature'):
|
||||
sign_request_item = http.request.env['sign.request.item'].sudo().search([
|
||||
('sign_request_id', '=', request_id),
|
||||
('access_token', '=', item_access_token)
|
||||
])
|
||||
if not sign_request_item:
|
||||
return False
|
||||
|
||||
sign_request_user = http.request.env['res.users'].sudo().search([('partner_id', '=', sign_request_item.partner_id.id)], limit=1)
|
||||
if sign_request_user and signature_type == 'signature':
|
||||
return sign_request_user.sign_signature
|
||||
elif sign_request_user and signature_type == 'initial':
|
||||
return sign_request_user.sign_initials
|
||||
return False
|
||||
|
||||
@http.route(['/odex25_sign/send_public/<int:id>/<token>'], type='json', auth='public')
|
||||
def make_public_user(self, id, token, name=None, mail=None):
|
||||
sign_request = http.request.env['sign.request'].sudo().search([('id', '=', id), ('access_token', '=', token)])
|
||||
if not sign_request or len(sign_request.request_item_ids) != 1 or sign_request.request_item_ids.partner_id:
|
||||
return False
|
||||
|
||||
ResPartner = http.request.env['res.partner'].sudo()
|
||||
partner = ResPartner.search([('email', '=', mail)], limit=1)
|
||||
if not partner:
|
||||
partner = ResPartner.create({'name': name, 'email': mail})
|
||||
sign_request.request_item_ids[0].write({'partner_id': partner.id})
|
||||
|
||||
@http.route([
|
||||
'/odex25_sign/send-sms/<int:id>/<token>/<phone_number>',
|
||||
], type='json', auth='public')
|
||||
def send_sms(self, id, token, phone_number):
|
||||
request_item = http.request.env['sign.request.item'].sudo().search([('sign_request_id', '=', id), ('access_token', '=', token), ('state', '=', 'sent')], limit=1)
|
||||
if not request_item:
|
||||
return False
|
||||
if request_item.role_id and request_item.role_id.sms_authentification:
|
||||
request_item.sms_number = phone_number
|
||||
try:
|
||||
request_item._send_sms()
|
||||
except iap_tools.InsufficientCreditError:
|
||||
_logger.warning('Unable to send SMS: no more credits')
|
||||
request_item.sign_request_id.activity_schedule(
|
||||
'mail.mail_activity_data_todo',
|
||||
note=_("%s couldn't sign the document due to an insufficient credit error.", request_item.partner_id.display_name),
|
||||
user_id=request_item.sign_request_id.create_uid.id
|
||||
)
|
||||
return False
|
||||
return True
|
||||
|
||||
@http.route([
|
||||
'/odex25_sign/odex25_sign/<int:id>/<token>',
|
||||
'/odex25_sign/odex25_sign/<int:id>/<token>/<sms_token>'
|
||||
], type='json', auth='public')
|
||||
def sign(self, id, token, sms_token=False, signature=None):
|
||||
request_item = http.request.env['sign.request.item'].sudo().search([('sign_request_id', '=', id), ('access_token', '=', token), ('state', '=', 'sent')], limit=1)
|
||||
if not request_item:
|
||||
return False
|
||||
if request_item.role_id and request_item.role_id.sms_authentification:
|
||||
if not sms_token:
|
||||
return {
|
||||
'sms': True
|
||||
}
|
||||
if sms_token != request_item.sms_token:
|
||||
return False
|
||||
if sms_token == request_item.sms_token:
|
||||
request_item.sign_request_id._message_log(body=_('%s validated the signature by SMS with the phone number %s.') % (request_item.partner_id.display_name, request_item.sms_number))
|
||||
|
||||
if not request_item.sign(signature):
|
||||
return False
|
||||
|
||||
# mark signature as done in next activity
|
||||
user_ids = http.request.env['res.users'].search([('partner_id', '=', request_item.partner_id.id)])
|
||||
sign_users = user_ids.filtered(lambda u: u.has_group('odex25_sign.group_sign_employee'))
|
||||
for sign_user in sign_users:
|
||||
request_item.sign_request_id.activity_feedback(['mail.mail_activity_data_todo'], user_id=sign_user.id)
|
||||
|
||||
Log = request.env['sign.log'].sudo()
|
||||
vals = Log._prepare_vals_from_item(request_item)
|
||||
vals['action'] = 'sign'
|
||||
vals['token'] = token
|
||||
vals = Log._update_vals_with_http_request(vals)
|
||||
Log.create(vals)
|
||||
request_item.action_completed()
|
||||
return True
|
||||
|
||||
@http.route(['/odex25_sign/password/<int:sign_request_id>'], type='json', auth='public')
|
||||
def check_password(self, sign_request_id, password=None):
|
||||
request_item = http.request.env['sign.request.item'].sudo().search([
|
||||
('sign_request_id', '=', sign_request_id),
|
||||
('state', '=', 'completed')], limit=1)
|
||||
if not request_item:
|
||||
return False
|
||||
template_id = request_item.sign_request_id.template_id
|
||||
|
||||
old_pdf = PdfFileReader(io.BytesIO(base64.b64decode(template_id.attachment_id.datas)), strict=False, overwriteWarnings=False)
|
||||
if old_pdf.isEncrypted and not old_pdf.decrypt(password):
|
||||
return False
|
||||
|
||||
# if the password is correct, we generate document and send it
|
||||
request_item.sign_request_id.generate_completed_document(password)
|
||||
request_item.sign_request_id.send_completed_document()
|
||||
return True
|
||||
|
||||
@http.route(['/odex25_sign/encrypted/<int:sign_request_id>'], type='json', auth='public')
|
||||
def check_encrypted(self, sign_request_id):
|
||||
request_item = http.request.env['sign.request.item'].sudo().search([('sign_request_id', '=', sign_request_id)], limit=1)
|
||||
if not request_item:
|
||||
return False
|
||||
|
||||
# we verify that the document is completed by all signor
|
||||
if request_item.sign_request_id.nb_total != request_item.sign_request_id.nb_closed:
|
||||
return False
|
||||
template_id = request_item.sign_request_id.template_id
|
||||
|
||||
old_pdf = PdfFileReader(io.BytesIO(base64.b64decode(template_id.attachment_id.datas)), strict=False, overwriteWarnings=False)
|
||||
return True if old_pdf.isEncrypted else False
|
||||
|
||||
@http.route(['/odex25_sign/save_location/<int:id>/<token>'], type='json', auth='public')
|
||||
def save_location(self, id, token, latitude=0, longitude=0):
|
||||
sign_request_item = http.request.env['sign.request.item'].sudo().search([('sign_request_id', '=', id), ('access_token', '=', token)], limit=1)
|
||||
sign_request_item.write({'latitude': latitude, 'longitude': longitude})
|
||||
|
||||
@http.route("/odex25_sign/render_assets_pdf_iframe", type="json", auth="public")
|
||||
def render_assets_pdf_iframe(self, **kw):
|
||||
context = {'debug': kw.get('debug')} if 'debug' in kw else {}
|
||||
return request.env['ir.ui.view'].sudo()._render_template('odex25_sign.compiled_assets_pdf_iframe', context)
|
||||
|
|
@ -1,128 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from operator import itemgetter
|
||||
from collections import OrderedDict
|
||||
|
||||
from odoo import http, _
|
||||
from odoo.exceptions import MissingError
|
||||
from odoo.http import request
|
||||
from odoo.addons.portal.controllers.portal import CustomerPortal, pager as portal_pager, get_records_pager
|
||||
|
||||
from odoo.tools import groupby as groupbyelem
|
||||
from odoo.osv.expression import AND
|
||||
|
||||
|
||||
class CustomerPortal(CustomerPortal):
|
||||
|
||||
def _prepare_home_portal_values(self, counters):
|
||||
values = super(CustomerPortal, self)._prepare_home_portal_values(counters)
|
||||
if 'sign_count' in counters:
|
||||
partner_id = request.env.user.partner_id
|
||||
values['sign_count'] = request.env['sign.request.item'].sudo().search_count([
|
||||
('partner_id', '=', partner_id.id), ('state', '!=', 'draft')
|
||||
])
|
||||
return values
|
||||
|
||||
@http.route(['/my/signatures', '/my/signatures/page/<int:page>'], type='http', auth='user', website=True)
|
||||
def portal_my_signatures(self, page=1, date_begin=None, date_end=None, sortby=None, search=None, search_in='all',
|
||||
groupby='none', filterby=None, **kw):
|
||||
|
||||
values = self._prepare_portal_layout_values()
|
||||
partner_id = request.env.user.partner_id
|
||||
SignRequestItem = request.env['sign.request.item'].sudo()
|
||||
default_domain = [('partner_id', '=', partner_id.id), ('state', '!=', 'draft')]
|
||||
|
||||
searchbar_sortings = {
|
||||
'new': {'label': _('Newest'), 'order': 'sign_request_id desc'},
|
||||
'date': {'label': _('Signing Date'), 'order': 'signing_date desc'},
|
||||
}
|
||||
|
||||
searchbar_filters = {
|
||||
'all': {'label': _('All'), 'domain': default_domain},
|
||||
'tosign': {'label': _('To sign'), 'domain': AND([default_domain, [('state', '=', 'sent'),
|
||||
('sign_request_id.state', '=', 'sent')]])},
|
||||
'completed': {'label': _('Completed'), 'domain': AND([default_domain, [('state', '=', 'completed')]])},
|
||||
'signed': {'label': _('Fully Signed'),
|
||||
'domain': AND([default_domain, [('sign_request_id.state', '=', 'signed')]])},
|
||||
}
|
||||
|
||||
searchbar_inputs = {
|
||||
'all': {'input': 'all', 'label': _('Search <span class="nolabel"> (in Document)</span>')},
|
||||
}
|
||||
|
||||
searchbar_groupby = {
|
||||
'none': {'input': 'none', 'label': _('None')},
|
||||
'state': {'input': 'state', 'label': _('Status')},
|
||||
}
|
||||
|
||||
# default sortby order
|
||||
if not sortby:
|
||||
sortby = 'new'
|
||||
sort_order = searchbar_sortings[sortby]['order']
|
||||
# default filter by value
|
||||
if not filterby:
|
||||
filterby = 'all'
|
||||
domain = searchbar_filters[filterby]['domain']
|
||||
if date_begin and date_end:
|
||||
domain = AND([domain, [('signing_date', '>', date_begin), ('signing_date', '<=', date_end)]])
|
||||
# search only the document name
|
||||
if search and search_in:
|
||||
domain = AND([domain, [('reference', 'ilike', search)]])
|
||||
pager = portal_pager(
|
||||
url='/my/signatures',
|
||||
url_args={'date_begin': date_begin, 'date_end': date_end, 'sortby': sortby, 'filterby': filterby,
|
||||
'search_in': search_in, 'search': search},
|
||||
total=SignRequestItem.search_count(domain),
|
||||
page=page,
|
||||
step=self._items_per_page
|
||||
)
|
||||
|
||||
# content according to pager and archive selected
|
||||
if groupby == 'state':
|
||||
sort_order = 'state, %s' % sort_order
|
||||
|
||||
# search the count to display, according to the pager data
|
||||
sign_requests_items = SignRequestItem.search(domain, order=sort_order, limit=self._items_per_page,
|
||||
offset=pager['offset'])
|
||||
request.session['my_signatures_history'] = sign_requests_items.ids[:100]
|
||||
if groupby == 'state':
|
||||
grouped_signatures = [SignRequestItem.concat(*g)
|
||||
for k, g in groupbyelem(sign_requests_items, itemgetter('state'))]
|
||||
else:
|
||||
grouped_signatures = [sign_requests_items]
|
||||
|
||||
values.update({
|
||||
'date': date_begin,
|
||||
'grouped_signatures': grouped_signatures,
|
||||
'page_name': 'signatures',
|
||||
'pager': pager,
|
||||
'default_url': '/my/signatures',
|
||||
'searchbar_sortings': searchbar_sortings,
|
||||
'searchbar_filters': OrderedDict(sorted(searchbar_filters.items())),
|
||||
'searchbar_groupby': searchbar_groupby,
|
||||
'searchbar_inputs': searchbar_inputs,
|
||||
'search_in': search_in,
|
||||
'groupby': groupby,
|
||||
'sortby': sortby,
|
||||
'filterby': filterby,
|
||||
})
|
||||
return request.render('odex25_sign.sign_portal_my_requests', values)
|
||||
|
||||
@http.route(['/my/signature/<int:item_id>'], type='http', auth='public', website=True)
|
||||
def portal_my_signature(self, item_id, access_token=None, **kwargs):
|
||||
partner_id = request.env.user.partner_id
|
||||
sign_item_sudo = request.env['sign.request.item'].sudo().browse(item_id)
|
||||
try:
|
||||
if sign_item_sudo.state != 'draft' and not sign_item_sudo.partner_id == partner_id:
|
||||
return request.redirect('/my/')
|
||||
url = f'/odex25_sign/document/{sign_item_sudo.sign_request_id.id}/{sign_item_sudo.access_token}?portal=1'
|
||||
values = {
|
||||
'page_name': 'signature',
|
||||
'my_sign_item': sign_item_sudo,
|
||||
'url': url
|
||||
}
|
||||
values = self._get_page_view_values(sign_item_sudo, sign_item_sudo.access_token, values,
|
||||
'my_signatures_history', False, **kwargs)
|
||||
return request.render('odex25_sign.sign_portal_my_request', values)
|
||||
except MissingError:
|
||||
return request.redirect('/my')
|
||||
|
|
@ -1,216 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<template id="sign_template_mail_request">
|
||||
<table border="0" cellpadding="0" style="background-color: white; padding: 0px; border-collapse:separate;">
|
||||
<tr><td valign="top">
|
||||
<t t-esc="record.create_uid.name"/>
|
||||
(<a t-att-href="'mailto:%s?subject=%s' % (record.create_uid.email, 'Re: %s' % subject)"
|
||||
style="color:#428BCA; text-decoration:none;"
|
||||
target="_blank"><t t-esc="record.create_uid.email"/></a>)
|
||||
has requested your signature on the document <t t-esc="record.sign_request_id.reference"/>.
|
||||
</td></tr>
|
||||
<tr t-if="body"><td valign="top">
|
||||
<div style="margin: 16px 0px 16px 0px; padding: 8px 16px 8px 16px; background-color: #F1F1F1;">
|
||||
<span>
|
||||
<t t-raw="body"/>
|
||||
</span>
|
||||
</div>
|
||||
</td></tr>
|
||||
<tr><td valign="top">
|
||||
<div style="margin:16px auto; text-align:center;">
|
||||
<a t-att-href="link"
|
||||
style="padding: 8px 16px 8px 16px; border-radius: 3px; background-color:#875A7B; text-align:center; text-decoration:none; color: #FFFFFF;">
|
||||
Sign document
|
||||
</a>
|
||||
</div>
|
||||
</td></tr>
|
||||
<tr><td valign="top">
|
||||
<div style="opacity: 0.7;">
|
||||
<strong>Warning</strong> do not forward this email to other people!<br/>
|
||||
They will be able to access this document and sign it as yourself.<br/>
|
||||
<span>Your IP address and localization are associated to your signature to ensure traceability.</span>
|
||||
</div>
|
||||
</td></tr>
|
||||
</table>
|
||||
</template>
|
||||
|
||||
<template id="sign_template_mail_follower">
|
||||
<table border="0" cellpadding="0" style="background-color: white; padding: 0px; border-collapse:separate;">
|
||||
<tr><td valign="top">
|
||||
<t t-esc="record.create_uid.name"/>
|
||||
(<a t-att-href="'mailto:%s?subject=%s' % (record.create_uid.email, 'Re: %s' % subject)"
|
||||
style="color:#428BCA; text-decoration:none;"
|
||||
target="_blank"><t t-esc="record.create_uid.email"/>
|
||||
</a>) has added you as a follower of the document <t t-esc="record.reference"/>.
|
||||
</td></tr>
|
||||
<tr t-if="body"><td valign="top">
|
||||
<div style="margin: 16px 0px 16px 0px;">
|
||||
<span style="padding: 16px 8px 16px 8px; border-radius: 3px;">
|
||||
<t t-raw="body"/>
|
||||
</span>
|
||||
</div>
|
||||
</td></tr>
|
||||
<tr><td valign="top">
|
||||
<div style="margin:16px 0px 16px 0px;">
|
||||
<a t-att-href="link"
|
||||
style="padding: 8px 16px 8px 16px; border-radius: 3px; background-color:#875A7B; text-align:center; text-decoration:none; color: #FFFFFF;">
|
||||
View signature request
|
||||
</a>
|
||||
</div>
|
||||
</td></tr>
|
||||
<tr><td valign="top">
|
||||
<div style="opacity: 0.7;">
|
||||
You will be notified once the document has been signed by everyone involved.
|
||||
</div>
|
||||
</td></tr>
|
||||
</table>
|
||||
</template>
|
||||
|
||||
<template id="sign_template_mail_completed">
|
||||
<table border="0" cellpadding="0" style="background-color: white; padding: 0px; border-collapse:separate;">
|
||||
<tr><td valign="top">
|
||||
The document <t t-esc="record.reference"/> has been completed and signed by everyone.
|
||||
</td></tr>
|
||||
<tr t-if="body"><td valign="top">
|
||||
<div style="margin: 16px 0px 16px 0px;">
|
||||
<span style="padding: 16px 8px 16px 8px; border-radius: 3px;">
|
||||
<t t-raw="body"/>
|
||||
</span>
|
||||
</div>
|
||||
</td></tr>
|
||||
<tr><td valign="top">
|
||||
<div style="margin:16px auto; text-align:center">
|
||||
<a t-att-href="link"
|
||||
style="padding: 8px 16px 8px 16px; border-radius: 3px; background-color:#875A7B; text-align:center; text-decoration:none; color: #FFFFFF;">
|
||||
View document
|
||||
</a>
|
||||
</div>
|
||||
</td></tr>
|
||||
<tr><td valign="top">
|
||||
<div style="opacity: 0.7;">
|
||||
<strong>Warning</strong> do not forward this email to other people!<br/>
|
||||
They will be able to access this document and sign it as yourself.<br/>
|
||||
<span>Your IP address and localization are associated to your signature to ensure traceability.</span>
|
||||
</div>
|
||||
</td></tr>
|
||||
</table>
|
||||
</template>
|
||||
|
||||
<!-- Tour template -->
|
||||
<record id="attachment_sign_tour" model="ir.attachment">
|
||||
<field name="name">Sample Contract.pdf</field>
|
||||
<field name="datas" type="base64" file="odex25_sign/static/demo/sample_contract.pdf"/>
|
||||
<field name="mimetype">application/pdf</field>
|
||||
</record>
|
||||
<record id="template_sign_tour" model="sign.template">
|
||||
<field name="attachment_id" ref="attachment_sign_tour"/>
|
||||
<field name="share_link">tour-template</field>
|
||||
<field name="active" eval="False"/>
|
||||
</record>
|
||||
|
||||
<!-- Item types -->
|
||||
<record model="sign.item.type" id="sign_item_type_signature">
|
||||
<field name="name">Signature</field>
|
||||
<field name="item_type">signature</field>
|
||||
<field name="tip">sign it</field>
|
||||
<field name="placeholder">Signature</field>
|
||||
<field name="default_width" type="float">0.200</field>
|
||||
<field name="default_height" type="float">0.050</field>
|
||||
</record>
|
||||
|
||||
<record model="sign.item.type" id="sign_item_type_initial">
|
||||
<field name="name">Initials</field>
|
||||
<field name="item_type">initial</field>
|
||||
<field name="tip">mark it</field>
|
||||
<field name="placeholder">Initials</field>
|
||||
<field name="default_width" type="float">0.085</field>
|
||||
<field name="default_height" type="float">0.030</field>
|
||||
</record>
|
||||
|
||||
<record model="sign.item.type" id="sign_item_type_name">
|
||||
<field name="name">Name</field>
|
||||
<field name="placeholder">Name</field>
|
||||
<field name="auto_field">name</field>
|
||||
</record>
|
||||
|
||||
<record model="sign.item.type" id="sign_item_type_email">
|
||||
<field name="name">Email</field>
|
||||
<field name="placeholder">Email</field>
|
||||
<field name="auto_field">email</field>
|
||||
</record>
|
||||
|
||||
<record model="sign.item.type" id="sign_item_type_phone">
|
||||
<field name="name">Phone</field>
|
||||
<field name="placeholder">Phone</field>
|
||||
<field name="auto_field">phone</field>
|
||||
</record>
|
||||
|
||||
<record model="sign.item.type" id="sign_item_type_company">
|
||||
<field name="name">Company</field>
|
||||
<field name="placeholder">Company</field>
|
||||
<field name="auto_field">company_id.name</field>
|
||||
</record>
|
||||
|
||||
<record model="sign.item.type" id="sign_item_type_text">
|
||||
<field name="name">Text</field> <!-- default: type 'text', tip 'fill in', empty placeholder, default_width and default_height, no auto_field -->
|
||||
<field name="placeholder">Text</field>
|
||||
</record>
|
||||
|
||||
<record model="sign.item.type" id="sign_item_type_multiline_text">
|
||||
<field name="name">Multiline Text</field>
|
||||
<field name="placeholder">Multiline Text</field>
|
||||
<field name="item_type">textarea</field>
|
||||
<field name="default_width" type="float">0.300</field>
|
||||
<field name="default_height" type="float">0.0500</field>
|
||||
</record>
|
||||
|
||||
<record model="sign.item.type" id="sign_item_type_checkbox">
|
||||
<field name="name">Checkbox</field>
|
||||
<field name="placeholder">☑</field>
|
||||
<field name="item_type">checkbox</field>
|
||||
<field name="default_width" type="float">0.028</field>
|
||||
<field name="default_height" type="float">0.025</field>
|
||||
</record>
|
||||
|
||||
<record model="sign.item.type" id="sign_item_type_selection">
|
||||
<field name="name">Selection</field>
|
||||
<field name="item_type">selection</field>
|
||||
<field name="placeholder">Selection</field>
|
||||
<field name="tip">Select an option</field>
|
||||
<field name="default_width" type="float">0.300</field>
|
||||
<field name="default_height" type="float">0.030</field>
|
||||
</record>
|
||||
|
||||
<record model="sign.item.type" id="sign_item_type_date">
|
||||
<field name="name">Date</field>
|
||||
<field name="placeholder">Date</field>
|
||||
</record>
|
||||
|
||||
<!-- Item responsibles -->
|
||||
<record model="sign.item.role" id="sign_item_role_customer">
|
||||
<field name="name">Customer</field>
|
||||
</record>
|
||||
|
||||
<record model="sign.item.role" id="sign_item_role_company">
|
||||
<field name="name">Company</field>
|
||||
</record>
|
||||
|
||||
<record model="sign.item.role" id="sign_item_role_employee">
|
||||
<field name="name">Employee</field>
|
||||
</record>
|
||||
|
||||
<!-- Template tags -->
|
||||
<record id="sign_template_tag_1" model="sign.template.tag">
|
||||
<field name="name">HR</field>
|
||||
<field name="color">2</field>
|
||||
</record>
|
||||
<record id="sign_template_tag_2" model="sign.template.tag">
|
||||
<field name="name">NDA</field>
|
||||
<field name="color">3</field>
|
||||
</record>
|
||||
<record id="sign_template_tag_3" model="sign.template.tag">
|
||||
<field name="name">Sales</field>
|
||||
<field name="color">4</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
|
|
@ -1,440 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo noupdate="1">
|
||||
|
||||
<record id="attachment_sign_1" model="ir.attachment">
|
||||
<field name="name">Employment Contract.pdf</field>
|
||||
<field name="datas" type="base64" file="odex25_sign/static/demo/employment.pdf"/>
|
||||
<field name="mimetype">application/pdf;base64</field>
|
||||
</record>
|
||||
<record id="template_sign_1" model="sign.template">
|
||||
<field name="attachment_id" ref="attachment_sign_1"/>
|
||||
<field name="share_link">demo-template-1</field>
|
||||
<field name="create_uid" ref="base.user_admin"/>
|
||||
<field name="tag_ids" eval="[(4, ref('odex25_sign.sign_template_tag_1'))]"/>
|
||||
</record>
|
||||
<record model="sign.item" id="sign_item_1">
|
||||
<field name="template_id" ref="template_sign_1"/>
|
||||
<field name="type_id" ref="sign_item_type_name"/>
|
||||
<field name="responsible_id" ref="sign_item_role_employee"/>
|
||||
<field name="page" type="int">1</field>
|
||||
<field name="posX" type="float">0.265</field>
|
||||
<field name="posY" type="float">0.160</field>
|
||||
<field name="width" type="float">0.375</field>
|
||||
<field name="height" type="float">0.015</field>
|
||||
</record>
|
||||
<record model="sign.item" id="sign_item_2">
|
||||
<field name="template_id" ref="template_sign_1"/>
|
||||
<field name="type_id" ref="sign_item_type_date"/>
|
||||
<field name="responsible_id" ref="sign_item_role_employee"/>
|
||||
<field name="page" type="int">1</field>
|
||||
<field name="posX" type="float">0.700</field>
|
||||
<field name="posY" type="float">0.160</field>
|
||||
<field name="width" type="float">0.170</field>
|
||||
<field name="height" type="float">0.015</field>
|
||||
</record>
|
||||
<record model="sign.item" id="sign_item_3">
|
||||
<field name="template_id" ref="template_sign_1"/>
|
||||
<field name="type_id" ref="sign_item_type_text"/>
|
||||
<field name="responsible_id" ref="sign_item_role_employee"/>
|
||||
<field name="page" type="int">1</field>
|
||||
<field name="posX" type="float">0.190</field>
|
||||
<field name="posY" type="float">0.185</field>
|
||||
<field name="width" type="float">0.680</field>
|
||||
<field name="height" type="float">0.015</field>
|
||||
</record>
|
||||
<record model="sign.item" id="sign_item_4">
|
||||
<field name="template_id" ref="template_sign_1"/>
|
||||
<field name="type_id" ref="sign_item_type_initial"/>
|
||||
<field name="responsible_id" ref="sign_item_role_employee"/>
|
||||
<field name="page" type="int">1</field>
|
||||
<field name="posX" type="float">0.915</field>
|
||||
<field name="posY" type="float">0.970</field>
|
||||
<field name="width" type="float">0.085</field>
|
||||
<field name="height" type="float">0.030</field>
|
||||
</record>
|
||||
<record model="sign.item" id="sign_item_5">
|
||||
<field name="template_id" ref="template_sign_1"/>
|
||||
<field name="type_id" ref="sign_item_type_initial"/>
|
||||
<field name="responsible_id" ref="sign_item_role_employee"/>
|
||||
<field name="page" type="int">2</field>
|
||||
<field name="posX" type="float">0.915</field>
|
||||
<field name="posY" type="float">0.970</field>
|
||||
<field name="width" type="float">0.085</field>
|
||||
<field name="height" type="float">0.030</field>
|
||||
</record>
|
||||
<record model="sign.item" id="sign_item_8">
|
||||
<field name="template_id" ref="template_sign_1"/>
|
||||
<field name="type_id" ref="sign_item_type_signature"/>
|
||||
<field name="responsible_id" ref="sign_item_role_employee"/>
|
||||
<field name="page" type="int">3</field>
|
||||
<field name="posX" type="float">0.325</field>
|
||||
<field name="posY" type="float">0.670</field>
|
||||
<field name="width" type="float">0.200</field>
|
||||
<field name="height" type="float">0.050</field>
|
||||
</record>
|
||||
<record model="sign.item" id="sign_item_9">
|
||||
<field name="template_id" ref="template_sign_1"/>
|
||||
<field name="type_id" ref="sign_item_type_date"/>
|
||||
<field name="responsible_id" ref="sign_item_role_employee"/>
|
||||
<field name="page" type="int">3</field>
|
||||
<field name="posX" type="float">0.655</field>
|
||||
<field name="posY" type="float">0.695</field>
|
||||
<field name="width" type="float">0.205</field>
|
||||
<field name="height" type="float">0.015</field>
|
||||
</record>
|
||||
|
||||
<record id="attachment_sign_2" model="ir.attachment">
|
||||
<field name="name">Non-Disclosure Agreement.pdf</field>
|
||||
<field name="datas" type="base64" file="odex25_sign/static/demo/nda.pdf"/>
|
||||
<field name="mimetype">application/pdf</field>
|
||||
</record>
|
||||
<record id="template_sign_2" model="sign.template">
|
||||
<field name="attachment_id" ref="attachment_sign_2"/>
|
||||
<field name="share_link">demo-template-2</field>
|
||||
<field name="create_uid" ref="base.user_admin"/>
|
||||
<field name="favorited_ids" eval="[(4, ref('base.user_admin'))]"/>
|
||||
<field name="tag_ids" eval="[(4, ref('odex25_sign.sign_template_tag_2')), (4, ref('odex25_sign.sign_template_tag_1'))]"/>
|
||||
</record>
|
||||
<record model="sign.item" id="sign_item_10">
|
||||
<field name="template_id" ref="template_sign_2"/>
|
||||
<field name="type_id" ref="sign_item_type_name"/>
|
||||
<field name="responsible_id" ref="sign_item_role_customer"/>
|
||||
<field name="page" type="int">1</field>
|
||||
<field name="posX" type="float">0.125</field>
|
||||
<field name="posY" type="float">0.175</field>
|
||||
<field name="width" type="float">0.140</field>
|
||||
<field name="height" type="float">0.015</field>
|
||||
</record>
|
||||
<record model="sign.item" id="sign_item_11">
|
||||
<field name="template_id" ref="template_sign_2"/>
|
||||
<field name="type_id" ref="sign_item_type_text"/>
|
||||
<field name="responsible_id" ref="sign_item_role_customer"/>
|
||||
<field name="page" type="int">1</field>
|
||||
<field name="posX" type="float">0.355</field>
|
||||
<field name="posY" type="float">0.175</field>
|
||||
<field name="width" type="float">0.230</field>
|
||||
<field name="height" type="float">0.015</field>
|
||||
</record>
|
||||
<record model="sign.item" id="sign_item_12">
|
||||
<field name="template_id" ref="template_sign_2"/>
|
||||
<field name="type_id" ref="sign_item_type_initial"/>
|
||||
<field name="responsible_id" ref="sign_item_role_customer"/>
|
||||
<field name="page" type="int">1</field>
|
||||
<field name="posX" type="float">0.915</field>
|
||||
<field name="posY" type="float">0.970</field>
|
||||
<field name="width" type="float">0.085</field>
|
||||
<field name="height" type="float">0.030</field>
|
||||
</record>
|
||||
<record model="sign.item" id="sign_item_13">
|
||||
<field name="template_id" ref="template_sign_2"/>
|
||||
<field name="type_id" ref="sign_item_type_initial"/>
|
||||
<field name="responsible_id" ref="sign_item_role_customer"/>
|
||||
<field name="page" type="int">2</field>
|
||||
<field name="posX" type="float">0.915</field>
|
||||
<field name="posY" type="float">0.970</field>
|
||||
<field name="width" type="float">0.085</field>
|
||||
<field name="height" type="float">0.030</field>
|
||||
</record>
|
||||
<record model="sign.item" id="sign_item_14">
|
||||
<field name="template_id" ref="template_sign_2"/>
|
||||
<field name="type_id" ref="sign_item_type_initial"/>
|
||||
<field name="responsible_id" ref="sign_item_role_customer"/>
|
||||
<field name="page" type="int">3</field>
|
||||
<field name="posX" type="float">0.915</field>
|
||||
<field name="posY" type="float">0.970</field>
|
||||
<field name="width" type="float">0.085</field>
|
||||
<field name="height" type="float">0.030</field>
|
||||
</record>
|
||||
<record model="sign.item" id="sign_item_15">
|
||||
<field name="template_id" ref="template_sign_2"/>
|
||||
<field name="type_id" ref="sign_item_type_initial"/>
|
||||
<field name="responsible_id" ref="sign_item_role_customer"/>
|
||||
<field name="page" type="int">4</field>
|
||||
<field name="posX" type="float">0.915</field>
|
||||
<field name="posY" type="float">0.970</field>
|
||||
<field name="width" type="float">0.085</field>
|
||||
<field name="height" type="float">0.030</field>
|
||||
</record>
|
||||
<record model="sign.item" id="sign_item_16">
|
||||
<field name="template_id" ref="template_sign_2"/>
|
||||
<field name="type_id" ref="sign_item_type_signature"/>
|
||||
<field name="responsible_id" ref="sign_item_role_customer"/>
|
||||
<field name="page" type="int">5</field>
|
||||
<field name="posX" type="float">0.215</field>
|
||||
<field name="posY" type="float">0.365</field>
|
||||
<field name="width" type="float">0.200</field>
|
||||
<field name="height" type="float">0.050</field>
|
||||
</record>
|
||||
<record model="sign.item" id="sign_item_18">
|
||||
<field name="template_id" ref="template_sign_2"/>
|
||||
<field name="type_id" ref="sign_item_type_name"/>
|
||||
<field name="responsible_id" ref="sign_item_role_customer"/>
|
||||
<field name="page" type="int">5</field>
|
||||
<field name="posX" type="float">0.215</field>
|
||||
<field name="posY" type="float">0.445</field>
|
||||
<field name="width" type="float">0.200</field>
|
||||
<field name="height" type="float">0.015</field>
|
||||
</record>
|
||||
|
||||
<record id="attachment_sign_3" model="ir.attachment">
|
||||
<field name="name">Odoo_Individual_Contributor_License_Agreement.pdf</field>
|
||||
<field name="datas" type="base64" file="odex25_sign/static/demo/cla.pdf"/>
|
||||
<field name="mimetype">application/pdf;base64</field>
|
||||
</record>
|
||||
<record id="template_sign_3" model="sign.template">
|
||||
<field name="attachment_id" ref="attachment_sign_3"/>
|
||||
<field name="share_link">demo-template-3</field>
|
||||
<field name="create_uid" ref="base.user_admin"/>
|
||||
</record>
|
||||
<record model="sign.item" id="sign_item_20">
|
||||
<field name="template_id" ref="template_sign_3"/>
|
||||
<field name="type_id" ref="sign_item_type_initial"/>
|
||||
<field name="responsible_id" ref="sign_item_role_customer"/>
|
||||
<field name="page" type="int">1</field>
|
||||
<field name="posX" type="float">0.915</field>
|
||||
<field name="posY" type="float">0.970</field>
|
||||
<field name="width" type="float">0.085</field>
|
||||
<field name="height" type="float">0.030</field>
|
||||
</record>
|
||||
<record model="sign.item" id="sign_item_21">
|
||||
<field name="template_id" ref="template_sign_3"/>
|
||||
<field name="type_id" ref="sign_item_type_name"/>
|
||||
<field name="responsible_id" ref="sign_item_role_customer"/>
|
||||
<field name="page" type="int">2</field>
|
||||
<field name="posX" type="float">0.290</field>
|
||||
<field name="posY" type="float">0.600</field>
|
||||
<field name="width" type="float">0.300</field>
|
||||
<field name="height" type="float">0.015</field>
|
||||
</record>
|
||||
<record model="sign.item" id="sign_item_22">
|
||||
<field name="template_id" ref="template_sign_3"/>
|
||||
<field name="type_id" ref="sign_item_type_email"/>
|
||||
<field name="responsible_id" ref="sign_item_role_customer"/>
|
||||
<field name="page" type="int">2</field>
|
||||
<field name="posX" type="float">0.290</field>
|
||||
<field name="posY" type="float">0.631</field>
|
||||
<field name="width" type="float">0.300</field>
|
||||
<field name="height" type="float">0.015</field>
|
||||
</record>
|
||||
<record model="sign.item" id="sign_item_23">
|
||||
<field name="template_id" ref="template_sign_3"/>
|
||||
<field name="type_id" ref="sign_item_type_text"/>
|
||||
<field name="responsible_id" ref="sign_item_role_customer"/>
|
||||
<field name="page" type="int">2</field>
|
||||
<field name="posX" type="float">0.290</field>
|
||||
<field name="posY" type="float">0.662</field>
|
||||
<field name="width" type="float">0.300</field>
|
||||
<field name="height" type="float">0.015</field>
|
||||
</record>
|
||||
<record model="sign.item" id="sign_item_24">
|
||||
<field name="template_id" ref="template_sign_3"/>
|
||||
<field name="type_id" ref="sign_item_type_date"/>
|
||||
<field name="responsible_id" ref="sign_item_role_customer"/>
|
||||
<field name="page" type="int">2</field>
|
||||
<field name="posX" type="float">0.290</field>
|
||||
<field name="posY" type="float">0.693</field>
|
||||
<field name="width" type="float">0.300</field>
|
||||
<field name="height" type="float">0.015</field>
|
||||
</record>
|
||||
<record model="sign.item" id="sign_item_25">
|
||||
<field name="template_id" ref="template_sign_3"/>
|
||||
<field name="type_id" ref="sign_item_type_signature"/>
|
||||
<field name="responsible_id" ref="sign_item_role_customer"/>
|
||||
<field name="page" type="int">2</field>
|
||||
<field name="posX" type="float">0.290</field>
|
||||
<field name="posY" type="float">0.725</field>
|
||||
<field name="width" type="float">0.200</field>
|
||||
<field name="height" type="float">0.050</field>
|
||||
</record>
|
||||
|
||||
<record id="attachment_sign_4" model="ir.attachment">
|
||||
<field name="name">Real_Estate_Listing_Agreement.pdf</field>
|
||||
<field name="datas" type="base64" file="odex25_sign/static/demo/rela.pdf"/>
|
||||
<field name="mimetype">application/pdf</field>
|
||||
</record>
|
||||
<record id="template_sign_4" model="sign.template">
|
||||
<field name="attachment_id" ref="attachment_sign_4"/>
|
||||
<field name="share_link">demo-template-4</field>
|
||||
<field name="create_uid" ref="base.user_admin"/>
|
||||
<field name="favorited_ids" eval="[(4, ref('base.user_admin'))]"/>
|
||||
<field name="tag_ids" eval="[(4, ref('odex25_sign.sign_template_tag_3'))]"/>
|
||||
</record>
|
||||
<record model="sign.item" id="sign_item_27">
|
||||
<field name="template_id" ref="template_sign_4"/>
|
||||
<field name="type_id" ref="sign_item_type_name"/>
|
||||
<field name="responsible_id" ref="sign_item_role_customer"/>
|
||||
<field name="page" type="int">1</field>
|
||||
<field name="posX" type="float">0.155</field>
|
||||
<field name="posY" type="float">0.206</field>
|
||||
<field name="width" type="float">0.200</field>
|
||||
<field name="height" type="float">0.012</field>
|
||||
</record>
|
||||
<record model="sign.item" id="sign_item_29">
|
||||
<field name="template_id" ref="template_sign_4"/>
|
||||
<field name="type_id" ref="sign_item_type_initial"/>
|
||||
<field name="responsible_id" ref="sign_item_role_customer"/>
|
||||
<field name="page" type="int">1</field>
|
||||
<field name="posX" type="float">0.915</field>
|
||||
<field name="posY" type="float">0.970</field>
|
||||
<field name="width" type="float">0.085</field>
|
||||
<field name="height" type="float">0.030</field>
|
||||
</record>
|
||||
<record model="sign.item" id="sign_item_31">
|
||||
<field name="template_id" ref="template_sign_4"/>
|
||||
<field name="type_id" ref="sign_item_type_name"/>
|
||||
<field name="responsible_id" ref="sign_item_role_customer"/>
|
||||
<field name="page" type="int">2</field>
|
||||
<field name="posX" type="float">0.535</field>
|
||||
<field name="posY" type="float">0.801</field>
|
||||
<field name="width" type="float">0.180</field>
|
||||
<field name="height" type="float">0.015</field>
|
||||
</record>
|
||||
<record model="sign.item" id="sign_item_33">
|
||||
<field name="template_id" ref="template_sign_4"/>
|
||||
<field name="type_id" ref="sign_item_type_signature"/>
|
||||
<field name="responsible_id" ref="sign_item_role_customer"/>
|
||||
<field name="page" type="int">2</field>
|
||||
<field name="posX" type="float">0.565</field>
|
||||
<field name="posY" type="float">0.820</field>
|
||||
<field name="width" type="float">0.200</field>
|
||||
<field name="height" type="float">0.050</field>
|
||||
</record>
|
||||
|
||||
<record id="attachment_sign_5" model="ir.attachment">
|
||||
<field name="name">YC_Form_SaaS_Agreement.pdf</field>
|
||||
<field name="datas" type="base64" file="odex25_sign/static/demo/saas.pdf"/>
|
||||
<field name="mimetype">application/pdf</field>
|
||||
</record>
|
||||
<record id="template_sign_5" model="sign.template">
|
||||
<field name="attachment_id" ref="attachment_sign_5"/>
|
||||
<field name="share_link">demo-template-5</field>
|
||||
<field name="create_uid" ref="base.user_demo"/>
|
||||
<field name="favorited_ids" eval="[(4, ref('base.user_demo'))]"/>
|
||||
<field name="tag_ids" eval="[(4, ref('odex25_sign.sign_template_tag_3'))]"/>
|
||||
</record>
|
||||
<record model="sign.item" id="sign_item_34">
|
||||
<field name="template_id" ref="template_sign_5"/>
|
||||
<field name="type_id" ref="sign_item_type_company"/>
|
||||
<field name="responsible_id" ref="sign_item_role_customer"/>
|
||||
<field name="page" type="int">1</field>
|
||||
<field name="posX" type="float">0.210</field>
|
||||
<field name="posY" type="float">0.125</field>
|
||||
<field name="width" type="float">0.315</field>
|
||||
<field name="height" type="float">0.015</field>
|
||||
</record>
|
||||
<record model="sign.item" id="sign_item_35">
|
||||
<field name="template_id" ref="template_sign_5"/>
|
||||
<field name="type_id" ref="sign_item_type_name"/>
|
||||
<field name="responsible_id" ref="sign_item_role_customer"/>
|
||||
<field name="page" type="int">1</field>
|
||||
<field name="posX" type="float">0.610</field>
|
||||
<field name="posY" type="float">0.125</field>
|
||||
<field name="width" type="float">0.315</field>
|
||||
<field name="height" type="float">0.015</field>
|
||||
</record>
|
||||
<record model="sign.item" id="sign_item_36">
|
||||
<field name="template_id" ref="template_sign_5"/>
|
||||
<field name="type_id" ref="sign_item_type_email"/>
|
||||
<field name="responsible_id" ref="sign_item_role_customer"/>
|
||||
<field name="page" type="int">1</field>
|
||||
<field name="posX" type="float">0.180</field>
|
||||
<field name="posY" type="float">0.150</field>
|
||||
<field name="width" type="float">0.345</field>
|
||||
<field name="height" type="float">0.015</field>
|
||||
</record>
|
||||
<record model="sign.item" id="sign_item_37">
|
||||
<field name="template_id" ref="template_sign_5"/>
|
||||
<field name="type_id" ref="sign_item_type_phone"/>
|
||||
<field name="responsible_id" ref="sign_item_role_customer"/>
|
||||
<field name="page" type="int">1</field>
|
||||
<field name="posX" type="float">0.600</field>
|
||||
<field name="posY" type="float">0.150</field>
|
||||
<field name="width" type="float">0.325</field>
|
||||
<field name="height" type="float">0.015</field>
|
||||
</record>
|
||||
<record model="sign.item" id="sign_item_43">
|
||||
<field name="template_id" ref="template_sign_5"/>
|
||||
<field name="type_id" ref="sign_item_type_company"/>
|
||||
<field name="responsible_id" ref="sign_item_role_customer"/>
|
||||
<field name="page" type="int">1</field>
|
||||
<field name="posX" type="float">0.570</field>
|
||||
<field name="posY" type="float">0.655</field>
|
||||
<field name="width" type="float">0.200</field>
|
||||
<field name="height" type="float">0.015</field>
|
||||
</record>
|
||||
<record model="sign.item" id="sign_item_45">
|
||||
<field name="template_id" ref="template_sign_5"/>
|
||||
<field name="type_id" ref="sign_item_type_signature"/>
|
||||
<field name="responsible_id" ref="sign_item_role_customer"/>
|
||||
<field name="page" type="int">1</field>
|
||||
<field name="posX" type="float">0.570</field>
|
||||
<field name="posY" type="float">0.675</field>
|
||||
<field name="width" type="float">0.200</field>
|
||||
<field name="height" type="float">0.050</field>
|
||||
</record>
|
||||
<record model="sign.item" id="sign_item_46">
|
||||
<field name="template_id" ref="template_sign_5"/>
|
||||
<field name="type_id" ref="sign_item_type_initial"/>
|
||||
<field name="responsible_id" ref="sign_item_role_customer"/>
|
||||
<field name="page" type="int">2</field>
|
||||
<field name="posX" type="float">0.915</field>
|
||||
<field name="posY" type="float">0.970</field>
|
||||
<field name="width" type="float">0.085</field>
|
||||
<field name="height" type="float">0.030</field>
|
||||
</record>
|
||||
<record model="sign.item" id="sign_item_47">
|
||||
<field name="template_id" ref="template_sign_5"/>
|
||||
<field name="type_id" ref="sign_item_type_initial"/>
|
||||
<field name="responsible_id" ref="sign_item_role_customer"/>
|
||||
<field name="page" type="int">3</field>
|
||||
<field name="posX" type="float">0.915</field>
|
||||
<field name="posY" type="float">0.970</field>
|
||||
<field name="width" type="float">0.085</field>
|
||||
<field name="height" type="float">0.030</field>
|
||||
</record>
|
||||
<record model="sign.item" id="sign_item_48">
|
||||
<field name="template_id" ref="template_sign_5"/>
|
||||
<field name="type_id" ref="sign_item_type_initial"/>
|
||||
<field name="responsible_id" ref="sign_item_role_customer"/>
|
||||
<field name="page" type="int">4</field>
|
||||
<field name="posX" type="float">0.915</field>
|
||||
<field name="posY" type="float">0.970</field>
|
||||
<field name="width" type="float">0.085</field>
|
||||
<field name="height" type="float">0.030</field>
|
||||
</record>
|
||||
|
||||
<record id="base.user_admin" model="res.users">
|
||||
<field name="sign_signature" type="base64" file="odex25_sign/static/demo/signature.png"/>
|
||||
</record>
|
||||
<record id="base.user_demo" model="res.users">
|
||||
<field name="groups_id" eval="[(4, ref('odex25_sign.group_sign_user'))]"/>
|
||||
</record>
|
||||
|
||||
<record model="sign.request" id="sign_request_01">
|
||||
<field name="template_id" ref="template_sign_4"/>
|
||||
<field name="reference">Your Real Estate Listing Agreement</field>
|
||||
<field name="state">sent</field>
|
||||
</record>
|
||||
<record model="sign.request.item" id="sign_request_item_01">
|
||||
<field name="partner_id" ref="base.partner_demo_portal"/>
|
||||
<field name="role_id" ref="sign_item_role_customer"/>
|
||||
<field name="sign_request_id" ref="sign_request_01"/>
|
||||
<field name="state">sent</field>
|
||||
<field name="signer_email">portal@example.com</field>
|
||||
</record>
|
||||
|
||||
<record model="sign.request" id="sign_request_02">
|
||||
<field name="template_id" ref="template_sign_5"/>
|
||||
<field name="reference">Your SAAS order</field>
|
||||
<field name="state">signed</field>
|
||||
</record>
|
||||
<record model="sign.request.item" id="sign_request_item_02">
|
||||
<field name="partner_id" ref="base.partner_demo_portal"/>
|
||||
<field name="role_id" ref="sign_item_role_customer"/>
|
||||
<field name="sign_request_id" ref="sign_request_02"/>
|
||||
<field name="state">completed</field>
|
||||
<field name="signer_email">portal@example.com</field>
|
||||
<field name="signing_date" eval="(DateTime.now() - timedelta(days=15))"/>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import res_partner
|
||||
from . import res_users
|
||||
from . import sign_request
|
||||
from . import sign_template
|
||||
from . import sign_log
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from odoo import fields, models, _
|
||||
|
||||
|
||||
class ResPartner(models.Model):
|
||||
_inherit = 'res.partner'
|
||||
|
||||
signature_count = fields.Integer(compute='_compute_signature_count', string="# Signatures")
|
||||
|
||||
def _compute_signature_count(self):
|
||||
signature_data = self.env['sign.request.item'].sudo().read_group([('partner_id', 'in', self.ids)], ['partner_id'], ['partner_id'])
|
||||
signature_data_mapped = dict((data['partner_id'][0], data['partner_id_count']) for data in signature_data)
|
||||
for partner in self:
|
||||
partner.signature_count = signature_data_mapped.get(partner.id, 0)
|
||||
|
||||
def open_signatures(self):
|
||||
self.ensure_one()
|
||||
request_ids = self.env['sign.request.item'].search([('partner_id', '=', self.id)]).mapped('sign_request_id')
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
'name': _('Signature(s)'),
|
||||
'view_mode': 'kanban,tree,form',
|
||||
'res_model': 'sign.request',
|
||||
'domain': [('id', 'in', request_ids.ids)],
|
||||
'context': {
|
||||
'search_default_reference': self.name,
|
||||
'search_default_signed': 1,
|
||||
'search_default_in_progress': 1,
|
||||
},
|
||||
}
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class ResUsers(models.Model):
|
||||
_inherit = 'res.users'
|
||||
|
||||
sign_signature = fields.Binary(string="Digital Signature", groups="base.group_system")
|
||||
sign_initials = fields.Binary(string="Digitial Initials", groups="base.group_system")
|
||||
|
|
@ -1,152 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from hashlib import sha256
|
||||
from json import dumps
|
||||
from datetime import datetime
|
||||
import logging
|
||||
|
||||
from odoo import fields, models, api, _
|
||||
from odoo.exceptions import ValidationError, UserError
|
||||
from odoo.http import request
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
LOG_FIELDS = ['log_date', 'action', 'partner_id', 'request_state', 'latitude', 'longitude', 'ip',]
|
||||
|
||||
class SignLog(models.Model):
|
||||
_name = 'sign.log'
|
||||
_order = 'log_date, id'
|
||||
_description = "Sign requests access history"
|
||||
|
||||
# Accessed on ?
|
||||
log_date = fields.Datetime(default=fields.Datetime.now, required=True)
|
||||
sign_request_id = fields.Many2one('sign.request', required=True, ondelete="cascade")
|
||||
sign_request_item_id = fields.Many2one('sign.request.item')
|
||||
# Accessed as ?
|
||||
user_id = fields.Many2one('res.users', groups="odex25_sign.group_sign_manager")
|
||||
partner_id = fields.Many2one('res.partner')
|
||||
|
||||
# Accessed from ?
|
||||
# If defined on request item when signing: take from it
|
||||
# Else : taken from geoip
|
||||
latitude = fields.Float(digits=(10, 7), groups="odex25_sign.group_sign_manager")
|
||||
longitude = fields.Float(digits=(10, 7), groups="odex25_sign.group_sign_manager")
|
||||
ip = fields.Char("IP address of the visitor", required=True, groups="odex25_sign.group_sign_manager")
|
||||
log_hash = fields.Char(string="Inalterability Hash", readonly=True, copy=False)
|
||||
token = fields.Char(string="User token")
|
||||
|
||||
# Accessed for ?
|
||||
action = fields.Selection(
|
||||
string="Action Performed",
|
||||
selection=[
|
||||
('create', 'Creation'),
|
||||
('open', 'View/Download'),
|
||||
('save', 'Save'),
|
||||
('sign', 'Signature'),
|
||||
], required=True,
|
||||
)
|
||||
|
||||
request_state = fields.Selection([
|
||||
("sent", "Before Signature"),
|
||||
("signed", "After Signature"),
|
||||
("canceled", "Canceled")
|
||||
], required=True, string="State of the request on action log", groups="odex25_sign.group_sign_manager")
|
||||
|
||||
# Not related on purpose :P
|
||||
|
||||
def write(self, vals):
|
||||
raise ValidationError(_("Log history of sign requests cannot be modified !"))
|
||||
|
||||
def unlink(self):
|
||||
raise ValidationError(_("Log history of sign requests cannot be deleted !"))
|
||||
|
||||
def create(self, vals):
|
||||
"""
|
||||
1/ if action=='create': get initial shasign from template (checksum pdf)
|
||||
2/ if action == 'sign': search for logs with hash for the same request and use that to compute new hash
|
||||
"""
|
||||
vals['log_date'] = datetime.utcnow()
|
||||
vals['log_hash'] = self._get_or_check_hash(vals)
|
||||
res = super(SignLog, self).create(vals)
|
||||
return res
|
||||
|
||||
def _get_or_check_hash(self, vals):
|
||||
""" Returns the hash to write on sign log entries """
|
||||
if vals['action'] not in ['sign', 'create']:
|
||||
return False
|
||||
# When we check the hash, we need to restrict the previous activity to logs created before
|
||||
domain = [('sign_request_id', '=', vals['sign_request_id']), ('action', 'in', ['create', 'sign'])]
|
||||
if 'id' in vals:
|
||||
domain.append(('id', '<', vals['id']))
|
||||
prev_activity = self.sudo().search(domain, limit=1, order='id desc')
|
||||
# Multiple signers lead to multiple creation actions but for them, the hash of the PDF must be calculated.
|
||||
previous_hash = ""
|
||||
if not prev_activity:
|
||||
sign_request = self.env['sign.request'].browse(vals['sign_request_id'])
|
||||
body = sign_request.template_id.with_context(bin_size=False).attachment_id.datas
|
||||
else:
|
||||
previous_hash = prev_activity.log_hash
|
||||
body = self._compute_string_to_hash(vals)
|
||||
hash = sha256((previous_hash + str(body)).encode('utf-8')).hexdigest()
|
||||
return hash
|
||||
|
||||
def _compute_string_to_hash(self, vals):
|
||||
values = {}
|
||||
for field in LOG_FIELDS:
|
||||
values[field] = str(vals[field])
|
||||
# Values are filtered based on the token
|
||||
# Signer is signing the document. We save the value of its field. self is an empty recordset.
|
||||
item_values = self.env['sign.request.item.value'].search([('sign_request_id', '=', vals['sign_request_id'])]).filtered(lambda item: item.sign_request_item_id.access_token == vals['token'])
|
||||
for item_value in item_values:
|
||||
values[str(item_value.id)] = str(item_value.value)
|
||||
return dumps(values, sort_keys=True, ensure_ascii=True, indent=None)
|
||||
|
||||
def _check_document_integrity(self):
|
||||
"""
|
||||
Check the integrity of a sign request by comparing the logs hash to the computed values.
|
||||
"""
|
||||
logs = self.filtered(lambda item: item.action in ['sign', 'create'])
|
||||
for log in logs:
|
||||
vals = {key: value[0] if isinstance(value, tuple) else value for key, value in log.read()[0].items()}
|
||||
hash = self._get_or_check_hash(vals)
|
||||
if hash != log.log_hash:
|
||||
# TODO add logs and comments
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def _prepare_vals_from_item(self, request_item):
|
||||
request = request_item.sign_request_id
|
||||
return dict(
|
||||
sign_request_item_id=request_item.id,
|
||||
sign_request_id=request.id,
|
||||
request_state=request.state,
|
||||
latitude=request_item.latitude or 0.0,
|
||||
longitude=request_item.longitude or 0.0,
|
||||
partner_id=request_item.partner_id.id)
|
||||
|
||||
def _prepare_vals_from_request(self, sign_request):
|
||||
return dict(
|
||||
sign_request_id=sign_request.id,
|
||||
request_state=sign_request.state,
|
||||
)
|
||||
|
||||
def _update_vals_with_http_request(self, vals):
|
||||
vals.update({
|
||||
'user_id': request.env.user.id if not request.env.user._is_public() else None,
|
||||
'ip': request.httprequest.remote_addr,
|
||||
})
|
||||
if not vals.get('partner_id', False):
|
||||
vals.update({
|
||||
'partner_id': request.env.user.partner_id.id if not request.env.user._is_public() else None
|
||||
})
|
||||
# NOTE: during signing, this method is always called after the log is generated based on the
|
||||
# request item. This means that if the signer accepted the browser geolocation request, the `vals`
|
||||
# will already contain much more precise coordinates. We should use the GeoIP ones only if the
|
||||
# browser did not send anything
|
||||
if 'geoip' in request.session and not (vals.get('latitude') and vals.get('longitude')):
|
||||
vals.update({
|
||||
'latitude': request.session['geoip'].get('latitude') or 0.0,
|
||||
'longitude': request.session['geoip'].get('longitude') or 0.0,
|
||||
})
|
||||
return vals
|
||||
|
|
@ -1,735 +0,0 @@
|
|||
# -*- 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()
|
||||
|
|
@ -1,288 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
import re
|
||||
import base64
|
||||
import io
|
||||
|
||||
from PyPDF2 import PdfFileReader
|
||||
from collections import defaultdict
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.exceptions import UserError, AccessError
|
||||
from odoo.tools import pdf
|
||||
|
||||
|
||||
class SignTemplate(models.Model):
|
||||
_name = "sign.template"
|
||||
_description = "Signature Template"
|
||||
_rec_name = "attachment_id"
|
||||
|
||||
def _default_favorited_ids(self):
|
||||
return [(4, self.env.user.id)]
|
||||
|
||||
attachment_id = fields.Many2one('ir.attachment', string="Attachment", required=True, ondelete='cascade')
|
||||
name = fields.Char(related='attachment_id.name', readonly=False)
|
||||
datas = fields.Binary(related='attachment_id.datas', readonly=False)
|
||||
sign_item_ids = fields.One2many('sign.item', 'template_id', string="Signature Items", copy=True)
|
||||
responsible_count = fields.Integer(compute='_compute_responsible_count', string="Responsible Count")
|
||||
|
||||
active = fields.Boolean(default=True, string="Active")
|
||||
privacy = fields.Selection([('employee', 'All Users'), ('invite', 'On Invitation')],
|
||||
string="Who can Sign", default="invite",
|
||||
help="Set who can use this template:\n"
|
||||
"- All Users: all users of the Sign application can view and use the template\n"
|
||||
"- On Invitation: only invited users can view and use the template\n"
|
||||
"Invited users can always edit the document template.\n"
|
||||
"Existing requests based on this template will not be affected by changes.")
|
||||
favorited_ids = fields.Many2many('res.users', string="Invited Users", default=lambda s: s._default_favorited_ids())
|
||||
|
||||
share_link = fields.Char(string="Share Link", copy=False)
|
||||
|
||||
sign_request_ids = fields.One2many('sign.request', 'template_id', string="Signature Requests")
|
||||
|
||||
tag_ids = fields.Many2many('sign.template.tag', string='Tags')
|
||||
color = fields.Integer()
|
||||
redirect_url = fields.Char(string="Redirect Link", default="",
|
||||
help="Optional link for redirection after signature")
|
||||
redirect_url_text = fields.Char(string="Link Label", default="Open Link", translate=True,
|
||||
help="Optional text to display on the button link")
|
||||
signed_count = fields.Integer(compute='_compute_signed_in_progress_template')
|
||||
in_progress_count = fields.Integer(compute='_compute_signed_in_progress_template')
|
||||
|
||||
group_ids = fields.Many2many("res.groups", string="Template Access Group")
|
||||
|
||||
@api.model
|
||||
def create(self, vals):
|
||||
if 'active' in vals and vals['active'] and not self.env.user.has_group('odex25_sign.group_sign_user'):
|
||||
raise AccessError(_("Do not have access to create templates"))
|
||||
|
||||
return super(SignTemplate, self).create(vals)
|
||||
|
||||
@api.depends('sign_item_ids.responsible_id')
|
||||
def _compute_responsible_count(self):
|
||||
for template in self:
|
||||
template.responsible_count = len(template.sign_item_ids.mapped('responsible_id'))
|
||||
|
||||
def _compute_signed_in_progress_template(self):
|
||||
sign_requests = self.env['sign.request'].read_group([('state', '!=', 'canceled')], ['state', 'template_id'], ['state', 'template_id'], lazy=False)
|
||||
signed_request_dict = defaultdict(int)
|
||||
in_progress_request_dict = defaultdict(int)
|
||||
for sign_request in sign_requests:
|
||||
if sign_request['state'] == 'sent':
|
||||
template_id = sign_request['template_id'][0]
|
||||
in_progress_request_dict[template_id] = sign_request['__count']
|
||||
elif sign_request['state'] == 'signed':
|
||||
template_id = sign_request['template_id'][0]
|
||||
signed_request_dict[template_id] = sign_request['__count']
|
||||
for template in self:
|
||||
template.signed_count = signed_request_dict[template.id]
|
||||
template.in_progress_count = in_progress_request_dict[template.id]
|
||||
|
||||
@api.model
|
||||
def get_empty_list_help(self, help):
|
||||
if not self.env.ref('sign.template_sign_tour', raise_if_not_found=False) or not self.env.user.has_group('odex25_sign.group_sign_user'):
|
||||
return '<p class="o_view_nocontent_smiling_face">%s</p>' % _('Upload a PDF')
|
||||
return super().get_empty_list_help(help)
|
||||
|
||||
def go_to_custom_template(self, sign_directly_without_mail=False):
|
||||
self.ensure_one()
|
||||
return {
|
||||
'name': "Template \"%(name)s\"" % {'name': self.attachment_id.name},
|
||||
'type': 'ir.actions.client',
|
||||
'tag': 'sign.template',
|
||||
'context': {
|
||||
'id': self.id,
|
||||
'sign_directly_without_mail': sign_directly_without_mail,
|
||||
},
|
||||
}
|
||||
|
||||
def toggle_favorited(self):
|
||||
self.ensure_one()
|
||||
self.write({'favorited_ids': [(3 if self.env.user in self[0].favorited_ids else 4, self.env.user.id)]})
|
||||
|
||||
def unlink(self):
|
||||
if self.filtered(lambda template: template.sign_request_ids):
|
||||
raise UserError(_("You can't delete a template for which signature requests exist but you can archive it instead."))
|
||||
return super(SignTemplate, self).unlink()
|
||||
|
||||
@api.model
|
||||
def upload_template(self, name=None, dataURL=None, active=True):
|
||||
mimetype = dataURL[dataURL.find(':')+1:dataURL.find(',')]
|
||||
datas = dataURL[dataURL.find(',')+1:]
|
||||
# TODO: for now, PDF files without extension are recognized as application/octet-stream;base64
|
||||
try:
|
||||
file_pdf = PdfFileReader(io.BytesIO(base64.b64decode(datas)), strict=False, overwriteWarnings=False)
|
||||
except Exception as e:
|
||||
raise UserError(_("This file cannot be read. Is it a valid PDF?"))
|
||||
file_type = mimetype.replace('application/', '').replace(';base64', '')
|
||||
extension = re.compile(re.escape(file_type), re.IGNORECASE)
|
||||
name = extension.sub(file_type, name)
|
||||
attachment = self.env['ir.attachment'].create({'name': name, 'datas': datas, 'mimetype': mimetype})
|
||||
template = self.create({'attachment_id': attachment.id, 'favorited_ids': [(4, self.env.user.id)], 'active': active})
|
||||
|
||||
return {'template': template.id, 'attachment': attachment.id}
|
||||
|
||||
@api.model
|
||||
def update_from_pdfviewer(self, template_id=None, duplicate=None, sign_items=None, name=None):
|
||||
template = self.browse(template_id)
|
||||
if not duplicate and len(template.sign_request_ids) > 0:
|
||||
return False
|
||||
|
||||
if duplicate:
|
||||
new_attachment = template.attachment_id.copy()
|
||||
r = re.compile(' \(v(\d+)\)$')
|
||||
m = r.search(name)
|
||||
v = str(int(m.group(1))+1) if m else "2"
|
||||
index = m.start() if m else len(name)
|
||||
new_attachment.name = name[:index] + " (v" + v + ")"
|
||||
template = template.copy({
|
||||
'attachment_id': new_attachment.id,
|
||||
'favorited_ids': [(4, self.env.user.id)]
|
||||
})
|
||||
|
||||
elif name:
|
||||
template.attachment_id.name = name
|
||||
|
||||
item_ids = {
|
||||
it
|
||||
for it in map(int, sign_items)
|
||||
if it > 0
|
||||
}
|
||||
template.sign_item_ids.filtered(lambda r: r.id not in item_ids).unlink()
|
||||
for item in template.sign_item_ids:
|
||||
values = sign_items.pop(str(item.id))
|
||||
values['option_ids'] = [(6, False, [int(op) for op in values.get('option_ids', [])])]
|
||||
item.write(values)
|
||||
for item in sign_items.values():
|
||||
item['template_id'] = template.id
|
||||
item['option_ids'] = [(6, False, [int(op) for op in item.get('option_ids', [])])]
|
||||
self.env['sign.item'].create(item)
|
||||
|
||||
if len(template.sign_item_ids.mapped('responsible_id')) > 1:
|
||||
template.share_link = None
|
||||
|
||||
return template.id
|
||||
|
||||
@api.model
|
||||
def rotate_pdf(self, template_id=None):
|
||||
template = self.browse(template_id)
|
||||
if len(template.sign_request_ids) > 0:
|
||||
return False
|
||||
|
||||
template.datas = base64.b64encode(pdf.rotate_pdf(base64.b64decode(template.datas)))
|
||||
|
||||
return True
|
||||
|
||||
@api.model
|
||||
def add_option(self, value):
|
||||
option = self.env['sign.item.option'].search([('value', '=', value)])
|
||||
option_id = option if option else self.env['sign.item.option'].create({'value': value})
|
||||
return option_id.id
|
||||
|
||||
def open_requests(self):
|
||||
return {
|
||||
"type": "ir.actions.act_window",
|
||||
"name": _("Sign requests"),
|
||||
"res_model": "sign.request",
|
||||
"res_id": self.id,
|
||||
"domain": [["template_id.id", "in", self.ids]],
|
||||
"views": [[False, 'kanban'], [False, "form"]],
|
||||
"context": {'search_default_signed': True}
|
||||
}
|
||||
|
||||
class SignTemplateTag(models.Model):
|
||||
|
||||
_name = "sign.template.tag"
|
||||
_description = "Sign Template Tag"
|
||||
_order = "name"
|
||||
|
||||
name = fields.Char('Tag Name', required=True, translate=True)
|
||||
color = fields.Integer('Color Index')
|
||||
|
||||
_sql_constraints = [
|
||||
('name_uniq', 'unique (name)', "Tag name already exists !"),
|
||||
]
|
||||
|
||||
|
||||
class SignItemSelectionOption(models.Model):
|
||||
_name = "sign.item.option"
|
||||
_description = "Option of a selection Field"
|
||||
|
||||
value = fields.Text(string="Option")
|
||||
|
||||
|
||||
class SignItem(models.Model):
|
||||
_name = "sign.item"
|
||||
_description = "Fields to be sign on Document"
|
||||
_rec_name = 'template_id'
|
||||
|
||||
template_id = fields.Many2one('sign.template', string="Document Template", required=True, ondelete='cascade')
|
||||
|
||||
type_id = fields.Many2one('sign.item.type', string="Type", required=True, ondelete='cascade')
|
||||
|
||||
required = fields.Boolean(default=True)
|
||||
responsible_id = fields.Many2one("sign.item.role", string="Responsible")
|
||||
|
||||
option_ids = fields.Many2many("sign.item.option", string="Selection options")
|
||||
|
||||
name = fields.Char(string="Field Name")
|
||||
page = fields.Integer(string="Document Page", required=True, default=1)
|
||||
posX = fields.Float(digits=(4, 3), string="Position X", required=True)
|
||||
posY = fields.Float(digits=(4, 3), string="Position Y", required=True)
|
||||
width = fields.Float(digits=(4, 3), required=True)
|
||||
height = fields.Float(digits=(4, 3), required=True)
|
||||
|
||||
def getByPage(self):
|
||||
items = {}
|
||||
for item in self:
|
||||
if item.page not in items:
|
||||
items[item.page] = []
|
||||
items[item.page].append(item)
|
||||
return items
|
||||
|
||||
|
||||
class SignItemType(models.Model):
|
||||
_name = "sign.item.type"
|
||||
_description = "Signature Item Type"
|
||||
|
||||
name = fields.Char(string="Field Name", required=True, translate=True)
|
||||
item_type = fields.Selection([
|
||||
('signature', "Signature"),
|
||||
('initial', "Initial"),
|
||||
('text', "Text"),
|
||||
('textarea', "Multiline Text"),
|
||||
('checkbox', "Checkbox"),
|
||||
('selection', "Selection"),
|
||||
], required=True, string='Type', default='text')
|
||||
|
||||
tip = fields.Char(required=True, default="fill in", translate=True)
|
||||
placeholder = fields.Char(translate=True)
|
||||
|
||||
default_width = fields.Float(string="Default Width", digits=(4, 3), required=True, default=0.150)
|
||||
default_height = fields.Float(string="Default Height", digits=(4, 3), required=True, default=0.015)
|
||||
auto_field = fields.Char(string="Automatic Partner Field", help="Partner field to use to auto-complete the fields of this type")
|
||||
|
||||
|
||||
class SignItemParty(models.Model):
|
||||
_name = "sign.item.role"
|
||||
_description = "Signature Item Party"
|
||||
|
||||
name = fields.Char(required=True, translate=True)
|
||||
|
||||
sms_authentification = fields.Boolean('SMS Authentication', default=False,)
|
||||
|
||||
@api.model
|
||||
def add(self, name):
|
||||
party = self.search([('name', '=', name)])
|
||||
return party.id if party else self.create({'name': name}).id
|
||||
|
||||
def buy_credits(self):
|
||||
service_name = 'sms'
|
||||
url = self.env['iap.account'].get_credits_url(service_name)
|
||||
return {
|
||||
'name': 'Buy SMS credits',
|
||||
'res_model': 'ir.actions.act_url',
|
||||
'type': 'ir.actions.act_url',
|
||||
'target': 'current',
|
||||
'url': url
|
||||
}
|
||||
|
|
@ -1,164 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<odoo>
|
||||
|
||||
<template id="sign_request_logs_user">
|
||||
<t t-call="web.html_container">
|
||||
<t t-set="print_date" t-value="datetime.datetime.utcnow().strftime('%Y-%m-%d - %H:%M:%S')"/>
|
||||
<t t-foreach="docs" t-as="o">
|
||||
<!-- we want UTC for the reports-->
|
||||
<t t-set="o" t-value="o.with_context(tz='UTC')"/>
|
||||
<t t-set="view_logs" t-value="o.sudo().sign_log_ids.filtered(lambda l: l.action == 'open')"/>
|
||||
<t t-set="change_logs" t-value="o.sudo().sign_log_ids - view_logs"/>
|
||||
<t t-set="creation_log" t-value="o.sudo().sign_log_ids.filtered(lambda l: l.action == 'create')"/>
|
||||
<t t-call="web.external_layout">
|
||||
<h2 class="mt16">
|
||||
Certificate of Completion<br/>
|
||||
</h2>
|
||||
<h4>
|
||||
<strong t-field="o.reference"/>
|
||||
</h4>
|
||||
<div class="text-muted small">Printed on <t t-esc="print_date"/> UTC</div>
|
||||
<h5 class="mt16">Document Details</h5>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-6">
|
||||
<div><b>Created by:</b> <span t-field="o.create_uid.sudo().partner_id"/></div>
|
||||
<div><b>Created on:</b> <span t-field="o.create_date"/> (UTC)</div>
|
||||
<div><b>Creation IP Address:</b> <span t-esc="creation_log and creation_log[0].ip"/></div>
|
||||
<div><b>Signers:</b> <span t-esc="len(o.sudo().request_item_ids)"/></div>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<div><b>Document ID:</b> <t t-esc="o.id"/></div>
|
||||
<div><b>Signature:</b> <small t-esc="creation_log and creation_log[0].log_hash"/></div>
|
||||
</div>
|
||||
</div>
|
||||
<br/>
|
||||
<h5>Signing Events</h5>
|
||||
<table class="table table-sm o_main_table small">
|
||||
<t t-set="has_sms_validation" t-value="o.sudo().request_item_ids.filtered(lambda i: i.sms_token)"/>
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="text-left">Action</th>
|
||||
<th class="text-left">By</th>
|
||||
<th class="text-left">Date (UTC)</th>
|
||||
<th class="text-left">Email Validation</th>
|
||||
<th class="text-left" t-if="has_sms_validation">SMS Validation</th>
|
||||
<th class="text-left" groups="odex25_sign.group_sign_manager">Geolocation</th>
|
||||
<th class="text-left" groups="odex25_sign.group_sign_manager">IP</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<t t-foreach="change_logs" t-as="log">
|
||||
<tr>
|
||||
<td>
|
||||
<span t-field="log.action"/>
|
||||
<img t-if="log.action != 'save' and log.sign_request_item_id.signature" class="text-left" t-att-src="image_data_uri(log.sign_request_item_id.signature)" style="max-height: 40px"/>
|
||||
</td>
|
||||
<td> <span t-field="log.partner_id"/>
|
||||
<t t-if="log.partner_id.email">
|
||||
<br/><span t-field="log.partner_id.email"/>
|
||||
</t>
|
||||
</td>
|
||||
<td> <span t-field="log.log_date"/> </td>
|
||||
<td>
|
||||
<t t-if="log.user_id != False and log.user_id in log.sign_request_item_id.partner_id.user_ids">
|
||||
<span class="text-muted text-nowrap"><i class="fa fa-user"/> <t t-esc="log.sign_request_item_id.partner_id.user_ids[0].email"/></span>
|
||||
</t>
|
||||
<t t-elif="log.sign_request_item_id.access_via_link">
|
||||
<span t-esc="log.sign_request_item_id.signer_email"/>
|
||||
</t>
|
||||
<t t-elif="log.action == 'sign'">
|
||||
<span class="text-muted"><i class="fa fa-times"/> No email Validation</span>
|
||||
</t>
|
||||
</td>
|
||||
<td t-if="has_sms_validation">
|
||||
<t t-if="log.sign_request_item_id.sms_token">
|
||||
<t t-set="number" t-value="''.join(['******', log.sign_request_item_id.sms_number[-4:]])"/>
|
||||
<span t-esc="number"/>
|
||||
</t>
|
||||
<t t-elif="log.action == 'sign'">
|
||||
<span class="text-muted"><i class="fa fa-times"/> No SMS Validation</span>
|
||||
</t>
|
||||
</td>
|
||||
<td groups="odex25_sign.group_sign_manager">
|
||||
<t t-if="log.latitude and log.longitude">
|
||||
<span t-field="log.latitude"/>,
|
||||
<span t-field="log.longitude"/>
|
||||
<div>
|
||||
<a t-attf-href="https://www.google.com/maps/search/?api=1&query={{log.latitude}},{{log.longitude}}">
|
||||
<i class="fa fa-globe"/> View
|
||||
</a>
|
||||
</div>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<span class="text-muted">Not available</span>
|
||||
</t>
|
||||
</td>
|
||||
<td groups="odex25_sign.group_sign_manager"> <span t-field="log.ip"/> </td>
|
||||
</tr>
|
||||
<tr t-if="log.log_hash">
|
||||
<td colspan="7" class="text-right text-muted small" style="border-right-style:hidden;border-left-style:hidden;">
|
||||
Signature: <pre style="display:inline" t-field="log.log_hash"/>
|
||||
</td>
|
||||
</tr>
|
||||
</t>
|
||||
</tbody>
|
||||
</table>
|
||||
<div t-if="o.integrity" class="small text-right text-success">
|
||||
<p><i class="fa fa-check"/> The document's integrity is valid.</p>
|
||||
</div>
|
||||
<div t-else="" class="small text-right text-muted">
|
||||
<p><i class="fa fa-exclamation-circle"/> The document's integrity could not be verified.</p>
|
||||
</div>
|
||||
<div class="small" t-if="o.state == 'signed'">
|
||||
<p>The final document and this completion history have been sent by email on&nbsp; <span t-field="o.completion_date"/> to: <t t-esc="', '.join(o.sudo()._get_final_recipients())"/>.</p>
|
||||
</div>
|
||||
<h5>Access Logs</h5>
|
||||
<table class="table table-sm o_main_table small">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="text-left">Viewed/downloaded by</th>
|
||||
<th class="text-left">Date (UTC)</th>
|
||||
<th class="text-left">State</th>
|
||||
<th class="text-left" groups="odex25_sign.group_sign_manager">Geolocation</th>
|
||||
<th class="text-left" groups="odex25_sign.group_sign_manager">ip</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr t-foreach="view_logs" t-as="log">
|
||||
<td> <span t-field="log.partner_id"/> <span t-field="log.partner_id.email"/></td>
|
||||
<td> <span t-field="log.log_date"/> </td>
|
||||
<td> <span t-field="log.request_state"/> </td>
|
||||
<td groups="odex25_sign.group_sign_manager">
|
||||
<t t-if="log.latitude and log.longitude">
|
||||
<span t-field="log.latitude"/>,
|
||||
<span t-field="log.longitude"/>
|
||||
<a t-attf-href="https://www.google.com/maps/search/?api=1&query={{log.latitude}},{{log.longitude}}">
|
||||
<i class="fa fa-globe"/> View
|
||||
</a>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<span class="text-muted">Not available</span>
|
||||
</t>
|
||||
</td>
|
||||
<td groups="odex25_sign.group_sign_manager"> <span t-field="log.ip"/> </td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</t>
|
||||
</t>
|
||||
</t>
|
||||
</template>
|
||||
|
||||
<record id="action_sign_request_print_logs" model="ir.actions.report">
|
||||
<field name="name">Activity Logs</field>
|
||||
<field name="model">sign.request</field>
|
||||
<field name="report_type">qweb-pdf</field>
|
||||
<field name="report_name">odex25_sign.sign_request_logs_user</field>
|
||||
<field name="report_file">odex25_sign.sign_request_logs_user</field>
|
||||
<field name="binding_model_id" ref="model_sign_request"/>
|
||||
<field name="binding_type">report</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_sign_request_all,sign_request all,model_sign_request,,0,0,0,0
|
||||
access_sign_request_group_employee,sign_request group_employee,model_sign_request,group_sign_employee,1,1,1,1
|
||||
access_sign_template_all,sign_template all,model_sign_template,,0,0,0,0
|
||||
access_sign_template_group_user,sign_template group_user,model_sign_template,group_sign_user,1,1,1,1
|
||||
access_sign_template_group_employee,sign_template group_employee,model_sign_template,group_sign_employee,1,1,1,1
|
||||
access_sign_request_item_all,sign_request_item all,model_sign_request_item,,0,0,0,0
|
||||
access_sign_request_item_group_user,sign_request_item group_user,model_sign_request_item,group_sign_user,1,1,1,1
|
||||
access_sign_request_item_group_employee,sign_request_item group_employee,model_sign_request_item,group_sign_employee,1,1,1,0
|
||||
access_sign_item_all,sign_item all,model_sign_item,,0,0,0,0
|
||||
access_sign_item_group_user,sign_item group_user,model_sign_item,group_sign_user,1,1,1,1
|
||||
access_sign_item_group_employee,sign_item group_employee,model_sign_item,group_sign_employee,1,1,1,1
|
||||
access_sign_item_option_group_user,sign_item_option group_user,model_sign_item_option,group_sign_user,1,1,1,1
|
||||
access_sign_item_option_group_employee,sign_item_option group_employee,model_sign_item_option,group_sign_employee,1,1,1,1
|
||||
access_sign_request_item_value_all,sign_request_item_value all,model_sign_request_item_value,,0,0,0,0
|
||||
access_sign_request_item_value_group_user,sign_request_item_value group_user,model_sign_request_item_value,group_sign_user,1,1,1,1
|
||||
access_sign_item_role_all,sign_item_role all,model_sign_item_role,,0,0,0,0
|
||||
access_sign_item_role_group_user,sign_item_role group_user,model_sign_item_role,group_sign_user,1,1,1,1
|
||||
access_sign_item_role_group_employee,sign_item_role group_employee,model_sign_item_role,group_sign_employee,1,0,0,0
|
||||
access_sign_item_type_all,sign_item_type all,model_sign_item_type,,0,0,0,0
|
||||
access_sign_item_type_group_user,sign_item_type group_user,model_sign_item_type,group_sign_user,1,1,1,1
|
||||
access_sign_item_type_group_employee,sign_item_type group_employee,model_sign_item_type,group_sign_employee,1,0,0,0
|
||||
access_sign_template_tag_user,access_sign_template_tag_user,model_sign_template_tag,group_sign_user,1,0,0,0
|
||||
access_sign_template_tag_employee,access_sign_template_tag_employee,model_sign_template_tag,group_sign_employee,1,0,0,0
|
||||
access_sign_template_tag_manager,access_sign_template_tag_manager,model_sign_template_tag,group_sign_manager,1,1,1,1
|
||||
access_sign_log_all,sign_log all,model_sign_log,,0,0,0,0
|
||||
access_sign_log_group_manager,sign_log group_manager,model_sign_log,group_sign_manager,1,0,0,0
|
||||
access_sign_request_portal,sign.request.portal,odex25_sign.model_sign_request,base.group_portal,1,0,0,0
|
||||
access_sign_send_request,access.sign.send.request,model_sign_send_request,odex25_sign.group_sign_user,1,1,1,0
|
||||
access_sign_send_request_employee,access.sign.send.request.employee,model_sign_send_request,odex25_sign.group_sign_employee,1,1,1,0
|
||||
access_sign_send_request_signer,access.sign.send.request.signer,model_sign_send_request_signer,odex25_sign.group_sign_user,1,1,1,0
|
||||
access_sign_send_request_signer_employee,access.sign.send.request.signer.employee,model_sign_send_request_signer,odex25_sign.group_sign_employee,1,1,1,0
|
||||
access_sign_template_share,access.sign.template.share,model_sign_template_share,odex25_sign.group_sign_user,1,1,1,0
|
||||
access_sign_template_share_employee,access.sign.template.share.employee,model_sign_template_share,odex25_sign.group_sign_employee,1,1,1,0
|
||||
access_sign_request_send_copy,access.sign.request.send.copy,model_sign_request_send_copy,odex25_sign.group_sign_user,1,1,1,0
|
||||
access_sign_request_send_copy_employee,access.sign.request.send.copy.employee,model_sign_request_send_copy,odex25_sign.group_sign_employee,1,1,1,0
|
||||
|
|
|
@ -1,189 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data noupdate="1">
|
||||
<record id="group_sign_employee" model="res.groups">
|
||||
<field name="name">Employee: Send file to sign</field>
|
||||
<field name="implied_ids" eval="[(4, ref('base.group_user'))]"/>
|
||||
<field name="category_id" ref="base.module_category_sales_sign"/>
|
||||
</record>
|
||||
|
||||
<record id="group_sign_user" model="res.groups">
|
||||
<field name="name">User: Own and Shared Templates</field>
|
||||
<field name="implied_ids" eval="[(4, ref('odex25_sign.group_sign_employee'))]"/>
|
||||
<field name="category_id" ref="base.module_category_sales_sign"/>
|
||||
</record>
|
||||
|
||||
<record id="group_sign_manager" model="res.groups">
|
||||
<field name="name">Administrator</field>
|
||||
<field name="category_id" ref="base.module_category_sales_sign"/>
|
||||
<field name="implied_ids" eval="[(4, ref('odex25_sign.group_sign_user'))]"/>
|
||||
<field name="users" eval="[(4, ref('base.user_root')), (4, ref('base.user_admin'))]"/>
|
||||
</record>
|
||||
|
||||
<record id="base.default_user" model="res.users">
|
||||
<field name="groups_id" eval="[(4,ref('group_sign_manager'))]"/>
|
||||
</record>
|
||||
|
||||
<record id="ir_rule_sign_template_group_sign_user" model="ir.rule">
|
||||
<field name="name">sign.template: group_sign_user: Manage favorited templates</field>
|
||||
<field name="model_id" ref="odex25_sign.model_sign_template"/>
|
||||
<field name="domain_force">[('favorited_ids', 'in', user.id)]</field>
|
||||
<field name="groups" eval="[(4, ref('odex25_sign.group_sign_user'))]"/>
|
||||
</record>
|
||||
|
||||
<record id="ir_rule_sign_template_group_sign_employee" model="ir.rule">
|
||||
<field name="name">sign.template: group_sign_employee: Manage favorited templates</field>
|
||||
<field name="model_id" ref="odex25_sign.model_sign_template"/>
|
||||
<field name="domain_force">[('favorited_ids', 'in', user.id)]</field>
|
||||
<field name="groups" eval="[(4, ref('odex25_sign.group_sign_employee'))]"/>
|
||||
</record>
|
||||
|
||||
<record id="ir_rule_sign_template_access_group_sign_employee" model="ir.rule">
|
||||
<field name="name">sign.template: group_sign_employee: seen template access</field>
|
||||
<field name="model_id" ref="odex25_sign.model_sign_template"/>
|
||||
<field name="domain_force">[('group_ids', 'in', user.groups_id.ids)]</field>
|
||||
<field name="groups" eval="[(4, ref('odex25_sign.group_sign_employee'))]"/>
|
||||
</record>
|
||||
|
||||
<record id="ir_rule_sign_template_item_access_group_sign_employee" model="ir.rule">
|
||||
<field name="name">sign.template: group_sign_employee: seen template item access</field>
|
||||
<field name="model_id" ref="odex25_sign.model_sign_item"/>
|
||||
<field name="domain_force">['|', ('template_id.group_ids', 'in', user.groups_id.ids), ('create_uid', '=', user.id)]</field>
|
||||
<field name="groups" eval="[(4, ref('odex25_sign.group_sign_employee'))]"/>
|
||||
</record>
|
||||
|
||||
<record id="ir_rule_sign_template_item_access_group_sign_user" model="ir.rule">
|
||||
<field name="name">sign.template: group_sign_user: seen all template item</field>
|
||||
<field name="model_id" ref="odex25_sign.model_sign_item"/>
|
||||
<field name="domain_force">[(1, '=', 1)]</field>
|
||||
<field name="groups" eval="[(4, ref('odex25_sign.group_sign_user'))]"/>
|
||||
</record>
|
||||
|
||||
<record id="ir_rule_sign_template_group_sign_manager" model="ir.rule">
|
||||
<field name="name">sign.template: group_sign_manager: Manage all templates</field>
|
||||
<field name="model_id" ref="odex25_sign.model_sign_template"/>
|
||||
<field name="domain_force">[(1, '=', 1)]</field>
|
||||
<field name="groups" eval="[(4, ref('odex25_sign.group_sign_manager'))]"/>
|
||||
</record>
|
||||
|
||||
<record id="ir_rule_sign_request_group_sign_user_create" model="ir.rule">
|
||||
<field name="name">sign.request: group_sign_user: Create requests on favorite and public templates</field>
|
||||
<field name="model_id" ref="odex25_sign.model_sign_request"/>
|
||||
<field name="domain_force">['|', ('template_id.favorited_ids', 'in', user.id), ('template_id.privacy', '=', 'employee')]</field>
|
||||
<field name="groups" eval="[(4, ref('odex25_sign.group_sign_user'))]"/>
|
||||
<field name="perm_read" eval="False"/>
|
||||
<field name="perm_write" eval="False"/>
|
||||
<field name="perm_create" eval="True"/>
|
||||
<field name="perm_unlink" eval="False"/>
|
||||
</record>
|
||||
|
||||
<record id="ir_rule_sign_request_group_sign_user_modify" model="ir.rule">
|
||||
<field name="name">sign.request: group_sign_user: Allow to edit favorited requests</field>
|
||||
<field name="model_id" ref="odex25_sign.model_sign_request"/>
|
||||
<field name="domain_force">[('favorited_ids', 'in', user.id)]</field>
|
||||
<field name="groups" eval="[(4, ref('odex25_sign.group_sign_user'))]"/>
|
||||
<field name="perm_read" eval="True"/>
|
||||
<field name="perm_write" eval="True"/>
|
||||
<field name="perm_create" eval="False"/>
|
||||
<field name="perm_unlink" eval="True"/>
|
||||
</record>
|
||||
|
||||
<record id="ir_rule_sign_request_group_sign_employee_modify" model="ir.rule">
|
||||
<field name="name">sign.request: group_sign_employee: Allow to edit favorited requests</field>
|
||||
<field name="model_id" ref="odex25_sign.model_sign_request"/>
|
||||
<field name="domain_force">[('favorited_ids', 'in', user.id)]</field>
|
||||
<field name="groups" eval="[(4, ref('odex25_sign.group_sign_employee'))]"/>
|
||||
<field name="perm_read" eval="True"/>
|
||||
<field name="perm_write" eval="True"/>
|
||||
<field name="perm_create" eval="False"/>
|
||||
<field name="perm_unlink" eval="True"/>
|
||||
</record>
|
||||
|
||||
<record id="ir_rule_sign_request_group_sign_user_modify" model="ir.rule">
|
||||
<field name="name">sign.request: group_sign_user: Read the requests that I follow</field>
|
||||
<field name="model_id" ref="odex25_sign.model_sign_request"/>
|
||||
<field name="domain_force">[('message_partner_ids', 'in', user.partner_id.ids)]</field>
|
||||
<field name="groups" eval="[(4, ref('odex25_sign.group_sign_user'))]"/>
|
||||
<field name="perm_read" eval="True"/>
|
||||
<field name="perm_write" eval="True"/>
|
||||
<field name="perm_create" eval="False"/>
|
||||
<field name="perm_unlink" eval="True"/>
|
||||
</record>
|
||||
|
||||
<record id="ir_rule_sign_request_group_sign_employee_modify" model="ir.rule">
|
||||
<field name="name">sign.request: group_sign_employee: Read the requests that I follow</field>
|
||||
<field name="model_id" ref="odex25_sign.model_sign_request"/>
|
||||
<field name="domain_force">[('message_partner_ids', 'in', user.partner_id.ids)]</field>
|
||||
<field name="groups" eval="[(4, ref('odex25_sign.group_sign_employee'))]"/>
|
||||
<field name="perm_read" eval="True"/>
|
||||
<field name="perm_write" eval="True"/>
|
||||
<field name="perm_create" eval="False"/>
|
||||
<field name="perm_unlink" eval="True"/>
|
||||
</record>
|
||||
|
||||
<record id="ir_rule_sign_request_group_sign_manager" model="ir.rule">
|
||||
<field name="name">sign.template: group_sign_manager: Manage all requests</field>
|
||||
<field name="model_id" ref="odex25_sign.model_sign_request"/>
|
||||
<field name="domain_force">[(1, '=', 1)]</field>
|
||||
<field name="groups" eval="[(4, ref('odex25_sign.group_sign_manager'))]"/>
|
||||
</record>
|
||||
|
||||
<record id="ir_rule_sign_log_group_sign_user_modify" model="ir.rule">
|
||||
<field name="name">sign.log: group_sign_user: Allow to read logs of own requests</field>
|
||||
<field name="model_id" ref="odex25_sign.model_sign_log"/>
|
||||
<field name="domain_force">[('sign_request_id.favorited_ids', 'in', user.id)]</field>
|
||||
<field name="groups" eval="[(4, ref('odex25_sign.group_sign_user'))]"/>
|
||||
<field name="perm_read" eval="True"/>
|
||||
<field name="perm_write" eval="False"/>
|
||||
<field name="perm_create" eval="False"/>
|
||||
<field name="perm_unlink" eval="False"/>
|
||||
</record>
|
||||
|
||||
<record id="ir_rule_sign_log_group_sign_employee_modify" model="ir.rule">
|
||||
<field name="name">sign.log: group_sign_employee: Allow to read logs of own requests</field>
|
||||
<field name="model_id" ref="odex25_sign.model_sign_log"/>
|
||||
<field name="domain_force">[('sign_request_id.favorited_ids', 'in', user.id)]</field>
|
||||
<field name="groups" eval="[(4, ref('odex25_sign.group_sign_employee'))]"/>
|
||||
<field name="perm_read" eval="True"/>
|
||||
<field name="perm_write" eval="False"/>
|
||||
<field name="perm_create" eval="False"/>
|
||||
<field name="perm_unlink" eval="False"/>
|
||||
</record>
|
||||
|
||||
<record id="ir_rule_sign_log_group_sign_manager" model="ir.rule">
|
||||
<field name="name">sign.log: group_sign_manager: See all logs</field>
|
||||
<field name="model_id" ref="odex25_sign.model_sign_log"/>
|
||||
<field name="domain_force">[(1, '=', 1)]</field>
|
||||
<field name="groups" eval="[(4, ref('odex25_sign.group_sign_manager'))]"/>
|
||||
<field name="perm_read" eval="True"/>
|
||||
<field name="perm_write" eval="False"/>
|
||||
<field name="perm_create" eval="False"/>
|
||||
<field name="perm_unlink" eval="False"/>
|
||||
</record>
|
||||
|
||||
<!-- Portal Access Rules -->
|
||||
<record id="ir_rule_sign_request_portal" model="ir.rule">
|
||||
<field name="name">Portal Personal Sign Request</field>
|
||||
<field name="model_id" ref="odex25_sign.model_sign_request"/>
|
||||
<field name="domain_force">[('message_partner_ids','child_of',[user.partner_id.id])]</field>
|
||||
<field name="groups" eval="[(4, ref('base.group_portal'))]"/>
|
||||
<field name="perm_unlink" eval="False"/>
|
||||
<field name="perm_write" eval="True"/>
|
||||
<field name="perm_read" eval="True"/>
|
||||
<field name="perm_create" eval="False"/>
|
||||
</record>
|
||||
|
||||
<record id="sign_request_line_rule_portal" model="ir.rule">
|
||||
<field name="name">Portal Sign Requests Line</field>
|
||||
<field name="model_id" ref="odex25_sign.model_sign_request_item"/>
|
||||
<field name="domain_force">[('message_partner_ids','child_of',[user.partner_id.id])]</field>
|
||||
<field name="perm_unlink" eval="False"/>
|
||||
<field name="perm_write" eval="True"/>
|
||||
<field name="perm_read" eval="True"/>
|
||||
<field name="perm_create" eval="False"/>
|
||||
<field name="groups" eval="[(4, ref('base.group_portal'))]"/>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
|
||||
</odoo>
|
||||
|
Before Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 6.2 KiB |
|
|
@ -1 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="70" height="70"><defs><path id="a" d="M4 0h61c4 0 5 1 5 5v60c0 4-1 5-5 5H4c-3 0-4-1-4-5V5c0-4 1-5 4-5z"/><linearGradient id="c" x1="98.162%" x2="0%" y1="1.838%" y2="100%"><stop offset="0%" stop-color="#797DA5"/><stop offset="50.799%" stop-color="#6D7194"/><stop offset="100%" stop-color="#626584"/></linearGradient></defs><g fill="none" fill-rule="evenodd"><mask id="b" fill="#fff"><use xlink:href="#a"/></mask><g mask="url(#b)"><path fill="url(#c)" d="M0 0H70V70H0z"/><path fill="#FFF" fill-opacity=".383" d="M4 1h61c2.667 0 4.333.667 5 2V0H0v3c.667-1.333 2-2 4-2z"/><path fill="#393939" d="M4 69c-2 0-4-1-4-4V36.285L18 14h28v15.96l3.608-3.433L56.775 29 43.03 45.984 46 54 31.248 69H4z" opacity=".324"/><path fill="#000" fill-opacity=".383" d="M4 69h61c2.667 0 4.333-1 5-3v4H0v-4c.667 2 2 3 4 3z"/><path fill="#000" d="M46 28.917l-3 2.75V19H21v35h22v-6l3-3v11H18V16h28v12.917zM24 47h15v2H24v-2zm32.448-15.453l-1.8 1.8a.47.47 0 0 1-.665 0l-4.335-4.335a.47.47 0 0 1 0-.664l1.8-1.8a1.88 1.88 0 0 1 2.653 0l2.347 2.347c.735.73.735 1.918 0 2.652zm-8.347-1.652a.474.474 0 0 1 .668 0l4.336 4.336a.47.47 0 0 1 0 .664L42.847 45.15l-4.746.832a.939.939 0 0 1-1.086-1.086l.828-4.745L48.1 29.895zm-6.254 9.378a.545.545 0 0 0 .773 0l6.016-6.015a.545.545 0 0 0 0-.773.545.545 0 0 0-.774 0L41.847 38.5a.545.545 0 0 0 0 .773zm-1.41 3.285v-1.875h-1.418l-.442 2.52 1.215 1.214 2.52-.441v-1.418h-1.875zm-4.897-14.56l1.258 1.23c.375.355.2.986-.292 1.103l-1.713.438.483 1.695a.662.662 0 0 1-.811.812l-1.695-.483-.437 1.713c-.119.497-.761.66-1.103.292L30 33.54l-1.23 1.258c-.346.365-.981.216-1.103-.292l-.437-1.713-1.695.483a.662.662 0 0 1-.811-.812l.483-1.695-1.713-.438a.651.651 0 0 1-.292-1.103l1.258-1.23-1.258-1.23a.651.651 0 0 1 .292-1.104l1.713-.437-.483-1.696a.662.662 0 0 1 .811-.811l1.695.483.437-1.714c.117-.49.757-.656 1.103-.291L30 22.465l1.23-1.267c.35-.37.987-.193 1.103.291l.437 1.714 1.695-.483a.662.662 0 0 1 .811.811l-.483 1.696 1.713.437a.651.651 0 0 1 .292 1.103l-1.258 1.23zm-8.446.513l2.085 2.084a.32.32 0 0 0 .453 0l3.689-3.688a.32.32 0 0 0 0-.454L32.867 26a.32.32 0 0 0-.453 0l-3.008 3.008L28 27.604a.32.32 0 0 0-.453 0l-.454.453a.32.32 0 0 0 0 .454z" opacity=".3"/><path fill="#FFF" d="M46 26.917l-3 2.75V17H21v35h22v-6l3-3v11H18V14h28v12.917zM24 45h15v2H24v-2zm32.448-15.453l-1.8 1.8a.47.47 0 0 1-.665 0l-4.335-4.335a.47.47 0 0 1 0-.664l1.8-1.8a1.88 1.88 0 0 1 2.653 0l2.347 2.347c.735.73.735 1.918 0 2.652zm-8.347-1.652a.474.474 0 0 1 .668 0l4.336 4.336a.47.47 0 0 1 0 .664L42.847 43.15l-4.746.832a.939.939 0 0 1-1.086-1.086l.828-4.745L48.1 27.895zm-6.254 9.378a.545.545 0 0 0 .773 0l6.016-6.015a.545.545 0 0 0 0-.773.545.545 0 0 0-.774 0L41.847 36.5a.545.545 0 0 0 0 .773zm-1.41 3.285v-1.875h-1.418l-.442 2.52 1.215 1.214 2.52-.441v-1.418h-1.875zm-4.897-14.56l1.258 1.23c.375.355.2.986-.292 1.103l-1.713.438.483 1.695a.662.662 0 0 1-.811.812l-1.695-.483-.437 1.713c-.119.497-.761.66-1.103.292L30 31.54l-1.23 1.258c-.346.365-.981.216-1.103-.292l-.437-1.713-1.695.483a.662.662 0 0 1-.811-.812l.483-1.695-1.713-.438a.651.651 0 0 1-.292-1.103l1.258-1.23-1.258-1.23a.651.651 0 0 1 .292-1.104l1.713-.437-.483-1.696a.662.662 0 0 1 .811-.811l1.695.483.437-1.714c.117-.49.757-.656 1.103-.291L30 20.465l1.23-1.267c.35-.37.987-.193 1.103.291l.437 1.714 1.695-.483a.662.662 0 0 1 .811.811l-.483 1.696 1.713.437a.651.651 0 0 1 .292 1.103l-1.258 1.23zm-8.446.513l2.085 2.084a.32.32 0 0 0 .453 0l3.689-3.688a.32.32 0 0 0 0-.454L32.867 24a.32.32 0 0 0-.453 0l-3.008 3.008L28 25.604a.32.32 0 0 0-.453 0l-.454.453a.32.32 0 0 0 0 .454z"/></g></g></svg>
|
||||
|
Before Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 4.9 KiB |
|
Before Width: | Height: | Size: 359 B |
|
Before Width: | Height: | Size: 714 B |
|
Before Width: | Height: | Size: 2.4 KiB |
|
|
@ -1,510 +0,0 @@
|
|||
.o_block_scroll {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#viewerContainer { /* PDFJS Viewer */
|
||||
visibility: hidden;
|
||||
opacity: 0.01;
|
||||
}
|
||||
|
||||
#viewer { /* PDFJS Viewer */
|
||||
padding-top: 21px; /* Allow space for the types toolbar */
|
||||
}
|
||||
|
||||
.page { /* PDFJS Viewer */
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.o_sign_sign_item {
|
||||
position: absolute;
|
||||
|
||||
-webkit-box-sizing: border-box;
|
||||
-moz-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
text-align: center;
|
||||
|
||||
border: 1px dashed #875A7B;
|
||||
vertical-align: middle;
|
||||
display: inline-block;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-shadow: 0px 0px 3px 1px #875A7B;
|
||||
resize: none;
|
||||
|
||||
background-color: rgba(255, 255, 255, 0.9);
|
||||
|
||||
font-family: Helvetica;
|
||||
line-height: 1.25;
|
||||
|
||||
z-index: 100;
|
||||
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.o_sign_sign_item.o_readonly_mode {
|
||||
border:none;
|
||||
}
|
||||
|
||||
.o_sign_sign_item .o_placeholder {
|
||||
font-size: 12pt !important;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
textarea.o_sign_sign_item {
|
||||
font-size: 12pt !important;
|
||||
}
|
||||
|
||||
.o_sign_sign_item_required {
|
||||
background-color: rgba(255, 235, 235, 0.9);
|
||||
font-size: 10pt;
|
||||
}
|
||||
|
||||
.o_sign_item_custom_popover .form-group {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.o_sign_item_custom_popover label {
|
||||
display: inline-block;
|
||||
max-width: 100%;
|
||||
margin-bottom: 5px;
|
||||
font-weight: bold;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
.o_sign_item_custom_popover .o_input {
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.o_sign_item_custom_popover .o_sign_responsible_select {
|
||||
display: block;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.o_sign_sign_item + .popover {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 1060;
|
||||
display: none;
|
||||
max-width: 276px;
|
||||
padding: 1px;
|
||||
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: normal;
|
||||
line-height: 1.42857143;
|
||||
text-align: left;
|
||||
text-align: start;
|
||||
text-decoration: none;
|
||||
text-shadow: none;
|
||||
text-transform: none;
|
||||
letter-spacing: normal;
|
||||
word-break: normal;
|
||||
word-spacing: normal;
|
||||
word-wrap: normal;
|
||||
white-space: normal;
|
||||
background-color: #fff;
|
||||
-webkit-background-clip: padding-box;
|
||||
background-clip: padding-box;
|
||||
border: 1px solid #ccc;
|
||||
border: 1px solid rgba(0, 0, 0, .2);
|
||||
border-radius: 6px;
|
||||
-webkit-box-shadow: 0 5px 10px rgba(0, 0, 0, .2);
|
||||
box-shadow: 0 5px 10px rgba(0, 0, 0, .2);
|
||||
line-break: auto;
|
||||
}
|
||||
|
||||
.o_sign_sign_item + .popover.top {
|
||||
margin-top: -10px;
|
||||
}
|
||||
|
||||
.o_sign_sign_item + .popover.right {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.o_sign_sign_item + .popover.bottom {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.o_sign_sign_item + .popover.left {
|
||||
margin-left: -10px;
|
||||
}
|
||||
|
||||
.o_sign_sign_item + .popover > .popover-header {
|
||||
padding: 8px 14px;
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
background-color: #f7f7f7;
|
||||
border-bottom: 1px solid #ebebeb;
|
||||
border-radius: 5px 5px 0 0;
|
||||
}
|
||||
|
||||
.o_sign_sign_item + .popover > .popover-body {
|
||||
padding: 9px 14px;
|
||||
}
|
||||
|
||||
.o_sign_sign_item + .popover > .arrow,
|
||||
.o_sign_sign_item + .popover > .arrow:after {
|
||||
position: absolute;
|
||||
display: block;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-color: transparent;
|
||||
border-style: solid;
|
||||
}
|
||||
|
||||
.o_sign_sign_item + .popover > .arrow {
|
||||
border-width: 11px;
|
||||
}
|
||||
|
||||
.o_sign_sign_item + .popover > .arrow:after {
|
||||
content: "";
|
||||
border-width: 10px;
|
||||
}
|
||||
|
||||
.o_sign_sign_item + .popover.top > .arrow {
|
||||
bottom: -11px;
|
||||
left: 50%;
|
||||
margin-left: -11px;
|
||||
border-top-color: #999;
|
||||
border-top-color: rgba(0, 0, 0, .25);
|
||||
border-bottom-width: 0;
|
||||
}
|
||||
|
||||
.o_sign_sign_item + .popover.top > .arrow:after {
|
||||
bottom: 1px;
|
||||
margin-left: -10px;
|
||||
content: " ";
|
||||
border-top-color: #fff;
|
||||
border-bottom-width: 0;
|
||||
}
|
||||
|
||||
.o_sign_sign_item + .popover.right > .arrow {
|
||||
top: 50%;
|
||||
left: -11px;
|
||||
margin-top: -11px;
|
||||
border-right-color: #999;
|
||||
border-right-color: rgba(0, 0, 0, .25);
|
||||
border-left-width: 0;
|
||||
}
|
||||
|
||||
.o_sign_sign_item + .popover.right > .arrow:after {
|
||||
bottom: -10px;
|
||||
left: 1px;
|
||||
content: " ";
|
||||
border-right-color: #fff;
|
||||
border-left-width: 0;
|
||||
}
|
||||
|
||||
.o_sign_sign_item + .popover.bottom > .arrow {
|
||||
top: -11px;
|
||||
left: 50%;
|
||||
margin-left: -11px;
|
||||
border-top-width: 0;
|
||||
border-bottom-color: #999;
|
||||
border-bottom-color: rgba(0, 0, 0, .25);
|
||||
}
|
||||
|
||||
.o_sign_sign_item + .popover.bottom > .arrow:after {
|
||||
top: 1px;
|
||||
margin-left: -10px;
|
||||
content: " ";
|
||||
border-top-width: 0;
|
||||
border-bottom-color: #fff;
|
||||
}
|
||||
|
||||
.o_sign_sign_item + .popover.left > .arrow {
|
||||
top: 50%;
|
||||
right: -11px;
|
||||
margin-top: -11px;
|
||||
border-right-width: 0;
|
||||
border-left-color: #999;
|
||||
border-left-color: rgba(0, 0, 0, .25);
|
||||
}
|
||||
|
||||
.o_sign_sign_item + .popover.left > .arrow:after {
|
||||
right: 1px;
|
||||
bottom: -10px;
|
||||
content: " ";
|
||||
border-right-width: 0;
|
||||
border-left-color: #fff;
|
||||
}
|
||||
|
||||
.popover .o_sign_delete_field_button {
|
||||
float: right;
|
||||
margin-top: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.popover .o_sign_delete_field_button.fa-trash {
|
||||
cursor: pointer;
|
||||
opacity: 0.5;
|
||||
transition: opacity ease 250ms;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.popover .o_sign_delete_field_button.fa-trash:hover {
|
||||
opacity: 1.0;
|
||||
}
|
||||
|
||||
.o_sign_sign_item + .popover .o_sign_validate_field_button {
|
||||
font-size: 13px;
|
||||
padding: 5px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.o_sign_sign_textarea {
|
||||
white-space: pre-wrap !important;
|
||||
font-size: 12pt !important;
|
||||
}
|
||||
|
||||
.o_sign_sign_item_pdfview {
|
||||
border: 1px dashed silver;
|
||||
box-shadow: 0px 0px 0px 0px white;
|
||||
overflow: visible;
|
||||
background: transparent;
|
||||
color: black;
|
||||
cursor: auto;
|
||||
white-space: pre;
|
||||
text-align: left;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.o_sign_sign_item .o_sign_helper {
|
||||
vertical-align: middle;
|
||||
height: 100%;
|
||||
color: #757575;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.o_sign_sign_item img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
vertical-align: middle;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.o_sign_sign_item .o_sign_config_area {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
font-size: 10pt;
|
||||
text-align: right;
|
||||
padding: 2px;
|
||||
margin: 2px;
|
||||
|
||||
}
|
||||
|
||||
.o_sign_sign_item .o_sign_config_area .fa-arrows {
|
||||
display: block;
|
||||
float: left;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.o_sign_sign_item .o_sign_config_area .o_sign_item_display {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.o_sign_select_options_display {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
margin-top: -22px;
|
||||
align-items: center;
|
||||
justify-content: left;
|
||||
}
|
||||
|
||||
.o_sign_item_option {
|
||||
padding: 5px;
|
||||
/* font-size: 12pt; */
|
||||
}
|
||||
|
||||
.o_sign_option_separator {
|
||||
padding: 5px;
|
||||
/* font-size: 12pt; */
|
||||
}
|
||||
|
||||
.o_sign_selected_option {
|
||||
text-decoration-line: none;
|
||||
}
|
||||
|
||||
.o_sign_not_selected_option {
|
||||
text-decoration-line: line-through;
|
||||
}
|
||||
|
||||
.ui-selected, .ui-selecting { /* jQuery UI */
|
||||
box-shadow: 0px 0px 5px 1px orange;
|
||||
}
|
||||
.ui-resizable {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.ui-draggable .ui-draggable-handle, .o_sign_field_type_button.ui-draggable { /* jQuery UI */
|
||||
cursor: grab;
|
||||
cursor: -moz-grab;
|
||||
cursor: -webkit-grab;
|
||||
}
|
||||
|
||||
.ui-draggable-dragging .ui-draggable-handle,
|
||||
.o_sign_sign_item_to_add.ui-draggable-dragging,
|
||||
.o_sign_sign_item.ui-draggable-dragging { /* jQuery UI */
|
||||
box-shadow: 0 5px 25px -10px black;
|
||||
transition: box-shadow 0.3s;
|
||||
cursor: grabbing;
|
||||
cursor: -moz-grabbing;
|
||||
cursor: -webkit-grabbing;
|
||||
}
|
||||
|
||||
.o_sign_sign_item_navigator {
|
||||
position: fixed;
|
||||
top: 15%;
|
||||
left: 0;
|
||||
background-color: transparent;
|
||||
background-image: url(../../img/navigator.png);
|
||||
background-size: 100% 100%;
|
||||
|
||||
line-height: 50px;
|
||||
height: 50px;
|
||||
font-size: 1.4em;
|
||||
text-transform: uppercase;
|
||||
|
||||
z-index: 90;
|
||||
|
||||
padding: 0 20px 0 10px;
|
||||
color: white;
|
||||
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@media (max-width: 767px) { /* @screen-xs-max */
|
||||
.o_sign_sign_item_navigator {
|
||||
width: 100%;
|
||||
top: 32px !important;
|
||||
z-index: 9999;
|
||||
line-height: 25px;
|
||||
height: 35px;
|
||||
padding: 5px 0 0;
|
||||
font-size: 0.8em;
|
||||
background-image: none;
|
||||
background-color: #875A7B;
|
||||
text-align: center;
|
||||
}
|
||||
.o_sign_sign_item_navline {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.o_sign_sign_item_navline {
|
||||
position: fixed;
|
||||
top: 15%;
|
||||
left: 1%;
|
||||
|
||||
pointer-events: none;
|
||||
z-index: 80;
|
||||
|
||||
width: 99%;
|
||||
height: 25px;
|
||||
border-bottom: 1px dashed silver;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
@media (max-width: 767px) { /* @screen-xs-max */
|
||||
.o_sign_sign_item_navline {
|
||||
line-height: 12.5px;
|
||||
height: 12.5px;
|
||||
}
|
||||
}
|
||||
|
||||
.o_sign_field_type_toolbar {
|
||||
width: 14rem;
|
||||
z-index: 1030;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
background-color: #404040;
|
||||
background-image: url(../../img/texture.png);
|
||||
}
|
||||
|
||||
.o_sign_field_type_button {
|
||||
-webkit-box-sizing: border-box;
|
||||
-moz-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
|
||||
border: none;
|
||||
|
||||
background-color: #00A09D;
|
||||
color: white;
|
||||
|
||||
min-height: 30px;
|
||||
margin: 0.5rem 0.5rem 0 0.5rem;
|
||||
padding: 0.5rem 2rem;
|
||||
}
|
||||
|
||||
.o_sign_field_type_button:hover {
|
||||
transition: transform 200ms ease 0s;
|
||||
box-shadow: 0 8px 15px -10px black;
|
||||
transform: translateX(1px) translateY(-1px) scale(1.05) rotate(1deg);
|
||||
}
|
||||
|
||||
.o_sign_close_button {
|
||||
padding: 0;
|
||||
background-color: transparent;
|
||||
border: 0;
|
||||
float: right;
|
||||
line-height: 1;
|
||||
text-shadow: 0 1px 0 #FFFFFF;
|
||||
opacity: .5;
|
||||
}
|
||||
|
||||
.o_sign_save {
|
||||
width: auto;
|
||||
background: #A4498C;
|
||||
}
|
||||
|
||||
.o_sign_save:hover {
|
||||
width: auto;
|
||||
background: #A4498C;
|
||||
opacity: .8;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.toolbarButton.rotateCw::before {
|
||||
content: url(../../img/secondaryToolbarButton-rotateCw.png);
|
||||
}
|
||||
|
||||
@media screen and (-webkit-min-device-pixel-ratio: 1.1), screen and (min-resolution: 1.1dppx) {
|
||||
.toolbarButton.rotateCw::before {
|
||||
content: url(../../img/secondaryToolbarButton-rotateCw@2x.png);
|
||||
}
|
||||
}
|
||||
|
||||
.o_sign_field_type_toolbar_title {
|
||||
height: 12px;
|
||||
color: white;
|
||||
font-size: 12px;
|
||||
background-color: #474747;
|
||||
background-image: url(../../img/texture.png), -webkit-gradient(linear, left top, left bottom, from(hsla(0,0%,32%,.99)), to(hsla(0,0%,27%,.95)));
|
||||
box-shadow: inset 0 1px 1px hsla(0,0%,0%,.15), inset 0 -1px 0 hsla(0,0%,100%,.05), 0 1px 0 hsla(0,0%,0%,.15), 0 1px 1px hsla(0,0%,0%,.1);
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
#outerContainer.o_sign_field_type_toolbar_visible {
|
||||
margin-left: 14rem;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
#outerContainer.o_sign_field_type_toolbar_visible #thumbnailView {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.o_tooltip > .o_tooltip_content {
|
||||
font-size: 12px !important;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
|
@ -1,73 +0,0 @@
|
|||
odoo.define('odex25_sign.tour', function(require) {
|
||||
"use strict";
|
||||
|
||||
var core = require('web.core');
|
||||
var tour = require('web_tour.tour');
|
||||
|
||||
var _t = core._t;
|
||||
|
||||
tour.register('sign_tour', {
|
||||
url: "/web",
|
||||
rainbowManMessage: "<b>Congratulations</b>, your first document is fully signed!",
|
||||
}, [tour.stepUtils.showAppsMenuItem(), {
|
||||
trigger: '.o_app[data-menu-xmlid="odex25_sign.menu_document"]',
|
||||
content: _t("Let's <b>prepare & sign</b> our first document."),
|
||||
position: 'bottom',
|
||||
edition: 'enterprise'
|
||||
}, {
|
||||
trigger: '.o_nocontent_help p a:contains("' + _t('start with our sample template') + '")',
|
||||
content: _t("Try out this sample contract."),
|
||||
position: "bottom",
|
||||
}, {
|
||||
trigger: 'iframe .o_sign_field_type_toolbar .o_sign_field_type_button:contains("' + _t('Name') + '")',
|
||||
content: _t("<b>Drag & drop “Name”</b> into the document."),
|
||||
position: "right",
|
||||
}, {
|
||||
trigger: 'iframe .o_sign_field_type_toolbar .o_sign_field_type_button:contains("' + _t('Date') + '")',
|
||||
content: _t("<b>Drag & drop “Date”</b> into the document."),
|
||||
position: "right",
|
||||
}, {
|
||||
trigger: 'iframe .o_sign_field_type_toolbar .o_sign_field_type_button:contains("' + _t('Signature') + '")',
|
||||
content: _t("And finally, <b>drag & drop “Signature”</b> into the bottom of the document."),
|
||||
position: "right",
|
||||
}, {
|
||||
trigger: '.o_control_panel .o_sign_template_sign_now',
|
||||
content: _t("Well done, your document is ready!<br>Let's sign it directly."),
|
||||
position: "bottom",
|
||||
}, {
|
||||
trigger: '.modal-dialog button[name="sign_directly_without_mail"]',
|
||||
content: _t("Ok, let’s sign the document now."),
|
||||
position: "bottom",
|
||||
}, {
|
||||
trigger: 'iframe .o_sign_sign_item_navigator',
|
||||
content: _t("Go to the first area you have to fill in."),
|
||||
position: "bottom",
|
||||
}, {
|
||||
trigger: 'iframe .o_sign_sign_item_navigator',
|
||||
alt_trigger: "iframe .o_sign_sign_item[placeholder='" + _t("Date") + "']",
|
||||
content: _t("Your name has been auto-completed. Let’s continue!"),
|
||||
position: "bottom",
|
||||
}, {
|
||||
trigger: 'iframe .o_sign_sign_item_navigator',
|
||||
content: _t("Let’s sign the document!"),
|
||||
position: "bottom",
|
||||
}, {
|
||||
trigger: 'iframe .o_sign_sign_item_navigator',
|
||||
alt_trigger: 'iframe .o_sign_sign_item[data-signature]',
|
||||
content: _t("Draw your most beautiful signature!<br>You can also create one automatically or load a signature from your computer."),
|
||||
position: "bottom",
|
||||
}, {
|
||||
trigger: '.modal-dialog button:contains("' + _t('Adopt and Sign') + '")',
|
||||
content: _t("Confirm and continue."),
|
||||
position: "bottom",
|
||||
}, {
|
||||
trigger: '.o_sign_validate_banner button.o_validate_button',
|
||||
content: _t("Great, the document is signed!<br>Let’s validate it."),
|
||||
position: "top",
|
||||
}, {
|
||||
trigger: '.modal-dialog button:contains("' + _t('View Document') + '")',
|
||||
content: _t("Let's view the document you have just signed!"),
|
||||
position: "bottom",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
|
@ -1,218 +0,0 @@
|
|||
.o_web_client {
|
||||
.o_content {
|
||||
.favorite_sign_button {
|
||||
position: absolute;
|
||||
color: $o-brand-secondary;
|
||||
&:hover {
|
||||
color: orange;
|
||||
}
|
||||
|
||||
&.favorite_sign_button_enabled {
|
||||
color: orange;
|
||||
&:hover {
|
||||
color: theme-color('danger');
|
||||
}
|
||||
}
|
||||
}
|
||||
.o_signers > div {
|
||||
&.o_sign_completed {
|
||||
color: theme-color('success');
|
||||
}
|
||||
}
|
||||
.o_form_view {
|
||||
.o_field_pdfviewer {
|
||||
width: 100%;
|
||||
height: 600px;
|
||||
border: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
.modal-content {
|
||||
|
||||
.o_field_one2many.o_sign_flat_o2m {
|
||||
// Ajust this value to match the standard components below
|
||||
padding-right: 42px;
|
||||
@include media-breakpoint-down(sm) {
|
||||
padding-right: 0;
|
||||
}
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
border: none;
|
||||
}
|
||||
thead {
|
||||
display: none;
|
||||
}
|
||||
tr.o_data_row {
|
||||
border-bottom: none !important;
|
||||
background-color: white !important;
|
||||
cursor: pointer !important;
|
||||
}
|
||||
tbody > tr {
|
||||
border: none;
|
||||
border-bottom: none
|
||||
}
|
||||
tbody > tr:not(.o_data_row) {
|
||||
display: none;
|
||||
}
|
||||
tbody .o_form_uri:first-line {
|
||||
color: #666666 !important;
|
||||
background-color: white;
|
||||
border: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
tbody > tr:nth-child(1) {
|
||||
box-shadow: none !important;
|
||||
border: none !important;
|
||||
}
|
||||
tbody > tr > td:nth-child(1) {
|
||||
width: 150px !important;
|
||||
color:#666666 !important;
|
||||
font-weight: bold;
|
||||
padding-left: 0px;
|
||||
cursor: default !important;
|
||||
border: none;
|
||||
border-bottom: none
|
||||
}
|
||||
tbody > tr > td:nth-child(2) {
|
||||
border-right-style: none;
|
||||
&:not([style]) {
|
||||
border: 1px solid #666666;
|
||||
border-top-style: none;
|
||||
border-right-style: none;
|
||||
border-left-style: none;
|
||||
}
|
||||
}
|
||||
tfoot {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Module pages */
|
||||
.o_sign_template, .o_sign_document {
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
@include o-position-absolute(0, 0, 0, 0);
|
||||
@include media-breakpoint-down(sm) {
|
||||
position: static;
|
||||
.o_sign_pdf_iframe {
|
||||
flex: 0 0 500px;
|
||||
}
|
||||
}
|
||||
background-color: $o-view-background-color;
|
||||
}
|
||||
|
||||
/* Template edit view */
|
||||
.o_sign_template {
|
||||
.fa-pencil {
|
||||
cursor: pointer;
|
||||
opacity: 0.5;
|
||||
transition: opacity ease 250ms;
|
||||
}
|
||||
:hover > .fa-pencil {
|
||||
opacity: 1.0;
|
||||
}
|
||||
|
||||
.o_sign_template_name_input {
|
||||
width: auto;
|
||||
display: inline-block;
|
||||
border: 1px dashed white;
|
||||
&:focus {
|
||||
border: 1px solid silver;
|
||||
}
|
||||
&[disabled] {
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
|
||||
.alert.o_sign_template_saved_info {
|
||||
padding: 5px;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.o_sign_duplicate_sign_template {
|
||||
padding: 0;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.o_sign_create_partner {
|
||||
.fa-exclamation-circle {
|
||||
padding: 0 10px;
|
||||
color: theme-color('danger');
|
||||
}
|
||||
.fa-check-circle {
|
||||
padding-left: 10px;
|
||||
color: theme-color('success');
|
||||
}
|
||||
}
|
||||
|
||||
.o_sign_add_partner {
|
||||
border-bottom: 1px dashed $o-brand-secondary;
|
||||
}
|
||||
|
||||
.o_sign_delete_field_button {
|
||||
float: right;
|
||||
&:before {
|
||||
font-family: FontAwesome;
|
||||
content: "";
|
||||
}
|
||||
}
|
||||
|
||||
#o_sign_pdf_ext {
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.o_sign_resend_access_button {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.o_popover_offset {
|
||||
top: 159px !important;
|
||||
left: 4px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.o_kanban_view {
|
||||
.o_kanban_record {
|
||||
.o_kanban_record_body {
|
||||
.o_signers {
|
||||
max-height: 6.3rem;
|
||||
overflow: auto;
|
||||
}
|
||||
}
|
||||
|
||||
&.o_sign_sticky_bottom {
|
||||
.o_kanban_record_body {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.o_kanban_record_bottom {
|
||||
position: absolute;
|
||||
bottom: 0.5rem;
|
||||
width: 100%;
|
||||
@extend .pr-4;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include media-breakpoint-down(sm) {
|
||||
.o_kanban_view {
|
||||
.o_kanban_record {
|
||||
&.o_sign_sticky_bottom {
|
||||
.o_kanban_record_body {
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
.o_kanban_record_bottom {
|
||||
position: absolute;
|
||||
bottom: 0.5rem;
|
||||
width: 100%;
|
||||
padding-right: 32px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,183 +0,0 @@
|
|||
.o_sign_pdf_iframe {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
flex: 0 1 100%;
|
||||
border: none;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.o_sign_request_reference_title {
|
||||
text-align: left;
|
||||
}
|
||||
.o_sign_signer_status_wrapper{
|
||||
max-height: 76px;
|
||||
overflow: auto;
|
||||
}
|
||||
.o_sign_signer_status {
|
||||
line-height: 32px;
|
||||
div {
|
||||
line-height: $o-line-height-base;
|
||||
}
|
||||
&.clearfix {
|
||||
text-align: right;
|
||||
}
|
||||
img {
|
||||
height: 32px;
|
||||
}
|
||||
}
|
||||
.o_sign_request_from {
|
||||
text-align: left;
|
||||
}
|
||||
.o_sign_request_signer {
|
||||
line-height: 38px;
|
||||
font-size: 1.15rem;
|
||||
margin: 0;
|
||||
}
|
||||
.o_sign_breadcrumb {
|
||||
font-size: 1.25rem !important;
|
||||
a:hover{
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
.o_sign_document_buttons {
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.o_sign_request_follower_name {
|
||||
&:before {
|
||||
content: ', ';
|
||||
}
|
||||
&:first-child:before {
|
||||
content: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* Document view */
|
||||
.o_sign_document {
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
background-color: $o-view-background-color;
|
||||
|
||||
> .container-fluid {
|
||||
flex: 0 0 auto;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
> .container {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.row {
|
||||
&.o_sign_page_info {
|
||||
max-height: 33vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
> div {
|
||||
> .o_page_header, > .alert {
|
||||
margin: 10px 0;
|
||||
}
|
||||
}
|
||||
|
||||
> .o_sign_sign_document_button {
|
||||
margin: 0 $o-horizontal-padding;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.o_sign_image_document {
|
||||
display: inline-block;
|
||||
max-width: 100%;
|
||||
margin-top: $o-horizontal-padding;
|
||||
|
||||
> img {
|
||||
min-width: 100px;
|
||||
}
|
||||
}
|
||||
|
||||
.o_sign_validate_banner {
|
||||
display: none;
|
||||
opacity: 0;
|
||||
|
||||
@include o-position-absolute($bottom: 0, $left: 0);
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
|
||||
background-color: $o-brand-odoo;
|
||||
color: white;
|
||||
font-size: 1.25em;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.modal.o_sign_thank_you_dialog {
|
||||
border: none;
|
||||
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.6), 0 6px 20px 0 rgba(0, 0, 0, 0.1);
|
||||
text-align: left;
|
||||
.btn {
|
||||
border-radius: 0;
|
||||
border: none;
|
||||
}
|
||||
.modal-header {
|
||||
border-top-left-radius: 0.0rem;
|
||||
border-top-right-radius: 0.0rem;
|
||||
background-color: $o-brand-odoo;
|
||||
color: #fff;
|
||||
.modal-title {
|
||||
line-height: 1;
|
||||
color: #fff;
|
||||
}
|
||||
.text-muted {
|
||||
color: #fff !important;
|
||||
}
|
||||
.close {
|
||||
color: #fff;
|
||||
opacity: 1;
|
||||
text-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
color: $o-main-text-color;
|
||||
.o_thankyou_message {
|
||||
margin-top: 14px;
|
||||
}
|
||||
.o_thankyou_link {
|
||||
margin-top: 25px;
|
||||
> img {
|
||||
width: 30px;
|
||||
}
|
||||
a {
|
||||
color: $o-main-text-color;
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
color: $o-brand-primary;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
border-top: none;
|
||||
text-align: left;
|
||||
display: block;
|
||||
&:empty {
|
||||
padding: 0px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.modal.o_sign_signature_dialog {
|
||||
.modal-footer {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
}
|
||||
@media only screen and (max-width:650px) {
|
||||
.mobile-hide{
|
||||
display: none !important;
|
||||
}
|
||||
.flex-fill {
|
||||
flex: none;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1,80 +0,0 @@
|
|||
|
||||
.o_sign_document {
|
||||
height: 100%;
|
||||
|
||||
> header {
|
||||
flex: 0 0 auto;
|
||||
height: $nav-link-height;
|
||||
background-color: $o-brand-lightsecondary;
|
||||
@include media-breakpoint-down(sm) {
|
||||
height: initial;
|
||||
min-height: $nav-link-height;
|
||||
}
|
||||
> div {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@include media-breakpoint-down(sm) {
|
||||
flex-direction: column;
|
||||
}
|
||||
a {
|
||||
flex: 0 0 auto;
|
||||
> img {
|
||||
height: $nav-link-height;
|
||||
}
|
||||
}
|
||||
|
||||
.o_sign_header_instruction {
|
||||
font-size: 1.2em;
|
||||
text-align: center;
|
||||
}
|
||||
.o_logo {
|
||||
text-align: left;
|
||||
}
|
||||
.o_odoo {
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
}
|
||||
@include media-breakpoint-down(sm) {
|
||||
.o_sign_page_info .col-lg-6 {
|
||||
padding: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include media-breakpoint-down(sm) {
|
||||
.modal.o_sign_signature_dialog {
|
||||
h1 {
|
||||
font-size: $h1-font-size * 3 / 4;
|
||||
}
|
||||
|
||||
h2, h3, h4, h5, h6 {
|
||||
font-size: $o-font-size-base-touch;
|
||||
}
|
||||
|
||||
.card.o_web_sign_signature_group {
|
||||
padding: 0;
|
||||
|
||||
.row {
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.o_sign_portal {
|
||||
.o_portal_category {
|
||||
color: $o-enterprise-color;
|
||||
font-weight:bold;
|
||||
}
|
||||
.o_sign_button {
|
||||
padding-right: 75%;
|
||||
}
|
||||
.o_sign_button_content{
|
||||
font-size: 1.25rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1,108 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
<t t-name="odex25_sign.sign_item_custom_popover">
|
||||
<div class="o_sign_item_custom_popover">
|
||||
<div class="form-group clearfix o_popover_placeholder" t-if="widget.debug">
|
||||
<label for="o_sign_name">Placeholder</label>
|
||||
<div>
|
||||
<input type="text" id="o_sign_name" class="o_input"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group o_sign_options_group">
|
||||
<label for="o_sign_select_options_input">Options</label>
|
||||
<div class="o_sign_options_select">
|
||||
<input id="o_sign_options_select_input" type="hidden"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="o_sign_responsible_select_input">Filled by</label>
|
||||
<div class="o_sign_responsible_select">
|
||||
<select id="o_sign_responsible_select_input"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div>
|
||||
<div class="checkbox">
|
||||
<label for="o_sign_required_field">
|
||||
<input type="checkbox" id="o_sign_required_field"/> Mandatory field
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<button class="btn o_sign_validate_field_button btn-primary">Validate</button>
|
||||
<span class="fa fa-trash fa-lg o_sign_delete_field_button"></span>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<t t-name="odex25_sign.initial_all_pages_dialog">
|
||||
<div class="form-group">
|
||||
<label for="responsible_select_initials_input" class="col-md-2">Responsible</label>
|
||||
<div class="o_sign_responsible_select_initials col-md-10">
|
||||
<select id="responsible_select_initials_input"/>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
<t t-name="sign.template_cp_buttons">
|
||||
<t t-if="action_type===''||action_type==='sign_send_request'">
|
||||
<button type="button" class="btn btn-primary mr-2 o_sign_template_send">Send</button>
|
||||
<button type="button" class="btn btn-primary mr-2 o_sign_template_sign_now">Sign Now</button>
|
||||
<t t-if="action_type!=='sign_send_request' && widget.sign_template.sign_request_ids.length<=1">
|
||||
<button type="button" class="btn btn-secondary mr-2 o_sign_template_share">Share</button>
|
||||
</t>
|
||||
</t>
|
||||
<t t-elif="action_type==='sign_template_edit'">
|
||||
<button type="button" class="btn btn-primary mr-2 o_sign_template_save">Save</button>
|
||||
</t>
|
||||
<t t-if="widget.has_sign_requests && widget.isPDF">
|
||||
<div class="alert o_duplicate d-inline">
|
||||
<span class="fa fa-exclamation-triangle"/> <button type="button" class="o_sign_template_duplicate btn btn-link mb-1 p-0">Duplicate</button> this template to modify it.
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
<t t-name="sign.template">
|
||||
<div t-att-class="(widget.has_sign_requests && widget.isPDF) ? 'd-none' : ''">
|
||||
<div class="o_sign_template_header_wrapper px-4 py-2 align-items-center d-flex">
|
||||
<div class="o_sign_template_header_document_name align-items-center d-flex mr-auto">
|
||||
<span t-if="!widget.has_sign_requests" class="fa fa-pencil mr-2" title="Edit template name" role="img" aria-label="Edit template name"/>
|
||||
<input type="text" class="o_sign_template_name_input o_input" t-att-value="widget.sign_template.display_name"/>
|
||||
</div>
|
||||
|
||||
<div t-if="widget.debug" class="o_sign_template_privacy_and_save align-items-center d-flex">
|
||||
<label for="o_sign_template_privacy" class="p-0 m-0 mr-2">Who can Sign:</label>
|
||||
<div id="o_sign_template_privacy" class="o_sign_template_privacy"/>
|
||||
<div class="o_sign_template_saved_info alert alert-success m-0 ml-2" role="status"><span class="fa fa-check"/>Saved</div>
|
||||
</div>
|
||||
|
||||
<div t-if="widget.debug" class="o_sign_template_group_id_and_save align-items-center d-flex">
|
||||
<label for="o_sign_template_group_id" class="p-0 m-0 mr-2">Template Access Group:</label>
|
||||
<div id="o_sign_template_groupe_id" class="o_sign_template_group_id"/>
|
||||
<div class="o_sign_template_saved_info alert alert-success m-0 ml-2" role="status"><span class="fa fa-check"/>Saved</div>
|
||||
</div>
|
||||
|
||||
<div class="o_sign_template_tags_and_save align-items-center d-flex">
|
||||
<label for="o_sign_template_tags" class="p-0 m-0 mr-2">Tags:</label>
|
||||
<div id="o_sign_template_tags" class="o_sign_template_tags"/>
|
||||
<div class="o_sign_template_saved_info alert alert-success m-0 ml-2" role="status"><span class="fa fa-check"/>Saved</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<t t-if="widget.isPDF">
|
||||
<iframe class="o_sign_pdf_iframe"/>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<div class="o_sign_image_document">
|
||||
<t t-set="webimage" t-value="new RegExp('image.*(gif|jpe|jpg|png)').test(widget.sign_template.attachment_id.mimetype)"/>
|
||||
<img t-if="webimage" class="img img-fluid" t-attf-src="/web/image/{{widget.sign_template.attachment_id.id}}" alt="Signature"/>
|
||||
<div t-if="!webimage" class="o_image" t-att-data-mimetype="widget.sign_template.attachment_id.mimetype"/>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
<t t-name="odex25_sign.type_buttons">
|
||||
<div class="o_sign_field_type_toolbar_title d-flex justify-content-center align-items-center">Fields</div>
|
||||
<t t-foreach="sign_item_types" t-as="item_type">
|
||||
<button type="button" class="o_sign_field_type_button btn btn-primary flex-shrink-0" t-att-data-item-type-id="item_type.id" title="Drag & Drop a field in the PDF"><t t-esc="item_type.name"/></button>
|
||||
</t>
|
||||
</t>
|
||||
</templates>
|
||||
|
|
@ -1,95 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
<t t-name="odex25_sign.sign_item" name="Signature Item">
|
||||
<t t-if="readonly">
|
||||
<!-- TEMPLATE EDITION or REQUEST DISPLAY -->
|
||||
<div t-if="type == 'signature' || type == 'initial'" t-att-title="role" class="o_sign_sign_item" style="text-align: center;" t-att-data-signature="value"><span class="o_sign_helper"/> <img t-if="value" t-att-src="value" alt="Signature"/> <t t-if="!value"><span class="o_placeholder"><t t-esc="placeholder"/></span></t> <t t-if="editMode" t-call="odex25_sign.sign_item_configuration"/></div>
|
||||
<div t-if="type == 'text'" t-att-title="role" class="o_sign_sign_item"><t t-if="!value"><span class="o_placeholder"><t t-esc="placeholder"/></span></t><t t-esc="value"/><t t-if="editMode" t-call="odex25_sign.sign_item_configuration"/></div>
|
||||
<div t-if="type == 'textarea'" t-att-title="role" class="o_sign_sign_item o_sign_sign_textarea"><t t-if="!value"><span class="o_placeholder"><t t-esc="placeholder"/></span></t><t t-esc="value"/><t t-if="editMode" t-call="odex25_sign.sign_item_configuration"/></div>
|
||||
<div t-if="type == 'checkbox'" t-att-title="role" class="o_sign_sign_item" style="margin: 2px; padding:2px"><t t-if="value == 'on'">☑</t><t t-if="value == 'off'">☐</t><t t-if="!value"><span class="o_placeholder">☑</span></t><t t-if="editMode" t-call="odex25_sign.sign_item_configuration"/></div>
|
||||
<div t-if="type == 'selection'" t-att-title="role" class="o_sign_sign_item" style="white-space: normal;text-align: center;"><t t-if="!value"><span class="o_placeholder"><t t-esc="placeholder"/></span></t><div class="o_sign_select_options_display"/><t t-if="editMode" t-call="odex25_sign.sign_item_configuration"/></div>
|
||||
</t>
|
||||
|
||||
<t t-if="!readonly">
|
||||
<!-- SIGN SESSION : filling the signature values -->
|
||||
<button t-if="type == 'signature' || type == 'initial'" t-att-title="role" class="o_sign_sign_item text-center" style="color:#757575;" t-att-data-signature="value"><span class="o_sign_helper"/><img t-if="value" t-att-src="value" alt="Signature"/> <t t-if="!value"><span class="o_placeholder"><t t-esc="placeholder"/></span></t></button>
|
||||
<input t-if="type == 'text'" t-att-title="role" type="text" class="o_sign_sign_item" t-att-placeholder="placeholder" t-att-value="value"/>
|
||||
<input t-if="type == 'checkbox' and value == 'on'" t-att-title="role" type="checkbox" class="o_sign_sign_item" checked="1"/>
|
||||
<input t-elif="type == 'checkbox'" t-att-title="role" type="checkbox" class="o_sign_sign_item"/>
|
||||
<textarea t-if="type == 'textarea'" t-att-title="role" class="o_sign_sign_item" t-att-placeholder="placeholder" t-att-value="value" t-esc="value"/>
|
||||
<div t-if="type == 'selection'" t-att-title="role" class="o_sign_sign_item" style="white-space: normal;color: #757575;" t-att-value="value"><div class="o_sign_select_options_display"/></div>
|
||||
</t>
|
||||
</t>
|
||||
|
||||
<div t-name="odex25_sign.sign_item_configuration" class="o_sign_config_area">
|
||||
<span class="fa fa-arrows" role="img" aria-label="Signature configuration" title="Signature configuration"/>
|
||||
<div class="o_sign_item_display">
|
||||
<!-- Don't display role for checkbox. We don't have place-->
|
||||
<t t-if="type != 'checkbox'"><span class="o_sign_responsible_display"/></t>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Signing part -->
|
||||
<div t-name="odex25_sign.signature_dialog">
|
||||
<div class="o_web_sign_name_and_signature"/>
|
||||
<div class="mt16 small">By clicking Adopt and Sign, I agree that the chosen signature/initials will be a valid electronic representation of my hand-written signature/initials for all purposes when it is used on documents, including legally binding contracts.</div>
|
||||
</div>
|
||||
|
||||
<div t-name="odex25_sign.public_signer_dialog">
|
||||
<div class="form-group row">
|
||||
<label for="o_sign_public_signer_name_input" class="col-lg-3 col-form-label">Your name</label>
|
||||
<div class="col-lg-9">
|
||||
<input type="text" id="o_sign_public_signer_name_input" placeholder="Your name" class="form-control"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label for="o_sign_public_signer_mail_input" class="col-lg-3 col-form-label">Your email</label>
|
||||
<div class="col-lg-9">
|
||||
<input type="email" id="o_sign_public_signer_mail_input" placeholder="Your email" class="form-control"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div t-name="odex25_sign.public_sms_signer">
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-3 col-form-label" for="phone">Phone Number</label>
|
||||
<div class="col-sm">
|
||||
<div class="input-group">
|
||||
<input type="text" name="phone" id="o_sign_phone_number_input" placeholder="e.g. +1 415 555 0100" class="form-control" t-att-value="widget.signerPhone"/>
|
||||
<button class='input-group-append btn btn-sm btn-primary o_sign_resend_sms'>Send SMS</button>
|
||||
</div>
|
||||
<span class="text-muted form-text">A SMS will be sent to the following phone number. Please update it if it's not relevant.</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-3 col-form-label" for="validation_code">Validation Code</label>
|
||||
<div class="col-sm">
|
||||
<input type="text" name="validation_code" id="o_sign_public_signer_sms_input" placeholder="e.g. 314159" class="form-control"/>
|
||||
<span class="text-muted form-text">Enter the code received through SMS to complete your signature</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div t-name="odex25_sign.public_password">
|
||||
<div class="form-group">
|
||||
<span>Your file is encrypted, PDF's password is required to generate final document. The final document will be encrypted with the same password.</span>
|
||||
<div>
|
||||
<input type="password" id="o_sign_public_signer_password_input" class="form-control"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div t-name="odex25_sign.thank_you_dialog">
|
||||
<div class="o_thankyou_message">You will receive a copy of the signed document by mail.</div>
|
||||
<div class="o_thankyou_message" t-if="widget.has_next_document">Other documents have to be signed.</div>
|
||||
</div>
|
||||
|
||||
<div t-name="odex25_sign.no_pub_thank_you_dialog" t-extend="odex25_sign.thank_you_dialog">
|
||||
<t t-jquery=".o_promote_esign" t-operation="replace"/>
|
||||
</div>
|
||||
|
||||
<div t-name="odex25_sign.next_direct_sign_dialog">
|
||||
<div class="o_nextdirectsign_message">We will send you this document by email once everyone has signed.<br/>
|
||||
</div>
|
||||
</div>
|
||||
</templates>
|
||||
|
|
@ -1,154 +0,0 @@
|
|||
odoo.define('odex25_sign.document_backend_tests', function (require) {
|
||||
"use strict";
|
||||
|
||||
var FormView = require('web.FormView');
|
||||
const framework = require('web.framework');
|
||||
var testUtils = require('web.test_utils');
|
||||
|
||||
var createActionManager = testUtils.createActionManager;
|
||||
var createView = testUtils.createView;
|
||||
|
||||
const DocumentBackend = require('odex25_sign.DocumentBackend');
|
||||
|
||||
QUnit.module('document_backend_tests', {
|
||||
beforeEach: function () {
|
||||
this.data = {
|
||||
'partner': {
|
||||
fields: {
|
||||
display_name: { string: "Displayed name", type: "char" },
|
||||
template_id: {string: "Template", type: "many2one", relation: 'sign.template'},
|
||||
},
|
||||
records: [{
|
||||
id: 1,
|
||||
display_name: "some record",
|
||||
template_id: 1,
|
||||
}],
|
||||
},
|
||||
'sign.template': {
|
||||
fields: {
|
||||
display_name: {string: "Template Name", type: "char"}
|
||||
},
|
||||
records: [{
|
||||
id: 1, display_name: "some template",
|
||||
}],
|
||||
},
|
||||
};
|
||||
}
|
||||
}, function () {
|
||||
QUnit.test('simple rendering', async function (assert) {
|
||||
assert.expect(4);
|
||||
|
||||
const hasFinishedProm = testUtils.makeTestPromise();
|
||||
testUtils.mock.patch(framework, {
|
||||
blockUI: function () {
|
||||
assert.step('blockUI');
|
||||
},
|
||||
unblockUI: function () {
|
||||
assert.step('unblockUI');
|
||||
hasFinishedProm.resolve();
|
||||
}
|
||||
});
|
||||
|
||||
var actionManager = await createActionManager({
|
||||
actions: [{
|
||||
id: 9,
|
||||
name: 'A Client Action',
|
||||
tag: 'odex25_sign.Document',
|
||||
type: 'ir.actions.client',
|
||||
context: {id: 5, token: 'abc'},
|
||||
}],
|
||||
mockRPC: function (route) {
|
||||
if (route === '/odex25_sign/get_document/5/abc') {
|
||||
return Promise.resolve('<span>def</span>');
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
await actionManager.doAction(9);
|
||||
await hasFinishedProm;
|
||||
|
||||
assert.verifySteps(['blockUI', 'unblockUI']);
|
||||
|
||||
assert.strictEqual(actionManager.$('.o_sign_document').text().trim(), 'def',
|
||||
'should display text from server');
|
||||
|
||||
actionManager.destroy();
|
||||
testUtils.mock.unpatch(framework);
|
||||
});
|
||||
|
||||
QUnit.test('do not crash when leaving the action', async function (assert) {
|
||||
assert.expect(0);
|
||||
|
||||
const proms = [];
|
||||
testUtils.mock.patch(DocumentBackend, {
|
||||
_init_page() {
|
||||
const prom = this._super.apply(this, arguments);
|
||||
proms.push(prom);
|
||||
return prom;
|
||||
}
|
||||
});
|
||||
|
||||
var actionManager = await createActionManager({
|
||||
actions: [{
|
||||
id: 9,
|
||||
name: 'A Client Action',
|
||||
tag: 'odex25_sign.Document',
|
||||
type: 'ir.actions.client',
|
||||
context: {id: 5, token: 'abc'},
|
||||
}],
|
||||
mockRPC: function (route) {
|
||||
if (route === '/odex25_sign/get_document/5/abc') {
|
||||
return Promise.resolve('<span>def</span>');
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
await actionManager.doAction(9);
|
||||
await actionManager.doAction(9);
|
||||
await Promise.all(proms);
|
||||
|
||||
actionManager.destroy();
|
||||
testUtils.mock.unpatch(DocumentBackend);
|
||||
});
|
||||
|
||||
QUnit.test('search more in many2one pointing to sign.template model', async function (assert) {
|
||||
// Addon sign patches the ListController for some models, like 'sign.template'.
|
||||
assert.expect(1);
|
||||
|
||||
this.data['sign.template'].records = this.data['sign.template'].records.concat([
|
||||
{id: 11, display_name: "Template 11"},
|
||||
{id: 12, display_name: "Template 12"},
|
||||
{id: 13, display_name: "Template 13"},
|
||||
{id: 14, display_name: "Template 14"},
|
||||
{id: 15, display_name: "Template 15"},
|
||||
{id: 16, display_name: "Template 16"},
|
||||
{id: 17, display_name: "Template 17"},
|
||||
]);
|
||||
|
||||
var form = await createView({
|
||||
View: FormView,
|
||||
model: 'partner',
|
||||
data: this.data,
|
||||
arch: '<form><field name="template_id"/></form>',
|
||||
archs: {
|
||||
'sign.template,false,list': '<tree><field name="display_name"/></tree>',
|
||||
'sign.template,false,search': '<search></search>',
|
||||
},
|
||||
});
|
||||
|
||||
await testUtils.fields.many2one.clickOpenDropdown('template_id');
|
||||
await testUtils.fields.many2one.clickItem('template_id', 'Search');
|
||||
|
||||
await testUtils.dom.click($('.modal .o_data_row:first'));
|
||||
|
||||
assert.strictEqual(form.$('.o_field_widget[name=template_id] input').val(), 'some template');
|
||||
|
||||
form.destroy();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
|
@ -1,92 +0,0 @@
|
|||
odoo.define('sign_widgets_tour', function (require) {
|
||||
"use strict";
|
||||
|
||||
var core = require('web.core');
|
||||
var tour = require('web_tour.tour');
|
||||
|
||||
var _t = core._t;
|
||||
|
||||
tour.register('sign_widgets_tour', {
|
||||
test: true,
|
||||
url: "/web",
|
||||
},
|
||||
[tour.stepUtils.showAppsMenuItem(),
|
||||
{
|
||||
content: "Settings",
|
||||
trigger: 'a[data-menu-xmlid="base.menu_administration"]',
|
||||
run: 'click',
|
||||
},
|
||||
{
|
||||
content: "Users",
|
||||
trigger: 'div#invite_users button.o_web_settings_access_rights',
|
||||
run: 'click',
|
||||
},
|
||||
{
|
||||
content: "Mitchel",
|
||||
trigger: 'table.o_list_table td.o_data_cell:contains(Admin)',
|
||||
run: 'click',
|
||||
},
|
||||
|
||||
{
|
||||
content: "Preference tab",
|
||||
trigger: 'a.nav-link:contains("' + _t("Preferences") + '")',
|
||||
run: 'click',
|
||||
},
|
||||
{
|
||||
content: "Check widget sign is present",
|
||||
trigger: '.o_signature',
|
||||
},
|
||||
{
|
||||
content: "Edit Mitchell",
|
||||
trigger: 'div.o_cp_buttons .o_form_button_edit',
|
||||
run: 'click',
|
||||
},
|
||||
{
|
||||
content: "Click on widget sign",
|
||||
trigger: '.o_signature:first',
|
||||
run: 'click',
|
||||
},
|
||||
{
|
||||
content: "Click on auto button",
|
||||
trigger: '.o_web_sign_auto_button',
|
||||
run: 'click',
|
||||
},
|
||||
{
|
||||
content: "Click on style button",
|
||||
trigger: 'div.o_web_sign_auto_select_style > a',
|
||||
run: 'click',
|
||||
},
|
||||
{
|
||||
content: "Select a style",
|
||||
trigger: 'div.o_web_sign_auto_font_list > a:nth-child(3)',
|
||||
run: 'click',
|
||||
},
|
||||
{
|
||||
content: "Click on style button",
|
||||
trigger: 'div.o_web_sign_auto_select_style > a',
|
||||
run: 'click',
|
||||
},
|
||||
{
|
||||
content: "Select a style",
|
||||
trigger: 'div.o_web_sign_auto_font_list > a:nth-child(2)',
|
||||
run: 'click',
|
||||
},
|
||||
{
|
||||
content: "Sign",
|
||||
trigger: 'button.btn-primary:contains("Adopt and Sign")',
|
||||
run: 'click',
|
||||
extra_trigger: 'canvas.jSignature',
|
||||
run: function () {
|
||||
setTimeout(() => {
|
||||
this.$anchor.click();
|
||||
}, 1000);
|
||||
},
|
||||
},
|
||||
{
|
||||
content: "Save Mitchell",
|
||||
trigger: 'button.o_form_button_save',
|
||||
run: 'click',
|
||||
},
|
||||
]
|
||||
);
|
||||
});
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
# import tests
|
||||
from . import test_ui
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
import odoo.tests
|
||||
|
||||
|
||||
@odoo.tests.tagged('-at_install', 'post_install')
|
||||
class TestUi(odoo.tests.HttpCase):
|
||||
def test_ui(self):
|
||||
self.start_tour("/web", 'sign_widgets_tour', login='admin')
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="res_partner_view_form" model="ir.ui.view">
|
||||
<field name="name">res.partner.form.odex25_sign.inherit</field>
|
||||
<field name="model">res.partner</field>
|
||||
<field name="inherit_id" ref="base.view_partner_form"/>
|
||||
<field name="groups_id" eval="[(4, ref('odex25_sign.group_sign_user'))]"/>
|
||||
<field name="arch" type="xml">
|
||||
<div name="button_box" position="inside">
|
||||
<button name="open_signatures" type="object" class="oe_stat_button" icon="fa-pencil"
|
||||
attrs="{'invisible': ['|', ('signature_count', '=', 0), ('is_company', '=', True)]}">
|
||||
<div class="o_field_widget o_stat_info">
|
||||
<span class="o_stat_value"><field name="signature_count"/></span>
|
||||
<span class="o_stat_text">Signature Requested</span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="res_users_view_form" model="ir.ui.view">
|
||||
<field name="name">res.users.form.odex25_sign.inherit</field>
|
||||
<field name="model">res.users</field>
|
||||
<field name="inherit_id" ref="base.view_users_form"/>
|
||||
<field name="priority">140</field>
|
||||
<field name="arch" type="xml">
|
||||
<group name="messaging" position="after">
|
||||
<group string="Digital Signatures" name="sign" groups="base.group_system">
|
||||
<field name="sign_signature" widget="signature" options="{'full_name': 'display_name', 'size': ['',200]}"/>
|
||||
<field name="sign_initials" widget="signature" options="{'size': [200,'']}"/>
|
||||
</group>
|
||||
</group>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<odoo>
|
||||
<record id="sign_log_view_tree" model="ir.ui.view">
|
||||
<field name="name">sign.log.tree</field>
|
||||
<field name="model">sign.log</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="Logs">
|
||||
<field name="log_date"/>
|
||||
<field name="action"/>
|
||||
<field name="partner_id"/>
|
||||
<field name="latitude"/>
|
||||
<field name="longitude"/>
|
||||
<field name="ip"/>
|
||||
<!-- who can acces what ?? groups on the fields for the moment -->
|
||||
<field name="request_state" optional="hide"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<template id="compiled_assets_pdf_iframe" name="Sign PDF IFrame Viewer">
|
||||
<t t-call-assets="odex25_sign.assets_pdf_iframe"/>
|
||||
</template>
|
||||
|
||||
<template id="assets_pdf_iframe" name="Sign PDF IFrame Viewer Assets">
|
||||
|
||||
<!-- lib -->
|
||||
<link rel="stylesheet" type="text/css" href="/web/static/lib/fontawesome/css/font-awesome.css"/>
|
||||
<link rel="stylesheet" type="text/css" href="/web/static/lib/jquery.ui/jquery-ui.css"/>
|
||||
<link rel="stylesheet" type="text/css" href="/web/static/lib/select2/select2.css"/>
|
||||
|
||||
<!-- odoo utils -->
|
||||
<link rel="stylesheet" type="text/css" href="/web/static/lib/bootstrap/scss/_functions.scss"/>
|
||||
<link rel="stylesheet" type="text/css" href="/web/static/lib/bootstrap/scss/_variables.scss"/>
|
||||
<link rel="stylesheet" type="text/css" href="/web/static/lib/bootstrap/scss/mixins/_breakpoints.scss"/>
|
||||
<link rel="stylesheet" type="text/css" href="/web/static/lib/bootstrap/scss/mixins/_grid-framework.scss"/>
|
||||
<link rel="stylesheet" type="text/css" href="/web/static/lib/bootstrap/scss/mixins/_grid.scss"/>
|
||||
<link rel="stylesheet" type="text/css" href="/web/static/lib/bootstrap/scss/utilities/_display.scss"/>
|
||||
<link rel="stylesheet" type="text/css" href="/web/static/lib/bootstrap/scss/_grid.scss"/>
|
||||
<link rel="stylesheet" type="text/css" href="/web/static/lib/bootstrap/scss/utilities/_flex.scss"/>
|
||||
<link rel="stylesheet" type="text/css" href="/web/static/src/scss/bs_mixins_overrides.scss"/>
|
||||
<link rel="stylesheet" type="text/css" href="/web/static/lib/bootstrap/scss/mixins/_deprecate.scss"/>
|
||||
<link rel="stylesheet" type="text/css" href="/web/static/lib/bootstrap/scss/mixins/_size.scss"/>
|
||||
<link rel="stylesheet" type="text/css" href="/web/static/src/scss/utils.scss"/>
|
||||
<link rel="stylesheet" type="text/css" href="/web/static/src/scss/primary_variables.scss"/>
|
||||
<link rel="stylesheet" type="text/css" href="/web_enterprise/static/src/scss/primary_variables.scss"/>
|
||||
<link rel="stylesheet" type="text/css" href="/web_tour/static/src/scss/tip.scss"/>
|
||||
<link rel="stylesheet" type="text/css" href="/web_tour/static/src/scss/keyframes.scss"/>
|
||||
|
||||
<!-- integration -->
|
||||
<link rel="stylesheet" type="text/css" href="/odex25_sign/static/src/css/iframe.css"/>
|
||||
|
||||
</template>
|
||||
</odoo>
|
||||
|
|
@ -1,156 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<template id="portal_my_home_menu_sign" name="Portal layout : sign menu entries" inherit_id="portal.portal_breadcrumbs" priority="60">
|
||||
<xpath expr="//ol[hasclass('o_portal_submenu')]" position="inside">
|
||||
<li t-if="page_name == 'signatures' or my_sign_item" t-attf-class="breadcrumb-item #{'active ' if not sign_requests else ''}">
|
||||
<a t-if="my_sign_item" t-attf-href="/my/signatures?{{ keep_query() }}">Signatures</a>
|
||||
<t t-else="">Signatures</t>
|
||||
</li>
|
||||
<li t-if="my_sign_item" class="breadcrumb-item active">
|
||||
<span t-field="my_sign_item.reference"/>
|
||||
</li>
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
<template id="portal_my_home_sign" name="Show Signatures" customize_show="True" inherit_id="portal.portal_my_home" priority="60">
|
||||
<xpath expr="//div[hasclass('o_portal_docs')]" position="inside">
|
||||
<t t-call="portal.portal_docs_entry">
|
||||
<t t-set="title">Signatures</t>
|
||||
<t t-set="url" t-value="'/my/signatures'"/>
|
||||
<t t-set="placeholder_count" t-value="'sign_count'"/>
|
||||
</t>
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
<template id="sign_portal_my_requests" name="My Signatures">
|
||||
<t t-call="portal.portal_layout">
|
||||
<t t-set="breadcrumbs_searchbar" t-value="True"/>
|
||||
<t t-call="portal.portal_searchbar">
|
||||
<t t-set="title">Signatures</t>
|
||||
</t>
|
||||
<div t-if="not grouped_signatures" class="alert alert-warning mt8" role="alert">
|
||||
There are no signatures request.
|
||||
</div>
|
||||
<t t-if="grouped_signatures" t-call="portal.portal_table">
|
||||
<t t-foreach="grouped_signatures" t-as="signature_requests">
|
||||
<thead>
|
||||
<tr t-attf-class="{{'thead-light' if not groupby == 'none' else ''}}">
|
||||
<th t-if="groupby == 'none'">Document</th>
|
||||
<th t-else="">
|
||||
<span t-field="signature_requests[0].sudo().state"/>
|
||||
</th>
|
||||
<th class="text-center">Signature Date</th>
|
||||
<th class="text-right">Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr t-foreach="signature_requests" t-as="sign_request_item">
|
||||
<td>
|
||||
<a t-attf-href="/my/signature/#{sign_request_item.id}?{{ keep_query() }}">
|
||||
<t t-esc="sign_request_item.reference"/>
|
||||
</a>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<span t-field="sign_request_item.signing_date"/>
|
||||
</td>
|
||||
<td class="text-right">
|
||||
<span class="badge badge-pill badge-info"
|
||||
t-field="sign_request_item.sudo().sign_request_id.state"/>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</t>
|
||||
</t>
|
||||
</t>
|
||||
</template>
|
||||
<template id="sign_portal_my_request" name="My Signature">
|
||||
<t t-call="portal.portal_layout">
|
||||
<t t-set="o_portal_fullwidth_alert" groups="odex25_sign.access_sign_request_item_all">
|
||||
<t t-call="portal.portal_back_in_edit_mode">
|
||||
<t t-set="backend_url" t-value="'/web#model=sign.request&id=%s&view_type=form' % (sign.request.id)"/>
|
||||
</t>
|
||||
</t>
|
||||
<t t-call="portal.portal_record_layout">
|
||||
<t t-set="card_header">
|
||||
<div class="row no-gutters">
|
||||
<div class="col-md">
|
||||
<h5 class="mb-1 mb-md-0">
|
||||
<span t-field="my_sign_item.reference"/>
|
||||
</h5>
|
||||
</div>
|
||||
<div class="col-md text-md-right">
|
||||
<span t-field="my_sign_item.sign_request_id.state" class=" badge badge-pill badge-info"
|
||||
title="Current status of the signature request"/>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
<t t-set="card_body">
|
||||
<div class="o_sign_portal">
|
||||
<div class="o_sign_button mb-4 text-center align-items-center">
|
||||
<a t-att-href="url" class="btn btn-primary btn-block">
|
||||
<div class="o_sign_button_content" t-if="my_sign_item.state == 'completed'">View Document</div>
|
||||
<div class="o_sign_button_content" t-else="">Sign</div>
|
||||
</a>
|
||||
</div>
|
||||
<div class="row mb-4" t-if="my_sign_item.partner_id">
|
||||
<div class="col-12 col-md-6 pb-2">
|
||||
<span class="o_portal_category">Summary</span>
|
||||
<div>
|
||||
<strong>Creation Date:</strong>
|
||||
<span t-field="my_sign_item.create_date" t-options="{'widget': 'date'}"/>
|
||||
</div>
|
||||
<div>
|
||||
<t t-if="my_sign_item.sign_request_id.state != 'signed'">
|
||||
<t t-foreach="my_sign_item.sign_request_id.request_item_ids" t-as="sign">
|
||||
<div t-if="odex25_sign.state != 'completed'" class="_status clearfix" t-att-data-id="odex25_sign.id">
|
||||
<b><t t-esc="odex25_sign.partner_id.name if odex25_sign.partner_id else 'Public user'"/></b>
|
||||
<t t-if="odex25_sign.role_id"><t t-esc="' - ' + odex25_sign.role_id.name"/></t> - <em>Waiting Signature</em>
|
||||
<em t-if="odex25_sign.state != 'sent'"><br/>(the email access has not been sent)</em>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
<t t-if="my_sign_item.sign_request_id.nb_closed > 0">
|
||||
<t t-foreach="my_sign_item.sign_request_id.request_item_ids" t-as="sign">
|
||||
<div t-if="odex25_sign.state == 'completed'" class="_status clearfix"><b>
|
||||
<t t-esc="odex25_sign.partner_id.name"/></b><t t-if="odex25_sign.role_id">
|
||||
<t t-esc="' - ' + odex25_sign.role_id.name"/></t> - <em>Signed on <span t-field="odex25_sign.signing_date"></span></em>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
<div class="coll-12 col-md-6 pb-2" t-if="my_sign_item.partner_id">
|
||||
<span class="o_portal_category">Your Information</span>
|
||||
<div class="row">
|
||||
<div class="col flex-grow-0 pr-3">
|
||||
<img t-if="my_sign_item.partner_id.image_1024" class="rounded-circle mt-1 o_portal_contact_img"
|
||||
t-att-src="image_data_uri(my_sign_item.partner_id.image_1024)" alt="Contact"/>
|
||||
<img t-else="" class="rounded-circle mt-1 o_portal_contact_img"
|
||||
src="/web/static/src/img/user_menu_avatar.png" alt="Contact"/>
|
||||
</div>
|
||||
<div class="col pl-md-0">
|
||||
<div t-field="my_sign_item.partner_id"
|
||||
t-options="{'widget': 'contact', 'fields': ['name', 'email', 'phone']}"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
<div class="mt32">
|
||||
<h4>
|
||||
<strong>Message and communication history</strong>
|
||||
</h4>
|
||||
<t t-call="portal.message_thread">
|
||||
<t t-set="object" t-value="my_sign_item.sign_request_id"/>
|
||||
<t t-set="token" t-value="my_sign_item.sign_request_id.access_token"/>
|
||||
<t t-set="pid" t-value="pid"/>
|
||||
<t t-set="hash" t-value="hash"/>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
</template>
|
||||
|
||||
</odoo>
|
||||
|
|
@ -1,234 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<template id="assets_common" inherit_id="web.assets_common">
|
||||
<xpath expr="script[last()]" position="after">
|
||||
<script type="text/javascript" src="/odex25_sign/static/src/js/sign_common.js"></script>
|
||||
</xpath>
|
||||
<xpath expr="link[last()]" position="after">
|
||||
<link rel="stylesheet" type="text/scss" href="/odex25_sign/static/src/scss/sign_common.scss"/>
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
<template id="assets_backend" inherit_id="web.assets_backend">
|
||||
<xpath expr="script[last()]" position="after">
|
||||
<script type="text/javascript" src="/odex25_sign/static/src/js/sign_backend.js"></script>
|
||||
<script type="text/javascript" src="/odex25_sign/static/src/js/tours/odex25_sign.js"></script>
|
||||
</xpath>
|
||||
<xpath expr="link[last()]" position="after">
|
||||
<link rel="stylesheet" type="text/scss" href="/odex25_sign/static/src/scss/sign_backend.scss"/>
|
||||
</xpath>
|
||||
<xpath expr="link[last()]" position="after">
|
||||
<link rel="stylesheet" type="text/css" href="/odex25_sign/static/src/css/iframe.css"/>
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
<template id="assets_frontend" inherit_id="web.assets_frontend">
|
||||
<xpath expr="link[last()]" position="after">
|
||||
<link rel="stylesheet" type="text/scss" href="/odex25_sign/static/src/scss/sign_frontend.scss"/>
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
<template id="assets_tests" name="Sign Assets Tests" inherit_id="web.assets_tests">
|
||||
<xpath expr="." position="inside">
|
||||
<script type="text/javascript" src="/odex25_sign/static/tests/tours/sign_widgets_tour.js"></script>
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
<template id="qunit_suite" name="sign_tests" inherit_id="web.qunit_suite_tests">
|
||||
<xpath expr="." position="inside">
|
||||
<script type="text/javascript" src="/odex25_sign/static/tests/document_backend_tests.js"></script>
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
<template id="odex25_sign._doc_sign" name="Document Sign">
|
||||
<div t-if="current_request_item and current_request_item.state == 'sent' and hasItems" class="o_sign_validate_banner">
|
||||
<button type="button" class="btn btn-primary o_validate_button">Validate & Send Completed Document</button>
|
||||
</div>
|
||||
|
||||
<div class="container-fluid">
|
||||
<button t-if="current_request_item and current_request_item.state == 'sent' and not hasItems" type="button" class="o_sign_sign_document_button btn btn-primary">Sign Document</button>
|
||||
<div class="row o_sign_page_info mobile-hide">
|
||||
<div class="col-lg-4">
|
||||
<div class="o_sign_request_from">
|
||||
<span t-if="portal" class="o_sign_breadcrumb"><a t-attf-href="/my/signature/{{current_request_item.id}}?{{ keep_query() }}"
|
||||
class="o_sign_portal_link">Portal</a> / </span>
|
||||
<span class="o_sign_request_reference_title o_sign_breadcrumb"><t t-esc="sign_request.reference"/></span>
|
||||
</div>
|
||||
<div class="o_sign_document_buttons">
|
||||
<a role="button" t-if="sign_request.state == 'signed'" t-attf-href="/odex25_sign/download/{{sign_request.id}}/{{sign_request.access_token}}/completed" class="btn btn-primary o_sign_download_document_button">Download Document</a>
|
||||
<a role="button" t-if="sign_request.state == 'signed'" t-attf-href="/odex25_sign/download/{{sign_request.id}}/{{sign_request.access_token}}/log" class="btn btn-secondary o_sign_download_log_button">Certificate</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-4 mobile-hide">
|
||||
<div class="o_sign_request_from">
|
||||
<t t-if="sign_request.create_uid.partner_id.image_128">
|
||||
<img class="float-left mr-2 mt-1" t-attf-src="data:image/png;base64,#{sign_request.create_uid.partner_id.image_128}" alt="Signature" style="max-width: 32px;"/>
|
||||
</t>
|
||||
<t t-if="sign_request.create_uid.partner_id.name">Requested by <b><t t-esc="sign_request.create_uid.partner_id.name"/></b><br/></t>
|
||||
<t t-if="sign_request.create_uid.partner_id.email"><a t-attf-href="mailto:{{sign_request.create_uid.partner_id.email}}"><t t-esc="sign_request.create_uid.partner_id.email"/></a><br/></t>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="o_sign_signer_status_wrapper col-lg-4 d-flex justify-content-end flex-wrap mobile-hide">
|
||||
<t t-if="sign_request.state != 'signed'">
|
||||
<t t-foreach="sign_request.request_item_ids" t-as="sign">
|
||||
<div t-if="odex25_sign.state != 'completed'" class="o_sign_signer_status clearfix pl-4 d-flex" t-att-data-id="odex25_sign.id">
|
||||
<div class="o_sign_signer_status_info d-flex flex-column">
|
||||
<div class="text-left"><b><t t-esc="odex25_sign.partner_id.name if odex25_sign.partner_id else 'Public user'"/></b></div>
|
||||
<div class="text-left"><small><i> Waiting Signature</i><em t-if="odex25_sign.state != 'sent'"><br/>(the email access has not been sent)</em></small></div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
<t t-if="sign_request.nb_closed > 0">
|
||||
<t t-foreach="sign_request.request_item_ids" t-as="sign">
|
||||
<div t-if="odex25_sign.state == 'completed'" class="o_sign_signer_status o_sign_signer_signed clearfix pl-4 d-flex">
|
||||
<div class="o_sign_signer_status_info d-flex flex-column">
|
||||
</div>
|
||||
<div class="text-left">
|
||||
<div class="text-left"><b><t t-esc="odex25_sign.partner_id.name if odex25_sign.partner_id else 'Public user'"/></b></div>
|
||||
<div class="text-left"><small><i> Signed on <t t-esc="odex25_sign.signing_date"/></i></small></div>
|
||||
</div>
|
||||
<img t-if="odex25_sign.signature" t-attf-src="/web/image/sign.request.item/{{odex25_sign.id}}/signature" alt="Signature"/>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div t-if="not hasItems and not isPDF" class="row">
|
||||
<div class="col-lg-12">
|
||||
<a class="o_sign_image_document" t-attf-href="/odex25_sign/download/{{sign_request.id}}/{{sign_request.access_token}}/origin" target="_blank">
|
||||
<img t-if="webimage" class="img img-fluid" t-attf-src="/odex25_sign/download/{{sign_request.id}}/{{sign_request.access_token}}/origin" alt="Signature"/>
|
||||
<div t-if="not webimage" class="o_image" t-att-data-mimetype="sign_request.template_id.attachment_id.mimetype"/>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<t t-if="hasItems or isPDF">
|
||||
<t t-call="odex25_sign.items_view"/>
|
||||
</t>
|
||||
|
||||
<input id="o_sign_input_sign_request_id" type="hidden" t-att-value="sign_request.id"/>
|
||||
<input id="o_sign_input_sign_request_token" type="hidden" t-att-value="sign_request.access_token"/>
|
||||
<input id="o_sign_input_access_token" type="hidden" t-att-value="token"/>
|
||||
<input id="o_sign_signer_name_input_info" type="hidden" t-att-value="current_request_item.partner_id.name if current_request_item and current_request_item.partner_id else None"/>
|
||||
<input id="o_sign_signer_phone_input_info" type="hidden" t-att-value="current_request_item.partner_id.mobile if current_request_item and current_request_item.partner_id else None"/>
|
||||
<input id="o_sign_input_optional_redirect_url" type="hidden" t-att-value="sign_request.template_id.redirect_url"/>
|
||||
<input id="o_sign_input_optional_redirect_url_text" type="hidden" t-att-value="sign_request.template_id.redirect_url_text"/>
|
||||
<input t-if="current_request_item and current_request_item.state == 'sent'" id="o_sign_ask_location_input" type="hidden"/>
|
||||
<t t-if="len(sign_request.request_item_ids) == 1 and not sign_request.request_item_ids[0].partner_id">
|
||||
<input id="o_sign_is_public_user" type="hidden"/>
|
||||
</t>
|
||||
</template>
|
||||
|
||||
<template id="odex25_sign.doc_sign" name="Document Sign">
|
||||
<t t-call="web.layout">
|
||||
<t t-if="current_request_item and current_request_item.partner_id.lang">
|
||||
<t t-set="html_data" t-value="{'lang': current_request_item.partner_id.lang.replace('_', '-')}"/>
|
||||
</t>
|
||||
<t t-set="head">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no" />
|
||||
<t t-call-assets="web.assets_common" t-js="false"/>
|
||||
<t t-call-assets="web.assets_frontend" t-js="false"/>
|
||||
<link rel="stylesheet" type="text/scss" href="/web_enterprise/static/src/scss/modal_mobile.scss"/>
|
||||
<t t-call-assets="web.assets_common" t-css="false"/>
|
||||
<t t-call-assets="web.assets_frontend" t-css="false"/>
|
||||
<t t-call="web.conditional_assets_tests"/>
|
||||
|
||||
<script type="text/javascript">
|
||||
odoo.define("odex25_sign.document_custom_page", function (require) {
|
||||
var ajax = require("web.ajax");
|
||||
var core = require("web.core");
|
||||
var document_signing = require("odex25_sign.document_signing");
|
||||
var rootWidget = require('root.widget');
|
||||
// YTI FIXME We need the tour to wait the Tip template to be loaded
|
||||
odoo.__TipTemplateDef = ajax.loadXML("/web_tour/static/src/xml/tip.xml", core.qweb).then(function () {
|
||||
ajax.loadXML("/odex25_sign/static/src/xml/sign_common.xml", core.qweb).then(function () {
|
||||
document_signing.initDocumentToSign(rootWidget);
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</t>
|
||||
<div class="o_sign_document">
|
||||
<header>
|
||||
<div class="container-fluid">
|
||||
<div class="d-flex justify-content-between flex-fill">
|
||||
<div class="col-lg-4 justify-content-start">
|
||||
<div class="o_logo">
|
||||
<a href="/"><img src="/logo.png" alt="Logo"/></a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-4 justify-content-center mobile-hide">
|
||||
<div t-if="sign_request" class="o_sign_header_instruction">
|
||||
<t t-if="not current_request_item">Need to sign? Check your inbox to get your secure access</t>
|
||||
<t t-if="current_request_item and current_request_item.state == 'sent'">Please Review And Act On This Document</t>
|
||||
<t t-if="current_request_item and current_request_item.state == 'completed'">You have completed the document</t>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-4 justify-content-end">
|
||||
<div class="o_odoo">
|
||||
<a href="https://www.odoo.com/page/sign?utm_source=db&utm_medium=sign"><img src="/odex25_sign/static/img/odoo_signed.png" alt="Signed"/></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<t t-call="odex25_sign._doc_sign"/>
|
||||
</div>
|
||||
</t>
|
||||
</template>
|
||||
|
||||
<template id="odex25_sign.encrypted_ask_password" name="PDF Encrypted Password Request">
|
||||
<t t-call="web.layout">
|
||||
<t t-set="head">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no" />
|
||||
<t t-call-assets="web.assets_common" t-js="false"/>
|
||||
<t t-call-assets="web.assets_frontend" t-js="false"/>
|
||||
<link rel="stylesheet" type="text/scss" href="/web_enterprise/static/src/scss/modal_mobile.scss"/>
|
||||
<t t-call-assets="web.assets_common" t-css="false"/>
|
||||
<t t-call-assets="web.assets_frontend" t-css="false"/>
|
||||
</t>
|
||||
<div class="container text-center">
|
||||
<h3>Missing Password</h3>
|
||||
<p>
|
||||
The PDF's password is required to generate the final document.
|
||||
</p>
|
||||
<form string="PDF is encrypted" role="form" method="post" onsubmit="this.action = this.action + location.hash">
|
||||
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
|
||||
<div>
|
||||
<p class="alert alert-danger mb8" t-if="error" role="alert">
|
||||
<t t-esc="error"/>
|
||||
</p>
|
||||
</div>
|
||||
<input type="password" required="required" name="password" t-att-autofocus="autofocus" maxlength="50"/>
|
||||
<input type="submit" value="Generate Document" ></input>
|
||||
</form>
|
||||
</div>
|
||||
</t>
|
||||
</template>
|
||||
|
||||
<template id="odex25_sign.deleted_sign_request" name="Missing Signature Request">
|
||||
<t t-call="web.layout">
|
||||
<t t-set="head">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no" />
|
||||
<t t-call-assets="web.assets_common" t-js="false"/>
|
||||
<t t-call-assets="web.assets_frontend" t-js="false"/>
|
||||
<link rel="stylesheet" type="text/scss" href="/web_enterprise/static/src/scss/modal_mobile.scss"/>
|
||||
<t t-call-assets="web.assets_common" t-css="false"/>
|
||||
<t t-call-assets="web.assets_frontend" t-css="false"/>
|
||||
</t>
|
||||
<div class="container">
|
||||
<h3>Missing signature request</h3>
|
||||
<p>
|
||||
The signature access you are trying to reach does not exist. Maybe the signature request has been deleted or modified. <br/>
|
||||
If there still exists a signature request for this document, check your inbox to get your access!
|
||||
</p>
|
||||
</div>
|
||||
</t>
|
||||
</template>
|
||||
|
||||
</odoo>
|
||||
|
|
@ -1,253 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<!-- Signature Request Views -->
|
||||
<record id="sign_request_view_kanban" model="ir.ui.view">
|
||||
<field name="name">sign.request.kanban</field>
|
||||
<field name="model">sign.request</field>
|
||||
<field name="arch" type="xml">
|
||||
<kanban quick_create="false" class="o_sign_request_kanban" default_order="create_date desc" sample="1">
|
||||
<field name="active"/>
|
||||
<field name="color"/>
|
||||
<field name="create_uid"/>
|
||||
<field name="favorited_ids"/>
|
||||
<field name="last_action_date"/>
|
||||
<field name="request_item_infos"/>
|
||||
<field name="state"/>
|
||||
<field name="activity_ids"/>
|
||||
<field name="activity_state"/>
|
||||
<field name="template_tags"/>
|
||||
<progressbar field="activity_state" colors='{"planned": "success", "overdue": "danger", "today": "warning"}'/>
|
||||
<templates>
|
||||
<div t-name="kanban-box" t-attf-class="o_sign_sticky_bottom oe_kanban_global_click {{!selection_mode ? kanban_color(record.color.raw_value) : ''}}">
|
||||
<div class="oe_kanban_main">
|
||||
<div class="o_kanban_record_top">
|
||||
<div class="o_kanban_record_headings">
|
||||
<div class="o_kanban_record_title">
|
||||
<t t-if="record.favorited_ids.raw_value.indexOf(user_context.uid) < 0">
|
||||
<a type="object" name="toggle_favorited" title="Not in favorites, add it" aria-label="Not in favorites, add it"
|
||||
class="fa fa-lg fa-star-o favorite_sign_button"/>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<a type="object" name="toggle_favorited" title="In favorites, remove it" aria-label="In favorites, remove it"
|
||||
class="fa fa-lg fa-star favorite_sign_button_enabled favorite_sign_button"/>
|
||||
</t>
|
||||
<span class="pl-4"><field name="reference"/></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="o_dropdown_kanban dropdown">
|
||||
<a role="button" class="dropdown-toggle o-no-caret btn" data-toggle="dropdown" href="#" aria-label="Dropdown menu" title="Dropdown menu">
|
||||
<span class="fa fa-ellipsis-v"/>
|
||||
</a>
|
||||
<div class="dropdown-menu" role="menu">
|
||||
<a role="menuitem" type="object" name="open_request" class="dropdown-item">Details</a>
|
||||
<a role="menuitem" type="object" name="toggle_active" class="dropdown-item">
|
||||
<t t-if="!record.active.raw_value">Restore</t>
|
||||
<t t-if="record.active.raw_value">Archive</t>
|
||||
</a>
|
||||
<a role="menuitem" type="delete" groups="base.group_no_one" class="dropdown-item">Delete</a>
|
||||
<ul class="oe_kanban_colorpicker" data-field="color"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="o_kanban_record_body">
|
||||
<field name="template_tags" widget="many2many_tags" options="{'color_field': 'color'}"/>
|
||||
<div class="o_signers container-fluid mt-2">
|
||||
<div t-foreach="record.request_item_infos.raw_value" t-as="request_item_info" t-att-class="(request_item_info.state === 'completed')? 'o_sign_completed row' : 'o_sign_waiting row'">
|
||||
<input t-att-checked="(request_item_info.state === 'completed') ? 'checked' : undefined" class="mt-1 mr-1" type="checkbox" disabled="True"/>
|
||||
<span>
|
||||
<t t-esc="request_item_info.partner_name"/><t t-if="request_item_info.signing_date" class="ml-1" t-esc="' ' + request_item_info.signing_date"/>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="o_kanban_record_bottom">
|
||||
<div class="oe_kanban_bottom_left">
|
||||
<em><t t-esc="moment(record.last_action_date.raw_value).format('L')" /></em>
|
||||
<div class="o_kanban_inline_block">
|
||||
<field name="activity_ids" widget="kanban_activity"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="oe_kanban_bottom_right">
|
||||
<field name="create_uid" widget="many2one_avatar_user"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</templates>
|
||||
</kanban>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="sign_request_view_tree" model="ir.ui.view">
|
||||
<field name="name">sign.request.tree</field>
|
||||
<field name="model">sign.request</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree sample="1">
|
||||
<field name="reference"/>
|
||||
<field name="template_id"/>
|
||||
<field name="state"/>
|
||||
<field name="progress"/>
|
||||
<field name="activity_exception_decoration" widget="activity_exception"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="sign_request_view_form" model="ir.ui.view">
|
||||
<field name="name">sign.request.form</field>
|
||||
<field name="model">sign.request</field>
|
||||
<field name="arch" type="xml">
|
||||
<form create="false" edit="false">
|
||||
<header>
|
||||
<button string="Resend" type="object" states="canceled" name="action_resend" class="oe_highlight oe_read_only"/>
|
||||
<button string="Preview" type="object" name="go_to_document" class="oe_highlight"/>
|
||||
<button string="Download Document" type="object" states="signed" name="get_completed_document" class="oe_read_only oe_highlight"/>
|
||||
<button name="%(odex25_sign.action_sign_request_send_copy)d" string="Send a Copy" type="action"/>
|
||||
<button string="Cancel" type="object" states="sent,signed" name="action_canceled" class="oe_read_only" confirm="This will delete all the already completed documents of this request and disable every sent accesses, are you sure?"/>
|
||||
<field name="state" widget="statusbar" statusbar_visible="sent,signed"/>
|
||||
</header>
|
||||
<sheet>
|
||||
<div class="oe_button_box" name="button_box">
|
||||
<button type="object" name="go_to_document" class="oe_read_only oe_stat_button" icon="fa-pencil">
|
||||
<div class="o_field_widget o_stat_info">
|
||||
<span class="o_stat_value"><field name="nb_closed"/>/<field name="nb_total"/><span class="o_stat_text"> Signed</span></span>
|
||||
<span class="o_stat_text">View</span>
|
||||
</div>
|
||||
</button>
|
||||
<button type="object" name="open_logs" class="oe_read_only oe_stat_button" icon="fa-pencil" groups="odex25_sign.group_sign_manager">
|
||||
<!-- TODO show only in debug to sign managers? -->
|
||||
<div class="o_field_widget o_stat_info">
|
||||
<span class="o_stat_text">Activity Logs</span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<widget name="web_ribbon" title="Fully Signed" attrs="{'invisible': [('state', '!=', 'signed')]}"/>
|
||||
<widget name="web_ribbon" title="Archived" bg_color="bg-danger" attrs="{'invisible': [('active', '=', True)]}"/>
|
||||
<field name="integrity" invisible="1"/>
|
||||
<div class="oe_title">
|
||||
<label for="reference" class="oe_edit_only"/>
|
||||
<h1><field name="reference"/></h1>
|
||||
</div>
|
||||
<div class="alert alert-warning" role="alert" attrs="{'invisible': [('integrity', '=', True)]}">
|
||||
The integrity of the document's history cannot be verified. This could mean that signature values or the underlying PDF document may have been modified after the fact.
|
||||
</div>
|
||||
|
||||
<field name="request_item_ids" context="{'default_sign_request_id': id}" nolabel="1"/>
|
||||
|
||||
<group>
|
||||
<group>
|
||||
<field name="active" invisible="1"/>
|
||||
<field name="template_id" invisible="1" kanban_view_ref="%(odex25_sign.sign_template_view_kanban_mobile)s"/>
|
||||
<field name="favorited_ids" widget="many2many_tags" options="{'color_field': 'color'}" invisible="1"/>
|
||||
</group>
|
||||
</group>
|
||||
</sheet>
|
||||
<div class="oe_chatter">
|
||||
<field name="message_follower_ids"/>
|
||||
<field name="activity_ids"/>
|
||||
<field name="message_ids"/>
|
||||
</div>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="sign_request_view_search" model="ir.ui.view">
|
||||
<field name="name">sign.request.search</field>
|
||||
<field name="model">sign.request</field>
|
||||
<field name="arch" type="xml">
|
||||
<search>
|
||||
<field name="reference" string="Document/Signer" filter_domain="['|', '|', ('reference', 'ilike', self), ('request_item_ids.partner_id.email', 'ilike', self), ('request_item_ids.partner_id.name', 'ilike', self)]"/>
|
||||
<field name="template_id" string="Template or Tag" filter_domain="['|', ('template_id.attachment_id.name', 'ilike', self), ('template_id.tag_ids.name', 'ilike', self)]"/>
|
||||
<filter name="my_docs" string="My Documents" domain="[('create_uid', '=', uid)]"/>
|
||||
<filter name="my_sign_request" string="My Requests" domain="[('request_item_ids.partner_id.user_ids', 'like', uid)]"/>
|
||||
<separator/>
|
||||
<filter name="favorite" string="My Favorites" domain="[('favorited_ids', 'in', uid)]"/>
|
||||
<separator/>
|
||||
<filter name="in_progress" string="To Sign" domain="[('state', '=', 'sent')]"/>
|
||||
<filter name="signed" string="Signed" domain="[('state', '=', 'signed')]"/>
|
||||
<filter invisible="1" string="Late Activities" name="activities_overdue"
|
||||
domain="[('activity_ids.date_deadline', '<', context_today().strftime('%Y-%m-%d'))]"
|
||||
help="Show all records which has next action date is before today"/>
|
||||
<filter invisible="1" string="Today Activities" name="activities_today"
|
||||
domain="[('activity_ids.date_deadline', '=', context_today().strftime('%Y-%m-%d'))]"/>
|
||||
<filter invisible="1" string="Future Activities" name="activities_upcoming_all"
|
||||
domain="[('activity_ids.date_deadline', '>', context_today().strftime('%Y-%m-%d'))
|
||||
]"/>
|
||||
<separator/>
|
||||
<filter name="all_documents" string="All Documents" domain="['|', ('active', '=', False), ('active', '=', True)]"/>
|
||||
<group expand="0" string="Group By">
|
||||
<filter name="group_by_template" string="Template" domain="[]" context="{'group_by': 'template_id'}"/>
|
||||
</group>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="sign_request_action" model="ir.actions.act_window">
|
||||
<field name="name">Documents</field>
|
||||
<field name="res_model">sign.request</field>
|
||||
<field name="view_mode">kanban,tree,form,activity</field>
|
||||
<field name="search_view_id" ref="sign_request_view_search"/>
|
||||
<field name="context" eval="{'search_default_my_docs': 1, 'search_default_my_sign_request': 1}"/>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_empty_folder">
|
||||
No documents to sign
|
||||
</p><p>
|
||||
Request a signature to upload a template (or use an existing one)
|
||||
to automate your signature process.
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Signature Request Item Views -->
|
||||
<record id="sign_request_item_view_tree" model="ir.ui.view">
|
||||
<field name="name">sign.request.item.tree</field>
|
||||
<field name="model">sign.request.item</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree>
|
||||
<field name="partner_id"/>
|
||||
<field name="signer_email" class="oe_read_only"/>
|
||||
<field name="role_id" class="oe_read_only"/>
|
||||
<field name="state" class="oe_read_only"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="sign_request_item_view_form" model="ir.ui.view">
|
||||
<field name="name">sign.request.item.form</field>
|
||||
<field name="model">sign.request.item</field>
|
||||
<field name="arch" type="xml">
|
||||
<form>
|
||||
<header>
|
||||
<field name="state" widget="statusbar"/>
|
||||
</header>
|
||||
<sheet>
|
||||
<group>
|
||||
<group>
|
||||
<field name="partner_id"/>
|
||||
<field name="role_id"/>
|
||||
<field name="signer_email"/>
|
||||
<field name="access_token" invisible="1"/>
|
||||
<field name="signing_date"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="latitude"/>
|
||||
<field name="longitude"/>
|
||||
<field name="sms_number"/>
|
||||
</group>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="sign_request_item_action" model="ir.actions.act_window">
|
||||
<field name="name">Signature Request Items</field>
|
||||
<field name="res_model">sign.request.item</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
</record>
|
||||
|
||||
<!-- After installation of the module, open the related menu -->
|
||||
<record id="base.open_menu" model="ir.actions.todo">
|
||||
<field name="action_id" ref="base.action_open_website"/>
|
||||
<field name="state">open</field>
|
||||
</record>
|
||||
</odoo>
|
||||
|
|
@ -1,56 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<template id="odex25_sign.items_view" name="Digital Signatures - Items View">
|
||||
<iframe class="o_sign_pdf_iframe" t-att-readonly="'readonly' if readonly else None"/>
|
||||
|
||||
<input id="o_sign_input_template_name" type="hidden" t-attf-value="{{sign_request.template_id.name}}"/>
|
||||
<input id="o_sign_input_attachment_location" type="hidden" t-attf-value="/odex25_sign/download/{{sign_request.id}}/{{sign_request.access_token}}/origin"/>
|
||||
<input id="o_sign_input_current_role" type="hidden" t-att-value="role"/>
|
||||
|
||||
<t t-call="odex25_sign.items_input_info"/>
|
||||
<t t-call="odex25_sign.item_types_input_info"/>
|
||||
<t t-call="odex25_sign.item_select_options_input_info"/>
|
||||
</template>
|
||||
|
||||
<template id="odex25_sign.items_input_info" name="Items input info">
|
||||
<t t-if="sign_items">
|
||||
<t t-foreach="sign_items" t-as="item">
|
||||
<input type="hidden" class="o_sign_item_input_info"
|
||||
t-att-data-id="item.id"
|
||||
t-att-data-type="item.type_id.id"
|
||||
t-att-data-required="item.required"
|
||||
t-att-data-name="item.name"
|
||||
t-att-data-responsible="item.responsible_id.id"
|
||||
t-att-data-responsible_name="item.responsible_id.name"
|
||||
t-att-data-page="item.page"
|
||||
t-att-data-option_ids="item.option_ids.ids"
|
||||
t-att-data-template_id="item.template_id.ids"
|
||||
t-att-data-pos-x="str(item.posX)"
|
||||
t-att-data-pos-y="str(item.posY)"
|
||||
t-att-data-width="item.width"
|
||||
t-att-data-height="item.height"
|
||||
t-att-data-value="item_values[item.id] if item_values and item.id in item_values else None"/>
|
||||
</t>
|
||||
</t>
|
||||
</template>
|
||||
|
||||
<template id="odex25_sign.item_types_input_info" name="Item types input info">
|
||||
<t t-foreach="sign_item_types" t-as="item_type">
|
||||
<input type="hidden" class="o_sign_field_type_input_info"
|
||||
t-att-data-id="item_type['id']"
|
||||
t-att-data-name="item_type['name']"
|
||||
t-att-data-item_type="item_type['item_type']"
|
||||
t-att-data-tip="item_type['tip']"
|
||||
t-att-data-placeholder="item_type['placeholder']"
|
||||
t-att-data-auto_field="item_type['auto_field']"/>
|
||||
</t>
|
||||
</template>
|
||||
|
||||
<template id="odex25_sign.item_select_options_input_info" name="Item select options input info">
|
||||
<t t-foreach="sign_item_select_options" t-as="item_select_option">
|
||||
<input type="hidden" class="o_sign_select_options_input_info"
|
||||
t-att-data-id="item_select_option['id']"
|
||||
t-att-data-value="item_select_option['value']"/>
|
||||
</t>
|
||||
</template>
|
||||
</odoo>
|
||||
|
|
@ -1,334 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<!-- Signature Request Template Views -->
|
||||
<record id="sign_template_view_kanban" model="ir.ui.view">
|
||||
<field name="name">sign.template.kanban</field>
|
||||
<field name="model">sign.template</field>
|
||||
<field name="arch" type="xml">
|
||||
<kanban quick_create="false" class="o_sign_template_kanban" default_order="create_date desc" sample="1">
|
||||
<field name="active"/>
|
||||
<field name="attachment_id"/>
|
||||
<field name="color"/>
|
||||
<field name="create_date"/>
|
||||
<field name="favorited_ids"/>
|
||||
<field name="responsible_count"/>
|
||||
<field name="create_uid"/>
|
||||
<field name="signed_count"/>
|
||||
<field name="in_progress_count"/>
|
||||
<templates>
|
||||
<div t-name="kanban-box" t-attf-class="o_sign_sticky_bottom oe_kanban_global_click {{kanban_color(record.color.raw_value)}}">
|
||||
<div class="oe_kanban_main">
|
||||
<div class="o_kanban_record_top">
|
||||
<div class="o_kanban_record_headings">
|
||||
<div class="o_kanban_record_title">
|
||||
<t t-if="record.favorited_ids.raw_value.indexOf(user_context.uid) < 0">
|
||||
<a type="object" name="toggle_favorited" aria-label="Not in favorites, set it" title="Not in favorites, add it"
|
||||
class="fa fa-lg fa-star-o favorite_sign_button" groups="odex25_sign.group_sign_manager"/>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<a type="object" name="toggle_favorited" aria-label="In favorites, remove it" title="In favorites, remove it"
|
||||
class="fa fa-lg fa-star favorite_sign_button_enabled favorite_sign_button"/>
|
||||
</t>
|
||||
<span class="pl-4"><field name="display_name"/></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="o_dropdown_kanban dropdown">
|
||||
<a role="button" class="o_kanban_manage_toggle_button o_left o-no-caret btn" data-toggle="dropdown" href="#" aria-label="Dropdown menu" title="Dropdown menu">
|
||||
<span class="fa fa-ellipsis-v"/>
|
||||
</a>
|
||||
<div class="o_kanban_card_manage_pane dropdown-menu" role="menu">
|
||||
<t t-if="widget.editable" groups="base.group_no_one"><a role="menuitem" type="edit" class="dropdown-item">Properties</a></t>
|
||||
<a role="menuitem" type="object" name="go_to_custom_template" class="d-none d-md-block dropdown-item" context="{'sign_edit_call': 'sign_template_edit'}">Modify Template</a>
|
||||
<a role="menuitem" type="object" name="open_requests" class="dropdown-item">Signed Documents</a>
|
||||
<a role="menuitem" type="object" name="toggle_active" class="dropdown-item">
|
||||
<t t-if="!record.active.raw_value">Restore</t>
|
||||
<t t-if="record.active.raw_value">Archive</t>
|
||||
</a>
|
||||
<a role="menuitem" type="delete" class="dropdown-item">Delete</a>
|
||||
|
||||
<ul role="menu" class="oe_kanban_colorpicker menu-item" data-field="color"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="o_kanban_record_body">
|
||||
<em><t t-esc="moment(record.create_date.raw_value).format('L')" /></em>
|
||||
<field name="tag_ids" widget="many2many_tags" options="{'color_field': 'color'}"/>
|
||||
</div>
|
||||
<div class="o_kanban_record_bottom">
|
||||
<div class="oe_kanban_bottom_left w-50">
|
||||
<button name="%(odex25_sign.action_sign_send_request)d" type="action" class="btn btn-primary btn-sm mt8 o_kanban_sign_send_request" context="{'sign_directly_without_mail': 0}">Send</button>
|
||||
<button name="%(odex25_sign.action_sign_send_request)d" type="action" class="btn btn-primary btn-sm mt8 o_kanban_sign_directly text-truncate" context="{'sign_directly_without_mail': 1}">Sign Now</button>
|
||||
<button name="%(odex25_sign.action_sign_template_share)d" type="action" class="btn btn-secondary btn-sm mt8" attrs="{'invisible': ['|', ('active', '=', False), ('responsible_count', '>', 1)]}">Share</button>
|
||||
</div>
|
||||
<div class="oe_kanban_bottom_right">
|
||||
<div class="float-right mt8">
|
||||
<span class="mr-2" title="Number of documents in progress for this template.">
|
||||
<span class="fa fa-hourglass-half"/>
|
||||
<field name="in_progress_count"/>
|
||||
</span>
|
||||
<span class="mr-2" title="Number of documents signed for this template.">
|
||||
<span class="fa fa-check ml-1"/>
|
||||
<field name="signed_count"/>
|
||||
</span>
|
||||
</div>
|
||||
<field name="create_uid" widget="many2one_avatar_user"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</templates>
|
||||
</kanban>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="sign_template_view_tree" model="ir.ui.view">
|
||||
<field name="name">sign.template.tree</field>
|
||||
<field name="model">sign.template</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree sample="1">
|
||||
<field name="attachment_id"/>
|
||||
<field name="create_date"/>
|
||||
<field name="sign_item_ids"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="sign_template_view_form" model="ir.ui.view">
|
||||
<field name="name">sign.template.form</field>
|
||||
<field name="model">sign.template</field>
|
||||
<field name="arch" type="xml">
|
||||
<form create="false">
|
||||
<sheet>
|
||||
<div class="oe_button_box" name="button_box">
|
||||
<button type="object" name="open_requests" class="oe_stat_button" icon="fa-pencil-square-o" >
|
||||
<div class="o_field_widget o_stat_info">
|
||||
<span class="o_stat_value">
|
||||
<field name="signed_count"/>
|
||||
</span>
|
||||
<span class="o_stat_text">Signed Document</span>
|
||||
</div>
|
||||
</button>
|
||||
<button string="Edit fields" icon="fa-wrench" type="object" name="go_to_custom_template" class="oe_stat_button"/>
|
||||
</div>
|
||||
<widget name="web_ribbon" title="Archived" bg_color="bg-danger" attrs="{'invisible': [('active', '=', True)]}"/>
|
||||
<div class="oe_title">
|
||||
<h1>
|
||||
<div class="o_row">
|
||||
<field name="name" placeholder="Name of the file" nolabel="1"/>
|
||||
</div>
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<group>
|
||||
<group>
|
||||
<field name="active" invisible="1"/>
|
||||
<field name="attachment_id" invisible="1"/>
|
||||
<field name="responsible_count" invisible="1"/>
|
||||
<field name="tag_ids" widget="many2many_tags" options="{'color_field': 'color'}" placeholder="Tags"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="redirect_url" widget="url"/>
|
||||
<field name="redirect_url_text" attrs="{'invisible':[('redirect_url','=','')]}"/>
|
||||
<field name="privacy"/>
|
||||
<field name="favorited_ids" widget="many2many_tags" options="{'color_field': 'color'}"/>
|
||||
<field name="group_ids" groups="base.group_no_one" widget="many2many_tags" />
|
||||
</group>
|
||||
</group>
|
||||
<notebook>
|
||||
<page name="document" string="Document">
|
||||
<field name="datas" widget="pdf_viewer"/>
|
||||
</page>
|
||||
<page string="Fields" name="signatures" groups="base.group_no_one">
|
||||
<field name="sign_item_ids"/>
|
||||
</page>
|
||||
</notebook>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="sign_template_view_search" model="ir.ui.view">
|
||||
<field name="name">sign.template.search</field>
|
||||
<field name="model">sign.template</field>
|
||||
<field name="arch" type="xml">
|
||||
<search>
|
||||
<field name="attachment_id" string="Document Name"/>
|
||||
<field name="tag_ids"/>
|
||||
<filter name="my_templates" string="My Templates" domain="[('create_uid', '=', uid)]"/>
|
||||
<filter name="favorite" string="My Favorites" domain="[('favorited_ids', 'in', uid)]"/>
|
||||
<searchpanel>
|
||||
<field name="tag_ids" select="multi" icon="fa-tag" enable_counters="1"/>
|
||||
</searchpanel>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="sign_template_tour_action" model="ir.actions.client">
|
||||
<field name="name">Template Sample Contract.pdf</field>
|
||||
<field name="tag">sign.template</field>
|
||||
<field name="context" eval="{'sign_edit_call': 'sign_send_request', 'id':ref('template_sign_tour'), 'sign_directly_without_mail': False}"/>
|
||||
</record>
|
||||
|
||||
<record id="sign_template_action" model="ir.actions.act_window">
|
||||
<field name="name">Templates</field>
|
||||
<field name="res_model">sign.template</field>
|
||||
<field name="view_mode">kanban,tree,form</field>
|
||||
<field name="search_view_id" ref="sign_template_view_search"/>
|
||||
<field name="context" eval="{'search_default_favorite': 1}"/>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_smiling_face">
|
||||
No template yet
|
||||
</p><p>
|
||||
<a type="action" name="%(odex25_sign.sign_template_tour_action)d" class="btn btn-primary text-white">start with our sample template</a>
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Signature Item Views -->
|
||||
<record id="sign_item_view_tree" model="ir.ui.view">
|
||||
<field name="name">sign.item.tree</field>
|
||||
<field name="model">sign.item</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree default_order="page,posY,posX,id" editable="bottom">
|
||||
<field name="type_id"/>
|
||||
<field name="required"/>
|
||||
<field name="responsible_id"/>
|
||||
<field name="page"/>
|
||||
<field name="posX"/>
|
||||
<field name="posY"/>
|
||||
<field name="width"/>
|
||||
<field name="height"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="sign_item_view_form" model="ir.ui.view">
|
||||
<field name="name">sign.item.form</field>
|
||||
<field name="model">sign.item</field>
|
||||
<field name="arch" type="xml">
|
||||
<form>
|
||||
<sheet>
|
||||
<group>
|
||||
<group string="Information">
|
||||
<field name="type_id"/>
|
||||
<field name="required"/>
|
||||
<field name="responsible_id"/>
|
||||
</group>
|
||||
|
||||
<group string="Display">
|
||||
<field name="page"/>
|
||||
<field name="posX"/>
|
||||
<field name="posY"/>
|
||||
<field name="width"/>
|
||||
<field name="height"/>
|
||||
</group>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Signature Item Type Views -->
|
||||
<record id="sign_item_type_view_tree" model="ir.ui.view">
|
||||
<field name="name">sign.item.type.tree</field>
|
||||
<field name="model">sign.item.type</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree>
|
||||
<field name="name"/>
|
||||
<field name="item_type"/>
|
||||
<field name="auto_field"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="sign_item_type_view_form" model="ir.ui.view">
|
||||
<field name="name">sign.item.type.form</field>
|
||||
<field name="model">sign.item.type</field>
|
||||
<field name="arch" type="xml">
|
||||
<form>
|
||||
<sheet>
|
||||
<div class="oe_title">
|
||||
<label for="name" class="oe_edit_only"/>
|
||||
<h1><field name="name"/></h1>
|
||||
</div>
|
||||
<group>
|
||||
<field name="item_type"/>
|
||||
<field name="auto_field"/>
|
||||
</group>
|
||||
<group>
|
||||
<group>
|
||||
<label for="default_width"/>
|
||||
<div class="o_row">
|
||||
<field name="default_width"/>
|
||||
<span>(1.0 = full page size)</span>
|
||||
</div>
|
||||
|
||||
<label for="default_height"/>
|
||||
<div class="o_row">
|
||||
<field name="default_height"/>
|
||||
<span>(1.0 = full page size)</span>
|
||||
</div>
|
||||
</group>
|
||||
<group>
|
||||
<field name="tip"/>
|
||||
<field name="placeholder"/>
|
||||
</group>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="sign_item_type_action" model="ir.actions.act_window">
|
||||
<field name="name">Signature Item Type</field>
|
||||
<field name="res_model">sign.item.type</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
</record>
|
||||
|
||||
<!-- Signature Item Party Views -->
|
||||
<record id="sign_item_role_view_tree" model="ir.ui.view">
|
||||
<field name="name">sign.item.role.tree</field>
|
||||
<field name="model">sign.item.role</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree editable="bottom">
|
||||
<field name="name" string="Role Name"/>
|
||||
<field name="sms_authentification"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="sign_item_role_view_form" model="ir.ui.view">
|
||||
<field name="name">sign.item.role.form</field>
|
||||
<field name="model">sign.item.role</field>
|
||||
<field name="arch" type="xml">
|
||||
<form>
|
||||
<sheet>
|
||||
<div class="oe_title">
|
||||
<label for="name" class="oe_edit_only"/>
|
||||
<h1><field name="name"/></h1>
|
||||
</div>
|
||||
<group>
|
||||
<field name="sms_authentification"/>
|
||||
<button type="object" name="buy_credits" string="Buy SMS credit" class="btn-primary"></button>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="sign_item_role_action" model="ir.actions.act_window">
|
||||
<field name="name">Signature Item Role</field>
|
||||
<field name="res_model">sign.item.role</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
</record>
|
||||
|
||||
<!-- Menus -->
|
||||
<menuitem id="menu_document" name="Sign" web_icon="sign,static/description/icon.png" groups="odex25_sign.group_sign_employee"/>
|
||||
<menuitem id="odex25_sign.sign_template_menu" name="Dashboard" parent="odex25_sign.menu_document" action="sign_template_action" sequence="10"/>
|
||||
<menuitem id="sign_request_menu" name="Documents" parent="menu_document" action="sign_request_action" groups="odex25_sign.group_sign_employee" sequence="20"/>
|
||||
<menuitem id="menu_sign_configuration" sequence="100" name="Configuration" parent="menu_document"/>
|
||||
<!-- Menus -->
|
||||
<menuitem id="odex25_sign.sign_item_type_menu" name="Field Types" parent="odex25_sign.menu_sign_configuration" action="sign_item_type_action" groups="base.group_no_one"/>
|
||||
<menuitem id="odex25_sign.sign_item_role_menu" name="Roles" parent="odex25_sign.menu_sign_configuration" action="sign_item_role_action" groups="odex25_sign.group_sign_manager"/>
|
||||
<menuitem id="sign_request_item_menu" name="Signature Requests Items" parent="menu_sign_configuration" action="sign_request_item_action" groups="base.group_no_one" sequence="30"/>
|
||||
</odoo>
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<!-- Signature Request Template Views -->
|
||||
<record id="sign_template_view_kanban_mobile" model="ir.ui.view">
|
||||
<field name="name">sign.template.kanban.mobile</field>
|
||||
<field name="model">sign.template</field>
|
||||
<field name="arch" type="xml">
|
||||
<kanban>
|
||||
<field name="color"/>
|
||||
<templates>
|
||||
<div t-name="kanban-box" class="oe_kanban_global_click">
|
||||
<div class="o_kanban_title clearfix">
|
||||
<span><field name="attachment_id"/></span>
|
||||
</div>
|
||||
<div class="oe_kanban_content">
|
||||
<em><field name="create_date"/></em>
|
||||
<field name="tag_ids" widget="many2many_tags" options="{'color_field': 'color'}"/>
|
||||
</div>
|
||||
</div>
|
||||
</templates>
|
||||
</kanban>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import sign_send_request
|
||||
from . import sign_template_share
|
||||
from . import sign_request_send_copy
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from odoo import api, fields, models
|
||||
|
||||
|
||||
class SignRequestSendCopy(models.TransientModel):
|
||||
_name = 'sign.request.send.copy'
|
||||
_description = 'Sign send request copy'
|
||||
|
||||
request_id = fields.Many2one(
|
||||
'sign.request',
|
||||
default=lambda self: self.env.context.get('active_id', None),
|
||||
)
|
||||
partner_ids = fields.Many2many('res.partner', string="Contact")
|
||||
|
||||
def send_a_copy(self):
|
||||
return self.env['sign.request'].add_followers(self.request_id.id, self.partner_ids.ids)
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="sign_request_send_copy_view_form" model="ir.ui.view">
|
||||
<field name="name">sign.request.send.copy.view.form</field>
|
||||
<field name="model">sign.request.send.copy</field>
|
||||
<field name="arch" type="xml">
|
||||
<form>
|
||||
<group>
|
||||
<field name="partner_ids" widget="many2many_tags"/>
|
||||
</group>
|
||||
<footer>
|
||||
<button string="Send" name="send_a_copy" type="object" class="btn-primary"/>
|
||||
<button string="Discard" class="btn-secondary" special="cancel"/>
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_sign_request_send_copy" model="ir.actions.act_window">
|
||||
<field name="name">Send a copy</field>
|
||||
<field name="res_model">sign.request.send.copy</field>
|
||||
<field name="target">new</field>
|
||||
<field name="view_mode">form</field>
|
||||
</record>
|
||||
</odoo>
|
||||
|
|
@ -1,140 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
|
||||
class SignSendRequest(models.TransientModel):
|
||||
_name = 'sign.send.request'
|
||||
_description = 'Sign send request'
|
||||
|
||||
@api.model
|
||||
def default_get(self, fields):
|
||||
res = super(SignSendRequest, self).default_get(fields)
|
||||
if not res.get('template_id'):
|
||||
return res
|
||||
template = self.env['sign.template'].browse(res['template_id'])
|
||||
if 'filename' in fields:
|
||||
res['filename'] = template.display_name
|
||||
if 'subject' in fields:
|
||||
res['subject'] = _("Signature Request - %(file_name)s", file_name=template.attachment_id.name)
|
||||
if 'signers_count' in fields or 'signer_ids' in fields or 'signer_id' in fields:
|
||||
roles = template.sign_item_ids.responsible_id
|
||||
if 'signers_count' in fields:
|
||||
res['signers_count'] = len(roles)
|
||||
if 'signer_ids' in fields:
|
||||
res['signer_ids'] = [(0, 0, {
|
||||
'role_id': role.id,
|
||||
'partner_id': False,
|
||||
}) for role in roles]
|
||||
if self.env.context.get('sign_directly_without_mail'):
|
||||
if len(roles) == 1 and 'signer_ids' in fields and res.get('signer_ids'):
|
||||
res['signer_ids'][0][2]['partner_id'] = self.env.user.partner_id.id
|
||||
elif not roles and 'signer_id' in fields:
|
||||
res['signer_id'] = self.env.user.partner_id.id
|
||||
return res
|
||||
|
||||
template_id = fields.Many2one(
|
||||
'sign.template', required=True, ondelete='cascade',
|
||||
default=lambda self: self.env.context.get('active_id', None),
|
||||
)
|
||||
signer_ids = fields.One2many('sign.send.request.signer', 'sign_send_request_id', string="Signers")
|
||||
signer_id = fields.Many2one('res.partner', string="Send To")
|
||||
signers_count = fields.Integer()
|
||||
follower_ids = fields.Many2many('res.partner', string="Copy to")
|
||||
is_user_signer = fields.Boolean(compute='_compute_is_user_signer')
|
||||
|
||||
subject = fields.Char(string="Subject", required=True)
|
||||
message = fields.Html("Message")
|
||||
filename = fields.Char("Filename", required=True)
|
||||
|
||||
@api.depends('signer_ids.partner_id', 'signer_id', 'signers_count')
|
||||
def _compute_is_user_signer(self):
|
||||
if self.signers_count and self.env.user.partner_id in self.signer_ids.mapped('partner_id'):
|
||||
self.is_user_signer = True
|
||||
elif not self.signers_count and self.env.user.partner_id == self.signer_id:
|
||||
self.is_user_signer = True
|
||||
else:
|
||||
self.is_user_signer = False
|
||||
|
||||
def create_request(self, send=True, without_mail=False):
|
||||
template_id = self.template_id.id
|
||||
if self.signers_count:
|
||||
signers = [{'partner_id': signer.partner_id.id, 'role': signer.role_id.id} for signer in self.signer_ids]
|
||||
else:
|
||||
signers = [{'partner_id': self.signer_id.id, 'role': False}]
|
||||
followers = self.follower_ids.ids
|
||||
reference = self.filename
|
||||
subject = self.subject
|
||||
message = self.message
|
||||
return self.env['sign.request'].initialize_new(template_id, signers, followers, reference, subject, message, send, without_mail)
|
||||
|
||||
def send_request(self):
|
||||
res = self.create_request()
|
||||
request = self.env['sign.request'].browse(res['id'])
|
||||
return request.go_to_document()
|
||||
|
||||
def sign_directly(self):
|
||||
res = self.create_request()
|
||||
request = self.env['sign.request'].browse(res['id'])
|
||||
user_item = request.request_item_ids.filtered(
|
||||
lambda item: item.partner_id == item.env.user.partner_id)[:1]
|
||||
return {
|
||||
'type': 'ir.actions.client',
|
||||
'tag': 'odex25_sign.SignableDocument',
|
||||
'name': _('Sign'),
|
||||
'context': {
|
||||
'id': request.id,
|
||||
'token': user_item.access_token,
|
||||
'sign_token': user_item.access_token,
|
||||
'create_uid': request.create_uid.id,
|
||||
'state': request.state,
|
||||
},
|
||||
}
|
||||
|
||||
def sign_directly_without_mail(self):
|
||||
res = self.create_request(False, True)
|
||||
request = self.env['sign.request'].browse(res['id'])
|
||||
|
||||
user_item = request.request_item_ids[0]
|
||||
|
||||
return {
|
||||
'type': 'ir.actions.client',
|
||||
'tag': 'odex25_sign.SignableDocument',
|
||||
'name': _('Sign'),
|
||||
'context': {
|
||||
'id': request.id,
|
||||
'token': user_item.access_token,
|
||||
'sign_token': user_item.access_token,
|
||||
'create_uid': request.create_uid.id,
|
||||
'state': request.state,
|
||||
# Don't use mapped to avoid ignoring duplicated signatories
|
||||
'token_list': [item.access_token for item in request.request_item_ids[1:]],
|
||||
'current_signor_name': user_item.partner_id.name,
|
||||
'name_list': [item.partner_id.name for item in request.request_item_ids[1:]],
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
class SignSendRequestSigner(models.TransientModel):
|
||||
_name = "sign.send.request.signer"
|
||||
_description = 'Sign send request signer'
|
||||
|
||||
role_id = fields.Many2one('sign.item.role', readonly=True, required=True)
|
||||
partner_id = fields.Many2one('res.partner', required=True, string="Contact")
|
||||
sign_send_request_id = fields.Many2one('sign.send.request')
|
||||
|
||||
def create(self, vals_list):
|
||||
missing_roles = []
|
||||
for vals in vals_list:
|
||||
if not vals.get('partner_id'):
|
||||
role_id = vals.get('role_id')
|
||||
role = self.env['sign.item.role'].browse(role_id)
|
||||
missing_roles.append(role.name)
|
||||
if missing_roles:
|
||||
missing_roles_str = ', '.join(missing_roles)
|
||||
raise UserError(_(
|
||||
'The following roles must be set to create the signature request: %(roles)s',
|
||||
roles=missing_roles_str,
|
||||
))
|
||||
return super().create(vals_list)
|
||||
|
|
@ -1,43 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="sign_send_request_view_form" model="ir.ui.view">
|
||||
<field name="name">sign.send.request.view.form</field>
|
||||
<field name="model">sign.send.request</field>
|
||||
<field name="arch" type="xml">
|
||||
<form>
|
||||
<group col="2">
|
||||
<field colspan="2" name="signer_ids" placeholder="Write email or search contact..." attrs="{'invisible': [('signers_count', '=', 0)]}" nolabel="1" class="o_sign_flat_o2m">
|
||||
<tree editable="bottom" create="false" delete="false" >
|
||||
<field name="role_id" force_save="1" options="{'no_open': True}"/>
|
||||
<field name="partner_id" placeholder="Name or email..." context="{'force_email': True, 'show_email': True}"/>
|
||||
</tree>
|
||||
</field>
|
||||
<field name="signer_id" attrs="{'invisible': [('signers_count', '!=', 0)], 'required': [('signers_count', '=', 0)]}" context="{'force_email':True, 'show_email': True}"/>
|
||||
<field name="signers_count" invisible="1"/>
|
||||
<field name="is_user_signer" invisible="1"/>
|
||||
<field name="template_id" invisible="1" kanban_view_ref="%(odex25_sign.sign_template_view_kanban_mobile)s"/>
|
||||
<field name="follower_ids" widget="many2many_tags" placeholder="Write email or search contact..." context="{'show_email': True}"/>
|
||||
<field name="subject" placeholder="Signature Request" invisible="context.get('sign_directly_without_mail',False)"/>
|
||||
<field name="message" placeholder="Optional Message..." invisible="context.get('sign_directly_without_mail',False)"/>
|
||||
<label for="filename" invisible="context.get('sign_directly_without_mail',True)"/>
|
||||
<div class="o_row" invisible="context.get('sign_directly_without_mail',True)">
|
||||
<field name="filename" placeholder="Name for the file" nolabel="1" />
|
||||
</div>
|
||||
</group>
|
||||
<footer>
|
||||
<button string="Sign Now" name="sign_directly" type="object" class="btn-primary" invisible="context.get('sign_directly_without_mail',False)" attrs="{'invisible': [('is_user_signer', '=', False)]}"/>
|
||||
<button string="Send" name="send_request" type="object" class="btn-primary" invisible="context.get('sign_directly_without_mail',False)"/>
|
||||
<button string="Sign Now" name="sign_directly_without_mail" type="object" class="btn-primary" invisible="not context.get('sign_directly_without_mail',False)"/>
|
||||
<button string="Cancel" class="btn-secondary" special="cancel"/>
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_sign_send_request" model="ir.actions.act_window">
|
||||
<field name="name">Signature Request</field>
|
||||
<field name="res_model">sign.send.request</field>
|
||||
<field name="target">new</field>
|
||||
<field name="view_mode">form</field>
|
||||
</record>
|
||||
</odoo>
|
||||
|
|
@ -1,38 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
import uuid
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
|
||||
|
||||
class SignTemplateShare(models.TransientModel):
|
||||
_name = 'sign.template.share'
|
||||
_description = 'Sign Share Template'
|
||||
|
||||
@api.model
|
||||
def default_get(self, fields):
|
||||
res = super(SignTemplateShare, self).default_get(fields)
|
||||
if 'url' in fields:
|
||||
template = self.env['sign.template'].browse(res.get('template_id'))
|
||||
if template.responsible_count > 1:
|
||||
res['url'] = False
|
||||
else:
|
||||
if not template.share_link:
|
||||
template.share_link = str(uuid.uuid4())
|
||||
base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url')
|
||||
res['url'] = "%s/odex25_sign/%s" % (base_url, template.share_link)
|
||||
return res
|
||||
|
||||
template_id = fields.Many2one(
|
||||
'sign.template', required=True, ondelete='cascade',
|
||||
default=lambda s: s.env.context.get("active_id", None),
|
||||
)
|
||||
url = fields.Char(string="Link to Share")
|
||||
is_one_responsible = fields.Boolean()
|
||||
|
||||
def open(self):
|
||||
return {
|
||||
'name': _('Sign'),
|
||||
'type': 'ir.actions.act_url',
|
||||
'url': '/odex25_sign/%s' % (self.template_id.share_link),
|
||||
}
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="sign_template_share_view_form" model="ir.ui.view">
|
||||
<field name="name">sign.template.share.view.form</field>
|
||||
<field name="model">sign.template.share</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Multiple Signature Requests">
|
||||
<field name="template_id" invisible="1"/>
|
||||
<span class="text-muted" attrs="{'invisible': [('url', '=', False)]}">Share this link and Odoo will create a new document per person who clicks on the link. The link is private, only those that receive the link will be able to sign it.</span>
|
||||
<group attrs="{'invisible': [('url', '=', False)]}">
|
||||
<field name="url" widget="CopyClipboardChar" readonly="1"/>
|
||||
</group>
|
||||
<span attrs="{'invisible': [('url', '!=', False)]}">To be able to share, there should be only one responsible for all the fields.</span>
|
||||
<footer>
|
||||
<button string="Close" class="btn-primary" special="cancel"/>
|
||||
<button string="Sign Now" name="open" type="object" attrs="{'invisible': [('url', '=', False)]}"/>
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_sign_template_share" model="ir.actions.act_window">
|
||||
<field name="name">Share Document by Link</field>
|
||||
<field name="res_model">sign.template.share</field>
|
||||
<field name="target">new</field>
|
||||
<field name="view_mode">form</field>
|
||||
</record>
|
||||
</odoo>
|
||||