diff --git a/agreement_sign_oca/README.rst b/agreement_sign_oca/README.rst new file mode 100644 index 00000000..4ba13fce --- /dev/null +++ b/agreement_sign_oca/README.rst @@ -0,0 +1,111 @@ +================== +Agreement Sign Oca +================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:42b4bf1e90c19330c7df5578c2ff9840a447d24944ea3ec12efe52febe2a310d + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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/18.0/agreement_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-18-0/sign-18-0-agreement_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=18.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +Create signature request from any agreement. When full signed agreement +will be moved automatically to defined stage attaching a copy of the +signed document. Also a smart-button will be displayed on the +agreement's form view showing the linked Sign Requests. + +**Table of contents** + +.. contents:: + :local: + +Configuration +============= + +1. Go to company settings and set "Signed Agreements Stage" + +Usage +===== + +Sign Request Creation Process + +1. Navigate to an existing Agreement. +2. Click the "Request Signature" button to generate a new sign request. +3. From the new Sign Request, click the "Configure Document" smart + button to set the position for signatures and other dynamic fields. +4. Once configured, return to the request and click the "Send" button. +5. After the document is fully signed, the related Agreement will be + automatically moved to the stage defined in the company settings. + +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 +------- + +* APSL Nagarro + +Contributors +------------ + +- `APSL Nagarro `__ + + - Miquel Alzanillas + +- `Heliconia Solutions Pvt. Ltd. `__ + + - Bhavesh Heliconia + +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-miquelalzanillas| image:: https://github.com/miquelalzanillas.png?size=40px + :target: https://github.com/miquelalzanillas + :alt: miquelalzanillas + +Current `maintainer `__: + +|maintainer-miquelalzanillas| + +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/agreement_sign_oca/__init__.py b/agreement_sign_oca/__init__.py new file mode 100644 index 00000000..0650744f --- /dev/null +++ b/agreement_sign_oca/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/agreement_sign_oca/__manifest__.py b/agreement_sign_oca/__manifest__.py new file mode 100644 index 00000000..5fbbddd9 --- /dev/null +++ b/agreement_sign_oca/__manifest__.py @@ -0,0 +1,22 @@ +# Copyright 2025 APSL-Nagarro - Miquel Alzanillas +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +{ + "name": "Agreement Sign Oca", + "version": "18.0.1.0.0", + "category": "Agreement", + "website": "https://github.com/OCA/sign", + "author": "APSL Nagarro, Odoo Community Association (OCA)", + "license": "AGPL-3", + "depends": ["sign_oca", "agreement_legal"], + "data": [ + "views/agreement_views.xml", + "views/res_config_settings_view.xml", + "views/sign_oca_request_views.xml", + "data/data.xml", + ], + "demo": [ + "demo/sign_oca_template.xml", + ], + "installable": True, + "maintainers": ["miquelalzanillas"], +} diff --git a/agreement_sign_oca/data/data.xml b/agreement_sign_oca/data/data.xml new file mode 100644 index 00000000..1840b7b0 --- /dev/null +++ b/agreement_sign_oca/data/data.xml @@ -0,0 +1,10 @@ + + + + Agreement Company Signatory Person + expression + {{object.company_signed_user_id.partner_id.id}} + + diff --git a/agreement_sign_oca/demo/sign_oca_template.xml b/agreement_sign_oca/demo/sign_oca_template.xml new file mode 100644 index 00000000..8652e7cd --- /dev/null +++ b/agreement_sign_oca/demo/sign_oca_template.xml @@ -0,0 +1,21 @@ + + + Agreement + + + + + + + + 1 + 10 + 10 + 10 + 10 + + + diff --git a/agreement_sign_oca/i18n/agreement_sign_oca.pot b/agreement_sign_oca/i18n/agreement_sign_oca.pot new file mode 100644 index 00000000..8fc3fb6e --- /dev/null +++ b/agreement_sign_oca/i18n/agreement_sign_oca.pot @@ -0,0 +1,142 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * agreement_sign_oca +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 17.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: agreement_sign_oca +#: model:ir.model,name:agreement_sign_oca.model_agreement +#: model:ir.model.fields,field_description:agreement_sign_oca.field_sign_oca_request__agreement_id +#: model_terms:ir.ui.view,arch_db:agreement_sign_oca.sign_oca_request_search_view +msgid "Agreement" +msgstr "" + +#. module: agreement_sign_oca +#: model:ir.model,name:agreement_sign_oca.model_res_company +msgid "Companies" +msgstr "" + +#. module: agreement_sign_oca +#: model:ir.model,name:agreement_sign_oca.model_res_config_settings +msgid "Config Settings" +msgstr "" + +#. module: agreement_sign_oca +#: model_terms:ir.ui.view,arch_db:agreement_sign_oca.agreement_sign_view_form +msgid "Create adn sent signature request for this agreement." +msgstr "" + +#. module: agreement_sign_oca +#: model:ir.model.fields,field_description:agreement_sign_oca.field_res_company__agreement_sign_oca_template_id +#: model:ir.model.fields,field_description:agreement_sign_oca.field_res_config_settings__agreement_sign_oca_template_id +msgid "Default Agreement Sign Template" +msgstr "" + +#. module: agreement_sign_oca +#. odoo-python +#: code:addons/agreement_sign_oca/models/agreement.py:0 +#, python-format +msgid "Please create a user for ther company signatory person" +msgstr "" + +#. module: agreement_sign_oca +#. odoo-python +#: code:addons/agreement_sign_oca/models/agreement.py:0 +#, python-format +msgid "" +"Please set a Company Primary Contact in order to set\n" +" the signatory person of the company in this document" +msgstr "" + +#. module: agreement_sign_oca +#. odoo-python +#: code:addons/agreement_sign_oca/models/agreement.py:0 +#, python-format +msgid "" +"Please set a Primary Contact in order to set the\n" +" signatory person of the counterpart in this document" +msgstr "" + +#. module: agreement_sign_oca +#: model_terms:ir.ui.view,arch_db:agreement_sign_oca.agreement_sign_view_form +msgid "Request Signature" +msgstr "" + +#. module: agreement_sign_oca +#: model_terms:ir.ui.view,arch_db:agreement_sign_oca.res_config_settings_view_form +msgid "Sign Oca" +msgstr "" + +#. module: agreement_sign_oca +#: model:ir.model,name:agreement_sign_oca.model_sign_oca_request +msgid "Sign Request" +msgstr "" + +#. module: agreement_sign_oca +#: model:ir.model.fields,field_description:agreement_sign_oca.field_agreement__sign_request_ids +msgid "Sign Requests" +msgstr "" + +#. module: agreement_sign_oca +#: model:ir.model.fields,field_description:agreement_sign_oca.field_agreement__sign_request_count +msgid "Sign request count" +msgstr "" + +#. module: agreement_sign_oca +#: model_terms:ir.ui.view,arch_db:agreement_sign_oca.agreement_sign_view_form +msgid "Signature Requests" +msgstr "" + +#. module: agreement_sign_oca +#: model:ir.model.fields,field_description:agreement_sign_oca.field_res_company__agreement_sign_oca_signed_stage_id +#: model:ir.model.fields,field_description:agreement_sign_oca.field_res_config_settings__agreement_sign_oca_signed_stage_id +msgid "Signed Agreements Stage" +msgstr "" + +#. module: agreement_sign_oca +#: model:ir.model.fields,field_description:agreement_sign_oca.field_agreement__signed_contract +msgid "Signed Document" +msgstr "" + +#. module: agreement_sign_oca +#. odoo-python +#: code:addons/agreement_sign_oca/models/agreement.py:0 +#, python-format +msgid "" +"The 'Agreement Company Signatory Person' role for the signature\n" +" was not found. Please update 'agreement_sign_oca' module." +msgstr "" + +#. module: agreement_sign_oca +#. odoo-python +#: code:addons/agreement_sign_oca/models/agreement.py:0 +#, python-format +msgid "" +"The 'Customer' role for the signature\n" +" was not found. Please update 'agreement' module." +msgstr "" + +#. module: agreement_sign_oca +#. odoo-python +#: code:addons/agreement_sign_oca/models/agreement.py:0 +#, python-format +msgid "The agreement must have an assigned contact (counterparty)." +msgstr "" + +#. module: agreement_sign_oca +#. odoo-python +#: code:addons/agreement_sign_oca/models/agreement.py:0 +#, python-format +msgid "" +"The agreement's counterparty contact\n" +" does not have an email configured." +msgstr "" diff --git a/agreement_sign_oca/i18n/es.po b/agreement_sign_oca/i18n/es.po new file mode 100644 index 00000000..439f6878 --- /dev/null +++ b/agreement_sign_oca/i18n/es.po @@ -0,0 +1,85 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * agreement_sign_oca +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 17.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2025-07-25 11:03+0000\n" +"PO-Revision-Date: 2025-07-25 11:03+0000\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: agreement_sign_oca +#: model:ir.model,name:agreement_sign_oca.model_agreement +#: model:ir.model.fields,field_description:agreement_sign_oca.field_sign_oca_request__agreement_id +#: model_terms:ir.ui.view,arch_db:agreement_sign_oca.sign_oca_request_search_view +msgid "Agreement" +msgstr "Acuerdo" + +#. module: agreement_sign_oca +#: model:ir.model,name:agreement_sign_oca.model_res_company +msgid "Companies" +msgstr "Compañías" + +#. module: agreement_sign_oca +#: model:ir.model,name:agreement_sign_oca.model_res_config_settings +msgid "Config Settings" +msgstr "Ajustes de configuración" + +#. module: agreement_sign_oca +#: model_terms:ir.ui.view,arch_db:agreement_sign_oca.agreement_sign_view_form +msgid "Create adn sent signature request for this agreement." +msgstr "" + +#. module: agreement_sign_oca +#: model:ir.model.fields,field_description:agreement_sign_oca.field_res_company__agreement_sign_oca_template_id +#: model:ir.model.fields,field_description:agreement_sign_oca.field_res_config_settings__agreement_sign_oca_template_id +msgid "Default Agreement Sign Template" +msgstr "" + +#. module: agreement_sign_oca +#: model_terms:ir.ui.view,arch_db:agreement_sign_oca.agreement_sign_view_form +msgid "Request Signature" +msgstr "Solicitud de firma" + +#. module: agreement_sign_oca +#: model_terms:ir.ui.view,arch_db:agreement_sign_oca.res_config_settings_view_form +msgid "Sign Oca" +msgstr "" + +#. module: agreement_sign_oca +#: model:ir.model,name:agreement_sign_oca.model_sign_oca_request +msgid "Sign Request" +msgstr "Solicitud de firma" + +#. module: agreement_sign_oca +#: model:ir.model.fields,field_description:agreement_sign_oca.field_agreement__sign_request_ids +msgid "Sign Requests" +msgstr "Solicitudes de Firma" + +#. module: agreement_sign_oca +#: model:ir.model.fields,field_description:agreement_sign_oca.field_agreement__sign_request_count +msgid "Sign request count" +msgstr "Solicitudes de Firmas" + +#. module: agreement_sign_oca +#: model_terms:ir.ui.view,arch_db:agreement_sign_oca.agreement_sign_view_form +msgid "Signature Requests" +msgstr "" + +#. module: agreement_sign_oca +#: model:ir.model.fields,field_description:agreement_sign_oca.field_res_company__agreement_sign_oca_signed_stage_id +#: model:ir.model.fields,field_description:agreement_sign_oca.field_res_config_settings__agreement_sign_oca_signed_stage_id +msgid "Signed Agreements Stage" +msgstr "Etapa para acuerdos firmados" + +#. module: agreement_sign_oca +#: model:ir.model.fields,field_description:agreement_sign_oca.field_agreement__signed_contract +msgid "Signed Document" +msgstr "Documento firmado" \ No newline at end of file diff --git a/agreement_sign_oca/i18n/it.po b/agreement_sign_oca/i18n/it.po new file mode 100644 index 00000000..1ef290bd --- /dev/null +++ b/agreement_sign_oca/i18n/it.po @@ -0,0 +1,143 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * agreement_sign_oca +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 17.0\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: it\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" + +#. module: agreement_sign_oca +#: model:ir.model,name:agreement_sign_oca.model_agreement +#: model:ir.model.fields,field_description:agreement_sign_oca.field_sign_oca_request__agreement_id +#: model_terms:ir.ui.view,arch_db:agreement_sign_oca.sign_oca_request_search_view +msgid "Agreement" +msgstr "" + +#. module: agreement_sign_oca +#: model:ir.model,name:agreement_sign_oca.model_res_company +msgid "Companies" +msgstr "" + +#. module: agreement_sign_oca +#: model:ir.model,name:agreement_sign_oca.model_res_config_settings +msgid "Config Settings" +msgstr "" + +#. module: agreement_sign_oca +#: model_terms:ir.ui.view,arch_db:agreement_sign_oca.agreement_sign_view_form +msgid "Create adn sent signature request for this agreement." +msgstr "" + +#. module: agreement_sign_oca +#: model:ir.model.fields,field_description:agreement_sign_oca.field_res_company__agreement_sign_oca_template_id +#: model:ir.model.fields,field_description:agreement_sign_oca.field_res_config_settings__agreement_sign_oca_template_id +msgid "Default Agreement Sign Template" +msgstr "" + +#. module: agreement_sign_oca +#. odoo-python +#: code:addons/agreement_sign_oca/models/agreement.py:0 +#, python-format +msgid "Please create a user for ther company signatory person" +msgstr "" + +#. module: agreement_sign_oca +#. odoo-python +#: code:addons/agreement_sign_oca/models/agreement.py:0 +#, python-format +msgid "" +"Please set a Company Primary Contact in order to set\n" +" the signatory person of the company in this document" +msgstr "" + +#. module: agreement_sign_oca +#. odoo-python +#: code:addons/agreement_sign_oca/models/agreement.py:0 +#, python-format +msgid "" +"Please set a Primary Contact in order to set the\n" +" signatory person of the counterpart in this document" +msgstr "" + +#. module: agreement_sign_oca +#: model_terms:ir.ui.view,arch_db:agreement_sign_oca.agreement_sign_view_form +msgid "Request Signature" +msgstr "" + +#. module: agreement_sign_oca +#: model_terms:ir.ui.view,arch_db:agreement_sign_oca.res_config_settings_view_form +msgid "Sign Oca" +msgstr "" + +#. module: agreement_sign_oca +#: model:ir.model,name:agreement_sign_oca.model_sign_oca_request +msgid "Sign Request" +msgstr "" + +#. module: agreement_sign_oca +#: model:ir.model.fields,field_description:agreement_sign_oca.field_agreement__sign_request_ids +msgid "Sign Requests" +msgstr "" + +#. module: agreement_sign_oca +#: model:ir.model.fields,field_description:agreement_sign_oca.field_agreement__sign_request_count +msgid "Sign request count" +msgstr "" + +#. module: agreement_sign_oca +#: model_terms:ir.ui.view,arch_db:agreement_sign_oca.agreement_sign_view_form +msgid "Signature Requests" +msgstr "" + +#. module: agreement_sign_oca +#: model:ir.model.fields,field_description:agreement_sign_oca.field_res_company__agreement_sign_oca_signed_stage_id +#: model:ir.model.fields,field_description:agreement_sign_oca.field_res_config_settings__agreement_sign_oca_signed_stage_id +msgid "Signed Agreements Stage" +msgstr "" + +#. module: agreement_sign_oca +#: model:ir.model.fields,field_description:agreement_sign_oca.field_agreement__signed_contract +msgid "Signed Document" +msgstr "" + +#. module: agreement_sign_oca +#. odoo-python +#: code:addons/agreement_sign_oca/models/agreement.py:0 +#, python-format +msgid "" +"The 'Agreement Company Signatory Person' role for the signature\n" +" was not found. Please update 'agreement_sign_oca' module." +msgstr "" + +#. module: agreement_sign_oca +#. odoo-python +#: code:addons/agreement_sign_oca/models/agreement.py:0 +#, python-format +msgid "" +"The 'Customer' role for the signature\n" +" was not found. Please update 'agreement' module." +msgstr "" + +#. module: agreement_sign_oca +#. odoo-python +#: code:addons/agreement_sign_oca/models/agreement.py:0 +#, python-format +msgid "The agreement must have an assigned contact (counterparty)." +msgstr "" + +#. module: agreement_sign_oca +#. odoo-python +#: code:addons/agreement_sign_oca/models/agreement.py:0 +#, python-format +msgid "" +"The agreement's counterparty contact\n" +" does not have an email configured." +msgstr "" diff --git a/agreement_sign_oca/models/__init__.py b/agreement_sign_oca/models/__init__.py new file mode 100644 index 00000000..1c1067e3 --- /dev/null +++ b/agreement_sign_oca/models/__init__.py @@ -0,0 +1,4 @@ +from . import agreement +from . import res_company +from . import res_config_settings +from . import sign_oca_request diff --git a/agreement_sign_oca/models/agreement.py b/agreement_sign_oca/models/agreement.py new file mode 100644 index 00000000..a5409bd8 --- /dev/null +++ b/agreement_sign_oca/models/agreement.py @@ -0,0 +1,154 @@ +# Copyright 2023-2024 Tecnativa - Víctor Martínez +# Copyright 2025 - APSL-Nagarro - Miquel Alzanillas +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). +import base64 + +from odoo import Command, api, fields, models +from odoo.exceptions import ValidationError + + +class Agreement(models.Model): + _inherit = "agreement" + + # Disable log to avoid 'Not implemented...' errors on ORM write operations + signed_contract = fields.Binary(string="Signed Document", tracking=False) + # This field is stored as a help to filter by. + sign_request_ids = fields.One2many( + comodel_name="sign.oca.request", + inverse_name="agreement_id", + string="Sign Requests", + ) + sign_request_count = fields.Integer( + string="Sign request count", + compute="_compute_sign_request_count", + compute_sudo=True, + store=True, + ) + + @api.depends("sign_request_ids") + def _compute_sign_request_count(self): + request_data = self.env["sign.oca.request"].read_group( + [("agreement_id", "in", self.ids)], + ["agreement_id"], + ["agreement_id"], + ) + mapped_data = { + x["agreement_id"][0]: x["agreement_id_count"] for x in request_data + } + for item in self: + item.sign_request_count = mapped_data.get(item.id, 0) + + def action_send_for_signature(self): + self.ensure_one() + signers_list = [] + if not self.partner_contact_id: + raise ValidationError( + self.env._( + "The agreement must have an assigned contact (counterparty)." + ) + ) + if not self.partner_contact_id.email: + raise ValidationError( + self.env._( + "The agreement's counterparty contact " + "does not have an email configured." + ) + ) + report = self.env.ref("agreement_legal.partner_agreement_contract_document") + pdf_document, content_type = self.env["ir.actions.report"]._render_qweb_pdf( + report.report_name, self.ids + ) + customer_role = self.env.ref( + "sign_oca.sign_role_customer", raise_if_not_found=False + ) + if not customer_role: + raise ValidationError( + self.env._( + "The 'Customer' role for the signature " + "was not found. Please update 'agreement' module." + ) + ) + company_signer_role = self.env.ref( + "agreement_sign_oca.role_agreement_signer", raise_if_not_found=False + ) + if not company_signer_role: + raise ValidationError( + self.env._( + "The 'Agreement Company Signatory Person' role for the signature " + "was not found. Please update 'agreement_sign_oca' module." + ) + ) + if self.partner_contact_id: + signers_list.append( + Command.create( + { + "role_id": customer_role.id, + "partner_id": self.partner_contact_id.id, + }, + ) + ) + else: + raise ValidationError( + self.env._( + "Please set a Primary Contact in order to set the " + "signatory person of the counterpart in this document" + ) + ) + + if not self.company_contact_id: + raise ValidationError( + self.env._( + "Please set a Company Primary Contact in order to set " + "the signatory person of the company in this document" + ) + ) + if not self.company_contact_id.user_ids: + raise ValidationError( + self.env._("Please create a user for their company signatory person") + ) + else: + signers_list.append( + Command.create( + { + "role_id": company_signer_role.id, + "partner_id": self.company_contact_id.id, + }, + ) + ) + + sign_request_vals = { + "name": self.name, + "user_id": self.env.user.id, + "data": base64.b64encode(pdf_document), + "record_ref": f"agreement,{self.id}", + "signer_ids": signers_list, + } + sign_request = self.env["sign.oca.request"].create(sign_request_vals) + action = self.env["ir.actions.act_window"]._for_xml_id( + "sign_oca.sign_oca_request_act_window" + ) + action.update( + { + "views": [ + [self.env.ref("sign_oca.sign_oca_request_form_view").id, "form"] + ], + "res_id": sign_request.id, + } + ) + return action + + def action_view_sign_requests(self): + self.ensure_one() + result = self.env["ir.actions.act_window"]._for_xml_id( + "sign_oca.sign_oca_request_act_window" + ) + result["domain"] = [("id", "in", self.sign_request_ids.ids)] + ctx = dict(self.env.context) + ctx.update( + { + "default_agreement_id": self.id, + "search_default_agreement_id": self.id, + } + ) + result["context"] = ctx + return result diff --git a/agreement_sign_oca/models/res_company.py b/agreement_sign_oca/models/res_company.py new file mode 100644 index 00000000..63e5832b --- /dev/null +++ b/agreement_sign_oca/models/res_company.py @@ -0,0 +1,27 @@ +# Copyright 2023 Tecnativa - Víctor Martínez +# Copyright 2025 - APSL-Nagarro - Miquel Alzanillas +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class ResCompany(models.Model): + _inherit = "res.company" + + def _default_signed_stage(self): + default_active_stage = self.env.ref( + "agreement_legal.agreement_stage_active", raise_if_not_found=False + ) + return default_active_stage + + agreement_sign_oca_template_id = fields.Many2one( + comodel_name="sign.oca.template", + domain="[('model_id.model', '=', 'agreement')]", + string="Default Agreement Sign Template", + ) + agreement_sign_oca_signed_stage_id = fields.Many2one( + comodel_name="agreement.stage", + default=_default_signed_stage, + string="Signed Agreements Stage", + required=True, + ) diff --git a/agreement_sign_oca/models/res_config_settings.py b/agreement_sign_oca/models/res_config_settings.py new file mode 100644 index 00000000..803760a9 --- /dev/null +++ b/agreement_sign_oca/models/res_config_settings.py @@ -0,0 +1,23 @@ +# Copyright 2023 Tecnativa - Víctor Martínez +# Copyright 2025 - APSL-Nagarro - Miquel Alzanillas +# License AGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +from odoo import fields, models + + +class ResConfigSettings(models.TransientModel): + _inherit = "res.config.settings" + + agreement_sign_oca_template_id = fields.Many2one( + comodel_name="sign.oca.template", + related="company_id.agreement_sign_oca_template_id", + string="Default Agreement Sign Template", + readonly=False, + ) + + agreement_sign_oca_signed_stage_id = fields.Many2one( + comodel_name="agreement.stage", + related="company_id.agreement_sign_oca_signed_stage_id", + string="Signed Agreements Stage", + readonly=False, + ) diff --git a/agreement_sign_oca/models/sign_oca_request.py b/agreement_sign_oca/models/sign_oca_request.py new file mode 100644 index 00000000..326fa8b7 --- /dev/null +++ b/agreement_sign_oca/models/sign_oca_request.py @@ -0,0 +1,64 @@ +# Copyright 2023 Tecnativa - Víctor Martínez +# Copyright 2025 - APSL-Nagarro - Miquel Alzanillas +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). +from odoo import api, fields, models + + +class SignOcaRequest(models.Model): + _inherit = "sign.oca.request" + + # This field is required for the inverse of maintenance.equipment. + agreement_id = fields.Many2one( + comodel_name="agreement", + compute="_compute_agreement_id", + string="Agreement", + readonly=True, + store=True, + ) + + @api.depends("record_ref") + def _compute_agreement_id(self): + for item in self.filtered( + lambda x: x.record_ref and x.record_ref._name == "agreement" + ): + item.agreement_id = item.record_ref.id + + def action_send_signed_request(self): + res = super().action_send_signed_request() + customer_role = self.env.ref( + "sign_oca.sign_role_customer", raise_if_not_found=False + ) + company_signer_role = self.env.ref( + "agreement_sign_oca.role_agreement_signer", raise_if_not_found=False + ) + + for request in self: + if request.state == "2_signed" and request.agreement_id and request.data: + signed_stage_id = ( + request.env.company.agreement_sign_oca_signed_stage_id.id + ) + signed_partner_on = False + signer_partner_contact_id = False + signed_company_on = False + signer_company_contact_id = False + for signer in request.signer_ids: + if signer.role_id == customer_role: + signer_partner_contact_id = signer.partner_id.id + signed_partner_on = signer.signed_on + elif signer.role_id == company_signer_role: + if signer.partner_id.user_ids: + signer_company_contact_id = signer.partner_id.user_ids[0].id + signed_company_on = signer.signed_on + vals = { + "partner_signed_date": signed_partner_on, + "partner_signed_user_id": signer_partner_contact_id, + "company_signed_date": signed_company_on, + "company_signed_user_id": signer_company_contact_id, + "stage_id": signed_stage_id, + "signed_contract": request.data, + "signed_contract_filename": request.name, + } + vals = {k: v for k, v in vals.items() if v} + if vals: + request.agreement_id.sudo().write(vals) + return res diff --git a/agreement_sign_oca/oca_dependencies.txt b/agreement_sign_oca/oca_dependencies.txt new file mode 100644 index 00000000..76b6d5a0 --- /dev/null +++ b/agreement_sign_oca/oca_dependencies.txt @@ -0,0 +1 @@ +maintenance diff --git a/agreement_sign_oca/pyproject.toml b/agreement_sign_oca/pyproject.toml new file mode 100644 index 00000000..4231d0cc --- /dev/null +++ b/agreement_sign_oca/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/agreement_sign_oca/readme/CONFIGURE.md b/agreement_sign_oca/readme/CONFIGURE.md new file mode 100644 index 00000000..3298e45a --- /dev/null +++ b/agreement_sign_oca/readme/CONFIGURE.md @@ -0,0 +1 @@ +1. Go to company settings and set "Signed Agreements Stage" diff --git a/agreement_sign_oca/readme/CONTRIBUTORS.md b/agreement_sign_oca/readme/CONTRIBUTORS.md new file mode 100644 index 00000000..1a681208 --- /dev/null +++ b/agreement_sign_oca/readme/CONTRIBUTORS.md @@ -0,0 +1,4 @@ +- [APSL Nagarro](https://nagarro.com) + - Miquel Alzanillas +- [Heliconia Solutions Pvt. Ltd.](https://www.heliconia.io) + - Bhavesh Heliconia diff --git a/agreement_sign_oca/readme/DESCRIPTION.md b/agreement_sign_oca/readme/DESCRIPTION.md new file mode 100644 index 00000000..a1df0efb --- /dev/null +++ b/agreement_sign_oca/readme/DESCRIPTION.md @@ -0,0 +1,4 @@ +Create signature request from any agreement. +When full signed agreement will be moved automatically to defined stage attaching a copy of the signed document. +Also a smart-button will be displayed on the agreement's form view showing the +linked Sign Requests. diff --git a/agreement_sign_oca/readme/USAGE.md b/agreement_sign_oca/readme/USAGE.md new file mode 100644 index 00000000..420d5681 --- /dev/null +++ b/agreement_sign_oca/readme/USAGE.md @@ -0,0 +1,6 @@ +Sign Request Creation Process +1. Navigate to an existing Agreement. +2. Click the "Request Signature" button to generate a new sign request. +3. From the new Sign Request, click the "Configure Document" smart button to set the position for signatures and other dynamic fields. +4. Once configured, return to the request and click the "Send" button. +5. After the document is fully signed, the related Agreement will be automatically moved to the stage defined in the company settings. diff --git a/agreement_sign_oca/static/description/icon.png b/agreement_sign_oca/static/description/icon.png new file mode 100644 index 00000000..3a0328b5 Binary files /dev/null and b/agreement_sign_oca/static/description/icon.png differ diff --git a/agreement_sign_oca/static/description/index.html b/agreement_sign_oca/static/description/index.html new file mode 100644 index 00000000..5e1479d3 --- /dev/null +++ b/agreement_sign_oca/static/description/index.html @@ -0,0 +1,456 @@ + + + + + +Agreement Sign Oca + + + +
+

Agreement Sign Oca

+ + +

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

+

Create signature request from any agreement. When full signed agreement +will be moved automatically to defined stage attaching a copy of the +signed document. Also a smart-button will be displayed on the +agreement’s form view showing the linked Sign Requests.

+

Table of contents

+ +
+

Configuration

+
    +
  1. Go to company settings and set “Signed Agreements Stage”
  2. +
+
+
+

Usage

+

Sign Request Creation Process

+
    +
  1. Navigate to an existing Agreement.
  2. +
  3. Click the “Request Signature” button to generate a new sign request.
  4. +
  5. From the new Sign Request, click the “Configure Document” smart +button to set the position for signatures and other dynamic fields.
  6. +
  7. Once configured, return to the request and click the “Send” button.
  8. +
  9. After the document is fully signed, the related Agreement will be +automatically moved to the stage defined in the company settings.
  10. +
+
+
+

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

+
    +
  • APSL Nagarro
  • +
+
+
+

Contributors

+ +
+
+

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:

+

miquelalzanillas

+

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/agreement_sign_oca/tests/__init__.py b/agreement_sign_oca/tests/__init__.py new file mode 100644 index 00000000..4c12411f --- /dev/null +++ b/agreement_sign_oca/tests/__init__.py @@ -0,0 +1,4 @@ +# Copyright 2023 Tecnativa - Víctor Martínez +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from . import test_agreement_sign_oca diff --git a/agreement_sign_oca/tests/test_agreement_sign_oca.py b/agreement_sign_oca/tests/test_agreement_sign_oca.py new file mode 100644 index 00000000..42abd9fa --- /dev/null +++ b/agreement_sign_oca/tests/test_agreement_sign_oca.py @@ -0,0 +1,315 @@ +# Copyright 2023-2024 Tecnativa - Víctor Martínez +# Copyright 2025 - APSL-Nagarro - Miquel Alzanillas +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +import base64 + +from odoo import Command +from odoo.exceptions import UserError, ValidationError +from odoo.tests.common import new_test_user + +from odoo.addons.base.tests.common import BaseCommon + + +class TestAgreementSignOca(BaseCommon): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.company = cls.env.company + cls.template = cls.env.ref( + "agreement_sign_oca.sign_oca_template_agreement_legal_demo" + ) + cls.model_agreement = cls.env.ref("agreement.model_agreement") + cls.user_a = new_test_user( + cls.env, + login="test-user-a", + groups="{},{},{}".format( + "agreement_legal.group_agreement_manager", + "sign_oca.sign_oca_group_user", + "base.group_partner_manager", + ), + ) + # Create a partner for the test agreement + cls.partner_a = cls.env["res.partner"].create( + {"name": "Test Partner A", "email": "partner.a@test.com"} + ) + cls.subpartner_a = cls.env["res.partner"].create( + { + "name": "SubPartner A", + "email": "sub.partner.a@test.com", + "parent_id": cls.partner_a.id, + } + ) + cls.company_signatory_person = cls.env["res.partner"].create( + { + "name": "Company Signatory", + "email": "company.signer@test.com", + "parent_id": cls.env.company.partner_id.id, + } + ) + cls.user_company = new_test_user( + cls.env, + login="test-user-company-signatory", + partner_id=cls.company_signatory_person.id, + groups="base.group_no_one", + ) + # Set a default to make it compatible with hr_maintenance + cls.agreement_model = cls.env["agreement"].with_context( + default_agreement_assign_to="other" + ) + cls.agreement_a = cls.agreement_model.with_user(cls.user_a).create( + { + "name": "Test agreement A", + "assigned_user_id": cls.user_a.id, + "partner_id": cls.partner_a.id, # Assign partner + "partner_contact_id": cls.subpartner_a.id, + "company_contact_id": cls.company_signatory_person.id, + } + ) + cls.user_b = new_test_user( + cls.env, + login="test-user-b", + groups="{},{},{}".format( + "agreement_legal.group_agreement_manager", + "sign_oca.sign_oca_group_user", + "base.group_partner_manager", + ), + ) + cls.partner_b = cls.env["res.partner"].create( + {"name": "Test Partner B", "email": "partner.b@test.com"} + ) + cls.subpartner_b = cls.env["res.partner"].create( + { + "name": "SubPartner B", + "email": "sub.partner.b@test.com", + "parent_id": cls.partner_b.id, + } + ) + cls.agreement_b = cls.agreement_model.with_user(cls.user_b).create( + { + "name": "Test agreement B", + "assigned_user_id": cls.user_b.id, + "partner_id": cls.partner_b.id, + "partner_contact_id": cls.subpartner_b.id, + "company_contact_id": cls.company_signatory_person.id, + } + ) + cls.active_stage = cls.env["agreement.stage"].create( + {"name": "Active", "stage_type": "agreement"} + ) + cls.company.agreement_sign_oca_signed_stage_id = cls.active_stage.id + + def test_action_send_for_signature(self): + """Test the action to send an agreement for signature (success case).""" + self.assertEqual(self.agreement_a.sign_request_count, 0) + action = self.agreement_a.action_send_for_signature() + self.sign_request = self.env["sign.oca.request"].browse(action["res_id"]) + self.assertEqual(self.agreement_a.sign_request_count, 1) + self.assertEqual( + self.sign_request.id, self.sign_request.agreement_id.sign_request_ids.id + ) + self.assertEqual(action["views"][0][1], "form") + sign_request = self.agreement_a.sign_request_ids + self.assertTrue(sign_request) + self.assertEqual(sign_request.name, self.agreement_a.name) + self.assertTrue(sign_request.data) + self.assertEqual(len(sign_request.signer_ids), 2) + self.assertIn(self.subpartner_a, sign_request.signer_ids.mapped("partner_id")) + self.assertIn( + self.company_signatory_person, sign_request.signer_ids.mapped("partner_id") + ) + + def test_action_send_for_signature_no_partner(self): + """Test that the action raises an error if the agreement has no partner.""" + agreement_no_partner = self.agreement_model.create( + { + "name": "Test agreement no partner", + } + ) + with self.assertRaises( + UserError, msg="The agreement must have an assigned contact (counterparty)." + ): + agreement_no_partner.action_send_for_signature() + + def test_action_send_for_signature_no_partner_email(self): + """Test that the action raises an error if the partner has no email.""" + self.agreement_a.partner_contact_id.email = False + with self.assertRaises( + UserError, + msg="""The agreement's counterparty contact + does not have an email configured.""", + ): + self.agreement_a.action_send_for_signature() + # Restore email for other tests + self.agreement_a.partner_contact_id.email = "partner.a@test.com" + # Test missing contact + self.agreement_a.partner_contact_id = False + with self.assertRaises(ValidationError): + self.agreement_a.action_send_for_signature() + + def test_action_send_signed_request(self): + """Test that when a request is signed, the agreement is updated.""" + dummy_pdf_content = base64.b64encode(b"This is a signed PDF.") + customer_role = self.env.ref("sign_oca.sign_role_customer") + company_signer_role = self.env.ref("agreement_sign_oca.role_agreement_signer") + sign_request = self.env["sign.oca.request"].create( + { + "data": dummy_pdf_content, + "name": "Signed Agreement A.pdf", + "record_ref": f"agreement,{self.agreement_a.id}", + "state": "0_sent", + "signer_ids": [ + Command.create( + { + "role_id": customer_role.id, + "partner_id": self.subpartner_a.id, + }, + ), + Command.create( + { + "role_id": company_signer_role.id, + "partner_id": self.company_signatory_person.id, + }, + ), + ], + } + ) + # Simulate signing + for signer in sign_request.signer_ids: + signer.signed_on = "2023-01-01 12:00:00" + sign_request.state = "2_signed" + self.assertNotEqual(self.agreement_a.stage_id, self.active_stage) + sign_request.action_send_signed_request() + self.assertEqual(self.agreement_a.stage_id, self.active_stage) + self.assertEqual(self.agreement_a.signed_contract, dummy_pdf_content) + self.assertEqual(self.agreement_a.signed_contract_filename, sign_request.name) + self.assertTrue(self.agreement_a.partner_signed_date) + self.assertEqual(self.agreement_a.partner_signed_user_id, self.subpartner_a) + self.assertTrue(self.agreement_a.company_signed_date) + self.assertEqual(self.agreement_a.company_signed_user_id, self.user_company) + + def test_action_send_signed_request_state_not_signed(self): + """Test that agreement is not updated if request is not signed.""" + sign_request = self.env["sign.oca.request"].create( + { + "data": base64.b64encode(b"PDF content"), + "name": "Test.pdf", + "record_ref": f"agreement,{self.agreement_a.id}", + "state": "0_sent", # Not signed + } + ) + original_stage = self.agreement_a.stage_id + sign_request.action_send_signed_request() + self.assertEqual(self.agreement_a.stage_id, original_stage) + self.assertFalse(self.agreement_a.signed_contract) + + def test_action_send_signed_request_no_agreement(self): + """Test that nothing happens if request has no agreement.""" + sign_request = self.env["sign.oca.request"].create( + { + "data": base64.b64encode(b"PDF content"), + "name": "Test.pdf", + "record_ref": False, # No agreement + "state": "2_signed", + } + ) + # This should not raise an error and not modify any agreement + sign_request.action_send_signed_request() + # Check that agreement_a is untouched + self.assertFalse(self.agreement_a.signed_contract) + + def test_action_send_signed_request_no_data(self): + """Test that agreement is not updated if request has no data.""" + sign_request = self.env["sign.oca.request"].create( + { + "data": False, # No data + "name": "Test.pdf", + "record_ref": f"agreement,{self.agreement_a.id}", + "state": "2_signed", + } + ) + original_stage = self.agreement_a.stage_id + sign_request.action_send_signed_request() + self.assertEqual(self.agreement_a.stage_id, original_stage) + self.assertFalse(self.agreement_a.signed_contract) + + def test_action_send_signed_request_company_signer_no_user(self): + """Test when company signer partner has no user.""" + company_signer_no_user = self.env["res.partner"].create( + { + "name": "Company Signer No User", + "email": "signer.no.user@test.com", + "parent_id": self.env.company.partner_id.id, + } + ) + self.assertFalse(company_signer_no_user.user_ids) + dummy_pdf_content = base64.b64encode(b"This is a signed PDF.") + customer_role = self.env.ref("sign_oca.sign_role_customer") + company_signer_role = self.env.ref("agreement_sign_oca.role_agreement_signer") + sign_request = self.env["sign.oca.request"].create( + { + "data": dummy_pdf_content, + "name": "Signed Agreement A.pdf", + "record_ref": f"agreement,{self.agreement_a.id}", + "state": "0_sent", + "signer_ids": [ + Command.create( + { + "role_id": customer_role.id, + "partner_id": self.subpartner_a.id, + }, + ), + Command.create( + { + "role_id": company_signer_role.id, + "partner_id": company_signer_no_user.id, + }, + ), + ], + } + ) + # Simulate signing + for signer in sign_request.signer_ids: + signer.signed_on = "2023-01-01 12:00:00" + sign_request.state = "2_signed" + sign_request.action_send_signed_request() + self.assertEqual(self.agreement_a.stage_id, self.active_stage) + self.assertEqual(self.agreement_a.signed_contract, dummy_pdf_content) + self.assertTrue(self.agreement_a.partner_signed_date) + self.assertEqual(self.agreement_a.partner_signed_user_id, self.subpartner_a) + self.assertTrue(self.agreement_a.company_signed_date) + # company_signed_user_id should not be set + self.assertFalse(self.agreement_a.company_signed_user_id) + + def test_action_send_signed_request_only_customer_signer(self): + """Test when there is only a customer signer.""" + dummy_pdf_content = base64.b64encode(b"This is a signed PDF.") + customer_role = self.env.ref("sign_oca.sign_role_customer") + sign_request = self.env["sign.oca.request"].create( + { + "data": dummy_pdf_content, + "name": "Signed Agreement B.pdf", + "record_ref": f"agreement,{self.agreement_b.id}", + "state": "0_sent", + "signer_ids": [ + Command.create( + { + "role_id": customer_role.id, + "partner_id": self.subpartner_b.id, + }, + ), + ], + } + ) + # Simulate signing + for signer in sign_request.signer_ids: + signer.signed_on = "2023-01-01 12:00:00" + sign_request.state = "2_signed" + sign_request.action_send_signed_request() + self.assertEqual(self.agreement_b.stage_id, self.active_stage) + self.assertEqual(self.agreement_b.signed_contract, dummy_pdf_content) + self.assertTrue(self.agreement_b.partner_signed_date) + self.assertEqual(self.agreement_b.partner_signed_user_id, self.subpartner_b) + # Company fields should not be set + self.assertFalse(self.agreement_b.company_signed_date) + self.assertFalse(self.agreement_b.company_signed_user_id) diff --git a/agreement_sign_oca/views/agreement_views.xml b/agreement_sign_oca/views/agreement_views.xml new file mode 100644 index 00000000..b7e736f4 --- /dev/null +++ b/agreement_sign_oca/views/agreement_views.xml @@ -0,0 +1,39 @@ + + + + agreement.legal.form + agreement + + + + + + + + + + + diff --git a/agreement_sign_oca/views/res_config_settings_view.xml b/agreement_sign_oca/views/res_config_settings_view.xml new file mode 100644 index 00000000..1b2d5635 --- /dev/null +++ b/agreement_sign_oca/views/res_config_settings_view.xml @@ -0,0 +1,21 @@ + + + + + res.config.settings + + + + + + + + + + + + + + + diff --git a/agreement_sign_oca/views/sign_oca_request_views.xml b/agreement_sign_oca/views/sign_oca_request_views.xml new file mode 100644 index 00000000..070c14f5 --- /dev/null +++ b/agreement_sign_oca/views/sign_oca_request_views.xml @@ -0,0 +1,24 @@ + + + + sign.oca.request.search + sign.oca.request + + + + + + + + + + +