[IMP] web Add widget signature on binary field
Add a new widget for binary fields. It open a dialog box and the user can sign manually, or an signature can be draw automatically or he can upload a picture of his signature. Move fonts, controller, scss,templates about signature from portal to web to avoid redundance closes odoo/odoo#30222
This commit is contained in:
@@ -188,27 +188,6 @@ class CustomerPortal(Controller):
|
||||
response.headers['X-Frame-Options'] = 'DENY'
|
||||
return response
|
||||
|
||||
@route(['/portal/sign/get_fonts'], type='json', auth='public')
|
||||
def get_fonts(self):
|
||||
"""This route will return a list of base64 encoded fonts.
|
||||
|
||||
Those fonts will be proposed to the user when creating a signature
|
||||
using mode 'auto'.
|
||||
|
||||
:return: base64 encoded fonts
|
||||
:rtype: list
|
||||
"""
|
||||
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
fonts_directory = os.path.join(current_dir, '..', 'static', 'font', 'sign')
|
||||
font_filenames = sorted(os.listdir(fonts_directory))
|
||||
|
||||
fonts = []
|
||||
for filename in font_filenames:
|
||||
font_file = open(os.path.join(fonts_directory, filename), 'rb')
|
||||
font = base64.b64encode(font_file.read())
|
||||
fonts.append(font)
|
||||
return fonts
|
||||
|
||||
def details_form_validate(self, data):
|
||||
error = dict()
|
||||
error_message = []
|
||||
|
||||
@@ -3,7 +3,7 @@ odoo.define('portal.signature_form', function (require) {
|
||||
|
||||
var core = require('web.core');
|
||||
var dom = require('web.dom');
|
||||
var NameAndSignature = require('portal.name_and_signature').NameAndSignature;
|
||||
var NameAndSignature = require('web.name_and_signature').NameAndSignature;
|
||||
var qweb = core.qweb;
|
||||
var Widget = require('web.Widget');
|
||||
|
||||
@@ -61,7 +61,7 @@ odoo.define('portal.signature_form', function (require) {
|
||||
start: function () {
|
||||
this.$confirm_btn = this.$('.o_portal_sign_submit');
|
||||
this.$controls = this.$('.o_portal_sign_controls');
|
||||
this.nameAndSignature.replace(this.$('.o_portal_sign_name_and_signature'));
|
||||
this.nameAndSignature.replace(this.$('.o_web_sign_name_and_signature'));
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
|
||||
@@ -168,7 +168,7 @@ $('.o_portal_signature_form').each(function () {
|
||||
var nameAndSignatureOptions = {
|
||||
defaultName: $elem.data('default-name'),
|
||||
mode: $elem.data('mode'),
|
||||
signatureRatio: $elem.data('signature-ratio'),
|
||||
displaySignatureRatio: $elem.data('signature-ratio'),
|
||||
signatureType: $elem.data('signature-type'),
|
||||
};
|
||||
var sendLabel = $elem.data('send-label');
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
.o_portal_sign_name_and_signature {
|
||||
// for the absolute of the font selection to work
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Signature Dialog */
|
||||
// The .card selector is important to make sure the position absolute will take
|
||||
// priority over the position relative of the card.
|
||||
.card.o_portal_sign_auto_font_selection {
|
||||
@include o-position-absolute(0, 0);
|
||||
|
||||
.o_portal_sign_auto_font_list {
|
||||
overflow: auto;
|
||||
|
||||
> a {
|
||||
height: 100px;
|
||||
|
||||
> img {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
<t t-name="portal.portal_signature">
|
||||
<form method="POST">
|
||||
<input type="hidden" name="csrf_token" t-att-value="widget.csrf_token"/>
|
||||
<div class="o_portal_sign_name_and_signature"/>
|
||||
<div class="o_web_sign_name_and_signature"/>
|
||||
<div class="o_portal_sign_controls my-3">
|
||||
<div class="text-center my-3">
|
||||
<button type="submit" class="o_portal_sign_submit btn btn-primary" disabled="disabled">
|
||||
|
||||
@@ -25,14 +25,4 @@
|
||||
<script type="text/javascript" src="/portal/static/src/js/portal_sidebar.js"></script>
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
<template id="assets_common" inherit_id="web.assets_common" name="Portal Common Assets">
|
||||
<xpath expr="link[last()]" position="after">
|
||||
<link rel="stylesheet" type="text/scss" href="/portal/static/src/scss/portal_name_and_signature.scss"/>
|
||||
</xpath>
|
||||
<xpath expr="script[last()]" position="after">
|
||||
<script type="text/javascript" src="/web/static/lib/jSignature/jSignatureCustom.js"></script>
|
||||
<script type="text/javascript" src="/portal/static/src/js/portal_name_and_signature.js"></script>
|
||||
</xpath>
|
||||
</template>
|
||||
</odoo>
|
||||
|
||||
@@ -26,7 +26,7 @@ tour.register('sale_signature', {
|
||||
},
|
||||
{
|
||||
content: "click Auto",
|
||||
trigger: '.o_portal_sign_auto_button',
|
||||
trigger: '.o_web_sign_auto_button',
|
||||
},
|
||||
{
|
||||
content: "check submit is enabled",
|
||||
@@ -35,11 +35,11 @@ tour.register('sale_signature', {
|
||||
},
|
||||
{
|
||||
content: "click select style",
|
||||
trigger: '.o_portal_sign_auto_select_style a',
|
||||
trigger: '.o_web_sign_auto_select_style a',
|
||||
},
|
||||
{
|
||||
content: "click style 4",
|
||||
trigger: '.o_portal_sign_auto_font_selection a:eq(3)',
|
||||
trigger: '.o_web_sign_auto_font_selection a:eq(3)',
|
||||
},
|
||||
{
|
||||
content: "click submit",
|
||||
|
||||
@@ -1185,6 +1185,37 @@ class Binary(http.Controller):
|
||||
|
||||
return response
|
||||
|
||||
@http.route(['/web/sign/get_fonts','/web/sign/get_fonts/<string:fontname>'], type='json', auth='public')
|
||||
def get_fonts(self, fontname=None):
|
||||
"""This route will return a list of base64 encoded fonts.
|
||||
|
||||
Those fonts will be proposed to the user when creating a signature
|
||||
using mode 'auto'.
|
||||
|
||||
:return: base64 encoded fonts
|
||||
:rtype: list
|
||||
"""
|
||||
|
||||
|
||||
fonts = []
|
||||
if fontname:
|
||||
font_path = get_resource_path('web', 'static/src/fonts/sign', fontname)
|
||||
if not font_path:
|
||||
return []
|
||||
font_file = open(font_path, 'rb')
|
||||
font = base64.b64encode(font_file.read())
|
||||
fonts.append(font)
|
||||
else:
|
||||
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
fonts_directory = os.path.join(current_dir, '..', 'static', 'src', 'fonts', 'sign')
|
||||
font_filenames = sorted(os.listdir(fonts_directory))
|
||||
|
||||
for filename in font_filenames:
|
||||
font_file = open(os.path.join(fonts_directory, filename), 'rb')
|
||||
font = base64.b64encode(font_file.read())
|
||||
fonts.append(font)
|
||||
return fonts
|
||||
|
||||
class Action(http.Controller):
|
||||
|
||||
@http.route('/web/action/load', type='json', auth="user")
|
||||
|
||||
Binary file not shown.
@@ -3005,6 +3005,7 @@ return {
|
||||
FieldEmail: FieldEmail,
|
||||
FieldBinaryFile: FieldBinaryFile,
|
||||
FieldPdfViewer: FieldPdfViewer,
|
||||
AbstractFieldBinary: AbstractFieldBinary,
|
||||
FieldBinaryImage: FieldBinaryImage,
|
||||
FieldBoolean: FieldBoolean,
|
||||
FieldBooleanButton: FieldBooleanButton,
|
||||
|
||||
@@ -37,6 +37,7 @@ registry
|
||||
.add('CopyClipboardText', basic_fields.TextCopyClipboard)
|
||||
.add('CopyClipboardChar', basic_fields.CharCopyClipboard)
|
||||
.add('image', basic_fields.FieldBinaryImage)
|
||||
.add('signature', basic_fields.FieldBinarySignature)
|
||||
.add('binary', basic_fields.FieldBinaryFile)
|
||||
.add('pdf_viewer', basic_fields.FieldPdfViewer)
|
||||
.add('monetary', basic_fields.FieldMonetary)
|
||||
|
||||
@@ -0,0 +1,272 @@
|
||||
odoo.define('web.Signature', function (require) {
|
||||
"use strict";
|
||||
|
||||
var AbstractFieldBinary = require('web.basic_fields').AbstractFieldBinary;
|
||||
var core = require('web.core');
|
||||
var Dialog = require('web.Dialog');
|
||||
var field_utils = require('web.field_utils');
|
||||
var NameAndSignature = require('web.name_and_signature').NameAndSignature;
|
||||
var registry = require('web.field_registry');
|
||||
var session = require('web.session');
|
||||
var utils = require('web.utils');
|
||||
|
||||
|
||||
var qweb = core.qweb;
|
||||
var _t = core._t;
|
||||
|
||||
// The goal of this dialog is to ask the user a signature request.
|
||||
// It uses @see SignNameAndSignature for the name and signature fields.
|
||||
var SignatureDialog = Dialog.extend({
|
||||
template: 'web.signature_dialog',
|
||||
xmlDependencies: Dialog.prototype.xmlDependencies.concat(
|
||||
['/web/static/src/xml/name_and_signature.xml']
|
||||
),
|
||||
custom_events: {
|
||||
'signature_changed': '_onChangeSignature',
|
||||
},
|
||||
|
||||
/**
|
||||
* @constructor
|
||||
* @param {Widget} parent
|
||||
* @param {Object} options
|
||||
* @param {string} [options.title='Adopt Your Signature'] - modal title
|
||||
* @param {string} [options.size='medium'] - modal size
|
||||
* @param {Object} [options.nameAndSignatureOptions={}] - options for
|
||||
* @see NameAndSignature.init()
|
||||
*/
|
||||
init: function (parent, options) {
|
||||
var self = this;
|
||||
options = options || {};
|
||||
|
||||
options.title = options.title || _t("Adopt Your Signature");
|
||||
options.size = options.size || 'medium';
|
||||
options.technical = false;
|
||||
|
||||
if (!options.buttons) {
|
||||
options.buttons = [];
|
||||
options.buttons.push({text: _t("Adopt and Sign"), classes: "btn-primary", disabled: true, click: function (e) {
|
||||
self._onConfirm();
|
||||
}});
|
||||
options.buttons.push({text: _t("Cancel"), close: true});
|
||||
}
|
||||
|
||||
this._super(parent, options);
|
||||
|
||||
this.nameAndSignature = new NameAndSignature(this, options.nameAndSignatureOptions);
|
||||
},
|
||||
/**
|
||||
* Inserts the name and signature.
|
||||
*
|
||||
* @override
|
||||
*/
|
||||
start: function () {
|
||||
var self = this;
|
||||
this.$primaryButton = this.$footer.find('.btn-primary');
|
||||
|
||||
this.opened().then(function () {
|
||||
self.nameAndSignature.replace(self.$('.o_web_sign_name_and_signature'));
|
||||
});
|
||||
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
|
||||
//----------------------------------------------------------------------
|
||||
// Public
|
||||
//----------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Returns whether the drawing area is currently empty.
|
||||
*
|
||||
* @see NameAndSignature.isSignatureEmpty()
|
||||
* @returns {boolean} Whether the drawing area is currently empty.
|
||||
*/
|
||||
isSignatureEmpty: function () {
|
||||
return this.nameAndSignature.isSignatureEmpty();
|
||||
},
|
||||
|
||||
//----------------------------------------------------------------------
|
||||
// Handlers
|
||||
//----------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Toggles the submit button depending on the signature state.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_onChangeSignature: function () {
|
||||
var isEmpty = this.nameAndSignature.isSignatureEmpty();
|
||||
this.$primaryButton.prop('disabled', isEmpty);
|
||||
},
|
||||
/**
|
||||
* Upload the signature image when confirm.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_onConfirm: function (fct) {
|
||||
this.trigger_up('upload_signature', {
|
||||
name: this.nameAndSignature.getName(),
|
||||
signatureImage: this.nameAndSignature.getSignatureImage(),
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
var FieldBinarySignature = AbstractFieldBinary.extend({
|
||||
fieldDependencies: _.extend({}, AbstractFieldBinary.prototype.fieldDependencies, {
|
||||
__last_update: {type: 'datetime'},
|
||||
}),
|
||||
custom_events: _.extend({}, AbstractFieldBinary.prototype.custom_events, {
|
||||
upload_signature: '_onUploadSignature',
|
||||
}),
|
||||
events: _.extend({}, AbstractFieldBinary.prototype.events, {
|
||||
'click .o_signature': '_onClickSignature',
|
||||
}),
|
||||
template: null,
|
||||
supportedFieldTypes: ['binary'],
|
||||
file_type_magic_word: {
|
||||
'/': 'jpg',
|
||||
'R': 'gif',
|
||||
'i': 'png',
|
||||
'P': 'svg+xml',
|
||||
},
|
||||
//--------------------------------------------------------------------------
|
||||
// Public
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* This widget must always have render even if there are no signature.
|
||||
* In edit mode, the real value is return to manage required fields.
|
||||
*
|
||||
* @override
|
||||
*/
|
||||
isSet: function () {
|
||||
if (this.mode === 'edit') {
|
||||
return this.value;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Renders an empty signature or the saved signature. Both must have the same size.
|
||||
*
|
||||
* @override
|
||||
* @private
|
||||
*/
|
||||
|
||||
_render: function () {
|
||||
var self = this;
|
||||
var displaySignatureRatio = 3;
|
||||
var url;
|
||||
var $img;
|
||||
var width = this.nodeOptions.size ? this.nodeOptions.size[0] : this.attrs.width;
|
||||
var height = this.nodeOptions.size ? this.nodeOptions.size[1] : this.attrs.height;
|
||||
if (this.value) {
|
||||
if (!utils.is_bin_size(this.value)) {
|
||||
// Use magic-word technique for detecting image type
|
||||
url = 'data:image/' + (this.file_type_magic_word[this.value[0]] || 'png') + ';base64,' + this.value;
|
||||
} else {
|
||||
url = session.url('/web/image', {
|
||||
model: this.model,
|
||||
id: JSON.stringify(this.res_id),
|
||||
field: this.nodeOptions.preview_image || this.name,
|
||||
// unique forces a reload of the image when the record has been updated
|
||||
unique: field_utils.format.datetime(this.recordData.__last_update).replace(/[^0-9]/g, ''),
|
||||
});
|
||||
}
|
||||
$img = $(qweb.render("FieldBinarySignature-img", {widget: this, url: url}));
|
||||
} else {
|
||||
$img = $('<div class="o_signature o_signature_empty"><svg></svg><p>' + _t('SIGNATURE') + '</p></div>');
|
||||
if (width && height) {
|
||||
width = Math.min(width, displaySignatureRatio * height);
|
||||
height = width / displaySignatureRatio;
|
||||
} else if (width) {
|
||||
height = width / displaySignatureRatio;
|
||||
} else if (height) {
|
||||
width = height * displaySignatureRatio;
|
||||
}
|
||||
}
|
||||
if (width) {
|
||||
$img.attr('width', width);
|
||||
$img.css('max-width', width + 'px');
|
||||
}
|
||||
if (height) {
|
||||
$img.attr('height', height);
|
||||
$img.css('max-height', height + 'px');
|
||||
}
|
||||
this.$('> div').remove();
|
||||
this.$('> img').remove();
|
||||
|
||||
this.$el.prepend($img);
|
||||
|
||||
$img.on('error', function () {
|
||||
self._clearFile();
|
||||
$img.attr('src', self.placeholder);
|
||||
self.do_warn(_t("Image"), _t("Could not display the selected image."));
|
||||
});
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Handlers
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* If the view is in edit mode, open dialog to sign.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_onClickSignature: function () {
|
||||
var self = this;
|
||||
if (this.mode === 'edit') {
|
||||
|
||||
var nameAndSignatureOptions = {
|
||||
mode: 'draw',
|
||||
displaySignatureRatio: 3,
|
||||
signatureType: 'signature',
|
||||
noInputName: true,
|
||||
};
|
||||
|
||||
if (this.nodeOptions.full_name) {
|
||||
var $search = $("[name='" + this.nodeOptions.full_name + "']");
|
||||
// If full_name correspond to field in read-only mode
|
||||
var signName = $($search).text();
|
||||
|
||||
// If full_name correspond to field to fill
|
||||
if (!signName && $($search)[0]) {
|
||||
signName = $($search)[0].value;
|
||||
}
|
||||
if (!signName) {
|
||||
// If full_name is in the recordData. Last option because this value might not be the last value.
|
||||
signName = this.recordData[this.nodeOptions.full_name] || '';
|
||||
}
|
||||
nameAndSignatureOptions.defaultName = (signName === '') ? undefined : signName;
|
||||
}
|
||||
|
||||
nameAndSignatureOptions.defaultFont = this.nodeOptions.default_font || '';
|
||||
this.signDialog = new SignatureDialog(self, {nameAndSignatureOptions: nameAndSignatureOptions});
|
||||
|
||||
this.signDialog.open();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Upload the signature image if valid and close the dialog.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_onUploadSignature: function (ev) {
|
||||
var signatureImage = ev.data.signatureImage;
|
||||
if (signatureImage !== this.signDialog.emptySignature) {
|
||||
var data = signatureImage[1];
|
||||
var type = signatureImage[0].split('/')[1];
|
||||
this.on_file_uploaded(data.length, ev.data.name, type, data);
|
||||
}
|
||||
this.signDialog.close();
|
||||
}
|
||||
});
|
||||
|
||||
registry.add('signature', FieldBinarySignature);
|
||||
|
||||
});
|
||||
+65
-40
@@ -1,4 +1,4 @@
|
||||
odoo.define('portal.name_and_signature', function (require) {
|
||||
odoo.define('web.name_and_signature', function (require) {
|
||||
'use strict';
|
||||
|
||||
var core = require('web.core');
|
||||
@@ -12,26 +12,26 @@ var _t = core._t;
|
||||
* the given name and a selected font, or loaded from an image file.
|
||||
*/
|
||||
var NameAndSignature = Widget.extend({
|
||||
template: 'portal.sign_name_and_signature',
|
||||
xmlDependencies: ['/portal/static/src/xml/portal_name_and_signature.xml'],
|
||||
template: 'web.sign_name_and_signature',
|
||||
xmlDependencies: ['/web/static/src/xml/name_and_signature.xml'],
|
||||
events: {
|
||||
// name
|
||||
'input .o_portal_sign_name_input': '_onInputSignName',
|
||||
'input .o_web_sign_name_input': '_onInputSignName',
|
||||
// signature
|
||||
'click .o_portal_sign_signature': '_onClickSignature',
|
||||
'change .o_portal_sign_signature': '_onChangeSignature',
|
||||
'click .o_web_sign_signature': '_onClickSignature',
|
||||
'change .o_web_sign_signature': '_onChangeSignature',
|
||||
// draw
|
||||
'click .o_portal_sign_draw_button': '_onClickSignDrawButton',
|
||||
'click .o_portal_sign_draw_clear a': '_onClickSignDrawClear',
|
||||
'click .o_web_sign_draw_button': '_onClickSignDrawButton',
|
||||
'click .o_web_sign_draw_clear a': '_onClickSignDrawClear',
|
||||
// auto
|
||||
'click .o_portal_sign_auto_button': '_onClickSignAutoButton',
|
||||
'click .o_portal_sign_auto_select_style a': '_onClickSignAutoSelectStyle',
|
||||
'click .o_portal_sign_auto_font_selection a': '_onClickSignAutoFontSelection',
|
||||
'mouseover .o_portal_sign_auto_font_selection a': '_onMouseOverSignAutoFontSelection',
|
||||
'touchmove .o_portal_sign_auto_font_selection a': '_onTouchStartSignAutoFontSelection',
|
||||
'click .o_web_sign_auto_button': '_onClickSignAutoButton',
|
||||
'click .o_web_sign_auto_select_style a': '_onClickSignAutoSelectStyle',
|
||||
'click .o_web_sign_auto_font_selection a': '_onClickSignAutoFontSelection',
|
||||
'mouseover .o_web_sign_auto_font_selection a': '_onMouseOverSignAutoFontSelection',
|
||||
'touchmove .o_web_sign_auto_font_selection a': '_onTouchStartSignAutoFontSelection',
|
||||
// load
|
||||
'click .o_portal_sign_load_button': '_onClickSignLoadButton',
|
||||
'change .o_portal_sign_load_file input': '_onChangeSignLoadInput',
|
||||
'click .o_web_sign_load_button': '_onClickSignLoadButton',
|
||||
'change .o_web_sign_load_file input': '_onChangeSignLoadInput',
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -40,10 +40,15 @@ var NameAndSignature = Widget.extend({
|
||||
* @constructor
|
||||
* @param {Widget} parent
|
||||
* @param {Object} [options={}]
|
||||
* @param {number} [options.signatureRatio=3.0] - The ratio used when
|
||||
* @param {number} [options.displaySignatureRatio=3.0] - The ratio used when
|
||||
* (re)computing the size of the signature (width = height * ratio)
|
||||
* @param {string} [options.defaultName=''] - The default name of
|
||||
* the signer.
|
||||
* @param {string} [options.defaultFont=''] - The unique and default
|
||||
* font for auto mode. If empty, all fonts are visible.
|
||||
* @param {string} [options.noInputName=false] - If set to true,
|
||||
* the user can not enter his name. If there aren't defaultName,
|
||||
* auto mode is hidden.
|
||||
* @param {string} [options.mode='draw'] - @see this.setMode
|
||||
* @param {string} [options.signatureType='signature'] - The type of
|
||||
* signature used in 'auto' mode. Can be one of the following values:
|
||||
@@ -58,9 +63,11 @@ var NameAndSignature = Widget.extend({
|
||||
options = options || {};
|
||||
this.htmlId = _.uniqueId();
|
||||
this.defaultName = options.defaultName || '';
|
||||
this.signatureRatio = options.signatureRatio || 3.0;
|
||||
this.defaultFont = options.defaultFont || '';
|
||||
this.displaySignatureRatio = options.displaySignatureRatio || 3.0;
|
||||
this.signatureType = options.signatureType || 'signature';
|
||||
this.signMode = options.mode || 'draw';
|
||||
this.noInputName = options.noInputName || false;
|
||||
this.currentFont = 0;
|
||||
this.drawTimeout = null;
|
||||
this.drawPreviewTimeout = null;
|
||||
@@ -72,12 +79,14 @@ var NameAndSignature = Widget.extend({
|
||||
*/
|
||||
willStart: function () {
|
||||
var self = this;
|
||||
|
||||
return $.when(
|
||||
this._super.apply(this, arguments),
|
||||
this._rpc({route: '/portal/sign/get_fonts'}).then(function (data) {
|
||||
this._rpc({route: '/web/sign/get_fonts/' + self.defaultFont}).then(function (data) {
|
||||
self.fonts = data;
|
||||
})
|
||||
);
|
||||
|
||||
},
|
||||
/**
|
||||
* Finds the DOM elements, initializes the signature area,
|
||||
@@ -88,22 +97,23 @@ var NameAndSignature = Widget.extend({
|
||||
start: function () {
|
||||
var self = this;
|
||||
// signature and name input
|
||||
this.$signatureGroup = this.$('.o_portal_sign_signature_group');
|
||||
this.$signatureField = this.$('.o_portal_sign_signature');
|
||||
this.$nameInput = this.$('.o_portal_sign_name_input');
|
||||
this.$signatureGroup = this.$('.o_web_sign_signature_group');
|
||||
this.$signatureField = this.$('.o_web_sign_signature');
|
||||
this.$nameInput = this.$('.o_web_sign_name_input');
|
||||
this.$nameInputGroup = this.$('.o_web_sign_name_group');
|
||||
|
||||
// mode selection buttons
|
||||
this.$drawButton = this.$('a.o_portal_sign_draw_button');
|
||||
this.$autoButton = this.$('a.o_portal_sign_auto_button');
|
||||
this.$loadButton = this.$('a.o_portal_sign_load_button');
|
||||
this.$drawButton = this.$('a.o_web_sign_draw_button');
|
||||
this.$autoButton = this.$('a.o_web_sign_auto_button');
|
||||
this.$loadButton = this.$('a.o_web_sign_load_button');
|
||||
|
||||
// mode: draw
|
||||
this.$drawClear = this.$('.o_portal_sign_draw_clear');
|
||||
this.$drawClear = this.$('.o_web_sign_draw_clear');
|
||||
|
||||
// mode: auto
|
||||
this.$autoSelectStyle = this.$('.o_portal_sign_auto_select_style');
|
||||
this.$autoFontSelection = this.$('.o_portal_sign_auto_font_selection');
|
||||
this.$autoFontList = this.$('.o_portal_sign_auto_font_list');
|
||||
this.$autoSelectStyle = this.$('.o_web_sign_auto_select_style');
|
||||
this.$autoFontSelection = this.$('.o_web_sign_auto_font_selection');
|
||||
this.$autoFontList = this.$('.o_web_sign_auto_font_list');
|
||||
for (var i in this.fonts) {
|
||||
var $img = $('<img/>').addClass('img-fluid');
|
||||
var $a = $('<a/>').addClass('btn p-0').append($img).data('fontNb', i);
|
||||
@@ -111,11 +121,22 @@ var NameAndSignature = Widget.extend({
|
||||
}
|
||||
|
||||
// mode: load
|
||||
this.$loadFile = this.$('.o_portal_sign_load_file');
|
||||
this.$loadInvalid = this.$('.o_portal_sign_load_invalid');
|
||||
this.$loadFile = this.$('.o_web_sign_load_file');
|
||||
this.$loadInvalid = this.$('.o_web_sign_load_invalid');
|
||||
|
||||
if (this.fonts && this.fonts.length < 2) {
|
||||
this.$autoSelectStyle.hide();
|
||||
}
|
||||
|
||||
if (this.noInputName) {
|
||||
if (this.defaultName === "") {
|
||||
this.$autoButton.hide();
|
||||
}
|
||||
this.$nameInputGroup.hide();
|
||||
}
|
||||
|
||||
// Resize the signature area if it is resized
|
||||
$(window).on('resize.o_portal_sign_name_and_signature', _.debounce(function () {
|
||||
$(window).on('resize.o_web_sign_name_and_signature', _.debounce(function () {
|
||||
if (self.isDestroyed()) {
|
||||
// May happen since this is debounced
|
||||
return;
|
||||
@@ -133,7 +154,7 @@ var NameAndSignature = Widget.extend({
|
||||
*/
|
||||
destroy: function () {
|
||||
this._super.apply(this, arguments);
|
||||
$(window).off('resize.o_portal_sign_name_and_signature');
|
||||
$(window).off('resize.o_web_sign_name_and_signature');
|
||||
},
|
||||
|
||||
//----------------------------------------------------------------------
|
||||
@@ -146,7 +167,6 @@ var NameAndSignature = Widget.extend({
|
||||
focusName: function () {
|
||||
this.$nameInput.focus();
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets the name currently given by the user.
|
||||
*
|
||||
@@ -201,7 +221,7 @@ var NameAndSignature = Widget.extend({
|
||||
// recompute size based on the current width
|
||||
this.$signatureField.css({width: 'unset'});
|
||||
var width = this.$signatureField.width();
|
||||
var height = parseInt(width / this.signatureRatio);
|
||||
var height = parseInt(width / this.displaySignatureRatio);
|
||||
|
||||
// necessary because the lib is adding invisible div with margin
|
||||
// signature field too tall without this code
|
||||
@@ -313,7 +333,8 @@ var NameAndSignature = Widget.extend({
|
||||
* @returns {string} cleaned name
|
||||
*/
|
||||
_getCleanedName: function () {
|
||||
var text = this.getName().replace(/[^[\w\u00E0-\u00FC]-'" ]/g, '');
|
||||
var regexRemoveSpecialCharacters = /[^\w\u00E0-\u00FC-'" ]/g;
|
||||
var text = this.getName().replace(regexRemoveSpecialCharacters, '');
|
||||
if (this.signatureType === 'initial') {
|
||||
return (text.split(' ').map(function (w) {
|
||||
return w[0];
|
||||
@@ -333,7 +354,7 @@ var NameAndSignature = Widget.extend({
|
||||
* @returns {string} image = mimetype + image data
|
||||
*/
|
||||
_getSVGText: function (font, text, width, height) {
|
||||
var $svg = $(core.qweb.render('portal.sign_svg_text', {
|
||||
var $svg = $(core.qweb.render('web.sign_svg_text', {
|
||||
width: width,
|
||||
height: height,
|
||||
font: font,
|
||||
@@ -421,7 +442,7 @@ var NameAndSignature = Widget.extend({
|
||||
clearTimeout(this.drawPreviewTimeout);
|
||||
this.drawPreviewTimeout = setTimeout(function () {
|
||||
var height = 100;
|
||||
var width = parseInt(height * self.signatureRatio);
|
||||
var width = parseInt(height * self.displaySignatureRatio);
|
||||
var $existingButtons = self.$autoFontList.find('a');
|
||||
for (var i = 0; i < self.fonts.length; i++) {
|
||||
var imgSrc = self._getSVGText(
|
||||
@@ -503,7 +524,7 @@ var NameAndSignature = Widget.extend({
|
||||
_onClickSignAutoSelectStyle: function (ev) {
|
||||
var self = this;
|
||||
var width = Math.min(
|
||||
self.$autoFontSelection.find('a').first().height() * self.signatureRatio * 1.25,
|
||||
self.$autoFontSelection.find('a').first().height() * self.displaySignatureRatio * 1.25,
|
||||
this.$signatureField.width()
|
||||
);
|
||||
|
||||
@@ -562,9 +583,14 @@ var NameAndSignature = Widget.extend({
|
||||
* @see mode 'load'
|
||||
* @private
|
||||
* @param {Event} ev
|
||||
* @return bool|undefined
|
||||
*/
|
||||
_onChangeSignLoadInput: function (ev) {
|
||||
var self = this;
|
||||
var f = ev.target.files[0];
|
||||
if (f === undefined) {
|
||||
return false;
|
||||
}
|
||||
if (f.type.substr(0, 5) !== 'image') {
|
||||
this.$signatureField.jSignature('reset');
|
||||
this.$loadInvalid.removeClass('d-none');
|
||||
@@ -572,10 +598,9 @@ var NameAndSignature = Widget.extend({
|
||||
}
|
||||
this.$loadInvalid.addClass('d-none');
|
||||
|
||||
var self = this;
|
||||
var reader = new FileReader();
|
||||
reader.onload = function (ev) {
|
||||
self._printImage(this.result);
|
||||
self._printImage(reader.result);
|
||||
};
|
||||
reader.readAsDataURL(f);
|
||||
},
|
||||
@@ -0,0 +1,49 @@
|
||||
.o_web_sign_name_and_signature {
|
||||
// for the absolute of the font selection to work
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Signature Dialog */
|
||||
// The .card selector is important to make sure the position absolute will take
|
||||
// priority over the position relative of the card.
|
||||
.card.o_web_sign_auto_font_selection {
|
||||
@include o-position-absolute(0, 0);
|
||||
|
||||
.o_web_sign_auto_font_list {
|
||||
overflow: auto;
|
||||
|
||||
> a {
|
||||
height: 100px;
|
||||
|
||||
> img {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.o_field_widget {
|
||||
.o_signature {
|
||||
outline: 1px solid rgba(theme-color('secondary'), 0.3);
|
||||
position: relative;
|
||||
> p {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.o_field_invalid {
|
||||
.o_signature {
|
||||
outline: 3px solid $o-notification-error-bg-color;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
.o_form_editable {
|
||||
.o_signature:hover {
|
||||
outline: 3px solid $o-enterprise-primary-color;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
@@ -1006,6 +1006,50 @@
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<t t-name="FieldBinarySignature-img">
|
||||
<img class="o_signature img img-fluid"
|
||||
alt="Binary file"
|
||||
t-att-src='url'
|
||||
t-att-border="1"
|
||||
t-att-name="widget.name"/>
|
||||
</t>
|
||||
|
||||
<div t-name="FieldBinarySignature">
|
||||
<div class="o_sign_signature_draw panel panel-default mt16 mb0">
|
||||
<div class="card-header">
|
||||
<a role="button" class="o_sign_select_style float-right btn btn-link">Select Signature Style</a>
|
||||
<a role="button" class="o_sign_clean float-right btn btn-link">Clear Signature</a>
|
||||
<a data-mode="draw" class="o_sign_mode o_sign_mode_draw btn btn-primary" role="button">Draw</a>
|
||||
<a data-mode="auto" class="o_sign_mode o_sign_mode_auto btn" role="button">Auto</a>
|
||||
<a data-mode="load" class="o_sign_mode o_sign_mode_load btn" role="button">Load</a>
|
||||
<input type="file" name="files[]" class="o_sign_load btn btn-link" role="button"/>
|
||||
</div>
|
||||
<div class="o_sign_signature card-body"/>
|
||||
</div>
|
||||
<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 class="o_sign_font_dialog card">
|
||||
<div class="card-header">Styles</div>
|
||||
<div class="o_sign_font_selection card-body"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<t t-name="SignatureSvgText" name="SVG Signature Text">
|
||||
<svg t-att-width="width" t-att-height="height">
|
||||
<defs>
|
||||
<style type="text/css">
|
||||
@font-face {
|
||||
font-family: "font_sign";
|
||||
src: url(data:font/ttf;base64,<t t-esc="font"/>) format("woff");
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<text x="50%" t-att-y="height*4/5" t-att-font-size="height*4/5" t-att-textLength="width * 4 / 5 * Math.min(1, text.length / 7)" lengthAdjust="spacingAndGlyphs" style="font-family:'font_sign'" fill="black" text-anchor="middle"><t t-esc="text"/></text>
|
||||
</svg>
|
||||
</t>
|
||||
|
||||
<t t-name="FieldPdfViewer">
|
||||
<div class="o_field_pdfviewer" aria-atomic="true">
|
||||
<div t-if="widget.mode !== 'readonly'" class="o_form_pdf_controls mt8" role="toolbar" aria-label="PDF controls">
|
||||
|
||||
+23
-18
@@ -16,7 +16,7 @@
|
||||
-> otherwise, it will adapt the space between characters to fill
|
||||
the image with the text.
|
||||
-->
|
||||
<t t-name="portal.sign_svg_text" name="SVG Signature Text">
|
||||
<t t-name="web.sign_svg_text" name="SVG Signature Text">
|
||||
<svg t-att-width="width" t-att-height="height">
|
||||
<defs>
|
||||
<style type="text/css">
|
||||
@@ -36,12 +36,12 @@
|
||||
</svg>
|
||||
</t>
|
||||
|
||||
<!-- Template for the widget PortalSignNameAndDraw. -->
|
||||
<t t-name="portal.sign_name_and_signature">
|
||||
<div class="o_portal_sign_name_and_signature">
|
||||
<div class="o_portal_sign_name_group form-group">
|
||||
<label class="col-form-label" t-att-for="'o_portal_sign_name_input_' + widget.htmlId">Full Name</label>
|
||||
<input type="text" name="signer" t-att-id="'o_portal_sign_name_input_' + widget.htmlId" class="o_portal_sign_name_input form-control" t-att-value="widget.defaultName" placeholder="Your name" required=""/>
|
||||
<!-- Template for the widget webSignNameAndDraw. -->
|
||||
<t t-name="web.sign_name_and_signature">
|
||||
<div class="o_web_sign_name_and_signature">
|
||||
<div class="o_web_sign_name_group form-group">
|
||||
<label class="col-form-label" t-att-for="'o_web_sign_name_input_' + widget.htmlId">Full Name</label>
|
||||
<input type="text" name="signer" t-att-id="'o_web_sign_name_input_' + widget.htmlId" class="o_web_sign_name_input form-control" t-att-value="widget.defaultName" placeholder="Your name" required=""/>
|
||||
</div>
|
||||
|
||||
<!--
|
||||
@@ -49,53 +49,58 @@
|
||||
border, but we remove the padding from the body, so the canvas
|
||||
will overflow on the corners without this rule.
|
||||
-->
|
||||
<div class="o_portal_sign_signature_group card mt-3" style="overflow: hidden;">
|
||||
<div class="o_web_sign_signature_group card mt-3" style="overflow: hidden;">
|
||||
<div class="card-header">
|
||||
<div class="row no-gutters">
|
||||
<div class="col-auto">
|
||||
<a role="button" href="#" t-attf-class="o_portal_sign_draw_button mr-2 btn btn-light {{ widget.signMode === 'draw' ? 'active': '' }}">
|
||||
<a role="button" href="#" t-attf-class="o_web_sign_draw_button mr-2 btn btn-light {{ widget.signMode === 'draw' ? 'active': '' }}">
|
||||
Draw
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<a role="button" href="#" t-attf-class="o_portal_sign_auto_button mr-2 btn btn-light {{ widget.signMode === 'auto' ? 'active': '' }}">
|
||||
<a role="button" href="#" t-attf-class="o_web_sign_auto_button mr-2 btn btn-light {{ widget.signMode === 'auto' ? 'active': '' }}">
|
||||
Auto
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<a role="button" href="#" t-attf-class="o_portal_sign_load_button mr-2 btn btn-light {{ widget.signMode === 'load' ? 'active': '' }}">
|
||||
<a role="button" href="#" t-attf-class="o_web_sign_load_button mr-2 btn btn-light {{ widget.signMode === 'load' ? 'active': '' }}">
|
||||
Load
|
||||
</a>
|
||||
</div>
|
||||
<div t-attf-class="o_portal_sign_draw_clear col-auto ml-auto {{ widget.signMode !== 'draw' ? 'd-none' : '' }}">
|
||||
<div t-attf-class="o_web_sign_draw_clear col-auto ml-auto {{ widget.signMode !== 'draw' ? 'd-none' : '' }}">
|
||||
<a role="button" href="#" class="btn btn-link">
|
||||
Clear
|
||||
</a>
|
||||
</div>
|
||||
<div t-attf-class="o_portal_sign_auto_select_style col-auto ml-auto {{ widget.signMode !== 'auto' ? 'd-none' : '' }}">
|
||||
<div t-attf-class="o_web_sign_auto_select_style col-auto ml-auto {{ widget.signMode !== 'auto' ? 'd-none' : '' }}">
|
||||
<a role="button" href="#" class="btn btn-link">
|
||||
Style
|
||||
</a>
|
||||
</div>
|
||||
<div t-attf-class="o_portal_sign_load_file col-auto {{ widget.signMode !== 'load' ? 'd-none' : '' }}">
|
||||
<div t-attf-class="o_web_sign_load_file col-auto {{ widget.signMode !== 'load' ? 'd-none' : '' }}">
|
||||
<input type="file" role="button" name="files[]" class="btn btn-link py-0"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="o_portal_sign_signature card-body p-0"/>
|
||||
<div class="o_web_sign_signature card-body p-0"/>
|
||||
|
||||
<div t-attf-class="o_portal_sign_load_invalid card-footer d-none">
|
||||
<div t-attf-class="o_web_sign_load_invalid card-footer d-none">
|
||||
<div class="alert alert-danger mb-0" role="alert">
|
||||
This file is invalid. Please select an image.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="o_portal_sign_auto_font_selection card d-none h-100 flex-column">
|
||||
<div class="o_web_sign_auto_font_selection card d-none h-100 flex-column">
|
||||
<div class="card-header">Styles</div>
|
||||
<div class="o_portal_sign_auto_font_list card-body text-center p-3 flex-grow-1"/>
|
||||
<div class="o_web_sign_auto_font_list card-body text-center p-3 flex-grow-1"/>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
<div t-name="web.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>
|
||||
</templates>
|
||||
@@ -61,6 +61,7 @@
|
||||
<link rel="stylesheet" type="text/scss" href="/web/static/src/scss/banner.scss"/>
|
||||
<link rel="stylesheet" type="text/scss" href="/web/static/src/scss/colorpicker.scss"/>
|
||||
<link rel="stylesheet" type="text/scss" href="/web/static/src/scss/keyboard.scss"/>
|
||||
<link rel="stylesheet" type="text/scss" href="/web/static/src/scss/name_and_signature.scss"/>
|
||||
|
||||
<t t-if="request and request.env['res.lang'].search([('code', '=', request.env.lang)]).direction == 'rtl'">
|
||||
<link rel="stylesheet" type="text/less" href="/web/static/src/scss/fontawesome_overriden.scss"/>
|
||||
@@ -136,6 +137,8 @@
|
||||
<script type="text/javascript" src="/web/static/src/js/services/session_storage_service.js"></script>
|
||||
<script type="text/javascript" src="/web/static/src/js/core/ram_storage.js"></script>
|
||||
<script type="text/javascript" src="/web/static/src/js/widgets/rainbow_man.js"></script>
|
||||
<script type="text/javascript" src="/web/static/lib/jSignature/jSignatureCustom.js"></script>
|
||||
<script type="text/javascript" src="/web/static/src/js/widgets/name_and_signature.js"></script>
|
||||
</template>
|
||||
|
||||
<template id="assets_backend" name="Backend Assets (used in backend interface)">
|
||||
@@ -322,6 +325,7 @@
|
||||
<script type="text/javascript" src="/web/static/src/js/views/calendar/calendar_view.js"></script>
|
||||
<script type="text/javascript" src="/web/static/src/js/views/qweb/qweb_view.js"></script>
|
||||
<script type="text/javascript" src="/web/static/src/js/widgets/attach_document.js"></script>
|
||||
<script type="text/javascript" src="/web/static/src/js/fields/signature.js"></script>
|
||||
|
||||
<script type="text/javascript" src="/web/static/src/js/report/utils.js"/>
|
||||
<script type="text/javascript" src="/web/static/src/js/report/client_action.js"/>
|
||||
|
||||
Reference in New Issue
Block a user