From d0d79eca2a6e8cc6479e877eb61a04bf9487eb1c Mon Sep 17 00:00:00 2001 From: alap-odoo Date: Tue, 8 Jul 2025 18:54:47 +0530 Subject: [PATCH 1/3] [ADD] stamp_sign: created new field in sign module added new field stamp in the sign module. Moreover, created static component for stamp field. Further, added logic to get the user details if internal user. --- stamp_sign/__init__.py | 1 + stamp_sign/__manifest__.py | 19 ++++++++++ stamp_sign/data/sign_data.xml | 12 ++++++ stamp_sign/models/__init__.py | 2 + stamp_sign/models/res_users.py | 20 ++++++++++ stamp_sign/models/sign_template.py | 37 +++++++++++++++++++ .../components/sign_request/sign_items.xml | 33 +++++++++++++++++ .../sign_request/signable_PDF_iframe.js | 22 +++++++++++ stamp_sign/views/sign_template_views.xml | 21 +++++++++++ 9 files changed, 167 insertions(+) create mode 100644 stamp_sign/__init__.py create mode 100644 stamp_sign/__manifest__.py create mode 100644 stamp_sign/data/sign_data.xml create mode 100644 stamp_sign/models/__init__.py create mode 100644 stamp_sign/models/res_users.py create mode 100644 stamp_sign/models/sign_template.py create mode 100644 stamp_sign/static/src/components/sign_request/sign_items.xml create mode 100644 stamp_sign/static/src/components/sign_request/signable_PDF_iframe.js create mode 100644 stamp_sign/views/sign_template_views.xml diff --git a/stamp_sign/__init__.py b/stamp_sign/__init__.py new file mode 100644 index 00000000000..0650744f6bc --- /dev/null +++ b/stamp_sign/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/stamp_sign/__manifest__.py b/stamp_sign/__manifest__.py new file mode 100644 index 00000000000..b6908d21282 --- /dev/null +++ b/stamp_sign/__manifest__.py @@ -0,0 +1,19 @@ +{ + "name": "Stamp Sign", + "version": "1.0", + "depends": ["sign"], + "category": "Sign", + "data": [ + "views/sign_template_views.xml", + "data/sign_data.xml", + ], + "assets": { + "sign.assets_pdf_iframe": [ + "stamp_sign/static/src/components/**/*", + ], + }, + "installable": True, + "sequence": 1, + "application": True, + "license": "OEEL-1", +} diff --git a/stamp_sign/data/sign_data.xml b/stamp_sign/data/sign_data.xml new file mode 100644 index 00000000000..b1e15f47bda --- /dev/null +++ b/stamp_sign/data/sign_data.xml @@ -0,0 +1,12 @@ + + + + Stamp + stamp + stamp + Stamp It + 0.200 + 0.050 + fa-certificate + + diff --git a/stamp_sign/models/__init__.py b/stamp_sign/models/__init__.py new file mode 100644 index 00000000000..5ed120ba5a9 --- /dev/null +++ b/stamp_sign/models/__init__.py @@ -0,0 +1,2 @@ +from . import sign_template +from . import res_users diff --git a/stamp_sign/models/res_users.py b/stamp_sign/models/res_users.py new file mode 100644 index 00000000000..59942bf7715 --- /dev/null +++ b/stamp_sign/models/res_users.py @@ -0,0 +1,20 @@ +from odoo import models, api + + +class ResUsers(models.Model): + _inherit = "res.users" + + @api.model + def get_current_user_company_details(self): + user = self.env.user + details = { + "name": user.name, + "company": user.company_id.name if user.company_id else "", + "address": user.company_id.street if user.company_id else "", + "city": user.company_id.city if user.company_id else "", + "country": user.company_id.country_id.name + if user.company_id and user.company_id.country_id + else "", + "vat": user.company_id.vat if user.company_id else "", + } + return details diff --git a/stamp_sign/models/sign_template.py b/stamp_sign/models/sign_template.py new file mode 100644 index 00000000000..443a1a84e69 --- /dev/null +++ b/stamp_sign/models/sign_template.py @@ -0,0 +1,37 @@ +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"}, + ) + + +class SignItem(models.Model): + _inherit = "sign.item" + + stamp_company = fields.Char("Company") + stamp_address = fields.Char("Address") + stamp_city = fields.Char("City") + stamp_country = fields.Char("Country") + stamp_vat = fields.Char("VAT Number") + stamp_logo = fields.Binary("Stamp Logo") + + def _get_stamp_details_for_user(self): + self.ensure_one() + user = self.env.user + details = {} + if user.has_group("base.group_user"): + details = { + "company": user.company_id.name, + "address": user.company_id.street, + "city": user.company_id.city, + "country": user.company_id.country_id.name, + "vat": user.company_id.vat, + } + return details 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..f3bfe22af61 --- /dev/null +++ b/stamp_sign/static/src/components/sign_request/sign_items.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + +
+
+ + 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..a03dbbed381 --- /dev/null +++ b/stamp_sign/static/src/components/sign_request/signable_PDF_iframe.js @@ -0,0 +1,22 @@ +/** @odoo-module **/ + +import { patch } from "@web/core/utils/patch"; +import { SignablePDFIframe } from "@sign/components/sign_request/signable_PDF_iframe"; + +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 === "stamp") { + signItemElement.addEventListener("click", (e) => { + this.handleSignatureDialogClick(e.currentTarget, signItemType); + }); + } + }, +}); diff --git a/stamp_sign/views/sign_template_views.xml b/stamp_sign/views/sign_template_views.xml new file mode 100644 index 00000000000..3ed1a88d76b --- /dev/null +++ b/stamp_sign/views/sign_template_views.xml @@ -0,0 +1,21 @@ + + + + sign.item.form.inherit.stamp + sign.item + + + + + + + + + + + + + + + From 39601a8ec7da01444967d3ac46733bcddbd27b72 Mon Sep 17 00:00:00 2001 From: alap-odoo Date: Wed, 9 Jul 2025 18:42:49 +0530 Subject: [PATCH 2/3] [IMP] stamp_sign: added dialog view and related fields on clicking stamp Created the dialog view along with the related fields and added the Functionality for the sign and sign all buttons. --- stamp_sign/__init__.py | 1 + stamp_sign/__manifest__.py | 11 +- stamp_sign/controllers/__init__.py | 1 + stamp_sign/controllers/main.py | 30 ++++ stamp_sign/models/__init__.py | 1 + stamp_sign/models/res_users.py | 11 +- stamp_sign/models/sign_request.py | 19 +++ .../sign_request/document_signable.js | 28 ++++ .../components/sign_request/sign_items.xml | 25 +-- .../sign_request/signable_PDF_iframe.js | 147 +++++++++++++++++- .../static/src/dialogs/stamp_add_dialog.js | 10 ++ .../static/src/dialogs/stamp_add_dialog.xml | 16 ++ .../static/src/dialogs/stamp_dialog_field.js | 31 ++++ .../static/src/dialogs/stamp_dialog_field.xml | 68 ++++++++ 14 files changed, 380 insertions(+), 19 deletions(-) create mode 100644 stamp_sign/controllers/__init__.py create mode 100644 stamp_sign/controllers/main.py create mode 100644 stamp_sign/models/sign_request.py create mode 100644 stamp_sign/static/src/components/sign_request/document_signable.js create mode 100644 stamp_sign/static/src/dialogs/stamp_add_dialog.js create mode 100644 stamp_sign/static/src/dialogs/stamp_add_dialog.xml create mode 100644 stamp_sign/static/src/dialogs/stamp_dialog_field.js create mode 100644 stamp_sign/static/src/dialogs/stamp_dialog_field.xml diff --git a/stamp_sign/__init__.py b/stamp_sign/__init__.py index 0650744f6bc..f7209b17100 100644 --- a/stamp_sign/__init__.py +++ b/stamp_sign/__init__.py @@ -1 +1,2 @@ from . import models +from . import controllers diff --git a/stamp_sign/__manifest__.py b/stamp_sign/__manifest__.py index b6908d21282..f205498106e 100644 --- a/stamp_sign/__manifest__.py +++ b/stamp_sign/__manifest__.py @@ -4,12 +4,17 @@ "depends": ["sign"], "category": "Sign", "data": [ - "views/sign_template_views.xml", "data/sign_data.xml", + "views/sign_template_views.xml", ], "assets": { - "sign.assets_pdf_iframe": [ - "stamp_sign/static/src/components/**/*", + "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, 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..f02b1351d24 --- /dev/null +++ b/stamp_sign/controllers/main.py @@ -0,0 +1,30 @@ +from odoo import http +from odoo.addons.sign.controllers.main import Sign + + +class SignController(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"] + data["logo"] = ( + "data:image/png;base64,%s" % http.request.env.user.company_id.logo.decode() + ) + + if current_request_item: + for item_type in sign_item_types: + if item_type["item_type"] == "stamp": + user_stamp = current_request_item._get_user_stamp() + user_stamp_frame = current_request_item._get_user_stamp_frame() + item_type["auto_value"] = ( + "data:image/png;base64,%s" % user_stamp.decode() + if user_stamp + else False + ) + item_type["frame_value"] = ( + "data:image/png;base64,%s" % user_stamp_frame.decode() + if user_stamp_frame + else False + ) + + return data diff --git a/stamp_sign/models/__init__.py b/stamp_sign/models/__init__.py index 5ed120ba5a9..b72c01e5b02 100644 --- a/stamp_sign/models/__init__.py +++ b/stamp_sign/models/__init__.py @@ -1,2 +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 index 59942bf7715..97138b50329 100644 --- a/stamp_sign/models/res_users.py +++ b/stamp_sign/models/res_users.py @@ -1,9 +1,13 @@ -from odoo import models, api +from odoo import models, api, fields +import base64 class ResUsers(models.Model): _inherit = "res.users" + stamp_sign = fields.Binary(string="Digital Stamp", copy=False, groups="base.group_user") + stamp_sign_frame = fields.Binary(string="Digital Stamp Frame", copy=False, groups="base.group_user") + @api.model def get_current_user_company_details(self): user = self.env.user @@ -16,5 +20,10 @@ def get_current_user_company_details(self): if user.company_id and user.company_id.country_id else "", "vat": user.company_id.vat if user.company_id else "", + "logo_url": False, } + if user.company_id and user.company_id.logo: + details["logo_url"] = "data:image/png;base64," + base64.b64encode( + user.company_id.logo + ).decode("utf-8") return details diff --git a/stamp_sign/models/sign_request.py b/stamp_sign/models/sign_request.py new file mode 100644 index 00000000000..a838e774c7d --- /dev/null +++ b/stamp_sign/models/sign_request.py @@ -0,0 +1,19 @@ +from odoo import models + + +class SignRequestItem(models.Model): + _inherit = "sign.request.item" + + def _get_user_stamp(self): + self.ensure_one() + sign_user = self.partner_id.user_ids[:1] + if sign_user: + return sign_user["stamp_sign"] + return False + + def _get_user_stamp_frame(self): + self.ensure_one() + sign_user = self.partner_id.user_ids[:1] + if sign_user: + return sign_user["stamp_sign_frame"] + return False 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..96f80868a20 --- /dev/null +++ b/stamp_sign/static/src/components/sign_request/document_signable.js @@ -0,0 +1,28 @@ +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.company = parentEl.querySelector("company_input")?.value; + this.address = parentEl.querySelector("address_input")?.value; + this.city = parentEl.querySelector("city_input")?.value; + this.country = parentEl.querySelector("country_input")?.value; + this.vat = parentEl.querySelector("vat_input")?.value; + this.logo = parentEl.querySelector("logo_input")?.value; + }, + get iframeProps() { + return { + ...super.iframeProps, + company: this.company, + address: this.address, + city: this.city, + country: this.country, + vat: this.vat, + logo: this.logo, + } + }, +}) diff --git a/stamp_sign/static/src/components/sign_request/sign_items.xml b/stamp_sign/static/src/components/sign_request/sign_items.xml index f3bfe22af61..877e902d3aa 100644 --- a/stamp_sign/static/src/components/sign_request/sign_items.xml +++ b/stamp_sign/static/src/components/sign_request/sign_items.xml @@ -1,29 +1,34 @@ - - - - + + + + - - - + -
+
+ 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 index a03dbbed381..798b7ee8174 100644 --- a/stamp_sign/static/src/components/sign_request/signable_PDF_iframe.js +++ b/stamp_sign/static/src/components/sign_request/signable_PDF_iframe.js @@ -1,22 +1,159 @@ -/** @odoo-module **/ - +import { user } from "@web/core/user"; +import { _t } from "@web/core/l10n/translation"; +import { StampDialog } from "../../dialogs/stamp_add_dialog"; import { patch } from "@web/core/utils/patch"; import { SignablePDFIframe } from "@sign/components/sign_request/signable_PDF_iframe"; +import { SignNameAndSignatureDialog } from "@sign/dialogs/dialogs" -patch(SignablePDFIframe.prototype, { +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 === "stamp") { + if (signItemType.item_type === 'stamp') { signItemElement.addEventListener("click", (e) => { this.handleSignatureDialogClick(e.currentTarget, signItemType); }); } }, + + openSignatureDialog(signatureItem, type) { + if (this.dialogOpen) { + return; + } + + this.dialogOpen = true; + + const dataRequired = this.stampDetailsInDialog(signatureItem, type); + + this.closeFn = this.dialog.add( + type.item_type === "stamp" ? StampDialog : SignNameAndSignatureDialog, + dataRequired, + { + onClose: () => { + this.dialogOpen = false; + }, + } + ); + }, + + stampDetailsInDialog(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 frame = {}; + const { height, width } = signatureItem.getBoundingClientRect(); + const signFrame = signatureItem.querySelector(".o_sign_frame"); + const signatureImage = signatureItem?.dataset?.signature; + const signMode = type.auto_value ? "draw" : "auto" + if (signMode == "draw" && signatureImage) { + signature.signatureImage = signatureImage; + } + + return { + 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: async () => { + if (!signature.isSignatureEmpty && signature.signatureChanged) { + const signatureName = signature.name; + this.signerName = signatureName; + 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, + }); + } else if (signature.signatureChanged) { + 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) + } + } + this.closeDialog(); + this.handleInput(); + }, + onConfirmAll: async () => { + const signatureName = signature.name; + this.signerName = signatureName; + 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); + } + for (const page in this.signItems) { + await Promise.all( + Object.values(this.signItems[page]).reduce((promishList, signItem) => { + if ( + signItem.data.responsible === this.currentRole && + signItem.data.type_id === type.id + ) { + promishList.push( + Promise.all([ + this.adjustSignatureSize(signatureSrc, signItem.el), + this.adjustSignatureSize(frameData, signItem.el), + ]).then(([data, frameData]) => { + this.fillItemWithSignature(signItem.el, data, { + frame: frameData, + hash: this.frameHash, + }); + }) + ); + } + return promishList; + }, []) + ); + } + this.closeDialog(); + this.handleInput(); + }, + onCancel: () => { + this.closeDialog(); + }, + }; + }, + + getSignatureValueFromElement(item) { + if (item.data.type === "stamp") { + return item.el.dataset.signature; + } + else { + return super.getSignatureValueFromElement(item); + } + }, + }); diff --git a/stamp_sign/static/src/dialogs/stamp_add_dialog.js b/stamp_sign/static/src/dialogs/stamp_add_dialog.js new file mode 100644 index 00000000000..807293eb82c --- /dev/null +++ b/stamp_sign/static/src/dialogs/stamp_add_dialog.js @@ -0,0 +1,10 @@ +import { Dialog } from "@web/core/dialog/dialog"; +import { SignNameAndSignatureDialog } from "@sign/dialogs/sign_name_and_signature_dialog"; +import { Stamp } from "./stamp_dialog_field"; + +export class StampDialog extends SignNameAndSignatureDialog { + static template = "stamp_sign.StampDialog"; + + static components = { Dialog, Stamp }; + +} diff --git a/stamp_sign/static/src/dialogs/stamp_add_dialog.xml b/stamp_sign/static/src/dialogs/stamp_add_dialog.xml new file mode 100644 index 00000000000..e427d0c14c5 --- /dev/null +++ b/stamp_sign/static/src/dialogs/stamp_add_dialog.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + diff --git a/stamp_sign/static/src/dialogs/stamp_dialog_field.js b/stamp_sign/static/src/dialogs/stamp_dialog_field.js new file mode 100644 index 00000000000..a7d579ba53c --- /dev/null +++ b/stamp_sign/static/src/dialogs/stamp_dialog_field.js @@ -0,0 +1,31 @@ +import { SignNameAndSignature } from "@sign/dialogs/sign_name_and_signature_dialog"; + +export class Stamp extends SignNameAndSignature { + static template = "stamp_sign.Stamp"; + + static props = { + ...SignNameAndSignature.props, + noInputName: Boolean, + }; + + triggerFileUpload() { + const fileInput = document.querySelector("input[name='logo']"); + if (fileInput) { + fileInput.click(); + } + } + + onInputSignName(ev) { + this.props.signature.name = ev.target.value; + this.props.signature = { ...this.props.signature, name: ev.target.value }; + this.render(); + } + + onInputData(ev) { + const fieldName = ev.target.name; + const value = ev.target.value; + this.props.signature[fieldName] = value; + this.props.signature = { ...this.props.signature, [fieldName]: value }; + this.render(); + } +} diff --git a/stamp_sign/static/src/dialogs/stamp_dialog_field.xml b/stamp_sign/static/src/dialogs/stamp_dialog_field.xml new file mode 100644 index 00000000000..be27894f3d9 --- /dev/null +++ b/stamp_sign/static/src/dialogs/stamp_dialog_field.xml @@ -0,0 +1,68 @@ + + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ + +
+
+
+
+
From 4a1c56fbe0a02153a415d17d2ba00837291feb8b Mon Sep 17 00:00:00 2001 From: alap-odoo Date: Thu, 10 Jul 2025 16:24:44 +0530 Subject: [PATCH 3/3] [IMP] stamp_sign: added values in stamp dialog & generated its stamp sign added auto-filling of user details (if internal user) in the stamp dialog. Furthermore, generated a stamp sign based on the user's details and logo, if provided. Moreover, changed the default fonts of the stamp signature. --- stamp_sign/__manifest__.py | 3 +- stamp_sign/controllers/main.py | 79 ++++-- stamp_sign/data/sign_data.xml | 6 +- stamp_sign/models/res_users.py | 39 ++- stamp_sign/models/sign_request.py | 252 +++++++++++++++++- stamp_sign/models/sign_template.py | 30 +-- .../sign_request/document_signable.js | 29 +- .../components/sign_request/sign_items.xml | 10 +- .../sign_request/signable_PDF_iframe.js | 250 +++++++++-------- .../static/src/dialogs/name_and_signature.js | 96 +++++++ .../static/src/dialogs/name_and_signature.xml | 41 +++ .../static/src/dialogs/stamp_add_dialog.js | 10 - .../static/src/dialogs/stamp_add_dialog.xml | 16 -- .../static/src/dialogs/stamp_dialog_field.js | 31 --- .../static/src/dialogs/stamp_dialog_field.xml | 68 ----- .../dialogs/stamp_sign_add_stamp_dialog.js | 25 ++ .../dialogs/stamp_sign_add_stamp_dialog.xml | 92 +++++++ stamp_sign/views/sign_request_templates.xml | 18 ++ stamp_sign/views/sign_template_views.xml | 21 -- 19 files changed, 756 insertions(+), 360 deletions(-) create mode 100644 stamp_sign/static/src/dialogs/name_and_signature.js create mode 100644 stamp_sign/static/src/dialogs/name_and_signature.xml delete mode 100644 stamp_sign/static/src/dialogs/stamp_add_dialog.js delete mode 100644 stamp_sign/static/src/dialogs/stamp_add_dialog.xml delete mode 100644 stamp_sign/static/src/dialogs/stamp_dialog_field.js delete mode 100644 stamp_sign/static/src/dialogs/stamp_dialog_field.xml create mode 100644 stamp_sign/static/src/dialogs/stamp_sign_add_stamp_dialog.js create mode 100644 stamp_sign/static/src/dialogs/stamp_sign_add_stamp_dialog.xml create mode 100644 stamp_sign/views/sign_request_templates.xml delete mode 100644 stamp_sign/views/sign_template_views.xml diff --git a/stamp_sign/__manifest__.py b/stamp_sign/__manifest__.py index f205498106e..4fb7f3b476f 100644 --- a/stamp_sign/__manifest__.py +++ b/stamp_sign/__manifest__.py @@ -5,7 +5,7 @@ "category": "Sign", "data": [ "data/sign_data.xml", - "views/sign_template_views.xml", + "views/sign_request_templates.xml", ], "assets": { "web.assets_backend": [ @@ -18,7 +18,6 @@ ], }, "installable": True, - "sequence": 1, "application": True, "license": "OEEL-1", } diff --git a/stamp_sign/controllers/main.py b/stamp_sign/controllers/main.py index f02b1351d24..8c2a6d8a8c7 100644 --- a/stamp_sign/controllers/main.py +++ b/stamp_sign/controllers/main.py @@ -2,29 +2,72 @@ from odoo.addons.sign.controllers.main import Sign -class SignController(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"] - data["logo"] = ( - "data:image/png;base64,%s" % http.request.env.user.company_id.logo.decode() - ) + 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: - for item_type in sign_item_types: - if item_type["item_type"] == "stamp": - user_stamp = current_request_item._get_user_stamp() - user_stamp_frame = current_request_item._get_user_stamp_frame() - item_type["auto_value"] = ( - "data:image/png;base64,%s" % user_stamp.decode() - if user_stamp - else False - ) - item_type["frame_value"] = ( - "data:image/png;base64,%s" % user_stamp_frame.decode() - if user_stamp_frame - else False - ) + 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 index b1e15f47bda..726e90191c7 100644 --- a/stamp_sign/data/sign_data.xml +++ b/stamp_sign/data/sign_data.xml @@ -4,9 +4,9 @@ Stamp stamp stamp - Stamp It - 0.200 - 0.050 + Stamp + 0.300 + 0.10 fa-certificate diff --git a/stamp_sign/models/res_users.py b/stamp_sign/models/res_users.py index 97138b50329..41db9ead672 100644 --- a/stamp_sign/models/res_users.py +++ b/stamp_sign/models/res_users.py @@ -1,29 +1,22 @@ -from odoo import models, api, fields -import base64 +from odoo import models, fields + +SIGN_USER_FIELDS = ["stamp_sign"] class ResUsers(models.Model): _inherit = "res.users" - stamp_sign = fields.Binary(string="Digital Stamp", copy=False, groups="base.group_user") - stamp_sign_frame = fields.Binary(string="Digital Stamp Frame", copy=False, groups="base.group_user") + @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 - @api.model - def get_current_user_company_details(self): - user = self.env.user - details = { - "name": user.name, - "company": user.company_id.name if user.company_id else "", - "address": user.company_id.street if user.company_id else "", - "city": user.company_id.city if user.company_id else "", - "country": user.company_id.country_id.name - if user.company_id and user.company_id.country_id - else "", - "vat": user.company_id.vat if user.company_id else "", - "logo_url": False, - } - if user.company_id and user.company_id.logo: - details["logo_url"] = "data:image/png;base64," + base64.b64encode( - user.company_id.logo - ).decode("utf-8") - return details + 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 index a838e774c7d..63f95714d64 100644 --- a/stamp_sign/models/sign_request.py +++ b/stamp_sign/models/sign_request.py @@ -1,19 +1,249 @@ -from odoo import models +import base64 +import io +import time +from PIL import UnidentifiedImageError +from reportlab.lib.utils import ImageReader +from reportlab.pdfgen import canvas -class SignRequestItem(models.Model): - _inherit = "sign.request.item" +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) - def _get_user_stamp(self): + +class SignRequest(models.Model): + _inherit = "sign.request" + + def _generate_completed_document(self, password=""): self.ensure_one() - sign_user = self.partner_id.user_ids[:1] - if sign_user: - return sign_user["stamp_sign"] - return False + 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_stamp_frame(self): + def _get_user_signature_asset(self, asset_type): self.ensure_one() sign_user = self.partner_id.user_ids[:1] - if sign_user: - return sign_user["stamp_sign_frame"] + 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 index 443a1a84e69..64fc233e7e5 100644 --- a/stamp_sign/models/sign_template.py +++ b/stamp_sign/models/sign_template.py @@ -5,33 +5,5 @@ class SignItemType(models.Model): _inherit = "sign.item.type" item_type = fields.Selection( - selection_add=[ - ("stamp", "Stamp"), - ], - ondelete={"stamp": "set default"}, + selection_add=[("stamp", "Stamp")], ondelete={"stamp": "set default"} ) - - -class SignItem(models.Model): - _inherit = "sign.item" - - stamp_company = fields.Char("Company") - stamp_address = fields.Char("Address") - stamp_city = fields.Char("City") - stamp_country = fields.Char("Country") - stamp_vat = fields.Char("VAT Number") - stamp_logo = fields.Binary("Stamp Logo") - - def _get_stamp_details_for_user(self): - self.ensure_one() - user = self.env.user - details = {} - if user.has_group("base.group_user"): - details = { - "company": user.company_id.name, - "address": user.company_id.street, - "city": user.company_id.city, - "country": user.company_id.country_id.name, - "vat": user.company_id.vat, - } - return details diff --git a/stamp_sign/static/src/components/sign_request/document_signable.js b/stamp_sign/static/src/components/sign_request/document_signable.js index 96f80868a20..5ad7897c07e 100644 --- a/stamp_sign/static/src/components/sign_request/document_signable.js +++ b/stamp_sign/static/src/components/sign_request/document_signable.js @@ -1,28 +1,25 @@ import { patch } from "@web/core/utils/patch"; import { Document } from "@sign/components/sign_request/document_signable"; - patch(Document.prototype, { getDataFromHTML() { - super.getDataFromHTML() + super.getDataFromHTML(); const { el: parentEl } = this.props.parent; - this.company = parentEl.querySelector("company_input")?.value; - this.address = parentEl.querySelector("address_input")?.value; - this.city = parentEl.querySelector("city_input")?.value; - this.country = parentEl.querySelector("country_input")?.value; - this.vat = parentEl.querySelector("vat_input")?.value; - this.logo = parentEl.querySelector("logo_input")?.value; + 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, - company: this.company, - address: this.address, - city: this.city, - country: this.country, - vat: this.vat, - logo: this.logo, - } + ...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 index 877e902d3aa..17b7075a486 100644 --- a/stamp_sign/static/src/components/sign_request/sign_items.xml +++ b/stamp_sign/static/src/components/sign_request/sign_items.xml @@ -2,18 +2,19 @@ + - - - - - - - diff --git a/stamp_sign/static/src/dialogs/stamp_dialog_field.js b/stamp_sign/static/src/dialogs/stamp_dialog_field.js deleted file mode 100644 index a7d579ba53c..00000000000 --- a/stamp_sign/static/src/dialogs/stamp_dialog_field.js +++ /dev/null @@ -1,31 +0,0 @@ -import { SignNameAndSignature } from "@sign/dialogs/sign_name_and_signature_dialog"; - -export class Stamp extends SignNameAndSignature { - static template = "stamp_sign.Stamp"; - - static props = { - ...SignNameAndSignature.props, - noInputName: Boolean, - }; - - triggerFileUpload() { - const fileInput = document.querySelector("input[name='logo']"); - if (fileInput) { - fileInput.click(); - } - } - - onInputSignName(ev) { - this.props.signature.name = ev.target.value; - this.props.signature = { ...this.props.signature, name: ev.target.value }; - this.render(); - } - - onInputData(ev) { - const fieldName = ev.target.name; - const value = ev.target.value; - this.props.signature[fieldName] = value; - this.props.signature = { ...this.props.signature, [fieldName]: value }; - this.render(); - } -} diff --git a/stamp_sign/static/src/dialogs/stamp_dialog_field.xml b/stamp_sign/static/src/dialogs/stamp_dialog_field.xml deleted file mode 100644 index be27894f3d9..00000000000 --- a/stamp_sign/static/src/dialogs/stamp_dialog_field.xml +++ /dev/null @@ -1,68 +0,0 @@ - - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- -
- - -
-
-
-
-
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 @@ + + + + diff --git a/stamp_sign/views/sign_template_views.xml b/stamp_sign/views/sign_template_views.xml deleted file mode 100644 index 3ed1a88d76b..00000000000 --- a/stamp_sign/views/sign_template_views.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - sign.item.form.inherit.stamp - sign.item - - - - - - - - - - - - - - -