From 16fbb78af6280fd5136682d98edea0f72b64b611 Mon Sep 17 00:00:00 2001 From: lavacano Date: Thu, 9 Apr 2026 19:25:17 -0500 Subject: [PATCH] [FIX] sign_oca: fix public access, iframe assets, and multi-page fields --- sign_oca/controllers/main.py | 12 ++- sign_oca/models/sign_oca_request.py | 21 ++++- sign_oca/security/ir.model.access.csv | 2 + sign_oca/security/security.xml | 21 +++++ .../sign_oca_pdf_common.esm.js | 81 +++++++++++++------ sign_oca/static/src/elements/signature.esm.js | 4 +- sign_oca/static/src/scss/sign.scss | 2 + 7 files changed, 115 insertions(+), 28 deletions(-) diff --git a/sign_oca/controllers/main.py b/sign_oca/controllers/main.py index 4d61d537..404127c4 100644 --- a/sign_oca/controllers/main.py +++ b/sign_oca/controllers/main.py @@ -1,3 +1,5 @@ +from werkzeug.wrappers import Response + from odoo import http from odoo.exceptions import AccessError, MissingError from odoo.http import request @@ -12,7 +14,15 @@ def get_sign_resources(self, ext): bundle = "sign_oca.sign_assets" files, _ = request.env["ir.qweb"]._get_asset_content(bundle) asset = AssetsBundle(bundle, files) - mock_attachment = getattr(asset, ext)() + try: + mock_attachment = getattr(asset, ext)() + except Exception: + # Bundle has no assets for this type (e.g. no JS in a CSS-only bundle) + content_type = "application/javascript" if ext == "js" else "text/css" + return Response("", content_type=content_type, status=200) + if not mock_attachment: + content_type = "application/javascript" if ext == "js" else "text/css" + return Response("", content_type=content_type, status=200) if isinstance( mock_attachment, list ): # suppose that CSS asset will not required to be split in pages diff --git a/sign_oca/models/sign_oca_request.py b/sign_oca/models/sign_oca_request.py index 0459e7d2..dabec98f 100644 --- a/sign_oca/models/sign_oca_request.py +++ b/sign_oca/models/sign_oca_request.py @@ -279,6 +279,12 @@ def action_send_signed_request(self): ] ) ) + # Set a proper filename for the PDF attachment + doc_name = self.template_id.name or self.name or "signed_document" + pdf_filename = doc_name + ".pdf" + for att in attachments: + if att.name != pdf_filename: + att.sudo().write({"name": pdf_filename}) # The message will not be linked to the record because we do not want # it happen. self.env["mail.thread"].message_notify( @@ -398,11 +404,20 @@ def _onchange_role_id(self): def get_info(self, access_token=False): self.ensure_one() self._set_action_log("view", access_token=access_token) + # Use sudo to read to_sign to avoid ACL issues when computed fields + # traverse signer_ids in the public user context. + # For logged-in users, the compute still correctly matches partner_id. + to_sign = self.sudo().request_id.to_sign + # For public/anonymous users, to_sign is always False because the + # public user's partner won't match any signer. Fall back to checking + # this specific signer's state directly. + if not to_sign and not self.signed_on and self.request_id.state == "0_sent": + to_sign = True return { "role_id": self.role_id.id if not self.signed_on else False, "name": self.request_id.template_id.name, "items": self.request_id.signatory_data, - "to_sign": self.request_id.to_sign, + "to_sign": to_sign, "ask_location": self.request_id.ask_location, "partner": { "id": self.partner_id.id, @@ -565,6 +580,10 @@ def _get_pdf_page_signature(self, item, box): new_pdf = PdfFileReader(packet) return new_pdf.getPage(0) + def _get_pdf_page_date(self, item, box): + """Render date field as text in the PDF.""" + return self._get_pdf_page_text(item, box) + def _get_pdf_page(self, item, box): return getattr(self, "_get_pdf_page_%s" % item["field_type"])(item, box) diff --git a/sign_oca/security/ir.model.access.csv b/sign_oca/security/ir.model.access.csv index a7530d69..cc336415 100644 --- a/sign_oca/security/ir.model.access.csv +++ b/sign_oca/security/ir.model.access.csv @@ -10,8 +10,10 @@ edit_sign_field_admin,edit_sign_field_admin,model_sign_oca_field,sign_oca_group_ edit_sign_request_base_user,edit_sign_request,model_sign_oca_request,base.group_user,1,0,0,0 edit_sign_request,edit_sign_request,model_sign_oca_request,sign_oca_group_user,1,1,1,0 edit_sign_request_admin,edit_sign_request_admin,model_sign_oca_request,sign_oca_group_admin,1,1,1,1 +access_sign_request_public,access_sign_request_public,model_sign_oca_request,base.group_public,1,0,0,0 edit_sign_request_signer_base_user,edit_sign_request_signer_base_user,model_sign_oca_request_signer,base.group_user,1,0,0,0 edit_sign_request_signer,edit_sign_request_signer,model_sign_oca_request_signer,sign_oca_group_user,1,1,1,1 +access_sign_request_signer_public,access_sign_request_signer_public,model_sign_oca_request_signer,base.group_public,1,0,0,0 edit_sign_generate,edit_sign_generate,model_sign_oca_template_generate,sign_oca_group_user,1,1,1,1 edit_sign_generate_signer,edit_sign_generate_signer,model_sign_oca_template_generate_signer,sign_oca_group_user,1,1,1,1 edit_sign_generate_multi,edit_sign_generate_multi,model_sign_oca_template_generate_multi,sign_oca_group_user,1,1,1,1 diff --git a/sign_oca/security/security.xml b/sign_oca/security/security.xml index d59777fa..b4279d36 100644 --- a/sign_oca/security/security.xml +++ b/sign_oca/security/security.xml @@ -96,4 +96,25 @@ + + + Sign Request public: token-based read + + [(1, '=', 1)] + + + + + + + + Sign Request Signer public: token-based read + + [(1, '=', 1)] + + + + + + diff --git a/sign_oca/static/src/components/sign_oca_pdf_common/sign_oca_pdf_common.esm.js b/sign_oca/static/src/components/sign_oca_pdf_common/sign_oca_pdf_common.esm.js index 26bd916f..5ecf7d38 100644 --- a/sign_oca/static/src/components/sign_oca_pdf_common/sign_oca_pdf_common.esm.js +++ b/sign_oca/static/src/components/sign_oca_pdf_common/sign_oca_pdf_common.esm.js @@ -20,6 +20,8 @@ export default class SignOcaPdfCommon extends Component { iframeReject = reject; }); this.items = {}; + this._assetsInjected = false; + this._initialFieldsDone = false; onWillUnmount(() => { clearTimeout(this.reviewFieldsTimeout); }); @@ -62,12 +64,16 @@ export default class SignOcaPdfCommon extends Component { } } reviewFields() { - if ( - this.iframe.el.contentDocument.getElementsByClassName("o_sign_oca_ready") - .length === 0 - ) { - this.postIframeFields(); - } + // Check each individual item — pdfjs re-renders pages on scroll, + // which destroys field overlays. Re-inject any missing ones. + $.each(this.info.items, (key) => { + var item = this.info.items[key]; + var el = this.items[item.id]; + // Check if the element is still attached to the DOM + if (!el || !el.isConnected) { + this.postIframeField(item); + } + }); this.reviewFieldsTimeout = setTimeout(this.reviewFields.bind(this), 1000); } postIframeFields() { @@ -81,37 +87,62 @@ export default class SignOcaPdfCommon extends Component { }, true ); - var iframeCss = document.createElement("link"); - iframeCss.setAttribute("rel", "stylesheet"); - iframeCss.setAttribute("href", "/sign_oca/get_assets.css"); - - var iframeJs = document.createElement("script"); - iframeJs.setAttribute("type", "text/javascript"); - iframeJs.setAttribute("src", "/sign_oca/get_assets.js"); - this.iframe.el.contentDocument - .getElementsByTagName("head")[0] - .append(iframeCss); - this.iframe.el.contentDocument.getElementsByTagName("head")[0].append(iframeJs); + // Only inject CSS once + if (!this._assetsInjected) { + var iframeCss = document.createElement("link"); + iframeCss.setAttribute("rel", "stylesheet"); + iframeCss.setAttribute("href", "/sign_oca/get_assets.css"); + this.iframe.el.contentDocument + .getElementsByTagName("head")[0] + .append(iframeCss); + // Inject critical CSS inline to avoid stale SCSS bundle cache issues + var inlineStyle = document.createElement("style"); + inlineStyle.textContent = [ + ".o_sign_oca_field {", + " z-index: 100 !important;", + " position: absolute;", + " cursor: pointer;", + "}", + ".textLayer {", + " pointer-events: none !important;", + "}", + ".annotationLayer {", + " pointer-events: none !important;", + "}", + ].join("\n"); + this.iframe.el.contentDocument + .getElementsByTagName("head")[0] + .append(inlineStyle); + this._assetsInjected = true; + } $.each(this.info.items, (key) => { this.postIframeField(this.info.items[key]); }); - $(this.iframe.el.contentDocument.getElementsByClassName("page")[0]).append( - $("
") - ); - - $(this.iframe.el.contentDocument.getElementById("viewer")).addClass( - "sign_oca_ready" - ); + if (!this._initialFieldsDone) { + $(this.iframe.el.contentDocument.getElementsByClassName("page")[0]).append( + $("
") + ); + $(this.iframe.el.contentDocument.getElementById("viewer")).addClass( + "sign_oca_ready" + ); + this._initialFieldsDone = true; + } this.iframeLoaded.resolve(); } postIframeField(item) { if (this.items[item.id]) { - this.items[item.id].remove(); + // Only remove if still in the DOM + if (this.items[item.id].isConnected) { + this.items[item.id].remove(); + } } var page = this.iframe.el.contentDocument.getElementsByClassName("page")[ item.page - 1 ]; + if (!page) { + return $(); + } var signatureItem = $( renderToString(this.field_template, { ...item, diff --git a/sign_oca/static/src/elements/signature.esm.js b/sign_oca/static/src/elements/signature.esm.js index 6f2d6675..decc2ccd 100644 --- a/sign_oca/static/src/elements/signature.esm.js +++ b/sign_oca/static/src/elements/signature.esm.js @@ -13,7 +13,9 @@ const signatureSignOca = { .filter((i) => i.tabindex > item.tabindex) .sort((a, b) => a.tabindex - b.tabindex); if (next_items.length > 0) { - parent.items[next_items[0].id].dispatchEvent(new Event("focus_signature")); + if (parent.items[next_items[0].id]) { + parent.items[next_items[0].id].dispatchEvent(new Event("focus_signature")); + } } }, generate: function (parent, item, signatureItem) { diff --git a/sign_oca/static/src/scss/sign.scss b/sign_oca/static/src/scss/sign.scss index 0d3c67a8..9ffb373d 100644 --- a/sign_oca/static/src/scss/sign.scss +++ b/sign_oca/static/src/scss/sign.scss @@ -1,5 +1,7 @@ .o_sign_oca_field { background-color: rgba(0, 128, 128, 0.3); + z-index: 10; + cursor: pointer; &.sign_oca_field_required { background-color: rgba(255, 113, 113, 0.5); }