diff --git a/stamp_sign/__init__.py b/stamp_sign/__init__.py new file mode 100644 index 00000000000..f7209b17100 --- /dev/null +++ b/stamp_sign/__init__.py @@ -0,0 +1,2 @@ +from . import models +from . import controllers diff --git a/stamp_sign/__manifest__.py b/stamp_sign/__manifest__.py new file mode 100644 index 00000000000..4fb7f3b476f --- /dev/null +++ b/stamp_sign/__manifest__.py @@ -0,0 +1,23 @@ +{ + "name": "Stamp Sign", + "version": "1.0", + "depends": ["sign"], + "category": "Sign", + "data": [ + "data/sign_data.xml", + "views/sign_request_templates.xml", + ], + "assets": { + "web.assets_backend": [ + "stamp_sign/static/src/components/sign_request/*", + "stamp_sign/static/src/dialogs/*", + ], + "sign.assets_public_sign": [ + "stamp_sign/static/src/components/sign_request/*", + "stamp_sign/static/src/dialogs/*", + ], + }, + "installable": True, + "application": True, + "license": "OEEL-1", +} diff --git a/stamp_sign/controllers/__init__.py b/stamp_sign/controllers/__init__.py new file mode 100644 index 00000000000..12a7e529b67 --- /dev/null +++ b/stamp_sign/controllers/__init__.py @@ -0,0 +1 @@ +from . import main diff --git a/stamp_sign/controllers/main.py b/stamp_sign/controllers/main.py new file mode 100644 index 00000000000..8c2a6d8a8c7 --- /dev/null +++ b/stamp_sign/controllers/main.py @@ -0,0 +1,73 @@ +from odoo import http +from odoo.addons.sign.controllers.main import Sign + + +class Sign(Sign): + def get_document_qweb_context(self, sign_request_id, token, **post): + data = super().get_document_qweb_context(sign_request_id, token, **post) + current_request_item = data["current_request_item"] + sign_item_types = data["sign_item_types"] + company_logo = http.request.env.user.company_id.logo + if company_logo: + data["logo"] = "data:image/png;base64,%s" % company_logo.decode() + else: + data["logo"] = False + + if current_request_item: + user_stamp = current_request_item._get_user_signature_asset("stamp_sign_stamp") + user_stamp_frame = current_request_item._get_user_signature_asset("stamp_sign_stamp_frame") + + encoded_user_stamp = ( + "data:image/png;base64,%s" % user_stamp.decode() + if user_stamp + else False + ) + encoded_user_stamp_frame = ( + "data:image/png;base64,%s" % user_stamp_frame.decode() + if user_stamp_frame + else False + ) + + stamp_item_type = next( + ( + item_type + for item_type in sign_item_types + if item_type["item_type"] == "stamp" + ), + None, + ) + + if stamp_item_type: + stamp_item_type["auto_value"] = encoded_user_stamp + stamp_item_type["frame_value"] = encoded_user_stamp_frame + + return data + + @http.route(["/sign/update_user_signature"], type="json", auth="user") + def update_signature( + self, sign_request_id, role, signature_type=None, datas=None, frame_datas=None + ): + user = http.request.env.user + if not user or signature_type not in [ + "sign_signature", + "sign_initials", + "stamp_sign_stamp", + ]: + return False + + sign_request_item_sudo = ( + http.request.env["sign.request.item"] + .sudo() + .search( + [("sign_request_id", "=", sign_request_id), ("role_id", "=", role)], + limit=1, + ) + ) + + allowed = sign_request_item_sudo.partner_id.id == user.partner_id.id + if not allowed: + return False + user[signature_type] = datas[datas.find(",") + 1 :] + if frame_datas: + user[signature_type + "_frame"] = frame_datas[frame_datas.find(",") + 1 :] + return True diff --git a/stamp_sign/data/sign_data.xml b/stamp_sign/data/sign_data.xml new file mode 100644 index 00000000000..726e90191c7 --- /dev/null +++ b/stamp_sign/data/sign_data.xml @@ -0,0 +1,12 @@ + + + + Stamp + stamp + stamp + Stamp + 0.300 + 0.10 + fa-certificate + + diff --git a/stamp_sign/models/__init__.py b/stamp_sign/models/__init__.py new file mode 100644 index 00000000000..b72c01e5b02 --- /dev/null +++ b/stamp_sign/models/__init__.py @@ -0,0 +1,3 @@ +from . import sign_template +from . import res_users +from . import sign_request diff --git a/stamp_sign/models/res_users.py b/stamp_sign/models/res_users.py new file mode 100644 index 00000000000..41db9ead672 --- /dev/null +++ b/stamp_sign/models/res_users.py @@ -0,0 +1,22 @@ +from odoo import models, fields + +SIGN_USER_FIELDS = ["stamp_sign"] + + +class ResUsers(models.Model): + _inherit = "res.users" + + @property + def SELF_READABLE_FIELDS(self): + return super().SELF_READABLE_FIELDS + SIGN_USER_FIELDS + + @property + def SELF_WRITEABLE_FIELDS(self): + return super().SELF_WRITEABLE_FIELDS + SIGN_USER_FIELDS + + stamp_sign_stamp = fields.Binary( + string="Company Stamp", copy=False, groups="base.group_user" + ) + stamp_sign_stamp_frame = fields.Binary( + string="Company Stamp Frame", copy=False, groups="base.group_user" + ) diff --git a/stamp_sign/models/sign_request.py b/stamp_sign/models/sign_request.py new file mode 100644 index 00000000000..63f95714d64 --- /dev/null +++ b/stamp_sign/models/sign_request.py @@ -0,0 +1,249 @@ +import base64 +import io +import time + +from PIL import UnidentifiedImageError +from reportlab.lib.utils import ImageReader +from reportlab.pdfgen import canvas + +from odoo import _, models, Command +from odoo.tools import format_date +from odoo.exceptions import UserError, ValidationError +from odoo.tools.pdf import PdfFileReader, PdfFileWriter + +try: + from PyPDF2.errors import PdfReadError +except ImportError: + from PyPDF2.utils import PdfReadError + + +def _fix_image_transparency(image): + 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): + _inherit = "sign.request" + + def _generate_completed_document(self, password=""): + self.ensure_one() + self._validate_document_state() + + if not self.template_id.sign_item_ids: + self._copy_template_to_completed_document() + else: + old_pdf = self._load_template_pdf(password) + new_pdf_data = self._create_signed_overlay(old_pdf) + self._merge_pdfs_and_store(old_pdf, new_pdf_data, password) + + attachment = self._create_attachment_from_completed_doc() + log_attachment = self._create_completion_certificate() + self._attach_completed_documents(attachment, log_attachment) + + def _validate_document_state(self): + if self.state != "signed": + raise UserError( + _( + "The completed document cannot be created because the sign request is not fully signed" + ) + ) + + def _copy_template_to_completed_document(self): + self.completed_document = self.template_id.attachment_id.datas + + def _load_template_pdf(self, password): + try: + pdf_reader = PdfFileReader( + io.BytesIO(base64.b64decode(self.template_id.attachment_id.datas)), + strict=False, + overwriteWarnings=False, + ) + pdf_reader.getNumPages() + except PdfReadError: + raise ValidationError(_("ERROR: Invalid PDF file!")) + + if pdf_reader.isEncrypted and not pdf_reader.decrypt(password): + return # Password invalid + + return pdf_reader + + def _create_signed_overlay(self, old_pdf): + font = self._get_font() + normalFontSize = self._get_normal_font_size() + packet = io.BytesIO() + can = canvas.Canvas(packet, pagesize=self.get_page_size(old_pdf)) + items_by_page, values = self._collect_items_and_values() + + for p in range(0, old_pdf.getNumPages()): + page = old_pdf.getPage(p) + width, height = self._get_page_dimensions(page) + self._apply_page_rotation(can, page, width, height) + + for item in items_by_page.get(p + 1, []): + self._draw_item( + can, item, values.get(item.id), width, height, font, normalFontSize + ) + can.showPage() + + can.save() + return PdfFileReader(packet, overwriteWarnings=False) + + def _collect_items_and_values(self): + items_by_page = self.template_id._get_sign_items_by_page() + item_ids = [id for items in items_by_page.values() for id in items.ids] + values_dict = self.env["sign.request.item.value"]._read_group( + [("sign_item_id", "in", item_ids), ("sign_request_id", "=", self.id)], + groupby=["sign_item_id"], + aggregates=[ + "value:array_agg", + "frame_value:array_agg", + "frame_has_hash:array_agg", + ], + ) + values = { + item: {"value": vals[0], "frame": frames[0], "frame_has_hash": hashes[0]} + for item, vals, frames, hashes in values_dict + } + return items_by_page, values + + def _get_page_dimensions(self, page): + width = float(abs(page.mediaBox.getWidth())) + height = float(abs(page.mediaBox.getHeight())) + return width, height + + def _apply_page_rotation(self, can, page, width, height): + rotation = page.get("/Rotate", 0) + if isinstance(rotation, int): + can.rotate(rotation) + 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) + + def _draw_item(self, can, item, value_dict, width, height, font, normalFontSize): + if not value_dict: + return + + value, frame = value_dict["value"], value_dict["frame"] + if frame: + self._draw_image(can, frame, item, width, height) + + draw_method = getattr(self, f"_draw_{item.type_id.item_type}", None) + if draw_method: + draw_method(can, item, value, width, height, font, normalFontSize) + + def _draw_image(self, can, frame_data, item, width, height): + try: + image_reader = ImageReader( + io.BytesIO(base64.b64decode(frame_data.split(",")[1])) + ) + except UnidentifiedImageError: + raise ValidationError( + _( + "There was an issue downloading your document. Please contact an administrator." + ) + ) + + _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, + ) + + def _draw_signature(self, can, item, value, width, height, *_): + self._draw_image(can, value, item, width, height) + + _draw_initial = _draw_signature + _draw_stamp = _draw_signature + + def _merge_pdfs_and_store(self, old_pdf, overlay_pdf, password): + new_pdf = PdfFileWriter() + for i in range(old_pdf.getNumPages()): + page = old_pdf.getPage(i) + page.mergePage(overlay_pdf.getPage(i)) + new_pdf.addPage(page) + if old_pdf.isEncrypted: + new_pdf.encrypt(password) + + output = io.BytesIO() + try: + new_pdf.write(output) + except PdfReadError: + raise ValidationError( + _( + "There was an issue downloading your document. Please contact an administrator." + ) + ) + self.completed_document = base64.b64encode(output.getvalue()) + output.close() + + def _create_attachment_from_completed_doc(self): + filename = ( + self.reference + if self.reference.endswith(".pdf") + else f"{self.reference}.pdf" + ) + return self.env["ir.attachment"].create( + { + "name": filename, + "datas": self.completed_document, + "type": "binary", + "res_model": self._name, + "res_id": self.id, + } + ) + + def _create_completion_certificate(self): + public_user = ( + self.env.ref("base.public_user", raise_if_not_found=False) or self.env.user + ) + pdf_content, _ = ( + self.env["ir.actions.report"] + .with_user(public_user) + .sudo() + ._render_qweb_pdf( + "sign.action_sign_request_print_logs", + self.ids, + data={ + "format_date": format_date, + "company_id": self.communication_company_id, + }, + ) + ) + return self.env["ir.attachment"].create( + { + "name": f"Certificate of completion - {time.strftime('%Y-%m-%d - %H:%M:%S')}.pdf", + "raw": pdf_content, + "type": "binary", + "res_model": self._name, + "res_id": self.id, + } + ) + + def _attach_completed_documents(self, doc_attachment, log_attachment): + self.completed_document_attachment_ids = [ + Command.set([doc_attachment.id, log_attachment.id]) + ] + + +class SignRequestItem(models.Model): + _inherit = "sign.request.item" + + def _get_user_signature_asset(self, asset_type): + self.ensure_one() + sign_user = self.partner_id.user_ids[:1] + if sign_user and asset_type in ["stamp_sign_stamp", "stamp_sign_stamp_frame"]: + return sign_user[asset_type] + return False diff --git a/stamp_sign/models/sign_template.py b/stamp_sign/models/sign_template.py new file mode 100644 index 00000000000..64fc233e7e5 --- /dev/null +++ b/stamp_sign/models/sign_template.py @@ -0,0 +1,9 @@ +from odoo import fields, models + + +class SignItemType(models.Model): + _inherit = "sign.item.type" + + item_type = fields.Selection( + selection_add=[("stamp", "Stamp")], ondelete={"stamp": "set default"} + ) diff --git a/stamp_sign/static/src/components/sign_request/document_signable.js b/stamp_sign/static/src/components/sign_request/document_signable.js new file mode 100644 index 00000000000..5ad7897c07e --- /dev/null +++ b/stamp_sign/static/src/components/sign_request/document_signable.js @@ -0,0 +1,25 @@ +import { patch } from "@web/core/utils/patch"; +import { Document } from "@sign/components/sign_request/document_signable"; + +patch(Document.prototype, { + getDataFromHTML() { + super.getDataFromHTML(); + const { el: parentEl } = this.props.parent; + + const fields = ["company", "address", "city", "country", "vat", "logo",]; + + this.signerInfo = {}; + + for (const field of fields) { + const element = parentEl.querySelector(`#o_sign_signer_${field}_input_info`); + this.signerInfo[field] = element?.value; + } + }, + + get iframeProps() { + return { + ...super.iframeProps, + ...this.signerInfo, + }; + }, +}); diff --git a/stamp_sign/static/src/components/sign_request/sign_items.xml b/stamp_sign/static/src/components/sign_request/sign_items.xml new file mode 100644 index 00000000000..17b7075a486 --- /dev/null +++ b/stamp_sign/static/src/components/sign_request/sign_items.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + +
+
+ + Frame + Stamp + + + + + + +
+
+
diff --git a/stamp_sign/static/src/components/sign_request/signable_PDF_iframe.js b/stamp_sign/static/src/components/sign_request/signable_PDF_iframe.js new file mode 100644 index 00000000000..f946d52815d --- /dev/null +++ b/stamp_sign/static/src/components/sign_request/signable_PDF_iframe.js @@ -0,0 +1,195 @@ +import { rpc } from "@web/core/network/rpc"; +import { _t } from "@web/core/l10n/translation"; +import { user } from "@web/core/user"; +import { patch } from "@web/core/utils/patch"; +import { SignablePDFIframe } from "@sign/components/sign_request/signable_PDF_iframe"; +import { SignNameAndSignatureDialog } from "@sign/dialogs/dialogs"; +import { StampSignDetailsDialog } from "../../dialogs/stamp_sign_add_stamp_dialog"; + +patch(SignablePDFIframe.prototype, { + enableCustom(signItem) { + super.enableCustom(signItem); + + const signItemElement = signItem.el; + const signItemData = signItem.data; + const signItemType = this.signItemTypesById[signItemData.type_id]; + const { item_type: type } = signItemType; + + if (type === _t("stamp")) { + signItemElement.addEventListener("click", (e) => { + this.handleSignatureDialogClick(e.currentTarget, signItemType); + }); + } + }, + + openSignatureDialog(signatureItem, type) { + if (this.dialogOpen) return; + + const { signature, signMode, signatureImage } = this._prepareSignatureData(signatureItem, type); + const frame = {}; + const { height, width } = signatureItem.getBoundingClientRect(); + const signFrame = signatureItem.querySelector(".o_sign_frame"); + + this.dialogOpen = true; + + this.closeFn = this.dialog.add( + type.item_type === "stamp" ? StampSignDetailsDialog : SignNameAndSignatureDialog, + { + frame, + signature, + signatureType: type.item_type, + displaySignatureRatio: width / height, + activeFrame: Boolean(signFrame) || !type.auto_value, + mode: signMode, + defaultFrame: type.frame_value || "", + hash: this.frameHash, + signatureImage, + onConfirm: () => this._handleConfirm(signature, frame, signatureItem, type), + onConfirmAll: () => this._handleConfirmAll(signature, frame, type), + onCancel: () => this.closeDialog(), + }, + { + onClose: () => { + this.dialogOpen = false; + }, + } + ); + }, + + _prepareSignatureData(signatureItem, type) { + const signature = { + name: this.props.signerName, + company: this.props.company, + address: this.props.address, + city: this.props.city, + country: this.props.country, + vat: this.props.vat, + logo: this.props.logo, + }; + + const signatureImage = signatureItem?.dataset?.signature; + const signMode = type.auto_value ? "auto" : "draw"; + + if (signMode === "draw" && signatureImage) { + signature.signatureImage = signatureImage; + } + + return { signature, signMode, signatureImage }; + }, + + async _handleConfirm(signature, frame, signatureItem, type) { + if (!signature.isSignatureEmpty && signature.signatureChanged) { + await this._applySignature(signature, frame, signatureItem, type); + } else if (signature.signatureChanged) { + this._resetSignatureItem(signatureItem, type); + } + this.closeDialog(); + this.handleInput(); + }, + + async _handleConfirmAll(signature, frame, type) { + this.signerName = signature.name; + await frame.updateFrame(); + + const frameData = frame.getFrameImageSrc(); + const signatureSrc = signature.getSignatureImage(); + type.auto_value = signatureSrc; + type.frame_value = frameData; + + if (user.userId) { + await this.updateUserSignature(type); + } + + await this._fillAllMatchingItems(signatureSrc, frameData, type); + this.closeDialog(); + this.handleInput(); + }, + + async _applySignature(signature, frame, signatureItem, type) { + this.signerName = signature.name; + await frame.updateFrame(); + + const frameData = frame.getFrameImageSrc(); + const signatureSrc = signature.getSignatureImage(); + + type.auto_value = frameData; + type.frame_value = frameData; + + if (user.userId) { + await this.updateUserSignature(type); + } + + this.fillItemWithSignature(signatureItem, signatureSrc, { + frame: frameData, + hash: this.frameHash, + }); + }, + + _resetSignatureItem(signatureItem, type) { + delete signatureItem.dataset.signature; + delete signatureItem.dataset.frame; + signatureItem.replaceChildren(); + + const signHelperspan = document.createElement("span"); + signHelperspan.classList.add("o_sign_helper"); + signatureItem.append(signHelperspan); + + if (type.placeholder) { + const placeholderSpan = document.createElement("span"); + placeholderSpan.classList.add("o_placeholder"); + placeholderSpan.innerText = type.placeholder; + signatureItem.append(placeholderSpan); + } + }, + + async _fillAllMatchingItems(signatureSrc, frameData, type) { + for (const page in this.signItems) { + await Promise.all( + Object.values(this.signItems[page]).reduce((promises, signItem) => { + if ( + signItem.data.responsible === this.currentRole && + signItem.data.type_id === type.id + ) { + promises.push( + Promise.all([ + this.adjustSignatureSize(signatureSrc, signItem.el), + this.adjustSignatureSize(frameData, signItem.el), + ]).then(([data, adjustedFrame]) => { + this.fillItemWithSignature(signItem.el, data, { + frame: adjustedFrame, + hash: this.frameHash, + }); + }) + ); + } + return promises; + }, []) + ); + } + }, + + updateUserSignature(type) { + return rpc("/sign/update_user_signature", { + sign_request_id: this.props.requestID, + role: this.currentRole, + signature_type: + type.item_type === "signature" + ? "sign_signature" + : type.item_type === "stamp" + ? "stamp_sign" + : "sign_initials", + datas: type.auto_value, + frame_datas: type.frame_value, + }); + }, + + getSignatureValueFromElement(item) { + const customTypes = { + stamp: () => item.el.dataset.signature, + }; + const type = item.data.type; + return type in customTypes + ? customTypes[type]() + : super.getSignatureValueFromElement(item); + }, +}); diff --git a/stamp_sign/static/src/dialogs/name_and_signature.js b/stamp_sign/static/src/dialogs/name_and_signature.js new file mode 100644 index 00000000000..05b9e54a7c1 --- /dev/null +++ b/stamp_sign/static/src/dialogs/name_and_signature.js @@ -0,0 +1,96 @@ +import { renderToString } from "@web/core/utils/render"; +import { patch } from "@web/core/utils/patch"; +import { NameAndSignature } from "@web/core/signature/name_and_signature"; +import { rpc } from "@web/core/network/rpc"; +import { onWillStart } from "@odoo/owl"; + + +patch(NameAndSignature.prototype, { + + setup() { + super.setup(...arguments); + + onWillStart(async () => { + this.Notofonts = await rpc(`/web/sign/get_fonts/NotoSans-Reg.ttf`); + }); + + }, + + /** + * Draws the current name with the current font in the signature field. + */ + async drawCurrentName() { + if (this.props.signatureType === "stamp") { + const font = this.Notofonts; + const stamp = this.getStampDetails(); + const canvas = this.signatureRef.el; + const img = this.getSVGStamp(font, stamp, canvas.width, canvas.height); + await this.printImage(img); + } + else { + super.drawCurrentName() + } + }, + + getStampDetails() { + return { + name: this.props.signature.name, + company: this.props.signature.company, + address: this.props.signature.address, + city: this.props.signature.city, + country: this.props.signature.country, + vat: this.props.signature.vat, + logo: this.props.signature.logo, + } + }, + + /** + * Gets an SVG matching the given parameters, output compatible with the + * src attribute of . + * + * @private + * @param {string} font: base64 encoded font to use + * @param {string} text: the name to draw + * @param {number} width: the width of the resulting image in px + * @param {number} height: the height of the resulting image in px + * @returns {string} image = mimetype + image data + */ + getSVGStamp(font, stampData, width, height) { + const svg = renderToString("stamp_sign.sign_svg_stamp", { + width: width, + height: height, + font: font, + name: stampData.name, + company: stampData.company, + address: stampData.address, + city: stampData.city, + country: stampData.country, + vat: stampData.vat, + logo: stampData.logo + }); + return "data:image/svg+xml," + encodeURI(svg) + }, + + onInputStampDetails(ev) { + if (ev.target.name === "logo") { + const file = ev.target.files[0]; + if (file && this.state.signMode === "auto") { + const reader = new FileReader(); + reader.onload = () => { + this.props.signature.logo = reader.result; + this.drawCurrentName(); + }; + reader.readAsDataURL(file) + } + } + else { + this.props.signature[ev.target.name] = ev.target.value; + } + if (!this.state.showSignatureArea && this.getStampData()) { + this.state.showSignatureArea = true; + } + if (this.state.signMode === "auto") { + this.drawCurrentName(); + } + }, +}) diff --git a/stamp_sign/static/src/dialogs/name_and_signature.xml b/stamp_sign/static/src/dialogs/name_and_signature.xml new file mode 100644 index 00000000000..4793f72e9dd --- /dev/null +++ b/stamp_sign/static/src/dialogs/name_and_signature.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/stamp_sign/static/src/dialogs/stamp_sign_add_stamp_dialog.js b/stamp_sign/static/src/dialogs/stamp_sign_add_stamp_dialog.js new file mode 100644 index 00000000000..414661914b9 --- /dev/null +++ b/stamp_sign/static/src/dialogs/stamp_sign_add_stamp_dialog.js @@ -0,0 +1,25 @@ +import { Dialog } from "@web/core/dialog/dialog"; +import { SignNameAndSignature, SignNameAndSignatureDialog } from "@sign/dialogs/sign_name_and_signature_dialog"; + +export class StampSignDetails extends SignNameAndSignature { + static template = "stamp_sign.StampSignDetails"; + + triggerFileUpload() { + const fileInput = document.querySelector("input[name='logo']"); + if (fileInput) { + fileInput.click(); + } + } +} + +export class StampSignDetailsDialog extends SignNameAndSignatureDialog { + static template = "stamp_sign.StampSignDetailsDialog"; + + static components = { Dialog, StampSignDetails }; + + get dialogProps() { + return { + title: "Adopt Your Stamp", + }; + } +} diff --git a/stamp_sign/static/src/dialogs/stamp_sign_add_stamp_dialog.xml b/stamp_sign/static/src/dialogs/stamp_sign_add_stamp_dialog.xml new file mode 100644 index 00000000000..149a57bfdd1 --- /dev/null +++ b/stamp_sign/static/src/dialogs/stamp_sign_add_stamp_dialog.xml @@ -0,0 +1,92 @@ + + + + + +
+ By clicking Adopt & 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. +
+ + + + + +
+
+ + + +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ + +
+
+
+
+
diff --git a/stamp_sign/views/sign_request_templates.xml b/stamp_sign/views/sign_request_templates.xml new file mode 100644 index 00000000000..7f82ab87ba4 --- /dev/null +++ b/stamp_sign/views/sign_request_templates.xml @@ -0,0 +1,18 @@ + + + +