Enable Dark Mode!
how-to-add-extra-fields-to-the-portal-signature-form-in-odoo-19.jpg
By: Sonu S

How to Add Extra Fields to the Portal Signature Form in Odoo 19

Technical Odoo 19 Odoo Enterprises Odoo Community

A Sign & Pay modal requesting the customer's name and signature appears when they accept a quote from the Odoo portal. Occasionally, you need to record a little extra information at the same time, such as the signer's email address and job title, which are included on the confirmed order.

This appears to be a five-minute task: simply add two <input> tags. It isn't. Data from the portal signature form, an OWL component, is sent to a controller via a certain RPC call. When an input is not plugged into that pipeline, it renders flawlessly and, upon submission, silently discards its value. Extra fields must be followed through each layer in order for them to truly stick.

How the signing flow works

Three pieces cooperate:

NameAndSignature (@web/core/signature/name_and_signature) - is a reusable widget that creates the signature pad and stores the "Full Name" input. It reads from and writes to a shared signature object.

SignatureForm (@portal/signature_form/signature_form) - is the portal wrapper that inserts the submit button, embeds NameAndSignature, and delivers { name, signature } to a route via RPC upon click.

The controller - portal_quote_accept in sale/controllers/portal.py handles the route /my/orders/<int:order_id>/accept for a sales order. The sale is marked with signed_by, signed_on, and signature in order.

The crucial realization is that NameAndSignature and SignatureForm have the same signature object; the form transmits its reactive state as a prop to the widget. Therefore, the form may read our additional inputs back at submit time without the need for additional plumbing if they write into that object.

Adding storage fields, adding inputs, capturing their values into the shared object, including them in the RPC, storing them in the controller, and displaying them on the backend creates a clear plan.

Step 1: Add the storage fields

The captured values must be placed adjacent to the normal signing fields.

Python code - sale_order.py:

from odoo import fields, models

class SaleOrder(models.Model):
    _inherit = 'sale.order'
    # Captured on the customer portal at signing time, next to the
    # standard signed_by / signed_on / signature fields.
    signed_email = fields.Char(string="Signed By Email")
    signed_designation = fields.Char(string="Signed By Designation")

Step 2: Add the inputs to the signature widget template

Take over the OWL template online.NameAndSignature and add two inputs after the current group of names. They bind to props and invoke handler methods that we define signature.

Xml code - name_and_signature_extra.xml:

<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
    <!--
        Extend the shared signature widget template to add two inputs.
        NOTE: extension mode is GLOBAL on the frontend - these inputs will
        appear in every NameAndSignature instance rendered on the portal
        (in practice, the Sale Order Sign & Pay modal).
    -->
    <t t-name="sale_portal_sign_extra.NameAndSignatureExtra"
       t-inherit="web.NameAndSignature"
       t-inherit-mode="extension">
        <xpath expr="//div[hasclass('o_web_sign_name_group')]" position="after">
            <div class="o_sps_email_group mt-2">
                <label class="col-form-label"
                       t-att-for="'o_sps_email_input_' + htmlId">Email</label>
                <input type="email" name="signer_email"
                       t-att-id="'o_sps_email_input_' + htmlId"
                       class="o_sps_email_input form-control"
                       t-on-input="onInputSignEmail"
                       t-att-value="props.signature.email"
                       placeholder="Email address"/>
            </div>
            <div class="o_sps_designation_group mt-2">
                <label class="col-form-label"
                       t-att-for="'o_sps_designation_input_' + htmlId">Designation</label>
                <input type="text" name="signer_designation"
                       t-att-id="'o_sps_designation_input_' + htmlId"
                       class="o_sps_designation_input form-control"
                       t-on-input="onInputSignDesignation"
                       t-att-value="props.signature.designation"
                       placeholder="Job title / designation"/>
            </div>
        </xpath>
    </t>
</templates>

Step 3: Capture the values into the shared object

Patch the NameAndSignature component to include onInputSignEmail and onInputSignDesignation, which are referenced in the template. Initialize the keys in setup so that the bindings have something to read on the first render.

JS code - name_and_signature_patch.js:

/** @odoo-module **/
import { patch } from "@web/core/utils/patch";
import { NameAndSignature } from "@web/core/signature/name_and_signature";
patch(NameAndSignature.prototype, {
    setup() {
        super.setup();
        // The `signature` prop is a shared (reactive) object. The parent
        // SignatureForm reads it back at submit time, so storing the extra
        // values here makes them available to the RPC payload.
        const sig = this.props.signature;
        if (sig.email === undefined) {
            sig.email = "";         }
        if (sig.designation === undefined) {
            sig.designation = "";
        }
    },
    onInputSignEmail(ev) {
        this.props.signature.email = ev.target.value;
    },
    onInputSignDesignation(ev) {
        this.props.signature.designation = ev.target.value;
    },
});

These data are now exposed to SignatureForm since props.signature is the same object that the parent form contains. Extra keys pass validation since the signature is typed as a normal object; therefore, there is no need to interact with the static properties of the component.

Step 4: Send the values in the RPC payload

People tend to overlook this stage. Since there isn't a smaller hook, we faithfully overwrite the method and add our two values when the parent form's onClickSubmit generates the payload inline as { name, signature } and initiates the RPC.

JS code - signature_form_patch.js:

/** @odoo-module **/
import { patch } from "@web/core/utils/patch";
import { rpc } from "@web/core/network/rpc";
import { redirect } from "@web/core/utils/urls";
import { addLoadingEffect } from "@web/core/utils/ui";
import { SignatureForm } from "@portal/signature_form/signature_form";
/**
 * Faithful override of the core onClickSubmit: same flow, but the two extra
 * values captured by the patched NameAndSignature widget are added to the
 * payload sent to the accept route. There is no smaller extension seam in
 * the core method, so the whole method is reimplemented.
 */
patch(SignatureForm.prototype, {
    async onClickSubmit() {
        const button = document.querySelector(".o_portal_sign_submit");
        const icon = button.removeChild(button.firstChild);
        const restoreBtnLoading = addLoadingEffect(button);
        const name = this.signature.name;
        const signature = this.signature.getSignatureImage().split(",")[1];
        const signed_email = this.signature.email || "";
        const signed_designation = this.signature.designation || "";
        const data = await rpc(this.props.callUrl, {
            name,
            signature,
            signed_email,
            signed_designation,
        });
        if (data.force_refresh) {
            restoreBtnLoading();
            button.prepend(icon);
            if (data.redirect_url) {
                redirect(data.redirect_url);
            } else {
                window.location.reload();
            }
            // do not resolve if we reload the page
            return new Promise(() => {});
        }
        this.state.error = data.error || false;
        this.state.success = !data.error && {
            message: data.message,
            redirectUrl: data.redirect_url,
            redirectMessage: data.redirect_message,
        };
    },
});

Keep an eye on it during point releases because this reimplements a fundamental method; if Odoo modifies onClickSubmit, synchronize your copy.

Step 5: Persist the values in the controller

Our additional keys are sent as keyword arguments to the accept route. Instead of copying Odoo's full method - which verifies the order, produces the PDF, posts to chatter, and more - call super() to execute all of that without making any changes, and then, assuming signing was successful, write our two fields.

Python code - portal.py:

# -*- coding: utf-8 -*-
from odoo.http import request, route
from odoo.exceptions import AccessError, MissingError
from odoo.addons.sale.controllers.portal import CustomerPortal

class CustomerPortalSignExtra(CustomerPortal):
    @route(
        ['/my/orders/<int:order_id>/accept'],
        type='jsonrpc', auth="public", website=True,
    )
    def portal_quote_accept(
        self, order_id, access_token=None, name=None, signature=None,
        signed_email=None, signed_designation=None, **kw
    ):
        # Run Odoo's standard signing logic (writes signature/signed_by,
        # confirms the order, posts the PDF, etc.).
        res = super().portal_quote_accept(
            order_id,
            access_token=access_token,
            name=name,
            signature=signature,
        )
        # Only persist the extra fields if signing actually succeeded.
        if isinstance(res, dict) and not res.get('error'):
            token = access_token or request.httprequest.args.get('access_token')
            try:
                order_sudo = self._document_check_access(
                    'sale.order', order_id, access_token=token,
                )
            except (AccessError, MissingError):
                return res
            order_sudo.write({
                'signed_email': signed_email or False,
                'signed_designation': signed_designation or False,
            })
            request.env.cr.flush()
        return res

Step 6: Show the fields in the backend

Stored data nobody can see isn't much use. Inherit the sales order form and surface the fields as read-only (they're captured on the portal, not edited internally).

Xml code - sale_order_views.xml:

<?xml version="1.0" encoding="utf-8"?>
<odoo>
    <record id="view_order_form_sign_extra" model="ir.ui.view">
        <field name="name">sale.order.form.sign.extra</field>
        <field name="model">sale.order</field>
        <field name="inherit_id" ref="sale.view_order_form"/>
        <field name="arch" type="xml">
            <!-- xpath extra fields after signed_on signature details field -->
            <field name="signed_on" position="after">
                <field name="signed_email" readonly="1"
                       invisible="not signed_email"/>
                <field name="signed_designation" readonly="1"
                       invisible="not signed_designation"/>
            </field>
        </field>
    </record>
</odoo>

Sign Wizard Without Customisation:

How to Add Extra Fields to the Portal Signature Form in Odoo 19-cybrosys

Sign Wizard With Customisation: Email & Designation Added

How to Add Extra Fields to the Portal Signature Form in Odoo 19-cybrosysHow to Add Extra Fields to the Portal Signature Form in Odoo 19-cybrosys

Sale Order Backend Form View with Additional Signature Details:

How to Add Extra Fields to the Portal Signature Form in Odoo 19-cybrosys

This is larger than it appears because the portal signature form is not a static page but rather a tiny data pipeline. Before a value input in the widget reaches the database, it must be recorded in the shared signature object, sent via the submit RPC, and stored by the controller. Extending it is simple once you realize those interconnected levels; the same structure holds true for whatever additional information you require at the time of signing, such as a phone number, a PO reference, or a terms tick.

To read more about An Overview of Odoo 19 Sign Module, refer to our blog An Overview of Odoo 19 Sign Module.


Frequently Asked Questions

Despite appearing on the form, why do my extra fields not save?

Because the value still needs to pass via the submit RPC and be written by the controller, adding the input just shows it. Any fields that are not wired into the payload that the portal signature form provides to the server are simply ignored upon submission. Not just the template, but all the layers must be extended.

Where do the captured values get stored?

Adjacent to Odoo's integrated signing fields, such as signed_by and signature, on the sale.order record itself. When the customer signs, the controller inserts the submitted data into the order. You can add your own fields to the model, such as email and designation.

Will these additional fields only be on the sales order form, or will they be on all signature forms?

It relies on how the widget is extended. The fields appear on all portal signature forms on the frontend because the straightforward method alters the shared signature widget globally. That's essentially simply the Sign & Pay modal on a regular install, so it's usually okay. However, if you only require them on the sales order flow, you would use a more scoped method.

If you need any assistance in odoo, we are online, please chat with us.



0
Comments



Leave a comment



whatsapp_icon
location

Calicut

Cybrosys Technologies Pvt. Ltd.
Neospace, Kinfra Techno Park
Kakkancherry, Calicut
Kerala, India - 673635

location

Kochi

Cybrosys Technologies Pvt. Ltd.
1st Floor, Thapasya Building,
Infopark, Kakkanad,
Kochi, India - 682030.

location

Bangalore

Cybrosys Techno Solutions
The Estate, 8th Floor,
Dickenson Road,
Bangalore, India - 560042

Send Us A Message