diff --git a/awesome_gallery/__manifest__.py b/awesome_gallery/__manifest__.py index 624766dca89..75414feae70 100644 --- a/awesome_gallery/__manifest__.py +++ b/awesome_gallery/__manifest__.py @@ -12,6 +12,7 @@ 'version': '0.1', 'application': True, 'category': 'Tutorials/AwesomeGallery', + 'author': 'arkp', 'installable': True, 'depends': ['web', 'contacts'], 'data': [ diff --git a/awesome_kanban/__manifest__.py b/awesome_kanban/__manifest__.py index affef78bb12..bc224d47718 100644 --- a/awesome_kanban/__manifest__.py +++ b/awesome_kanban/__manifest__.py @@ -12,6 +12,7 @@ 'version': '0.1', 'application': True, 'category': 'Tutorials/AwesomeKanban', + 'author': 'arkp', 'installable': True, 'depends': ['web', 'crm'], 'data': [ diff --git a/stamp_sign/__init__.py b/stamp_sign/__init__.py new file mode 100644 index 00000000000..9afa0533172 --- /dev/null +++ b/stamp_sign/__init__.py @@ -0,0 +1,4 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import models +from . import controllers diff --git a/stamp_sign/__manifest__.py b/stamp_sign/__manifest__.py new file mode 100644 index 00000000000..b10bf389187 --- /dev/null +++ b/stamp_sign/__manifest__.py @@ -0,0 +1,26 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +{ + "name": "Stamp Sign", + "version": "1.0", + "depends": ["sign"], + "category": "Sign", + "author": "arkp", + "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": "LGPL-3", +} diff --git a/stamp_sign/controllers/__init__.py b/stamp_sign/controllers/__init__.py new file mode 100644 index 00000000000..80ee4da1c5e --- /dev/null +++ b/stamp_sign/controllers/__init__.py @@ -0,0 +1,3 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import main diff --git a/stamp_sign/controllers/main.py b/stamp_sign/controllers/main.py new file mode 100644 index 00000000000..bf0c78b0477 --- /dev/null +++ b/stamp_sign/controllers/main.py @@ -0,0 +1,51 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import http +from odoo.addons.sign.controllers.main import Sign # type: ignore + + +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) + 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 + + 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", + ]: + 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 + if datas: + user[signature_type] = datas[datas.find(",") + 1 :] + else: + user[signature_type] = False + + if frame_datas: + user[signature_type + "_frame"] = frame_datas[frame_datas.find(",") + 1 :] + else: + user[signature_type + "_frame"] = False + + return True diff --git a/stamp_sign/data/sign_data.xml b/stamp_sign/data/sign_data.xml new file mode 100644 index 00000000000..d4195848ab9 --- /dev/null +++ b/stamp_sign/data/sign_data.xml @@ -0,0 +1,12 @@ + + + + Stamp + stamp + stamp + Stamp + 0.300 + 0.10 + fa-legal + + diff --git a/stamp_sign/models/__init__.py b/stamp_sign/models/__init__.py new file mode 100644 index 00000000000..761e2e3d48f --- /dev/null +++ b/stamp_sign/models/__init__.py @@ -0,0 +1,4 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import sign_template +from . import sign_request diff --git a/stamp_sign/models/sign_request.py b/stamp_sign/models/sign_request.py new file mode 100644 index 00000000000..838875b6b40 --- /dev/null +++ b/stamp_sign/models/sign_request.py @@ -0,0 +1,228 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +import base64 +import io +import time +from PIL import UnidentifiedImageError +from reportlab.lib.utils import ImageReader +from reportlab.pdfgen import canvas +from reportlab.lib.styles import ParagraphStyle +from reportlab.platypus import Paragraph +from reportlab.pdfbase.pdfmetrics import stringWidth +from reportlab.pdfbase import pdfmetrics +from reportlab.pdfbase.ttfonts import TTFont + +from odoo import _, models +from odoo.tools import format_date, is_html_empty +from odoo.exceptions import UserError, ValidationError +from odoo.tools.pdf import PdfFileReader, PdfFileWriter +from odoo.tools.misc import get_lang + +try: + from PyPDF2.errors import PdfReadError # type: ignore +except ImportError: + try: + from PyPDF2.utils import PdfReadError + except ImportError: + PdfReadError = Exception + + +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() + if self.state != 'signed': + raise UserError(_("The completed document cannot be created because the sign request is not fully signed")) + if not self.template_id.sign_item_ids: + self.completed_document = self.template_id.attachment_id.datas + else: + try: + old_pdf = PdfFileReader(io.BytesIO(base64.b64decode(self.template_id.attachment_id.datas)), strict=False, overwriteWarnings=False) + old_pdf.getNumPages() + except Exception: + raise ValidationError(_("ERROR: Invalid PDF file!")) + + isEncrypted = old_pdf.isEncrypted + if isEncrypted and not old_pdf.decrypt(password): + return + + font = self._get_font() + normalFontSize = self._get_normal_font_size() + + packet = io.BytesIO() + can = canvas.Canvas(packet, pagesize=self.get_page_size(old_pdf)) + itemsByPage = self.template_id._get_sign_items_by_page() + items_ids = [id for items in itemsByPage.values() for id in items.ids] + values_dict = self.env['sign.request.item.value']._read_group( + [('sign_item_id', 'in', items_ids), ('sign_request_id', '=', self.id)], + groupby=['sign_item_id'], + aggregates=['value:array_agg', 'frame_value:array_agg', 'frame_has_hash:array_agg'] + ) + values = { + sign_item.id : { + 'value': values[0], + 'frame': frame_values[0], + 'frame_has_hash': frame_has_hashes[0], + } + for sign_item, values, frame_values, frame_has_hashes in values_dict + } + + for p in range(0, old_pdf.getNumPages()): + page = old_pdf.getPage(p) + width = float(abs(page.mediaBox.getWidth())) + height = float(abs(page.mediaBox.getHeight())) + + rotation = page.get('/Rotate', 0) + if rotation and 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) + + items = itemsByPage[p + 1] if p + 1 in itemsByPage else [] + for item in items: + value_dict = values.get(item.id) + if not value_dict: + continue + value = value_dict['value'] + frame = value_dict['frame'] + + if frame: + try: + image_reader = ImageReader(io.BytesIO(base64.b64decode(frame[frame.find(',')+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 + ) + + if item.type_id.item_type == "text": + can.setFont(font, height*item.height*0.8) + if item.alignment == "left": + can.drawString(width*item.posX, height*(1-item.posY-item.height*0.9), value) + elif item.alignment == "right": + can.drawRightString(width*(item.posX+item.width), height*(1-item.posY-item.height*0.9), value) + else: + can.drawCentredString(width*(item.posX+item.width/2), height*(1-item.posY-item.height*0.9), value) + + elif item.type_id.item_type == "selection": + content = [] + for option in item.option_ids: + if option.id != int(value): + content.append("%s" % (option.value)) + else: + content.append(option.value) + font_size = height * normalFontSize * 0.8 + text = " / ".join(content) + string_width = stringWidth(text.replace("", "").replace("", ""), font, font_size) + p = Paragraph(text, ParagraphStyle(name='Selection Paragraph', fontName=font, fontSize=font_size, leading=12)) + posX = width * (item.posX + item.width * 0.5) - string_width // 2 + posY = height * (1 - item.posY - item.height * 0.5) - p.wrap(width, height)[1] // 2 + p.drawOn(can, posX, posY) + + elif item.type_id.item_type == "textarea": + font_size = height * normalFontSize * 0.8 + can.setFont(font, font_size) + lines = value.split('\n') + y = (1-item.posY) + for line in lines: + empty_space = width * item.width - can.stringWidth(line, font, font_size) + x_shift = 0 + if item.alignment == 'center': + x_shift = empty_space / 2 + elif item.alignment == 'right': + x_shift = empty_space + y -= normalFontSize * 0.9 + can.drawString(width * item.posX + x_shift, height * y, line) + y -= normalFontSize * 0.1 + + elif item.type_id.item_type == "checkbox": + can.setFont(font, height*item.height*0.8) + value = 'X' if value == 'on' else '' + can.drawString(width*item.posX, height*(1-item.posY-item.height*0.9), value) + elif item.type_id.item_type == "radio": + x = width * item.posX + y = height * (1 - item.posY) + w = item.width * width + h = item.height * height + c_x = x + w * 0.5 + c_y = y - h * 0.5 + can.circle(c_x, c_y, h * 0.5) + if value == "on": + can.circle(x_cen=c_x, y_cen=c_y, r=h * 0.5 * 0.75, fill=1) + elif item.type_id.item_type in ["signature", "initial", "stamp"]: + try: + image_reader = ImageReader(io.BytesIO(base64.b64decode(value[value.find(',')+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) + + can.showPage() + + can.save() + + item_pdf = PdfFileReader(packet, overwriteWarnings=False) + new_pdf = PdfFileWriter() + + for p in range(0, old_pdf.getNumPages()): + page = old_pdf.getPage(p) + page.mergePage(item_pdf.getPage(p)) + new_pdf.addPage(page) + + if isEncrypted: + new_pdf.encrypt(password) + + try: + output = io.BytesIO() + 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() + + attachment = self.env['ir.attachment'].create({ + 'name': "%s.pdf" % self.reference if self.reference.split('.')[-1] != 'pdf' else self.reference, + 'datas': self.completed_document, + 'type': 'binary', + 'res_model': self._name, + 'res_id': self.id, + }) + public_user = self.env.ref('base.public_user', raise_if_not_found=False) + if not public_user: + public_user = 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} + ) + attachment_log = self.env['ir.attachment'].create({ + 'name': "Certificate of completion - %s.pdf" % time.strftime('%Y-%m-%d - %H:%M:%S'), + 'raw': pdf_content, + 'type': 'binary', + 'res_model': self._name, + 'res_id': self.id, + }) + self.completed_document_attachment_ids = [(6, 0, [attachment.id, attachment_log.id])] diff --git a/stamp_sign/models/sign_template.py b/stamp_sign/models/sign_template.py new file mode 100644 index 00000000000..eae52a8da41 --- /dev/null +++ b/stamp_sign/models/sign_template.py @@ -0,0 +1,9 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +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..7ab2d0301d5 --- /dev/null +++ b/stamp_sign/static/src/components/sign_request/document_signable.js @@ -0,0 +1,23 @@ +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..19c45327ec8 --- /dev/null +++ b/stamp_sign/static/src/components/sign_request/sign_items.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + +
+
+ + 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..b795c9d0ba2 --- /dev/null +++ b/stamp_sign/static/src/components/sign_request/signable_PDF_iframe.js @@ -0,0 +1,189 @@ +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 "@stamp_sign/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]; + + if (!signItemType) { + return; + } + const { item_type: type } = signItemType; + if (type === "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; + + 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 = signatureSrc; + type.frame_value = frameData; + + 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..d5e6ea52796 --- /dev/null +++ b/stamp_sign/static/src/dialogs/name_and_signature.js @@ -0,0 +1,88 @@ +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`); + }); + + }, + + 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, + } + }, + /** + * @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..61bbd193b46 --- /dev/null +++ b/stamp_sign/static/src/dialogs/stamp_sign_add_stamp_dialog.xml @@ -0,0 +1,96 @@ + + + + + +
+ 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. +
+ + + + + +
+
+ + + +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ + +
+
+
+ + + props.signatureType !== 'stamp' + +
+
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 @@ + + + +