[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:
jbm-odoo
2019-02-14 12:59:10 +00:00
parent 7410de111f
commit 4960453db9
22 changed files with 497 additions and 119 deletions
-21
View File
@@ -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">
-10
View File
@@ -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",
+31
View File
@@ -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);
});
@@ -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;
}
}
+44
View File
@@ -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">
@@ -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>
+4
View File
@@ -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"/>