From d7e96ba47df2cede678542164a3887140c2765bf Mon Sep 17 00:00:00 2001 From: Enric Tobella Date: Mon, 24 Jul 2023 10:26:34 +0200 Subject: [PATCH 001/127] [ADD] sign_oca --- sign_oca/README.rst | 119 ++ sign_oca/__init__.py | 3 + sign_oca/__manifest__.py | 37 + sign_oca/controllers/__init__.py | 1 + sign_oca/controllers/main.py | 110 ++ sign_oca/data/data.xml | 37 + sign_oca/demo/sign_oca_template.xml | 12 + sign_oca/i18n/sign_oca.pot | 1073 +++++++++++++++++ sign_oca/models/__init__.py | 4 + sign_oca/models/sign_oca_field.py | 17 + sign_oca/models/sign_oca_request.py | 472 ++++++++ sign_oca/models/sign_oca_role.py | 12 + sign_oca/models/sign_oca_template.py | 108 ++ sign_oca/readme/CONTRIBUTORS.rst | 1 + sign_oca/readme/DESCRIPTION.rst | 1 + sign_oca/readme/ROADMAP.rst | 6 + sign_oca/readme/USAGE.rst | 21 + sign_oca/security/ir.model.access.csv | 10 + sign_oca/security/security.xml | 22 + sign_oca/static/description.svg | 1 + sign_oca/static/description/icon.png | Bin 0 -> 10887 bytes sign_oca/static/description/icon.svg | 1 + sign_oca/static/description/index.html | 470 ++++++++ .../sign_oca_configure/sign_oca_configure.js | 451 +++++++ .../sign_oca_configure/sign_oca_configure.xml | 124 ++ .../components/sign_oca_pdf/sign_oca_pdf.js | 99 ++ .../components/sign_oca_pdf/sign_oca_pdf.xml | 15 + .../sign_oca_pdf/sign_oca_pdf_action.js | 47 + .../sign_oca_pdf_common.js | 148 +++ .../sign_oca_pdf_common.xml | 20 + .../sign_oca_pdf_portal.js | 75 ++ .../sign_oca_pdf_portal.xml | 44 + sign_oca/static/src/elements/check.js | 62 + sign_oca/static/src/elements/elements.xml | 45 + sign_oca/static/src/elements/registry.js | 5 + sign_oca/static/src/elements/signature.js | 123 ++ sign_oca/static/src/elements/text.js | 62 + sign_oca/static/src/scss/portal.scss | 20 + sign_oca/static/src/scss/sign.scss | 48 + sign_oca/static/src/scss/sign_oca.scss | 41 + sign_oca/templates/assets.xml | 193 +++ sign_oca/tests/__init__.py | 2 + sign_oca/tests/empty.pdf | Bin 0 -> 1174 bytes sign_oca/tests/test_sign.py | 160 +++ sign_oca/tests/test_sign_portal.py | 73 ++ sign_oca/views/menu.xml | 21 + sign_oca/views/sign_oca_field.xml | 62 + sign_oca/views/sign_oca_request.xml | 344 ++++++ sign_oca/views/sign_oca_request_log.xml | 41 + sign_oca/views/sign_oca_role.xml | 58 + sign_oca/views/sign_oca_template.xml | 187 +++ sign_oca/wizards/__init__.py | 1 + .../wizards/sign_oca_template_generate.py | 108 ++ .../wizards/sign_oca_template_generate.xml | 48 + 54 files changed, 5265 insertions(+) create mode 100644 sign_oca/README.rst create mode 100644 sign_oca/__init__.py create mode 100644 sign_oca/__manifest__.py create mode 100644 sign_oca/controllers/__init__.py create mode 100644 sign_oca/controllers/main.py create mode 100644 sign_oca/data/data.xml create mode 100644 sign_oca/demo/sign_oca_template.xml create mode 100644 sign_oca/i18n/sign_oca.pot create mode 100644 sign_oca/models/__init__.py create mode 100644 sign_oca/models/sign_oca_field.py create mode 100644 sign_oca/models/sign_oca_request.py create mode 100644 sign_oca/models/sign_oca_role.py create mode 100644 sign_oca/models/sign_oca_template.py create mode 100644 sign_oca/readme/CONTRIBUTORS.rst create mode 100644 sign_oca/readme/DESCRIPTION.rst create mode 100644 sign_oca/readme/ROADMAP.rst create mode 100644 sign_oca/readme/USAGE.rst create mode 100644 sign_oca/security/ir.model.access.csv create mode 100644 sign_oca/security/security.xml create mode 100644 sign_oca/static/description.svg create mode 100644 sign_oca/static/description/icon.png create mode 100644 sign_oca/static/description/icon.svg create mode 100644 sign_oca/static/description/index.html create mode 100644 sign_oca/static/src/components/sign_oca_configure/sign_oca_configure.js create mode 100644 sign_oca/static/src/components/sign_oca_configure/sign_oca_configure.xml create mode 100644 sign_oca/static/src/components/sign_oca_pdf/sign_oca_pdf.js create mode 100644 sign_oca/static/src/components/sign_oca_pdf/sign_oca_pdf.xml create mode 100644 sign_oca/static/src/components/sign_oca_pdf/sign_oca_pdf_action.js create mode 100644 sign_oca/static/src/components/sign_oca_pdf_common/sign_oca_pdf_common.js create mode 100644 sign_oca/static/src/components/sign_oca_pdf_common/sign_oca_pdf_common.xml create mode 100644 sign_oca/static/src/components/sign_oca_pdf_portal/sign_oca_pdf_portal.js create mode 100644 sign_oca/static/src/components/sign_oca_pdf_portal/sign_oca_pdf_portal.xml create mode 100644 sign_oca/static/src/elements/check.js create mode 100644 sign_oca/static/src/elements/elements.xml create mode 100644 sign_oca/static/src/elements/registry.js create mode 100644 sign_oca/static/src/elements/signature.js create mode 100644 sign_oca/static/src/elements/text.js create mode 100644 sign_oca/static/src/scss/portal.scss create mode 100644 sign_oca/static/src/scss/sign.scss create mode 100644 sign_oca/static/src/scss/sign_oca.scss create mode 100644 sign_oca/templates/assets.xml create mode 100644 sign_oca/tests/__init__.py create mode 100644 sign_oca/tests/empty.pdf create mode 100644 sign_oca/tests/test_sign.py create mode 100644 sign_oca/tests/test_sign_portal.py create mode 100644 sign_oca/views/menu.xml create mode 100644 sign_oca/views/sign_oca_field.xml create mode 100644 sign_oca/views/sign_oca_request.xml create mode 100644 sign_oca/views/sign_oca_request_log.xml create mode 100644 sign_oca/views/sign_oca_role.xml create mode 100644 sign_oca/views/sign_oca_template.xml create mode 100644 sign_oca/wizards/__init__.py create mode 100644 sign_oca/wizards/sign_oca_template_generate.py create mode 100644 sign_oca/wizards/sign_oca_template_generate.xml diff --git a/sign_oca/README.rst b/sign_oca/README.rst new file mode 100644 index 00000000..4a4f9337 --- /dev/null +++ b/sign_oca/README.rst @@ -0,0 +1,119 @@ +======== +Sign Oca +======== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:d00cef183007ed9fa38ed45a39017a2e6d6e014d9473c803435ed96b4b0757dd + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fsign-lightgray.png?logo=github + :target: https://github.com/OCA/sign/tree/14.0/sign_oca + :alt: OCA/sign +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/sign-14-0/sign-14-0-sign_oca + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/builds?repo=OCA/sign&target_branch=14.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module allows to create documents for signature inside Odoo using OWL. + +**Table of contents** + +.. contents:: + :local: + +Usage +===== + +Creation of templates +~~~~~~~~~~~~~~~~~~~~~ + +* Access `Sign / Templates` +* Create a new template +* Add a PDF File +* Access the configuration menu +* You can add a field by doing a right click inside a page +* Click on the field in order to delete or edit some configuration of it +* The template is autosaved + +Sign a document +~~~~~~~~~~~~~~~ + +* Access `Sign / Templates` +* Press the `Sign` button from a template +* Fill all the possible partners that will sign the document +* The signature action will be opened. +* There, you can fill all the data you need. +* Once you finish, press the sign button on the top +* When the last signer signs it, the final file will be generated as a PDF + +Known issues / Roadmap +====================== + +Tasks +~~~~~ + +* Ensure that the signature is inalterable. + Maybe we might need to use some tools like endevise or pyHanko with a certificate. + Signer can be authenticated using OTP. + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* Dixmit + +Contributors +~~~~~~~~~~~~ + +* Enric Tobella (www.dixmit.com) + +Maintainers +~~~~~~~~~~~ + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +.. |maintainer-etobella| image:: https://github.com/etobella.png?size=40px + :target: https://github.com/etobella + :alt: etobella + +Current `maintainer `__: + +|maintainer-etobella| + +This module is part of the `OCA/sign `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/sign_oca/__init__.py b/sign_oca/__init__.py new file mode 100644 index 00000000..ada0b87b --- /dev/null +++ b/sign_oca/__init__.py @@ -0,0 +1,3 @@ +from . import controllers +from . import models +from . import wizards diff --git a/sign_oca/__manifest__.py b/sign_oca/__manifest__.py new file mode 100644 index 00000000..21d832a3 --- /dev/null +++ b/sign_oca/__manifest__.py @@ -0,0 +1,37 @@ +# Copyright 2023 Dixmit +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +{ + "name": "Sign Oca", + "summary": """ + Allow to sign documents inside Odoo CE""", + "version": "14.0.1.0.0", + "license": "AGPL-3", + "author": "Dixmit,Odoo Community Association (OCA)", + "website": "https://github.com/OCA/sign", + "depends": ["web_editor", "portal", "base_sparse_field"], + "data": [ + "security/security.xml", + "views/menu.xml", + "data/data.xml", + "wizards/sign_oca_template_generate.xml", + "views/sign_oca_request_log.xml", + "views/sign_oca_request.xml", + "security/ir.model.access.csv", + "views/sign_oca_field.xml", + "views/sign_oca_role.xml", + "views/sign_oca_template.xml", + "templates/assets.xml", + ], + "demo": [ + "demo/sign_oca_template.xml", + ], + "qweb": [ + "static/src/components/sign_oca_pdf_common/sign_oca_pdf_common.xml", + "static/src/components/sign_oca_configure/sign_oca_configure.xml", + "static/src/components/sign_oca_pdf/sign_oca_pdf.xml", + "static/src/components/sign_oca_pdf_portal/sign_oca_pdf_portal.xml", + "static/src/elements/elements.xml", + ], + "maintainers": ["etobella"], +} diff --git a/sign_oca/controllers/__init__.py b/sign_oca/controllers/__init__.py new file mode 100644 index 00000000..12a7e529 --- /dev/null +++ b/sign_oca/controllers/__init__.py @@ -0,0 +1 @@ +from . import main diff --git a/sign_oca/controllers/main.py b/sign_oca/controllers/main.py new file mode 100644 index 00000000..01648db7 --- /dev/null +++ b/sign_oca/controllers/main.py @@ -0,0 +1,110 @@ +import base64 +import io + +from odoo import http +from odoo.exceptions import AccessError, MissingError +from odoo.http import request + +from odoo.addons.base.models.assetsbundle import AssetsBundle +from odoo.addons.portal.controllers.portal import CustomerPortal + + +class SignController(http.Controller): + @http.route("/sign_oca/get_assets.", type="http", auth="public") + def get_sign_resources(self, ext): + xmlid = "sign_oca.sign_assets" + files, _remains = request.env["ir.qweb"]._get_asset_content( + xmlid, options=request.context + ) + asset = AssetsBundle(xmlid, files) + mock_attachment = getattr(asset, ext)() + if isinstance( + mock_attachment, list + ): # suppose that CSS asset will not required to be split in pages + mock_attachment = mock_attachment[0] + _status, headers, content = request.env["ir.http"].binary_content( + id=mock_attachment.id, unique=asset.checksum + ) + content_base64 = base64.b64decode(content) if content else "" + headers.append(("Content-Length", len(content_base64))) + return request.make_response(content_base64, headers) + + +class PortalSign(CustomerPortal): + @http.route( + ["/sign_oca/document//"], + type="http", + auth="public", + website=True, + ) + def get_sign_oca_access(self, signer_id, access_token, **kwargs): + try: + signer_sudo = self._document_check_access( + "sign.oca.request.signer", signer_id, access_token + ) + except (AccessError, MissingError): + return request.redirect("/my") + if signer_sudo.signed_on: + return request.render( + "sign_oca.portal_sign_document_signed", + { + "signer": signer_sudo, + "company": signer_sudo.request_id.company_id, + }, + ) + return request.render( + "sign_oca.portal_sign_document", + { + "doc": signer_sudo.request_id, + "partner": signer_sudo.partner_id, + "signer": signer_sudo, + "access_token": access_token, + }, + ) + + @http.route( + ["/sign_oca/content//"], + type="http", + auth="public", + website=True, + ) + def get_sign_oca_content_access(self, signer_id, access_token): + try: + signer_sudo = self._document_check_access( + "sign.oca.request.signer", signer_id, access_token + ) + except (AccessError, MissingError): + return request.redirect("/my") + data = io.BytesIO(base64.standard_b64decode(signer_sudo.request_id.data)) + return http.send_file(data) + + @http.route( + ["/sign_oca/info//"], + type="json", + auth="public", + website=True, + ) + def get_sign_oca_info_access(self, signer_id, access_token): + try: + signer_sudo = self._document_check_access( + "sign.oca.request.signer", signer_id, access_token + ) + except (AccessError, MissingError): + return request.redirect("/my") + return signer_sudo.get_info(access_token=access_token) + + @http.route( + ["/sign_oca/sign//"], + type="json", + auth="public", + website=True, + ) + def get_sign_oca_sign_access(self, signer_id, access_token, items): + try: + signer_sudo = self._document_check_access( + "sign.oca.request.signer", signer_id, access_token + ) + except (AccessError, MissingError): + return request.redirect("/my") + signer_sudo.action_sign(items, access_token=access_token) + return True diff --git a/sign_oca/data/data.xml b/sign_oca/data/data.xml new file mode 100644 index 00000000..0b7a6393 --- /dev/null +++ b/sign_oca/data/data.xml @@ -0,0 +1,37 @@ + + + + Name + text + name + + + Email + text + email + + + Phone + text + phone + + + Text + text + + + Signature + signature + + + Check + check + + + Customer + [] + + + Employee + + diff --git a/sign_oca/demo/sign_oca_template.xml b/sign_oca/demo/sign_oca_template.xml new file mode 100644 index 00000000..aa88a5fa --- /dev/null +++ b/sign_oca/demo/sign_oca_template.xml @@ -0,0 +1,12 @@ + + + + + + + diff --git a/sign_oca/i18n/sign_oca.pot b/sign_oca/i18n/sign_oca.pot new file mode 100644 index 00000000..c02cd611 --- /dev/null +++ b/sign_oca/i18n/sign_oca.pot @@ -0,0 +1,1073 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * sign_oca +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 14.0\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: sign_oca +#: model_terms:ir.ui.view,arch_db:sign_oca.sign_oca_template_mail +msgid "" +")\n" +" has requested your signature on a document." +msgstr "" + +#. module: sign_oca +#: model_terms:ir.ui.view,arch_db:sign_oca.sign_oca_request_form_view +msgid " Signed" +msgstr "" + +#. module: sign_oca +#: model_terms:ir.ui.view,arch_db:sign_oca.sign_oca_template_mail +msgid "" +"Warning do not forward this email to other people!
\n" +" They will be able to access this document and sign it as yourself.
" +msgstr "" + +#. module: sign_oca +#: model:ir.model.fields,field_description:sign_oca.field_sign_oca_request_log__access_token +msgid "Access Token" +msgstr "" + +#. module: sign_oca +#: model:ir.model.fields,field_description:sign_oca.field_sign_oca_request_signer__access_warning +msgid "Access warning" +msgstr "" + +#. module: sign_oca +#: model:ir.model.fields,field_description:sign_oca.field_sign_oca_request_log__action +msgid "Action" +msgstr "" + +#. module: sign_oca +#: model:ir.model.fields,field_description:sign_oca.field_sign_oca_request__message_needaction +msgid "Action Needed" +msgstr "" + +#. module: sign_oca +#: model:ir.model.fields,field_description:sign_oca.field_sign_oca_request__active +#: model:ir.model.fields,field_description:sign_oca.field_sign_oca_template__active +msgid "Active" +msgstr "" + +#. module: sign_oca +#: model:ir.model.fields,field_description:sign_oca.field_sign_oca_request__activity_ids +msgid "Activities" +msgstr "" + +#. module: sign_oca +#: model:ir.model.fields,field_description:sign_oca.field_sign_oca_request__activity_exception_decoration +msgid "Activity Exception Decoration" +msgstr "" + +#. module: sign_oca +#: model_terms:ir.ui.view,arch_db:sign_oca.sign_oca_request_form_view +msgid "Activity Log" +msgstr "" + +#. module: sign_oca +#: model:ir.model.fields,field_description:sign_oca.field_sign_oca_request__activity_state +msgid "Activity State" +msgstr "" + +#. module: sign_oca +#: model:ir.model.fields,field_description:sign_oca.field_sign_oca_request__activity_type_icon +msgid "Activity Type Icon" +msgstr "" + +#. module: sign_oca +#: model:ir.model.fields.selection,name:sign_oca.selection__sign_oca_request_log__action__add_field +msgid "Add field" +msgstr "" + +#. module: sign_oca +#. openerp-web +#: code:addons/sign_oca/static/src/elements/signature.js:0 +#, python-format +msgid "Adopt Your Signature" +msgstr "" + +#. module: sign_oca +#: model_terms:ir.ui.view,arch_db:sign_oca.sign_oca_request_kanban_view +msgid "Archive" +msgstr "" + +#. module: sign_oca +#: model_terms:ir.ui.view,arch_db:sign_oca.sign_oca_request_form_view +#: model_terms:ir.ui.view,arch_db:sign_oca.sign_oca_request_search_view +msgid "Archived" +msgstr "" + +#. module: sign_oca +#: model_terms:ir.ui.view,arch_db:sign_oca.portal_sign_document_signed +msgid "" +"As soon as all signers have signed the document, you will receive an email " +"with the full document" +msgstr "" + +#. module: sign_oca +#: model:ir.model.fields,field_description:sign_oca.field_sign_oca_request__message_attachment_count +msgid "Attachment Count" +msgstr "" + +#. module: sign_oca +#. openerp-web +#: code:addons/sign_oca/static/src/elements/elements.xml:0 +#, python-format +msgid "" +"By clicking Adopt and 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." +msgstr "" + +#. module: sign_oca +#. openerp-web +#: code:addons/sign_oca/static/src/components/sign_oca_configure/sign_oca_configure.js:0 +#: code:addons/sign_oca/static/src/elements/signature.js:0 +#: model:ir.model.fields.selection,name:sign_oca.selection__sign_oca_request_log__action__cancel +#: model_terms:ir.ui.view,arch_db:sign_oca.sign_oca_request_form_view +#: model_terms:ir.ui.view,arch_db:sign_oca.sign_oca_template_generate_form_view +#, python-format +msgid "Cancel" +msgstr "" + +#. module: sign_oca +#: model_terms:ir.ui.view,arch_db:sign_oca.sign_oca_request_search_view +msgid "Canceled" +msgstr "" + +#. module: sign_oca +#: model:ir.model.fields.selection,name:sign_oca.selection__sign_oca_request__state__cancel +#: model_terms:ir.ui.view,arch_db:sign_oca.sign_oca_request_form_view +msgid "Cancelled" +msgstr "" + +#. module: sign_oca +#: model:ir.model.fields.selection,name:sign_oca.selection__sign_oca_field__field_type__check +msgid "Check" +msgstr "" + +#. module: sign_oca +#. openerp-web +#: code:addons/sign_oca/static/src/components/sign_oca_configure/sign_oca_configure.xml:0 +#, python-format +msgid "Click on the field that you want to add" +msgstr "" + +#. module: sign_oca +#: model:ir.model.fields,field_description:sign_oca.field_sign_oca_request__company_id +msgid "Company" +msgstr "" + +#. module: sign_oca +#: model_terms:ir.ui.view,arch_db:sign_oca.sign_oca_request_form_view +#: model_terms:ir.ui.view,arch_db:sign_oca.sign_oca_template_form_view +#: model_terms:ir.ui.view,arch_db:sign_oca.sign_oca_template_kanban_view +#: model_terms:ir.ui.view,arch_db:sign_oca.sign_oca_template_tree_view +msgid "Configure" +msgstr "" + +#. module: sign_oca +#: model:ir.model.fields.selection,name:sign_oca.selection__sign_oca_request_log__action__create +msgid "Create" +msgstr "" + +#. module: sign_oca +#: model:ir.model.fields,field_description:sign_oca.field_sign_oca_field__create_uid +#: model:ir.model.fields,field_description:sign_oca.field_sign_oca_request__create_uid +#: model:ir.model.fields,field_description:sign_oca.field_sign_oca_request_signer__create_uid +#: model:ir.model.fields,field_description:sign_oca.field_sign_oca_role__create_uid +#: model:ir.model.fields,field_description:sign_oca.field_sign_oca_template__create_uid +#: model:ir.model.fields,field_description:sign_oca.field_sign_oca_template_generate__create_uid +#: model:ir.model.fields,field_description:sign_oca.field_sign_oca_template_generate_signer__create_uid +#: model:ir.model.fields,field_description:sign_oca.field_sign_oca_template_item__create_uid +msgid "Created by" +msgstr "" + +#. module: sign_oca +#: model:ir.model.fields,field_description:sign_oca.field_sign_oca_field__create_date +#: model:ir.model.fields,field_description:sign_oca.field_sign_oca_request__create_date +#: model:ir.model.fields,field_description:sign_oca.field_sign_oca_request_signer__create_date +#: model:ir.model.fields,field_description:sign_oca.field_sign_oca_role__create_date +#: model:ir.model.fields,field_description:sign_oca.field_sign_oca_template__create_date +#: model:ir.model.fields,field_description:sign_oca.field_sign_oca_template_generate__create_date +#: model:ir.model.fields,field_description:sign_oca.field_sign_oca_template_generate_signer__create_date +#: model:ir.model.fields,field_description:sign_oca.field_sign_oca_template_item__create_date +msgid "Created on" +msgstr "" + +#. module: sign_oca +#: model:ir.model.fields,field_description:sign_oca.field_sign_oca_request__current_hash +msgid "Current Hash" +msgstr "" + +#. module: sign_oca +#: model:ir.model.fields,help:sign_oca.field_sign_oca_request_signer__access_url +msgid "Customer Portal URL" +msgstr "" + +#. module: sign_oca +#: model:ir.model.fields,field_description:sign_oca.field_sign_oca_request__data +#: model:ir.model.fields,field_description:sign_oca.field_sign_oca_request_signer__data +#: model:ir.model.fields,field_description:sign_oca.field_sign_oca_template__data +msgid "Data" +msgstr "" + +#. module: sign_oca +#. openerp-web +#: code:addons/sign_oca/static/src/components/sign_oca_configure/sign_oca_configure.xml:0 +#, python-format +msgid "Data is saved automatically when editing" +msgstr "" + +#. module: sign_oca +#. openerp-web +#: code:addons/sign_oca/static/src/components/sign_oca_configure/sign_oca_configure.xml:0 +#, python-format +msgid "Data is saved automatically when editing." +msgstr "" + +#. module: sign_oca +#: model:ir.model.fields,field_description:sign_oca.field_sign_oca_request_log__date +msgid "Date" +msgstr "" + +#. module: sign_oca +#: model:ir.model.fields,field_description:sign_oca.field_sign_oca_field__default_value +msgid "Default Value" +msgstr "" + +#. module: sign_oca +#. openerp-web +#: code:addons/sign_oca/static/src/components/sign_oca_configure/sign_oca_configure.js:0 +#, python-format +msgid "Delete" +msgstr "" + +#. module: sign_oca +#: model:ir.model.fields.selection,name:sign_oca.selection__sign_oca_request_log__action__delete_field +msgid "Delete field" +msgstr "" + +#. module: sign_oca +#: model_terms:ir.ui.view,arch_db:sign_oca.sign_oca_request_kanban_view +msgid "Details" +msgstr "" + +#. module: sign_oca +#: model:ir.model.fields,field_description:sign_oca.field_sign_oca_field__display_name +#: model:ir.model.fields,field_description:sign_oca.field_sign_oca_request__display_name +#: model:ir.model.fields,field_description:sign_oca.field_sign_oca_request_log__display_name +#: model:ir.model.fields,field_description:sign_oca.field_sign_oca_request_signer__display_name +#: model:ir.model.fields,field_description:sign_oca.field_sign_oca_role__display_name +#: model:ir.model.fields,field_description:sign_oca.field_sign_oca_template__display_name +#: model:ir.model.fields,field_description:sign_oca.field_sign_oca_template_generate__display_name +#: model:ir.model.fields,field_description:sign_oca.field_sign_oca_template_generate_signer__display_name +#: model:ir.model.fields,field_description:sign_oca.field_sign_oca_template_item__display_name +msgid "Display Name" +msgstr "" + +#. module: sign_oca +#: model_terms:ir.ui.view,arch_db:sign_oca.portal_sign_document_signed +msgid "Document" +msgstr "" + +#. module: sign_oca +#: model:ir.model.fields,field_description:sign_oca.field_sign_oca_role__domain +msgid "Domain" +msgstr "" + +#. module: sign_oca +#: model:ir.model.fields.selection,name:sign_oca.selection__sign_oca_request__state__draft +msgid "Draft" +msgstr "" + +#. module: sign_oca +#: model_terms:ir.ui.view,arch_db:sign_oca.sign_oca_request_kanban_view +msgid "Dropdown menu" +msgstr "" + +#. module: sign_oca +#. openerp-web +#: code:addons/sign_oca/static/src/components/sign_oca_configure/sign_oca_configure.js:0 +#: model:ir.model.fields.selection,name:sign_oca.selection__sign_oca_request_log__action__edit_field +#, python-format +msgid "Edit field" +msgstr "" + +#. module: sign_oca +#. openerp-web +#: code:addons/sign_oca/static/src/components/sign_oca_configure/sign_oca_configure.xml:0 +#: model:ir.model.fields,field_description:sign_oca.field_sign_oca_template_item__field_id +#, python-format +msgid "Field" +msgstr "" + +#. module: sign_oca +#: code:addons/sign_oca/models/sign_oca_request.py:0 +#, python-format +msgid "Field %s is not filled" +msgstr "" + +#. module: sign_oca +#: model:ir.model.fields,field_description:sign_oca.field_sign_oca_field__field_type +msgid "Field Type" +msgstr "" + +#. module: sign_oca +#: model:ir.actions.act_window,name:sign_oca.sign_oca_field_act_window +#: model:ir.ui.menu,name:sign_oca.sign_oca_field_menu +#: model_terms:ir.ui.view,arch_db:sign_oca.sign_oca_template_form_view +msgid "Fields" +msgstr "" + +#. module: sign_oca +#: model:ir.model.fields,field_description:sign_oca.field_sign_oca_template__filename +msgid "Filename" +msgstr "" + +#. module: sign_oca +#. openerp-web +#: code:addons/sign_oca/static/src/components/sign_oca_configure/sign_oca_configure.xml:0 +#, python-format +msgid "Filled by" +msgstr "" + +#. module: sign_oca +#: model:ir.model.fields,field_description:sign_oca.field_sign_oca_request__message_follower_ids +msgid "Followers" +msgstr "" + +#. module: sign_oca +#: model:ir.model.fields,field_description:sign_oca.field_sign_oca_request__message_channel_ids +msgid "Followers (Channels)" +msgstr "" + +#. module: sign_oca +#: model:ir.model.fields,field_description:sign_oca.field_sign_oca_request__message_partner_ids +msgid "Followers (Partners)" +msgstr "" + +#. module: sign_oca +#: model:ir.model.fields,help:sign_oca.field_sign_oca_request__activity_type_icon +msgid "Font awesome icon e.g. fa-tasks" +msgstr "" + +#. module: sign_oca +#: model_terms:ir.ui.view,arch_db:sign_oca.sign_oca_request_form_view +msgid "Fully Signed" +msgstr "" + +#. module: sign_oca +#: model_terms:ir.ui.view,arch_db:sign_oca.sign_oca_request_search_view +msgid "Future Activities" +msgstr "" + +#. module: sign_oca +#: model_terms:ir.ui.view,arch_db:sign_oca.sign_oca_template_generate_form_view +msgid "Generate & sign" +msgstr "" + +#. module: sign_oca +#: model:ir.model,name:sign_oca.model_sign_oca_template_generate +msgid "Generate a signature request" +msgstr "" + +#. module: sign_oca +#: model_terms:ir.ui.view,arch_db:sign_oca.sign_oca_request_search_view +msgid "Group By" +msgstr "" + +#. module: sign_oca +#: model:ir.model.fields,field_description:sign_oca.field_sign_oca_template_item__height +msgid "Height" +msgstr "" + +#. module: sign_oca +#. openerp-web +#: code:addons/sign_oca/static/src/components/sign_oca_configure/sign_oca_configure.xml:0 +#, python-format +msgid "Help" +msgstr "" + +#. module: sign_oca +#: model:ir.model.fields,field_description:sign_oca.field_sign_oca_field__id +#: model:ir.model.fields,field_description:sign_oca.field_sign_oca_request__id +#: model:ir.model.fields,field_description:sign_oca.field_sign_oca_request_log__id +#: model:ir.model.fields,field_description:sign_oca.field_sign_oca_request_signer__id +#: model:ir.model.fields,field_description:sign_oca.field_sign_oca_role__id +#: model:ir.model.fields,field_description:sign_oca.field_sign_oca_template__id +#: model:ir.model.fields,field_description:sign_oca.field_sign_oca_template_generate__id +#: model:ir.model.fields,field_description:sign_oca.field_sign_oca_template_generate_signer__id +#: model:ir.model.fields,field_description:sign_oca.field_sign_oca_template_item__id +msgid "ID" +msgstr "" + +#. module: sign_oca +#: model:ir.model.fields,field_description:sign_oca.field_sign_oca_request__activity_exception_icon +msgid "Icon" +msgstr "" + +#. module: sign_oca +#: model:ir.model.fields,help:sign_oca.field_sign_oca_request__activity_exception_icon +msgid "Icon to indicate an exception activity." +msgstr "" + +#. module: sign_oca +#: model:ir.model.fields,help:sign_oca.field_sign_oca_request__message_needaction +#: model:ir.model.fields,help:sign_oca.field_sign_oca_request__message_unread +msgid "If checked, new messages require your attention." +msgstr "" + +#. module: sign_oca +#: model:ir.model.fields,help:sign_oca.field_sign_oca_request__message_has_error +msgid "If checked, some messages have a delivery error." +msgstr "" + +#. module: sign_oca +#. openerp-web +#: code:addons/sign_oca/static/src/components/sign_oca_configure/sign_oca_configure.xml:0 +#, python-format +msgid "" +"If you do a click over a field, you will be able to change the default " +"configurations of the field" +msgstr "" + +#. module: sign_oca +#. openerp-web +#: code:addons/sign_oca/static/src/components/sign_oca_configure/sign_oca_configure.xml:0 +#, python-format +msgid "" +"In order to add a new field, do a right click over the PDF page. You will be" +" able to select the field that you will import" +msgstr "" + +#. module: sign_oca +#. openerp-web +#: code:addons/sign_oca/static/src/components/sign_oca_configure/sign_oca_configure.xml:0 +#, python-format +msgid "" +"In order to add a new field, do a right click over the PDF page. You will be able to select the field that you will import.\n" +" Then, you can move and resize the fields over the PDF page using the move icons.\n" +" If you do a click over a field, you will be able to change the default configurations of the field" +msgstr "" + +#. module: sign_oca +#: model:ir.model.fields,field_description:sign_oca.field_sign_oca_request_log__ip +msgid "Ip" +msgstr "" + +#. module: sign_oca +#: model:ir.model.fields,field_description:sign_oca.field_sign_oca_request__message_is_follower +msgid "Is Follower" +msgstr "" + +#. module: sign_oca +#: model:ir.model.fields,field_description:sign_oca.field_sign_oca_template__item_ids +msgid "Item" +msgstr "" + +#. module: sign_oca +#: model:ir.model.fields,field_description:sign_oca.field_sign_oca_field____last_update +#: model:ir.model.fields,field_description:sign_oca.field_sign_oca_request____last_update +#: model:ir.model.fields,field_description:sign_oca.field_sign_oca_request_log____last_update +#: model:ir.model.fields,field_description:sign_oca.field_sign_oca_request_signer____last_update +#: model:ir.model.fields,field_description:sign_oca.field_sign_oca_role____last_update +#: model:ir.model.fields,field_description:sign_oca.field_sign_oca_template____last_update +#: model:ir.model.fields,field_description:sign_oca.field_sign_oca_template_generate____last_update +#: model:ir.model.fields,field_description:sign_oca.field_sign_oca_template_generate_signer____last_update +#: model:ir.model.fields,field_description:sign_oca.field_sign_oca_template_item____last_update +msgid "Last Modified on" +msgstr "" + +#. module: sign_oca +#: model:ir.model.fields,field_description:sign_oca.field_sign_oca_field__write_uid +#: model:ir.model.fields,field_description:sign_oca.field_sign_oca_request__write_uid +#: model:ir.model.fields,field_description:sign_oca.field_sign_oca_request_signer__write_uid +#: model:ir.model.fields,field_description:sign_oca.field_sign_oca_role__write_uid +#: model:ir.model.fields,field_description:sign_oca.field_sign_oca_template__write_uid +#: model:ir.model.fields,field_description:sign_oca.field_sign_oca_template_generate__write_uid +#: model:ir.model.fields,field_description:sign_oca.field_sign_oca_template_generate_signer__write_uid +#: model:ir.model.fields,field_description:sign_oca.field_sign_oca_template_item__write_uid +msgid "Last Updated by" +msgstr "" + +#. module: sign_oca +#: model:ir.model.fields,field_description:sign_oca.field_sign_oca_field__write_date +#: model:ir.model.fields,field_description:sign_oca.field_sign_oca_request__write_date +#: model:ir.model.fields,field_description:sign_oca.field_sign_oca_request_signer__write_date +#: model:ir.model.fields,field_description:sign_oca.field_sign_oca_role__write_date +#: model:ir.model.fields,field_description:sign_oca.field_sign_oca_template__write_date +#: model:ir.model.fields,field_description:sign_oca.field_sign_oca_template_generate__write_date +#: model:ir.model.fields,field_description:sign_oca.field_sign_oca_template_generate_signer__write_date +#: model:ir.model.fields,field_description:sign_oca.field_sign_oca_template_item__write_date +msgid "Last Updated on" +msgstr "" + +#. module: sign_oca +#: model_terms:ir.ui.view,arch_db:sign_oca.sign_oca_request_search_view +msgid "Late Activities" +msgstr "" + +#. module: sign_oca +#. openerp-web +#: code:addons/sign_oca/static/src/components/sign_oca_pdf_portal/sign_oca_pdf_portal.xml:0 +#: model_terms:ir.ui.view,arch_db:sign_oca.portal_sign_document_signed +#, python-format +msgid "Logo" +msgstr "" + +#. module: sign_oca +#: model:ir.model.fields,field_description:sign_oca.field_sign_oca_request__message_main_attachment_id +msgid "Main Attachment" +msgstr "" + +#. module: sign_oca +#. openerp-web +#: code:addons/sign_oca/static/src/components/sign_oca_pdf/sign_oca_pdf.xml:0 +#, python-format +msgid "Main actions" +msgstr "" + +#. module: sign_oca +#: model:ir.model.fields,field_description:sign_oca.field_sign_oca_template_generate__message +msgid "Message" +msgstr "" + +#. module: sign_oca +#: model:ir.model.fields,field_description:sign_oca.field_sign_oca_request__message_has_error +msgid "Message Delivery error" +msgstr "" + +#. module: sign_oca +#: model:ir.model.fields,field_description:sign_oca.field_sign_oca_request__message_ids +msgid "Messages" +msgstr "" + +#. module: sign_oca +#: model:ir.model.fields,field_description:sign_oca.field_sign_oca_request__my_activity_date_deadline +msgid "My Activity Deadline" +msgstr "" + +#. module: sign_oca +#: model_terms:ir.ui.view,arch_db:sign_oca.sign_oca_request_search_view +msgid "My Documents" +msgstr "" + +#. module: sign_oca +#: model_terms:ir.ui.view,arch_db:sign_oca.sign_oca_request_search_view +msgid "My Requests" +msgstr "" + +#. module: sign_oca +#: model:ir.model.fields,field_description:sign_oca.field_sign_oca_field__name +#: model:ir.model.fields,field_description:sign_oca.field_sign_oca_request__name +#: model:ir.model.fields,field_description:sign_oca.field_sign_oca_request_signer__partner_name +#: model:ir.model.fields,field_description:sign_oca.field_sign_oca_role__name +#: model:ir.model.fields,field_description:sign_oca.field_sign_oca_template__name +msgid "Name" +msgstr "" + +#. module: sign_oca +#. openerp-web +#: code:addons/sign_oca/static/src/components/sign_oca_pdf_common/sign_oca_pdf_common.js:0 +#, python-format +msgid "Need a valid PDF to add signature fields !" +msgstr "" + +#. module: sign_oca +#: code:addons/sign_oca/models/sign_oca_request.py:0 +#, python-format +msgid "New document to sign" +msgstr "" + +#. module: sign_oca +#: model:ir.model.fields,field_description:sign_oca.field_sign_oca_request__activity_date_deadline +msgid "Next Activity Deadline" +msgstr "" + +#. module: sign_oca +#: model:ir.model.fields,field_description:sign_oca.field_sign_oca_request__activity_summary +msgid "Next Activity Summary" +msgstr "" + +#. module: sign_oca +#: model:ir.model.fields,field_description:sign_oca.field_sign_oca_request__activity_type_id +msgid "Next Activity Type" +msgstr "" + +#. module: sign_oca +#: model:ir.model.fields,field_description:sign_oca.field_sign_oca_request__next_item_id +msgid "Next Item" +msgstr "" + +#. module: sign_oca +#: model:ir.model.fields,field_description:sign_oca.field_sign_oca_request__message_needaction_counter +msgid "Number of Actions" +msgstr "" + +#. module: sign_oca +#: model:ir.model.fields,field_description:sign_oca.field_sign_oca_request__message_has_error_counter +msgid "Number of errors" +msgstr "" + +#. module: sign_oca +#: model:ir.model.fields,help:sign_oca.field_sign_oca_request__message_needaction_counter +msgid "Number of messages which requires an action" +msgstr "" + +#. module: sign_oca +#: model:ir.model.fields,help:sign_oca.field_sign_oca_request__message_has_error_counter +msgid "Number of messages with delivery error" +msgstr "" + +#. module: sign_oca +#: model:ir.model.fields,help:sign_oca.field_sign_oca_request__message_unread_counter +msgid "Number of unread messages" +msgstr "" + +#. module: sign_oca +#: model:ir.model.fields,field_description:sign_oca.field_sign_oca_template_item__page +msgid "Page" +msgstr "" + +#. module: sign_oca +#: model:ir.model.fields,field_description:sign_oca.field_sign_oca_request_log__partner_id +#: model:ir.model.fields,field_description:sign_oca.field_sign_oca_request_signer__partner_id +#: model:ir.model.fields,field_description:sign_oca.field_sign_oca_template_generate_signer__partner_id +msgid "Partner" +msgstr "" + +#. module: sign_oca +#. openerp-web +#: code:addons/sign_oca/static/src/components/sign_oca_configure/sign_oca_configure.xml:0 +#: model:ir.model.fields,field_description:sign_oca.field_sign_oca_template_item__placeholder +#, python-format +msgid "Placeholder" +msgstr "" + +#. module: sign_oca +#. openerp-web +#: code:addons/sign_oca/static/src/components/sign_oca_pdf_portal/sign_oca_pdf_portal.xml:0 +#, python-format +msgid "Please Review And Act On This Document" +msgstr "" + +#. module: sign_oca +#: model:ir.model.fields,field_description:sign_oca.field_sign_oca_request_signer__access_url +msgid "Portal Access URL" +msgstr "" + +#. module: sign_oca +#: model:ir.model.fields,field_description:sign_oca.field_sign_oca_template_item__position_x +msgid "Position X" +msgstr "" + +#. module: sign_oca +#: model:ir.model.fields,field_description:sign_oca.field_sign_oca_template_item__position_y +msgid "Position Y" +msgstr "" + +#. module: sign_oca +#: model:ir.model.fields,field_description:sign_oca.field_sign_oca_request_log__request_id +#: model:ir.model.fields,field_description:sign_oca.field_sign_oca_request_signer__request_id +#: model:ir.model.fields,field_description:sign_oca.field_sign_oca_template__request_ids +msgid "Request" +msgstr "" + +#. module: sign_oca +#: model:ir.model.fields,field_description:sign_oca.field_sign_oca_template__request_count +msgid "Request Count" +msgstr "" + +#. module: sign_oca +#: code:addons/sign_oca/models/sign_oca_request.py:0 +#, python-format +msgid "Request cannot be signed" +msgstr "" + +#. module: sign_oca +#: model:ir.ui.menu,name:sign_oca.sign_oca_request_menu +#: model_terms:ir.ui.view,arch_db:sign_oca.sign_oca_template_form_view +msgid "Requests" +msgstr "" + +#. module: sign_oca +#. openerp-web +#: code:addons/sign_oca/static/src/components/sign_oca_configure/sign_oca_configure.xml:0 +#: model:ir.model.fields,field_description:sign_oca.field_sign_oca_template_item__required +#, python-format +msgid "Required" +msgstr "" + +#. module: sign_oca +#: model:ir.model.fields,field_description:sign_oca.field_sign_oca_request__activity_user_id +msgid "Responsible User" +msgstr "" + +#. module: sign_oca +#: model_terms:ir.ui.view,arch_db:sign_oca.sign_oca_request_kanban_view +msgid "Restore" +msgstr "" + +#. module: sign_oca +#: model:ir.model.fields,field_description:sign_oca.field_sign_oca_request_signer__role_id +#: model:ir.model.fields,field_description:sign_oca.field_sign_oca_template_generate_signer__role_id +#: model:ir.model.fields,field_description:sign_oca.field_sign_oca_template_item__role_id +msgid "Role" +msgstr "" + +#. module: sign_oca +#: model:ir.actions.act_window,name:sign_oca.sign_oca_role_act_window +#: model:ir.ui.menu,name:sign_oca.sign_oca_role_menu +msgid "Roles" +msgstr "" + +#. module: sign_oca +#. openerp-web +#: code:addons/sign_oca/static/src/components/sign_oca_configure/sign_oca_configure.js:0 +#, python-format +msgid "Save" +msgstr "" + +#. module: sign_oca +#: model:ir.model.fields,field_description:sign_oca.field_sign_oca_request_signer__access_token +msgid "Security Token" +msgstr "" + +#. module: sign_oca +#: model_terms:ir.ui.view,arch_db:sign_oca.sign_oca_request_form_view +#: model_terms:ir.ui.view,arch_db:sign_oca.sign_oca_template_kanban_view +#: model_terms:ir.ui.view,arch_db:sign_oca.sign_oca_template_tree_view +msgid "Send" +msgstr "" + +#. module: sign_oca +#: model_terms:ir.ui.view,arch_db:sign_oca.sign_oca_template_form_view +msgid "Send to sign" +msgstr "" + +#. module: sign_oca +#: model:ir.model.fields.selection,name:sign_oca.selection__sign_oca_request__state__sent +#: model_terms:ir.ui.view,arch_db:sign_oca.sign_oca_request_search_view +msgid "Sent" +msgstr "" + +#. module: sign_oca +#: model:ir.ui.menu,name:sign_oca.sign_oca_settings_menu +msgid "Settings" +msgstr "" + +#. module: sign_oca +#: model_terms:ir.ui.view,arch_db:sign_oca.sign_oca_request_search_view +msgid "Show all records which has next action date is before today" +msgstr "" + +#. module: sign_oca +#. openerp-web +#: code:addons/sign_oca/static/src/components/sign_oca_pdf/sign_oca_pdf.xml:0 +#: code:addons/sign_oca/static/src/elements/signature.js:0 +#: model:ir.model.fields.selection,name:sign_oca.selection__sign_oca_request_log__action__sign +#: model:ir.module.category,name:sign_oca.sign_oca_module_category +#: model:ir.ui.menu,name:sign_oca.sign_oca_root_menu +#: model_terms:ir.ui.view,arch_db:sign_oca.sign_oca_request_tree_view +#, python-format +msgid "Sign" +msgstr "" + +#. module: sign_oca +#: model:ir.model.fields,field_description:sign_oca.field_sign_oca_template_generate__sign_now +#: model_terms:ir.ui.view,arch_db:sign_oca.sign_oca_template_kanban_view +msgid "Sign Now" +msgstr "" + +#. module: sign_oca +#: model:ir.model,name:sign_oca.model_sign_oca_template +msgid "Sign Oca Template" +msgstr "" + +#. module: sign_oca +#: model:ir.actions.act_window,name:sign_oca.sign_oca_template_generate_act_window +#: model_terms:ir.ui.view,arch_db:sign_oca.sign_oca_template_generate_form_view +msgid "Sign Oca Template Generate" +msgstr "" + +#. module: sign_oca +#: model:ir.model,name:sign_oca.model_sign_oca_template_item +msgid "Sign Oca Template Item" +msgstr "" + +#. module: sign_oca +#: model:ir.model,name:sign_oca.model_sign_oca_request +msgid "Sign Request" +msgstr "" + +#. module: sign_oca +#: model:ir.model,name:sign_oca.model_sign_oca_request_signer +msgid "Sign Request Value" +msgstr "" + +#. module: sign_oca +#: model:ir.actions.act_window,name:sign_oca.sign_oca_request_act_window +#: model:ir.actions.act_window,name:sign_oca.sign_oca_request_log_act_window +#: model:ir.actions.act_window,name:sign_oca.sign_oca_request_template_act_window +msgid "Sign Requests" +msgstr "" + +#. module: sign_oca +#: model:ir.model,name:sign_oca.model_sign_oca_role +msgid "Sign Role" +msgstr "" + +#. module: sign_oca +#: model_terms:ir.ui.view,arch_db:sign_oca.sign_oca_template_mail +msgid "Sign document" +msgstr "" + +#. module: sign_oca +#: model_terms:ir.ui.view,arch_db:sign_oca.sign_oca_template_tree_view +msgid "Sign now" +msgstr "" + +#. module: sign_oca +#: model:ir.model.fields,field_description:sign_oca.field_sign_oca_request__signatory_data +msgid "Signatory Data" +msgstr "" + +#. module: sign_oca +#: model:ir.model.fields.selection,name:sign_oca.selection__sign_oca_field__field_type__signature +msgid "Signature" +msgstr "" + +#. module: sign_oca +#: model:ir.model,name:sign_oca.model_sign_oca_field +msgid "Signature Field Type" +msgstr "" + +#. module: sign_oca +#: model:ir.model.fields,field_description:sign_oca.field_sign_oca_request_signer__signature_hash +msgid "Signature Hash" +msgstr "" + +#. module: sign_oca +#. openerp-web +#: code:addons/sign_oca/static/src/components/sign_oca_configure/sign_oca_configure.xml:0 +#: code:addons/sign_oca/static/src/components/sign_oca_configure/sign_oca_configure.xml:0 +#, python-format +msgid "Signature configuration" +msgstr "" + +#. module: sign_oca +#: model:ir.model,name:sign_oca.model_sign_oca_template_generate_signer +msgid "Signature request signers" +msgstr "" + +#. module: sign_oca +#: model:ir.model.fields,field_description:sign_oca.field_sign_oca_request__signed +#: model:ir.model.fields.selection,name:sign_oca.selection__sign_oca_request__state__signed +#: model_terms:ir.ui.view,arch_db:sign_oca.sign_oca_request_search_view +msgid "Signed" +msgstr "" + +#. module: sign_oca +#: model:ir.model.fields,field_description:sign_oca.field_sign_oca_request__signed_count +msgid "Signed Count" +msgstr "" + +#. module: sign_oca +#: model:ir.model.fields,field_description:sign_oca.field_sign_oca_request_signer__signed_on +msgid "Signed On" +msgstr "" + +#. module: sign_oca +#: model:ir.model.fields,field_description:sign_oca.field_sign_oca_request__signer_ids +#: model:ir.model.fields,field_description:sign_oca.field_sign_oca_request_log__signer_id +#: model:ir.model.fields,field_description:sign_oca.field_sign_oca_template_generate__signer_ids +msgid "Signer" +msgstr "" + +#. module: sign_oca +#: model:ir.model.fields,field_description:sign_oca.field_sign_oca_request__signer_count +msgid "Signer Count" +msgstr "" + +#. module: sign_oca +#: model:ir.model.fields,field_description:sign_oca.field_sign_oca_request__state +#: model_terms:ir.ui.view,arch_db:sign_oca.sign_oca_request_search_view +msgid "State" +msgstr "" + +#. module: sign_oca +#: model:ir.model.fields,help:sign_oca.field_sign_oca_request__activity_state +msgid "" +"Status based on activities\n" +"Overdue: Due date is already passed\n" +"Today: Activity date is today\n" +"Planned: Future activities." +msgstr "" + +#. module: sign_oca +#: model:ir.model.fields,field_description:sign_oca.field_sign_oca_request__template_id +#: model:ir.model.fields,field_description:sign_oca.field_sign_oca_template_generate__template_id +#: model:ir.model.fields,field_description:sign_oca.field_sign_oca_template_item__template_id +#: model_terms:ir.ui.view,arch_db:sign_oca.sign_oca_request_kanban_view +#: model_terms:ir.ui.view,arch_db:sign_oca.sign_oca_request_search_view +msgid "Template" +msgstr "" + +#. module: sign_oca +#: model:ir.actions.act_window,name:sign_oca.sign_oca_template_act_window +#: model:ir.ui.menu,name:sign_oca.sign_oca_template_menu +msgid "Templates" +msgstr "" + +#. module: sign_oca +#: model:ir.model.fields.selection,name:sign_oca.selection__sign_oca_field__field_type__text +msgid "Text" +msgstr "" + +#. module: sign_oca +#: model_terms:ir.ui.view,arch_db:sign_oca.portal_sign_document_signed +msgid "The document has been cancelled" +msgstr "" + +#. module: sign_oca +#. openerp-web +#: code:addons/sign_oca/static/src/components/sign_oca_configure/sign_oca_configure.xml:0 +#, python-format +msgid "" +"Then, you can move and resize the fields over the PDF page using the move " +"icons." +msgstr "" + +#. module: sign_oca +#: code:addons/sign_oca/models/sign_oca_request.py:0 +#, python-format +msgid "There are no signers, please fill them before configuring it" +msgstr "" + +#. module: sign_oca +#: model:ir.model.fields,field_description:sign_oca.field_sign_oca_request__to_sign +msgid "To Sign" +msgstr "" + +#. module: sign_oca +#: model_terms:ir.ui.view,arch_db:sign_oca.sign_oca_request_search_view +msgid "To sign" +msgstr "" + +#. module: sign_oca +#: model_terms:ir.ui.view,arch_db:sign_oca.sign_oca_request_search_view +msgid "Today Activities" +msgstr "" + +#. module: sign_oca +#: model:ir.model.fields,help:sign_oca.field_sign_oca_request__activity_exception_decoration +msgid "Type of the exception activity on record." +msgstr "" + +#. module: sign_oca +#: model:ir.model.fields,field_description:sign_oca.field_sign_oca_request_log__uid +msgid "Uid" +msgstr "" + +#. module: sign_oca +#: model:ir.model.fields,field_description:sign_oca.field_sign_oca_request__message_unread +msgid "Unread Messages" +msgstr "" + +#. module: sign_oca +#: model:ir.model.fields,field_description:sign_oca.field_sign_oca_request__message_unread_counter +msgid "Unread Messages Counter" +msgstr "" + +#. module: sign_oca +#: model:res.groups,name:sign_oca.sign_oca_group_manager +msgid "User: All Documents" +msgstr "" + +#. module: sign_oca +#: model:res.groups,name:sign_oca.sign_oca_group_user +msgid "User: Own Documents Only" +msgstr "" + +#. module: sign_oca +#: code:addons/sign_oca/models/sign_oca_request.py:0 +#, python-format +msgid "Users %s has already signed the document" +msgstr "" + +#. module: sign_oca +#: model:ir.model.fields.selection,name:sign_oca.selection__sign_oca_request_log__action__validate +msgid "Validate" +msgstr "" + +#. module: sign_oca +#. openerp-web +#: code:addons/sign_oca/static/src/components/sign_oca_pdf_portal/sign_oca_pdf_portal.xml:0 +#, python-format +msgid "Validate & Send document" +msgstr "" + +#. module: sign_oca +#: model:ir.model.fields.selection,name:sign_oca.selection__sign_oca_request_log__action__view +msgid "View Document" +msgstr "" + +#. module: sign_oca +#: model:ir.model.fields,field_description:sign_oca.field_sign_oca_request__website_message_ids +msgid "Website Messages" +msgstr "" + +#. module: sign_oca +#: model:ir.model.fields,help:sign_oca.field_sign_oca_request__website_message_ids +msgid "Website communication history" +msgstr "" + +#. module: sign_oca +#: model:ir.model.fields,field_description:sign_oca.field_sign_oca_template_item__width +msgid "Width" +msgstr "" + +#. module: sign_oca +#: model:ir.model.fields,field_description:sign_oca.field_sign_oca_template_generate_signer__wizard_id +msgid "Wizard" +msgstr "" + +#. module: sign_oca +#: code:addons/sign_oca/models/sign_oca_request.py:0 +#, python-format +msgid "You can only configure requests in draft state" +msgstr "" + +#. module: sign_oca +#: model_terms:ir.ui.view,arch_db:sign_oca.portal_sign_document_signed +msgid "" +"You should have received an email with the final document.
\n" +" Check on your mailbox." +msgstr "" + +#. module: sign_oca +#: model_terms:ir.ui.view,arch_db:sign_oca.sign_oca_request_form_view +msgid "" +"You will cancel the request and all the accesses. Are you sure about it?" +msgstr "" + +#. module: sign_oca +#: model_terms:ir.ui.view,arch_db:sign_oca.portal_sign_document_signed +msgid "has already been signed" +msgstr "" + +#. module: sign_oca +#: model:ir.model,name:sign_oca.model_sign_oca_request_log +msgid "sign.oca.request.log" +msgstr "" diff --git a/sign_oca/models/__init__.py b/sign_oca/models/__init__.py new file mode 100644 index 00000000..0ae418e9 --- /dev/null +++ b/sign_oca/models/__init__.py @@ -0,0 +1,4 @@ +from . import sign_oca_template +from . import sign_oca_role +from . import sign_oca_field +from . import sign_oca_request diff --git a/sign_oca/models/sign_oca_field.py b/sign_oca/models/sign_oca_field.py new file mode 100644 index 00000000..05736ff0 --- /dev/null +++ b/sign_oca/models/sign_oca_field.py @@ -0,0 +1,17 @@ +# Copyright 2023 Dixmit +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class SignOcaField(models.Model): + _name = "sign.oca.field" + _description = "Signature Field Type" + + name = fields.Char(required=True) + field_type = fields.Selection( + [("text", "Text"), ("signature", "Signature"), ("check", "Check")], + required=True, + default="text", + ) + default_value = fields.Char() diff --git a/sign_oca/models/sign_oca_request.py b/sign_oca/models/sign_oca_request.py new file mode 100644 index 00000000..cf7bc16b --- /dev/null +++ b/sign_oca/models/sign_oca_request.py @@ -0,0 +1,472 @@ +# Copyright 2023 Dixmit +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +import hashlib +from base64 import b64decode, b64encode +from io import BytesIO + +from PyPDF2 import PdfFileReader, PdfFileWriter +from reportlab.graphics.shapes import Drawing, Line, Rect +from reportlab.lib.colors import black, transparent +from reportlab.lib.styles import ParagraphStyle +from reportlab.pdfgen import canvas +from reportlab.platypus import Image, Paragraph + +from odoo import _, api, fields, models +from odoo.exceptions import ValidationError +from odoo.http import request + + +class SignOcaRequest(models.Model): + + _name = "sign.oca.request" + _inherit = ["mail.thread", "mail.activity.mixin"] + _description = "Sign Request" + + name = fields.Char(required=True) + active = fields.Boolean(default=True) + template_id = fields.Many2one("sign.oca.template", readonly=True) + data = fields.Binary( + required=True, readonly=True, states={"draft": [("readonly", False)]} + ) + signed = fields.Boolean(copy=False) + signer_ids = fields.One2many( + "sign.oca.request.signer", + inverse_name="request_id", + auto_join=True, + copy=True, + readonly=True, + states={"draft": [("readonly", False)]}, + ) + state = fields.Selection( + [ + ("draft", "Draft"), + ("sent", "Sent"), + ("signed", "Signed"), + ("cancel", "Cancelled"), + ], + default="draft", + readonly=True, + required=True, + copy=False, + tracking=True, + ) + signed_count = fields.Integer(compute="_compute_signed_count") + signer_count = fields.Integer(compute="_compute_signer_count") + to_sign = fields.Boolean(compute="_compute_to_sign") + signatory_data = fields.Serialized( + default=lambda r: {}, + readonly=True, + copy=False, + ) + current_hash = fields.Char(readonly=True, copy=False) + company_id = fields.Many2one( + "res.company", + default=lambda r: r.env.company.id, + required=True, + readonly=True, + states={"draft": [("readonly", False)]}, + ) + next_item_id = fields.Integer(compute="_compute_next_item_id") + + @api.depends("signatory_data") + def _compute_next_item_id(self): + for record in self: + record.next_item_id = ( + record.signatory_data and max(record.signatory_data.keys()) or 0 + ) + 1 + + def get_info(self): + self.ensure_one() + return { + "name": self.name, + "items": self.signatory_data, + "roles": [ + {"id": signer.id, "name": signer.role_id.name} + for signer in self.signer_ids + ], + "fields": [ + {"id": field.id, "name": field.name} + for field in self.env["sign.oca.field"].search([]) + ], + } + + def _ensure_draft(self): + self.ensure_one() + if not self.signer_ids: + raise ValidationError( + _("There are no signers, please fill them before configuring it") + ) + if not self.state == "draft": + raise ValidationError(_("You can only configure requests in draft state")) + + def configure(self): + self._ensure_draft() + self._set_action_log("configure") + return { + "type": "ir.actions.client", + "tag": "sign_oca_configure", + "name": self.name, + "params": { + "res_model": self._name, + "res_id": self.id, + }, + } + + def delete_item(self, item_id): + self._ensure_draft() + data = self.signatory_data + data.pop(item_id) + self.signatory_data = data + self._set_action_log("delete_field") + + def set_item_data(self, item_id, vals): + self._ensure_draft() + data = self.signatory_data + data[str(item_id)].update(vals) + self.signatory_data = data + self._set_action_log("edit_field") + + def add_item(self, item_vals): + self._ensure_draft() + item_id = self.next_item_id + field_id = self.env["sign.oca.field"].browse(item_vals["field_id"]) + signatory_data = self.signatory_data + signatory_data[item_id] = { + "id": item_id, + "field_id": field_id.id, + "field_type": field_id.field_type, + "required": False, + "name": field_id.name, + "role": self.signer_ids[0].role_id.id, + "page": 1, + "position_x": 0, + "position_y": 0, + "width": 0, + "height": 0, + "value": False, + "default_value": field_id.default_value, + "placeholder": "", + } + signatory_data[item_id].update(item_vals) + self.signatory_data = signatory_data + self._set_action_log("add_field") + return signatory_data[item_id] + + def cancel(self): + self.write({"state": "cancel"}) + self._set_action_log("cancel") + + @api.depends("signer_ids") + def _compute_signer_count(self): + for record in self: + record.signer_count = len(record.signer_ids) + + @api.depends("signer_ids", "signer_ids.signed_on") + def _compute_signed_count(self): + for record in self: + record.signed_count = len(record.signer_ids.filtered(lambda r: r.signed_on)) + + def open_template(self): + return self.template_id.configure() + + def action_send(self, sign_now=False, message=""): + self.ensure_one() + if self.state != "draft": + return + self._set_action_log("validate") + self.state = "sent" + for signer in self.signer_ids: + signer._portal_ensure_token() + if sign_now and signer.partner_id == self.env.user.partner_id: + continue + view = self.env.ref("sign_oca.sign_oca_template_mail") + render_result = view._render( + {"record": signer, "body": message, "link": signer.access_url}, + engine="ir.qweb", + minimal_qcontext=True, + ) + self.env["mail.thread"].message_notify( + body=render_result, + partner_ids=signer.partner_id.ids, + subject=_("New document to sign"), + subtype_id=self.env.ref("mail.mt_comment").id, + mail_auto_delete=False, + email_layout_xmlid="mail.mail_notification_light", + ) + + @api.depends("signer_ids.role_id", "signatory_data") + @api.depends_context("uid") + def _compute_to_sign(self): + for record in self: + record.to_sign = record.signer_ids.filtered( + lambda r: r.partner_id.id == self.env.user.partner_id.id + and not r.signed_on + ).mapped("role_id") + + def _check_signed(self): + self.ensure_one() + if self.state != "sent": + return + if all(self.mapped("signer_ids.signed_on")): + self.state = "signed" + + def sign(self): + self.ensure_one() + signer = self.signer_ids.filtered( + lambda r: r.partner_id == self.env.user.partner_id + ) + if not signer: + return self.get_formview_action() + return { + "type": "ir.actions.client", + "tag": "sign_oca", + "name": self.template_id.name, + "params": { + "res_model": signer[0]._name, + "res_id": signer[0].id, + }, + } + + def _set_action_log_vals(self, action, **kwargs): + vals = kwargs.copy() + vals.update( + {"action": action, "request_id": self.id, "ip": self._get_action_log_ip()} + ) + return vals + + def _get_action_log_ip(self): + if not request or not hasattr(request, "httprequest"): + # This comes from a server call. Set as localhost + return "0.0.0.0" + return request.httprequest.access_route[-1] + + def _set_action_log(self, action, **kwargs): + self.ensure_one() + return ( + self.env["sign.oca.request.log"] + .sudo() + .create(self._set_action_log_vals(action, **kwargs)) + ) + + @api.model_create_multi + def create(self, vals_list): + records = super().create(vals_list) + for record in records: + record._set_action_log("create") + return records + + +class SignOcaRequestSigner(models.Model): + + _name = "sign.oca.request.signer" + _inherit = "portal.mixin" + _description = "Sign Request Value" + + data = fields.Binary(related="request_id.data") + request_id = fields.Many2one("sign.oca.request", required=True, ondelete="cascade") + partner_name = fields.Char(related="partner_id.name") + partner_id = fields.Many2one("res.partner", required=True, ondelete="restrict") + role_id = fields.Many2one("sign.oca.role", required=True, ondelete="restrict") + signed_on = fields.Datetime(readonly=True) + signature_hash = fields.Char(readonly=True) + + def _compute_access_url(self): + super()._compute_access_url() + for record in self: + record.access_url = "/sign_oca/document/%s/%s" % ( + record.id, + record.access_token, + ) + + def get_info(self, access_token=False): + self.ensure_one() + self._set_action_log("view", access_token=access_token) + return { + "role": 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, + "partner": { + "id": self.env.user.partner_id.id, + "name": self.env.user.partner_id.name, + "email": self.env.user.partner_id.email, + "phone": self.env.user.partner_id.phone, + }, + } + + def action_sign(self, items, access_token=False): + self.ensure_one() + if self.signed_on: + raise ValidationError( + _("Users %s has already signed the document") % self.partner_id.name + ) + if self.request_id.state != "sent": + raise ValidationError(_("Request cannot be signed")) + self.signed_on = fields.Datetime.now() + # current_hash = self.request_id.current_hash + signatory_data = self.request_id.signatory_data + + input_data = BytesIO(b64decode(self.request_id.data)) + reader = PdfFileReader(input_data) + output = PdfFileWriter() + pages = {} + for page_number in range(1, reader.numPages + 1): + pages[page_number] = reader.getPage(page_number - 1) + + for key in signatory_data: + if signatory_data[key]["role"] == self.role_id.id: + signatory_data[key] = items[key] + self._check_signable(items[key]) + item = items[key] + page = pages[item["page"]] + new_page = self._get_pdf_page(item, page.mediaBox) + if new_page: + page.mergePage(new_page) + pages[item["page"]] = page + for page_number in pages: + output.addPage(pages[page_number]) + output_stream = BytesIO() + output.write(output_stream) + output_stream.seek(0) + signed_pdf = output_stream.read() + final_hash = hashlib.sha1(signed_pdf).hexdigest() + # TODO: Review that the hash has not been changed... + self.request_id.write( + { + "signatory_data": signatory_data, + "data": b64encode(signed_pdf), + "current_hash": final_hash, + } + ) + self.signature_hash = final_hash + self.request_id._check_signed() + self._set_action_log("sign", access_token=access_token) + # TODO: Add a return + + def _check_signable(self, item): + if not item["required"]: + return + if not item["value"]: + raise ValidationError(_("Field %s is not filled") % item["name"]) + + def _get_pdf_page_text(self, item, box): + packet = BytesIO() + can = canvas.Canvas(packet, pagesize=(box.getWidth(), box.getHeight())) + if not item["value"]: + return False + par = Paragraph(item["value"], style=self._getParagraphStyle()) + par.wrap( + item["width"] / 100 * float(box.getWidth()), + item["height"] / 100 * float(box.getHeight()), + ) + par.drawOn( + can, + item["position_x"] / 100 * float(box.getWidth()), + (100 - item["position_y"] - item["height"]) / 100 * float(box.getHeight()), + ) + can.save() + packet.seek(0) + new_pdf = PdfFileReader(packet) + return new_pdf.getPage(0) + + def _getParagraphStyle(self): + return ParagraphStyle(name="Oca Sign Style") + + def _get_pdf_page_check(self, item, box): + packet = BytesIO() + can = canvas.Canvas(packet, pagesize=(box.getWidth(), box.getHeight())) + width = item["width"] / 100 * float(box.getWidth()) + height = item["height"] / 100 * float(box.getHeight()) + drawing = Drawing(width=width, height=height) + drawing.add( + Rect( + 0, + 0, + width, + height, + strokeWidth=3, + strokeColor=black, + fillColor=transparent, + ) + ) + if item["value"]: + drawing.add(Line(0, 0, width, height, strokeColor=black, strokeWidth=3)) + drawing.add(Line(0, height, width, 0, strokeColor=black, strokeWidth=3)) + drawing.drawOn( + can, + item["position_x"] / 100 * float(box.getWidth()), + (100 - item["position_y"] - item["height"]) / 100 * float(box.getHeight()), + ) + can.save() + packet.seek(0) + new_pdf = PdfFileReader(packet) + return new_pdf.getPage(0) + + def _get_pdf_page_signature(self, item, box): + packet = BytesIO() + can = canvas.Canvas(packet, pagesize=(box.getWidth(), box.getHeight())) + if not item["value"]: + return False + par = Image( + BytesIO(b64decode(item["value"])), + width=item["width"] / 100 * float(box.getWidth()), + height=item["height"] / 100 * float(box.getHeight()), + ) + par.drawOn( + can, + item["position_x"] / 100 * float(box.getWidth()), + (100 - item["position_y"] - item["height"]) / 100 * float(box.getHeight()), + ) + can.save() + packet.seek(0) + new_pdf = PdfFileReader(packet) + return new_pdf.getPage(0) + + def _get_pdf_page(self, item, box): + return getattr(self, "_get_pdf_page_%s" % item["field_type"])(item, box) + + def _set_action_log(self, action, **kwargs): + self.ensure_one() + return self.request_id._set_action_log(action, signer_id=self.id, **kwargs) + + def name_get(self): + result = [(signer.id, (signer.partner_id.display_name)) for signer in self] + return result + + +class SignRequestLog(models.Model): + _name = "sign.oca.request.log" + _log_access = False + + uid = fields.Many2one( + "res.users", + required=True, + readonly=True, + ondelete="cascade", + default=lambda r: r.env.user.id, + ) + date = fields.Datetime( + required=True, readonly=True, default=lambda r: fields.Datetime.now() + ) + partner_id = fields.Many2one( + "res.partner", required=True, default=lambda r: r.env.user.partner_id.id + ) + request_id = fields.Many2one("sign.oca.request", required=True, ondelete="cascade") + signer_id = fields.Many2one("sign.oca.request.signer") + action = fields.Selection( + [ + ("create", "Create"), + ("validate", "Validate"), + ("view", "View Document"), + ("sign", "Sign"), + ("add_field", "Add field"), + ("edit_field", "Edit field"), + ("delete_field", "Delete field"), + ("cancel", "Cancel"), + ], + required=True, + readonly=True, + ) + access_token = fields.Char(readonly=True) + ip = fields.Char(readonly=True) diff --git a/sign_oca/models/sign_oca_role.py b/sign_oca/models/sign_oca_role.py new file mode 100644 index 00000000..25f71157 --- /dev/null +++ b/sign_oca/models/sign_oca_role.py @@ -0,0 +1,12 @@ +# Copyright 2023 Dixmit +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class SignOcaRole(models.Model): + _name = "sign.oca.role" + _description = "Sign Role" + + name = fields.Char(required=True) + domain = fields.Char(required=True, default=[]) diff --git a/sign_oca/models/sign_oca_template.py b/sign_oca/models/sign_oca_template.py new file mode 100644 index 00000000..5f75c0b2 --- /dev/null +++ b/sign_oca/models/sign_oca_template.py @@ -0,0 +1,108 @@ +# Copyright 2023 Dixmit +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import api, fields, models + + +class SignOcaTemplate(models.Model): + + _name = "sign.oca.template" + _description = "Sign Oca Template" # TODO + + name = fields.Char(required=True) + data = fields.Binary(attachment=True, required=True) + filename = fields.Char() + item_ids = fields.One2many("sign.oca.template.item", inverse_name="template_id") + request_count = fields.Integer(compute="_compute_request_count") + active = fields.Boolean(default=True) + request_ids = fields.One2many("sign.oca.request", inverse_name="template_id") + + @api.depends("request_ids") + def _compute_request_count(self): + for record in self: + record.request_count = len(record.request_ids) + + def configure(self): + self.ensure_one() + return { + "type": "ir.actions.client", + "tag": "sign_oca_configure", + "name": self.name, + "params": { + "res_model": self._name, + "res_id": self.id, + }, + } + + def get_info(self): + self.ensure_one() + return { + "name": self.name, + "items": {item.id: item.get_info() for item in self.item_ids}, + "roles": [ + {"id": role.id, "name": role.name} + for role in self.env["sign.oca.role"].search([]) + ], + "fields": [ + {"id": field.id, "name": field.name} + for field in self.env["sign.oca.field"].search([]) + ], + } + + def delete_item(self, item_id): + self.ensure_one() + item = self.item_ids.browse(item_id) + assert item.template_id == self + item.unlink() + + def set_item_data(self, item_id, vals): + self.ensure_one() + item = self.env["sign.oca.template.item"].browse(item_id) + assert item.template_id == self + item.write(vals) + + def add_item(self, item_vals): + self.ensure_one() + item_vals["template_id"] = self.id + return self.env["sign.oca.template.item"].create(item_vals).get_info() + + +class SignOcaTemplateItem(models.Model): + + _name = "sign.oca.template.item" + _description = "Sign Oca Template Item" # TODO + + template_id = fields.Many2one( + "sign.oca.template", required=True, ondelete="cascade" + ) + field_id = fields.Many2one("sign.oca.field", ondelete="restrict") + role_id = fields.Many2one( + "sign.oca.role", default=lambda r: r._get_default_role(), ondelete="restrict" + ) + required = fields.Boolean() + # If no role, it will be editable by everyone... + page = fields.Integer(required=True, default=1) + position_x = fields.Float(required=True) + position_y = fields.Float(required=True) + width = fields.Float() + height = fields.Float() + placeholder = fields.Char() + + @api.model + def _get_default_role(self): + return self.env.ref("sign_oca.sign_role_customer") + + def get_info(self): + self.ensure_one() + return { + "id": self.id, + "field_id": self.field_id.id, + "name": self.field_id.name, + "role": self.role_id.id, + "page": self.page, + "position_x": self.position_x, + "position_y": self.position_y, + "width": self.width, + "height": self.height, + "placeholder": self.placeholder, + } diff --git a/sign_oca/readme/CONTRIBUTORS.rst b/sign_oca/readme/CONTRIBUTORS.rst new file mode 100644 index 00000000..c0465388 --- /dev/null +++ b/sign_oca/readme/CONTRIBUTORS.rst @@ -0,0 +1 @@ +* Enric Tobella (www.dixmit.com) diff --git a/sign_oca/readme/DESCRIPTION.rst b/sign_oca/readme/DESCRIPTION.rst new file mode 100644 index 00000000..e24bf705 --- /dev/null +++ b/sign_oca/readme/DESCRIPTION.rst @@ -0,0 +1 @@ +This module allows to create documents for signature inside Odoo using OWL. diff --git a/sign_oca/readme/ROADMAP.rst b/sign_oca/readme/ROADMAP.rst new file mode 100644 index 00000000..3391c0b0 --- /dev/null +++ b/sign_oca/readme/ROADMAP.rst @@ -0,0 +1,6 @@ +Tasks +~~~~~ + +* Ensure that the signature is inalterable. + Maybe we might need to use some tools like endevise or pyHanko with a certificate. + Signer can be authenticated using OTP. diff --git a/sign_oca/readme/USAGE.rst b/sign_oca/readme/USAGE.rst new file mode 100644 index 00000000..688783b1 --- /dev/null +++ b/sign_oca/readme/USAGE.rst @@ -0,0 +1,21 @@ +Creation of templates +~~~~~~~~~~~~~~~~~~~~~ + +* Access `Sign / Templates` +* Create a new template +* Add a PDF File +* Access the configuration menu +* You can add a field by doing a right click inside a page +* Click on the field in order to delete or edit some configuration of it +* The template is autosaved + +Sign a document +~~~~~~~~~~~~~~~ + +* Access `Sign / Templates` +* Press the `Sign` button from a template +* Fill all the possible partners that will sign the document +* The signature action will be opened. +* There, you can fill all the data you need. +* Once you finish, press the sign button on the top +* When the last signer signs it, the final file will be generated as a PDF diff --git a/sign_oca/security/ir.model.access.csv b/sign_oca/security/ir.model.access.csv new file mode 100644 index 00000000..57569719 --- /dev/null +++ b/sign_oca/security/ir.model.access.csv @@ -0,0 +1,10 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +edit_sign_template,edit_sign_template,model_sign_oca_template,sign_oca_group_user,1,1,1,1 +edit_sign_template_item,edit_sign_template_item,model_sign_oca_template_item,sign_oca_group_user,1,1,1,1 +edit_sign_role,edit_sign_role,model_sign_oca_role,sign_oca_group_user,1,1,1,1 +edit_sign_field,edit_sign_field,model_sign_oca_field,sign_oca_group_user,1,1,1,1 +edit_sign_request,edit_sign_field,model_sign_oca_request,sign_oca_group_user,1,1,1,1 +edit_sign_request_signer,edit_sign_field,model_sign_oca_request_signer,sign_oca_group_user,1,1,1,1 +edit_sign_generate,edit_sign_field,model_sign_oca_template_generate,sign_oca_group_user,1,1,1,1 +edit_sign_generate_signer,edit_sign_field,model_sign_oca_template_generate_signer,sign_oca_group_user,1,1,1,1 +access_sign_request_log,access_sign_request_log,model_sign_oca_request_log,sign_oca_group_user,1,0,0,0 diff --git a/sign_oca/security/security.xml b/sign_oca/security/security.xml new file mode 100644 index 00000000..bcbdb1ac --- /dev/null +++ b/sign_oca/security/security.xml @@ -0,0 +1,22 @@ + + + + + Sign + + + + User: Own Documents Only + + + + + User: All Documents + + + + + diff --git a/sign_oca/static/description.svg b/sign_oca/static/description.svg new file mode 100644 index 00000000..7de8b679 --- /dev/null +++ b/sign_oca/static/description.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/sign_oca/static/description/icon.png b/sign_oca/static/description/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..7f4cb77b606f1feabf99959e76be9081eb6ae970 GIT binary patch literal 10887 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 literal 0 HcmV?d00001 diff --git a/sign_oca/static/description/icon.svg b/sign_oca/static/description/icon.svg new file mode 100644 index 00000000..7de8b679 --- /dev/null +++ b/sign_oca/static/description/icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/sign_oca/static/description/index.html b/sign_oca/static/description/index.html new file mode 100644 index 00000000..633347a3 --- /dev/null +++ b/sign_oca/static/description/index.html @@ -0,0 +1,470 @@ + + + + + + +Sign Oca + + + +
+

Sign Oca

+ + +

Beta License: AGPL-3 OCA/sign Translate me on Weblate Try me on Runboat

+

This module allows to create documents for signature inside Odoo using OWL.

+

Table of contents

+ +
+

Usage

+
+

Creation of templates

+
    +
  • Access Sign / Templates
  • +
  • Create a new template
  • +
  • Add a PDF File
  • +
  • Access the configuration menu
  • +
  • You can add a field by doing a right click inside a page
  • +
  • Click on the field in order to delete or edit some configuration of it
  • +
  • The template is autosaved
  • +
+
+
+

Sign a document

+
    +
  • Access Sign / Templates
  • +
  • Press the Sign button from a template
  • +
  • Fill all the possible partners that will sign the document
  • +
  • The signature action will be opened.
  • +
  • There, you can fill all the data you need.
  • +
  • Once you finish, press the sign button on the top
  • +
  • When the last signer signs it, the final file will be generated as a PDF
  • +
+
+
+
+

Known issues / Roadmap

+
+

Tasks

+
    +
  • Ensure that the signature is inalterable. +Maybe we might need to use some tools like endevise or pyHanko with a certificate. +Signer can be authenticated using OTP.
  • +
+
+
+
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • Dixmit
  • +
+
+
+

Contributors

+
    +
  • Enric Tobella (www.dixmit.com)
  • +
+
+
+

Maintainers

+

This module is maintained by the OCA.

+Odoo Community Association +

OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use.

+

Current maintainer:

+

etobella

+

This module is part of the OCA/sign project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + diff --git a/sign_oca/static/src/components/sign_oca_configure/sign_oca_configure.js b/sign_oca/static/src/components/sign_oca_configure/sign_oca_configure.js new file mode 100644 index 00000000..b0cd6b0a --- /dev/null +++ b/sign_oca/static/src/components/sign_oca_configure/sign_oca_configure.js @@ -0,0 +1,451 @@ +odoo.define( + "sign_oca/static/src/components/sign_oca_configure/sign_oca_configure.js", + function (require) { + "use strict"; + + const {ComponentWrapper} = require("web.OwlCompatibility"); + const AbstractAction = require("web.AbstractAction"); + const Dialog = require("web.Dialog"); + const core = require("web.core"); + var ControlPanel = require("web.ControlPanel"); + const SignOcaPdfCommon = require("sign_oca/static/src/components/sign_oca_pdf_common/sign_oca_pdf_common.js"); + const _t = core._t; + class SignOcaConfigureControlPanel extends ControlPanel {} + SignOcaConfigureControlPanel.template = "sign_oca.SignOcaConfigureControlPanel"; + class SignOcaConfigure extends SignOcaPdfCommon { + constructor() { + super(...arguments); + this.field_template = "sign_oca.sign_iframe_field_configure"; + this.contextMenu = undefined; + this.isMobile = + this.env.device.isMobile || this.env.device.isMobileDevice; + } + postIframeFields() { + super.postIframeFields(...arguments); + _.each( + this.iframe.el.contentDocument.getElementsByClassName("page"), + (page) => { + page.addEventListener("mousedown", (e) => { + e.preventDefault(); + e.stopPropagation(); + return false; + }); + page.addEventListener("contextmenu", (e) => { + e.preventDefault(); + e.stopPropagation(); + if (this.contextMenu !== undefined) { + this.contextMenu.remove(); + this.contextMenu = undefined; + } + var position = page.getBoundingClientRect(); + this.contextMenu = $( + core.qweb.render("sign_oca.sign_iframe_contextmenu", { + page, + e, + left: + ((e.pageX - position.x) * 100) / + position.width + + "%", + top: + ((e.pageY - position.y) * 100) / + position.height + + "%", + info: this.info, + page_id: parseInt(page.dataset.pageNumber, 10), + }) + ); + page.append(this.contextMenu[0]); + }); + } + ); + this.iframe.el.contentDocument.addEventListener( + "click", + (ev) => { + if (this.contextMenu && !this.creatingItem) { + if (this.contextMenu[0].contains(ev.target)) { + this.creatingItem = true; + this.env.services + .rpc({ + model: this.props.model, + method: "add_item", + args: [ + [this.props.res_id], + { + field_id: parseInt( + ev.target.dataset.field, + 10 + ), + page: parseInt( + ev.target.dataset.page, + 10 + ), + position_x: parseFloat( + ev.target.parentElement.style.left + ), + position_y: parseFloat( + ev.target.parentElement.style.top + ), + width: 20, + height: 1.5, + }, + ], + }) + .then((data) => { + this.info.items[data.id] = data; + this.postIframeField(data); + this.contextMenu.remove(); + this.contextMenu = undefined; + this.creatingItem = false; + }); + } else { + this.contextMenu.remove(); + this.contextMenu = undefined; + } + } + }, + // We need to enforce it to happen no matter what + true + ); + this.iframeLoaded.resolve(); + } + postIframeField(item) { + var signatureItem = super.postIframeField(...arguments); + var dragItem = signatureItem[0].getElementsByClassName( + "o_sign_oca_draggable" + )[0]; + var resizeItems = signatureItem[0].getElementsByClassName( + "o_sign_oca_resize" + ); + signatureItem[0].addEventListener( + "click", + (e) => { + if ( + e.target.classList.contains("o_sign_oca_resize") || + e.target.classList.contains("o_sign_oca_draggable") + ) { + return; + } + var target = e.currentTarget; + // TODO: Open Dialog for configuration + var dialog = new Dialog(this, { + title: _t("Edit field"), + $content: $( + core.qweb.render("sign_oca.sign_oca_field_edition", { + item, + info: this.info, + }) + ), + + buttons: [ + { + text: _t("Save"), + classes: "btn-primary", + close: true, + click: () => { + var field_id = parseInt( + dialog.$el + .find('select[name="field_id"]') + .val(), + 10 + ); + var role_id = parseInt( + dialog.$el + .find('select[name="role_id"]') + .val(), + 10 + ); + var required = dialog.$el + .find("input[name='required']") + .prop("checked"); + var placeholder = dialog.$el + .find("input[name='placeholder']") + .val(); + this.env.services + .rpc({ + model: this.props.model, + method: "set_item_data", + args: [ + [this.props.res_id], + item.id, + { + field_id, + role_id, + required, + placeholder, + }, + ], + }) + .then(() => { + item.field_id = field_id; + item.name = _.filter( + this.info.fields, + (field) => field.id === field_id + )[0].name; + item.role_id = role_id; + item.required = required; + item.placeholder = placeholder; + target.remove(); + this.postIframeField(item); + }); + }, + }, + { + text: _t("Delete"), + classes: "btn-danger", + close: true, + click: () => { + this.env.services + .rpc({ + model: this.props.model, + method: "delete_item", + args: [[this.props.res_id], item.id], + }) + .then(() => { + delete this.info.items[item.id]; + target.remove(); + }); + }, + }, + { + text: _t("Cancel"), + close: true, + }, + ], + }).open(); + }, + true + ); + var startFunction = "mousedown"; + var endFunction = "mouseup"; + var moveFunction = "mousemove"; + if (this.isMobile) { + startFunction = "touchstart"; + endFunction = "touchend"; + moveFunction = "touchmove"; + } + dragItem.addEventListener(startFunction, (mousedownEvent) => { + mousedownEvent.preventDefault(); + var parentPage = mousedownEvent.target.parentElement.parentElement; + this.movingItem = mousedownEvent.target.parentElement; + var mousemove = this._onDragItem.bind(this); + parentPage.addEventListener(moveFunction, mousemove); + parentPage.addEventListener( + endFunction, + (mouseupEvent) => { + mouseupEvent.currentTarget.removeEventListener( + moveFunction, + mousemove + ); + var target = $(this.movingItem); + var position = target.parent()[0].getBoundingClientRect(); + var newPosition = mouseupEvent; + if (mouseupEvent.changedTouches) { + newPosition = mouseupEvent.changedTouches[0]; + } + var left = + (Math.max( + 0, + Math.min( + position.width, + newPosition.pageX - position.x + ) + ) * + 100) / + position.width; + var top = + (Math.max( + 0, + Math.min( + position.height, + newPosition.pageY - position.y + ) + ) * + 100) / + position.height; + target.css("left", left + "%"); + target.css("top", top + "%"); + item.position_x = left; + item.position_y = top; + this.env.services.rpc({ + model: this.props.model, + method: "set_item_data", + args: [ + [this.props.res_id], + item.id, + { + position_x: left, + position_y: top, + }, + ], + }); + this.movingItem = undefined; + }, + {once: true} + ); + }); + _.each(resizeItems, (resizeItem) => { + resizeItem.addEventListener(startFunction, (mousedownEvent) => { + mousedownEvent.preventDefault(); + var parentPage = + mousedownEvent.target.parentElement.parentElement; + this.resizingItem = mousedownEvent.target.parentElement; + var mousemove = this._onResizeItem.bind(this); + parentPage.addEventListener(moveFunction, mousemove); + parentPage.addEventListener( + endFunction, + (mouseupEvent) => { + mouseupEvent.stopPropagation(); + mouseupEvent.preventDefault(); + mouseupEvent.currentTarget.removeEventListener( + moveFunction, + mousemove + ); + var target = $(this.resizingItem); + var newPosition = mouseupEvent; + if (mouseupEvent.changedTouches) { + newPosition = mouseupEvent.changedTouches[0]; + } + var targetPosition = target + .find(".o_sign_oca_resize")[0] + .getBoundingClientRect(); + var itemPosition = target[0].getBoundingClientRect(); + var pagePosition = target + .parent()[0] + .getBoundingClientRect(); + var width = + (Math.max( + 0, + newPosition.pageX + + targetPosition.width - + itemPosition.x + ) * + 100) / + pagePosition.width; + var height = + (Math.max( + 0, + newPosition.pageY + + targetPosition.height - + itemPosition.y + ) * + 100) / + pagePosition.height; + target.css("width", width + "%"); + target.css("height", height + "%"); + item.width = width; + item.height = height; + this.env.services.rpc({ + model: this.props.model, + method: "set_item_data", + args: [ + [this.props.res_id], + item.id, + { + width: width, + height: height, + }, + ], + }); + }, + {once: true} + ); + }); + }); + return signatureItem; + } + _onResizeItem(e) { + e.stopPropagation(); + e.preventDefault(); + var target = $(this.resizingItem); + var targetPosition = target + .find(".o_sign_oca_resize")[0] + .getBoundingClientRect(); + var itemPosition = target[0].getBoundingClientRect(); + var newPosition = e; + if (e.targetTouches) { + newPosition = e.targetTouches[0]; + } + var pagePosition = target.parent()[0].getBoundingClientRect(); + var width = + (Math.max( + 0, + newPosition.pageX + targetPosition.width - itemPosition.x + ) * + 100) / + pagePosition.width; + var height = + (Math.max( + 0, + newPosition.pageY + targetPosition.height - itemPosition.y + ) * + 100) / + pagePosition.height; + target.css("width", width + "%"); + target.css("height", height + "%"); + } + _onDragItem(e) { + e.stopPropagation(); + e.preventDefault(); + var target = $(this.movingItem); + var position = target.parent()[0].getBoundingClientRect(); + var newPosition = e; + if (e.targetTouches) { + newPosition = e.targetTouches[0]; + } + var left = + (Math.max( + 0, + Math.min(position.width, newPosition.pageX - position.x) + ) * + 100) / + position.width; + var top = + (Math.max( + 0, + Math.min(position.height, newPosition.pageY - position.y) + ) * + 100) / + position.height; + target.css("left", left + "%"); + target.css("top", top + "%"); + } + } + + const SignOcaConfigureAction = AbstractAction.extend({ + hasControlPanel: true, + init: function (parent, action) { + this._super.apply(this, arguments); + this.model = + (action.params.res_model !== undefined && + action.params.res_model) || + action.context.params.res_model; + this.res_id = + (action.params.res_id !== undefined && action.params.res_id) || + action.context.params.id; + }, + async start() { + await this._super(...arguments); + this.component = new ComponentWrapper(this, SignOcaConfigure, { + model: this.model, + res_id: this.res_id, + }); + this.$el.addClass("o_sign_oca_action"); + return this.component.mount(this.$(".o_content")[0]); + }, + getState: function () { + var result = this._super(...arguments); + result = _.extend({}, result, { + res_model: this.model, + res_id: this.res_id, + }); + return result; + }, + }); + core.action_registry.add("sign_oca_configure", SignOcaConfigureAction); + SignOcaConfigure.template = "sign_oca.SignOcaConfigure"; + return { + SignOcaConfigure, + SignOcaConfigureAction, + }; + } +); diff --git a/sign_oca/static/src/components/sign_oca_configure/sign_oca_configure.xml b/sign_oca/static/src/components/sign_oca_configure/sign_oca_configure.xml new file mode 100644 index 00000000..2e3027ec --- /dev/null +++ b/sign_oca/static/src/components/sign_oca_configure/sign_oca_configure.xml @@ -0,0 +1,124 @@ + + + + + + + +
+
In order to add a new field, do a right click over the PDF page. You will be able to select the field that you will import
+
Then, you can move and resize the fields over the PDF page using the move icons.
+
If you do a click over a field, you will be able to change the default configurations of the field
+
Data is saved automatically when editing
+
+
+ +
+ +
+ + +
+ + +
+
Click on the field that you want to add
+ +
+ +
+
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+
+
+ + + + + + diff --git a/sign_oca/static/src/components/sign_oca_pdf/sign_oca_pdf.js b/sign_oca/static/src/components/sign_oca_pdf/sign_oca_pdf.js new file mode 100644 index 00000000..64b79530 --- /dev/null +++ b/sign_oca/static/src/components/sign_oca_pdf/sign_oca_pdf.js @@ -0,0 +1,99 @@ +odoo.define("sign_oca/static/src/components/sign_oca_pdf/sign_oca_pdf.js", function ( + require +) { + "use strict"; + + const core = require("web.core"); + const SignOcaPdfCommon = require("sign_oca/static/src/components/sign_oca_pdf_common/sign_oca_pdf_common.js"); + const SignRegistry = require("sign_oca.SignRegistry"); + class SignOcaPdf extends SignOcaPdfCommon { + constructor() { + super(...arguments); + this.to_sign = false; + } + async willStart() { + await super.willStart(...arguments); + this.checkFilledAll(); + } + checkToSign() { + this.props.updateControlPanel({ + cp_content: { + $buttons: this.renderButtons(this.to_sign_update), + }, + }); + this.to_sign = this.to_sign_update; + } + renderButtons(to_sign) { + var $buttons = $( + core.qweb.render("oca_sign_oca.SignatureButtons", { + to_sign: to_sign, + }) + ); + $buttons.on("click.o_sign_oca_button_sign", () => { + this.env.services + .rpc({ + model: this.props.model, + method: "action_sign", + args: [[this.props.res_id], this.info.items], + }) + .then(() => { + this.props.trigger("history_back"); + }); + }); + return $buttons; + } + _trigger_up(ev) { + const evType = ev.name; + const payload = ev.data; + if (evType === "call_service") { + let args = payload.args || []; + if (payload.service === "ajax" && payload.method === "rpc") { + // Ajax service uses an extra 'target' argument for rpc + args = args.concat(ev.target); + } + const service = this.env.services[payload.service]; + const result = service[payload.method].apply(service, args); + payload.callback(result); + } else if (evType === "get_session") { + if (payload.callback) { + payload.callback(this.env.session); + } + } else if (evType === "load_views") { + const params = { + model: payload.modelName, + context: payload.context, + views_descr: payload.views, + }; + this.env.dataManager + .load_views(params, payload.options || {}) + .then(payload.on_success); + } else if (evType === "load_filters") { + return this.env.dataManager + .load_filters(payload) + .then(payload.on_success); + } else { + payload.__targetWidget = ev.target; + this.trigger(evType.replace(/_/g, "-"), payload); + } + } + postIframeField(item) { + var signatureItem = super.postIframeField(...arguments); + signatureItem[0].append( + SignRegistry.map[item.field_type].generate(this, item, signatureItem) + ); + return signatureItem; + } + checkFilledAll() { + this.to_sign_update = + _.filter(this.info.items, (item) => { + return ( + item.required && + item.role === this.info.role && + !SignRegistry.map[item.field_type].check(item) + ); + }).length === 0; + this.checkToSign(); + } + } + return SignOcaPdf; +}); diff --git a/sign_oca/static/src/components/sign_oca_pdf/sign_oca_pdf.xml b/sign_oca/static/src/components/sign_oca_pdf/sign_oca_pdf.xml new file mode 100644 index 00000000..5fcc92d0 --- /dev/null +++ b/sign_oca/static/src/components/sign_oca_pdf/sign_oca_pdf.xml @@ -0,0 +1,15 @@ + + + + + + diff --git a/sign_oca/static/src/components/sign_oca_pdf/sign_oca_pdf_action.js b/sign_oca/static/src/components/sign_oca_pdf/sign_oca_pdf_action.js new file mode 100644 index 00000000..f3b1a3b0 --- /dev/null +++ b/sign_oca/static/src/components/sign_oca_pdf/sign_oca_pdf_action.js @@ -0,0 +1,47 @@ +odoo.define( + "sign_oca/static/src/components/sign_oca_pdf/sign_oca_pdf_action.js", + function (require) { + "use strict"; + + const {ComponentWrapper} = require("web.OwlCompatibility"); + const AbstractAction = require("web.AbstractAction"); + const core = require("web.core"); + const SignOcaPdf = require("sign_oca/static/src/components/sign_oca_pdf/sign_oca_pdf.js"); + + const SignOcaPdfAction = AbstractAction.extend({ + className: "o_sign_oca_content", + hasControlPanel: true, + init: function (parent, action) { + this._super.apply(this, arguments); + this.model = + (action.params.res_model !== undefined && + action.params.res_model) || + action.context.params.res_model; + this.res_id = + (action.params.res_id !== undefined && action.params.res_id) || + action.context.params.id; + }, + async start() { + await this._super(...arguments); + this.component = new ComponentWrapper(this, SignOcaPdf, { + model: this.model, + res_id: this.res_id, + updateControlPanel: this.updateControlPanel.bind(this), + trigger: this.trigger_up.bind(this), + }); + return this.component.mount(this.$(".o_content")[0]); + }, + getState: function () { + var result = this._super(...arguments); + result = _.extend({}, result, { + res_model: this.model, + res_id: this.res_id, + }); + return result; + }, + }); + core.action_registry.add("sign_oca", SignOcaPdfAction); + + return SignOcaPdfAction; + } +); diff --git a/sign_oca/static/src/components/sign_oca_pdf_common/sign_oca_pdf_common.js b/sign_oca/static/src/components/sign_oca_pdf_common/sign_oca_pdf_common.js new file mode 100644 index 00000000..43a5b614 --- /dev/null +++ b/sign_oca/static/src/components/sign_oca_pdf_common/sign_oca_pdf_common.js @@ -0,0 +1,148 @@ +odoo.define( + "sign_oca/static/src/components/sign_oca_pdf_common/sign_oca_pdf_common.js", + function (require) { + "use strict"; + const {Component} = owl; + const {onMounted, onWillStart, onWillUnmount, useRef} = owl.hooks; + const Dialog = require("web.Dialog"); + const core = require("web.core"); + const _t = core._t; + class SignOcaPdfCommon extends Component { + constructor() { + super(...arguments); + this.field_template = "sign_oca.sign_iframe_field"; + this.pdf_url = this.getPdfUrl(); + this.viewer_url = + "/web/static/lib/pdfjs/web/viewer.html?file=" + this.pdf_url; + this.iframe = useRef("sign_oca_iframe"); + var iframeResolve = undefined; + var iframeReject = undefined; + this.iframeLoaded = new Promise(function (resolve, reject) { + iframeResolve = resolve; + iframeReject = reject; + }); + this.items = {}; + onWillUnmount(() => { + clearTimeout(this.reviewFieldsTimeout); + }); + this.iframeLoaded.resolve = iframeResolve; + this.iframeLoaded.reject = iframeReject; + onWillStart(this.willStart.bind(this)); + onMounted(() => { + this.waitIframeLoaded(); + }); + } + getPdfUrl() { + return ( + "/web/content/" + + this.props.model + + "/" + + this.props.res_id + + "/data" + ); + } + async willStart() { + this.info = await this.env.services.rpc({ + model: this.props.model, + method: "get_info", + args: [[this.props.res_id]], + }); + } + waitIframeLoaded() { + var error = this.iframe.el.contentDocument.getElementById( + "errorWrapper" + ); + if (error && window.getComputedStyle(error).display !== "none") { + this.iframeLoaded.resolve(); + return Dialog.alert( + this, + _t("Need a valid PDF to add signature fields !") + ); + } + var nbPages = this.iframe.el.contentDocument.getElementsByClassName( + "page" + ).length; + var nbLayers = this.iframe.el.contentDocument.getElementsByClassName( + "endOfContent" + ).length; + if (nbPages > 0 && nbLayers > 0) { + this.postIframeFields(); + this.reviewFields(); + } else { + var self = this; + setTimeout(function () { + self.waitIframeLoaded(); + }, 50); + } + } + reviewFields() { + if ( + this.iframe.el.contentDocument.getElementsByClassName( + "o_sign_oca_ready" + ).length === 0 + ) { + this.postIframeFields(); + } + this.reviewFieldsTimeout = setTimeout( + this.reviewFields.bind(this), + 1000 + ); + } + postIframeFields() { + this.iframe.el.contentDocument + .getElementById("viewerContainer") + .addEventListener( + "drop", + (e) => { + e.stopImmediatePropagation(); + e.stopPropagation(); + }, + 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); + _.each(this.info.items, (item) => { + this.postIframeField(item); + }); + $( + this.iframe.el.contentDocument.getElementsByClassName("page")[0] + ).append($("
")); + + $(this.iframe.el.contentDocument.getElementById("viewer")).addClass( + "sign_oca_ready" + ); + this.iframeLoaded.resolve(); + } + postIframeField(item) { + if (this.items[item.id]) { + this.items[item.id].remove(); + } + var page = this.iframe.el.contentDocument.getElementsByClassName( + "page" + )[item.page - 1]; + var signatureItem = $( + core.qweb.render(this.field_template, { + ...item, + }) + ); + page.append(signatureItem[0]); + this.items[item.id] = signatureItem[0]; + return signatureItem; + } + } + SignOcaPdfCommon.template = "sign_oca.SignOcaPdfCommon"; + + return SignOcaPdfCommon; + } +); diff --git a/sign_oca/static/src/components/sign_oca_pdf_common/sign_oca_pdf_common.xml b/sign_oca/static/src/components/sign_oca_pdf_common/sign_oca_pdf_common.xml new file mode 100644 index 00000000..2a313a14 --- /dev/null +++ b/sign_oca/static/src/components/sign_oca_pdf_common/sign_oca_pdf_common.xml @@ -0,0 +1,20 @@ + + + +
+