diff --git a/sign_stamp/__init__.py b/sign_stamp/__init__.py new file mode 100644 index 00000000000..0650744f6bc --- /dev/null +++ b/sign_stamp/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/sign_stamp/__manifest__.py b/sign_stamp/__manifest__.py new file mode 100644 index 00000000000..842d7bc24ba --- /dev/null +++ b/sign_stamp/__manifest__.py @@ -0,0 +1,33 @@ +{ + 'author': 'Odoo S.A.', + 'name': 'Sign Stamp', + 'description': """ + Adds a Company Stamp feature to the Sign application. + This module introduces a new Stamp sign item that allows companies to + apply an official stamp containing company details such as name, address, + VAT number, and logo. The stamp is rendered dynamically and can be used + as a valid electronic signature when signing documents. + """, + 'depends': ['sign', 'web'], + 'data': [ + 'data/sign_data.xml', + 'views/sign_request_templates.xml' + ], + 'assets': { + 'web.assets_backend': [ + 'sign_stamp/static/src/components/**/*', + 'sign_stamp/static/src/dialogs/**/*', + ], + 'web.assets_frontend': [ + 'sign_stamp/static/src/components/**/*', + 'sign_stamp/static/src/dialogs/**/*', + ], + 'sign.assets_public_sign': [ + 'sign_stamp/static/src/components/**/*', + 'sign_stamp/static/src/dialogs/**/*', + ], + }, + 'license': 'LGPL-3', + 'application': True, + 'installable': True +} diff --git a/sign_stamp/data/sign_data.xml b/sign_stamp/data/sign_data.xml new file mode 100644 index 00000000000..074abd25986 --- /dev/null +++ b/sign_stamp/data/sign_data.xml @@ -0,0 +1,16 @@ + + + + 0 + + + + + 0 + stamp + Company Address City Country VAT + 0.3 + 0.1 + fa-circle + + diff --git a/sign_stamp/models/__init__.py b/sign_stamp/models/__init__.py new file mode 100644 index 00000000000..ac5bea22810 --- /dev/null +++ b/sign_stamp/models/__init__.py @@ -0,0 +1 @@ +from . import sign_item_type diff --git a/sign_stamp/models/sign_item_type.py b/sign_stamp/models/sign_item_type.py new file mode 100644 index 00000000000..9a42e5683cb --- /dev/null +++ b/sign_stamp/models/sign_item_type.py @@ -0,0 +1,17 @@ +from odoo import api, fields, models + + +class SignItemType(models.Model): + _inherit = "sign.item.type" + _order = "sequence" + + sequence = fields.Integer(string="Sequence", default=1) + display_name = fields.Char(compute="_compute_display_name") + + @api.depends_context('company') + def _compute_display_name(self): + self.display_name = self.env.company.name + for record in self: + if record.item_type == "stamp" and record.sequence == 0: + record.name = record.display_name + break diff --git a/sign_stamp/static/src/components/sign_request/document_signable.js b/sign_stamp/static/src/components/sign_request/document_signable.js new file mode 100644 index 00000000000..d5371ba2ba7 --- /dev/null +++ b/sign_stamp/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; + this.companyInfo = {}; + this.companyInfo.company = parentEl.querySelector("#o_sign_signer_company_input_info")?.value; + this.companyInfo.address = parentEl.querySelector("#o_sign_signer_address_input_info")?.value; + this.companyInfo.city = parentEl.querySelector("#o_sign_signer_city_input_info")?.value; + this.companyInfo.country = parentEl.querySelector("#o_sign_signer_country_input_info")?.value; + this.companyInfo.vat = parentEl.querySelector("#o_sign_signer_vat_input_info")?.value; + }, + + getIframeProps(sign_document_id) { + const props = super.getIframeProps(sign_document_id); + return { + ...props, + companyInfo: this.companyInfo + }; + }, +}); diff --git a/sign_stamp/static/src/components/sign_request/sign_item.xml b/sign_stamp/static/src/components/sign_request/sign_item.xml new file mode 100644 index 00000000000..6403535ccf0 --- /dev/null +++ b/sign_stamp/static/src/components/sign_request/sign_item.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/sign_stamp/static/src/components/sign_request/signable_PDF_iframe.js b/sign_stamp/static/src/components/sign_request/signable_PDF_iframe.js new file mode 100644 index 00000000000..8f590493fe4 --- /dev/null +++ b/sign_stamp/static/src/components/sign_request/signable_PDF_iframe.js @@ -0,0 +1,72 @@ +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_dialog"; + +patch(SignablePDFIframe.prototype, { + enableCustom(signItem) { + super.enableCustom(signItem); + const signItemType = this.signItemTypesById[signItem.data.type_id]; + if (!signItemType || signItemType.item_type !== "stamp") { + return; + } + signItem.el.addEventListener("click", (e) => { + this.openSignatureDialog(e.currentTarget, signItemType); + }); + }, + + openSignatureDialog(signatureItem, type) { + if (this.dialogOpen) { + return; + } + const signature = { + name: this.signerName || "", + company: this.props.companyInfo?.company || "", + address: this.props.companyInfo?.address || "", + city: this.props.companyInfo?.city || "", + country: this.props.companyInfo?.country || "", + vat: this.props.companyInfo?.vat || "", + }; + 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: "auto", + defaultFrame: type.frame_value || "", + hash: this.frameHash, + onConfirm: async () => { + if (!signature.isSignatureEmpty && signature.signatureChanged) { + const signatureName = signature.name; + this.props.updateSignerName(signatureName); + await frame.updateFrame(); + const frameData = frame.getFrameImageSrc(); + const signatureSrc = signature.getSignatureImage(); + this.fillItemWithSignature(signatureItem, signatureSrc, { + frame: frameData, + hash: this.frameHash, + }); + } + this.closeDialog(); + this.handleInput(); + }, + }, + { + onClose: () => { + this.dialogOpen = false; + }, + } + ); + }, + + getSignatureValueFromElement(item) { + return item.data.type === "stamp" ? item.el.dataset.signature : super.getSignatureValueFromElement(item) + }, +}); diff --git a/sign_stamp/static/src/dialogs/name_and_sign.js b/sign_stamp/static/src/dialogs/name_and_sign.js new file mode 100644 index 00000000000..2c0d5a8c225 --- /dev/null +++ b/sign_stamp/static/src/dialogs/name_and_sign.js @@ -0,0 +1,45 @@ +import { renderToString } from "@web/core/utils/render"; +import { patch } from "@web/core/utils/patch"; +import { NameAndSignature } from "@web/core/signature/name_and_signature"; + +patch(NameAndSignature.prototype, { + async drawCurrentName() { + if (this.props.signatureType === "stamp") { + const font = this.fonts[this.currentFont]; + 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, + image: this.props.signature.image, + }; + }, + + 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, + image: stampData.image, + }); + return "data:image/svg+xml," + encodeURI(svg); + }, +}); diff --git a/sign_stamp/static/src/dialogs/name_and_sing.xml b/sign_stamp/static/src/dialogs/name_and_sing.xml new file mode 100644 index 00000000000..9e9926287c6 --- /dev/null +++ b/sign_stamp/static/src/dialogs/name_and_sing.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/sign_stamp/static/src/dialogs/stamp_dialog.js b/sign_stamp/static/src/dialogs/stamp_dialog.js new file mode 100644 index 00000000000..7a822d8b58b --- /dev/null +++ b/sign_stamp/static/src/dialogs/stamp_dialog.js @@ -0,0 +1,46 @@ +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(); + } + } + + onInputStampDetails(ev) { + const field = ev.target.name; + if (field === "logo") { + const file = ev.target.files[0]; + if (!file) + return; + const reader = new FileReader(); + reader.onload = (e) => { + this.props.signature.image = e.target.result; + this.drawCurrentName(); + }; + reader.readAsDataURL(file); + return; + } + const value = ev.target.value; + if (field && this.props.signature?.hasOwnProperty(field)) { + this.props.signature[field] = value; + this.drawCurrentName(); + } + } +} + +export class StampSignDetailsDialog extends SignNameAndSignatureDialog { + static template = "stamp_sign.StampSignDetailsDialog"; + + static components = { Dialog, StampSignDetails }; + + get dialogProps() { + return { + title: "Adopt Your Stamp", + }; + } +} diff --git a/sign_stamp/static/src/dialogs/stamp_dialog.xml b/sign_stamp/static/src/dialogs/stamp_dialog.xml new file mode 100644 index 00000000000..7e9e4b7fe39 --- /dev/null +++ b/sign_stamp/static/src/dialogs/stamp_dialog.xml @@ -0,0 +1,51 @@ + + + + + + + By clicking Sign, I confirm this stamp will be used as my electronic signature. + + + Sign + Cancel + + + + + + + + Full Name + + + + Company + + + + Address + + + + City + + + + Country + + + + VAT Number + + + + Logo + + + Upload + + + + + diff --git a/sign_stamp/views/sign_request_templates.xml b/sign_stamp/views/sign_request_templates.xml new file mode 100644 index 00000000000..24c26fd7cd0 --- /dev/null +++ b/sign_stamp/views/sign_request_templates.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + +