GuideMarch 13, 2026

Odoo 19 Quotation Builder:
Dynamic Templates, Optional Products & E-Signature

INTRODUCTION

Your Quotation Is the First Document a Prospect Reads Before Becoming a Customer

Most Odoo implementations send quotations as plain text line items with a total at the bottom. No cover page, no terms, no upsell suggestions, no way to sign online. The prospect receives something that looks like a spreadsheet export—not a professional sales proposal that builds confidence in a six-figure engagement.

The business impact is measurable. Quotations without clear terms generate scope disputes. Missing optional products leave upsell revenue on the table. Quotations that require printing, signing, and scanning add 3-5 days to every deal cycle. Every one of these friction points reduces your close rate and extends your sales cycle.

Odoo 19's Sales module ships with a powerful quotation builder that most teams barely touch: dynamic quotation templates with configurable sections, optional products for upselling, full PDF layout customization, built-in e-signature, online payment links, and automated expiration follow-ups. This guide walks you through every feature and the configuration patterns that turn a basic quote into a deal-closing machine.

01

Building Dynamic Quotation Templates with Sections and Configurable Terms

Quotation templates are the foundation of Odoo's quotation builder. Instead of building every quote from scratch, you define reusable templates that pre-populate products, sections, notes, and terms. Sales reps select a template, adjust quantities, and send—consistent formatting every time.

Enabling Quotation Templates

Quotation templates are not enabled by default. Navigate to Sales → Configuration → Settings and enable Quotation Templates under the Quotations & Orders section. This unlocks the Sales → Configuration → Quotation Templates menu.

Template Structure: Lines, Sections, and Notes

A quotation template supports three types of lines:

Line TypePurposeAppears on PDFExample
Product LineBillable item with quantity, price, taxYes — with subtotalOdoo Implementation — 40 hours
SectionVisual grouping header (no price)Yes — as bold divider"Phase 1: Discovery & Analysis"
NoteExplanatory text (no price)Yes — as paragraph"Includes on-site workshops with key stakeholders"

Creating a Template Programmatically

Python — Creating a quotation template via code
from odoo import api, SUPERUSER_ID


def create_implementation_template(env):
    """Create a reusable ERP implementation quotation template."""
    template = env['sale.order.template'].create({
        'name': 'ERP Implementation - Standard',
        'number_of_days': 30,  # Validity in days
        'note': (
            'This quotation is valid for 30 days. '
            'All prices exclude applicable taxes.'
        ),
        'sale_order_template_line_ids': [
            # Section header
            (0, 0, {
                'display_type': 'line_section',
                'name': 'Phase 1: Discovery & Gap Analysis',
                'sequence': 10,
            }),
            # Product line
            (0, 0, {
                'product_id': env.ref('my_module.product_discovery').id,
                'product_uom_qty': 24,
                'sequence': 20,
            }),
            # Note
            (0, 0, {
                'display_type': 'line_note',
                'name': 'Includes stakeholder interviews, '
                        'process mapping, and gap report.',
                'sequence': 30,
            }),
            # Section header
            (0, 0, {
                'display_type': 'line_section',
                'name': 'Phase 2: Configuration & Development',
                'sequence': 40,
            }),
            (0, 0, {
                'product_id': env.ref('my_module.product_config').id,
                'product_uom_qty': 80,
                'sequence': 50,
            }),
            (0, 0, {
                'product_id': env.ref('my_module.product_custom_dev').id,
                'product_uom_qty': 40,
                'sequence': 60,
            }),
        ],
    })
    return template

Dynamic Terms and Conditions

Each template can carry its own terms and conditions via the note field on the template. But Odoo 19 also supports rich-text quotation descriptions that appear as a cover page or introduction before the line items on the customer portal. Configure this under the Description tab of the template form:

XML — Setting terms via data file
<odoo>
  <record id="template_implementation_standard"
          model="sale.order.template">
    <field name="name">ERP Implementation - Standard</field>
    <field name="number_of_days">30</field>
    <field name="note"><![CDATA[
      <h3>Terms &amp; Conditions</h3>
      <ol>
        <li>Payment: 50% on signature, 50% on go-live.</li>
        <li>Change requests outside the agreed scope
            are billed at the standard daily rate.</li>
        <li>Project timeline starts upon receipt of
            signed quotation and initial payment.</li>
      </ol>
    ]]></field>
  </record>
</odoo>
Template per Sales Flow

Create one template per sales flow, not per customer. A "SaaS Subscription" template, a "Fixed-Price Project" template, and a "Time & Materials" template cover most B2B scenarios. Sales reps pick the right template and adjust quantities—they don't rebuild structure from scratch.

02

Optional Products: The Built-In Upsell Engine That Most Teams Ignore

Optional products appear as a separate section on the quotation portal page. The customer can add them to the order with a single click before confirming. This is Odoo's native upselling mechanism—no custom development needed, no third-party apps.

How Optional Products Work

When a customer views the quotation on the portal, optional products appear below the main order lines in an "Options" section. Each optional product has an Add to Order button. When clicked, the product moves from the optional section into the main order lines and the total updates in real time. The sales rep receives a notification that the customer added items.

Configuring Optional Products on Templates

Python — Adding optional products to a template
# Add optional products to an existing template
template.write({
    'sale_order_template_option_ids': [
        (0, 0, {
            'product_id': env.ref('my_module.product_training').id,
            'name': 'End-User Training (2-day on-site)',
            'quantity': 1,
            'uom_id': env.ref('uom.product_uom_unit').id,
        }),
        (0, 0, {
            'product_id': env.ref('my_module.product_support').id,
            'name': 'Priority Support Package (12 months)',
            'quantity': 1,
            'uom_id': env.ref('uom.product_uom_unit').id,
        }),
        (0, 0, {
            'product_id': env.ref('my_module.product_data_migration').id,
            'name': 'Historical Data Migration (up to 50k records)',
            'quantity': 1,
            'uom_id': env.ref('uom.product_uom_unit').id,
        }),
    ],
})

Product-Level Optional Products

Beyond templates, you can configure optional products directly on the product form under the Sales tab. When a sales rep adds that product to any quotation, Odoo automatically suggests the optional products. This works independently of templates:

XML — Optional products on a product form
<odoo>
  <record id="product_erp_implementation" model="product.template">
    <field name="name">ERP Implementation</field>
    <field name="type">service</field>
    <field name="list_price">15000.00</field>
    <field name="optional_product_ids"
           eval="[(6, 0, [
             ref('product_training'),
             ref('product_support'),
             ref('product_data_migration'),
           ])]"/>
  </record>
</odoo>
Upsell Strategy

The highest-converting optional products are logical extensions of what the customer already selected: training for software, installation for hardware, extended warranty for equipment. A 15-25% attach rate on optional products is typical. At $2,000 average optional product value across 100 quotes per month, that's $30,000-$50,000 in incremental monthly revenue from a checkbox.

03

Customizing the Quotation PDF Layout: From Generic to Professional

The quotation PDF is rendered by the same QWeb engine and wkhtmltopdf binary used for invoices. The template to customize is sale.report_saleorder_document. The same CSS constraints apply: no flexbox, no grid, floats and tables only.

Key Template Targets for Quotation PDFs

TemplateModuleWhat It Controls
sale.report_saleorder_documentsaleQuotation body: lines, sections, totals, terms
sale.report_saleordersaleBatch printing wrapper
web.external_layoutwebHeader, footer, page margins

Adding a Cover Page to the Quotation PDF

A professional quotation starts with a cover page showing the customer name, project title, date, and your company branding. Odoo doesn't include a cover page by default, but you can add one:

XML — Quotation cover page template
<odoo>
  <template id="report_saleorder_cover"
            inherit_id="sale.report_saleorder_document">
    <xpath expr="//div[@class='page']" position="before">
      <div class="page" style="page-break-after: always;
           text-align: center; padding-top: 200px;">

        <img t-att-src="'/web/image/res.company/%s/logo' %
              o.company_id.id"
             style="max-height: 80px; margin-bottom: 40px;"/>

        <h1 style="font-size: 28pt; color: #1a5c2e;
             margin-bottom: 16px;">
          <span t-field="o.name"/>
        </h1>

        <h2 style="font-size: 16pt; color: #333;
             margin-bottom: 40px;">
          Prepared for
          <span t-field="o.partner_id.name"/>
        </h2>

        <p style="font-size: 11pt; color: #666;">
          Date: <span t-field="o.date_order"
                       t-options='{"widget": "date"}'/>
          <br/>
          Valid until: <span t-field="o.validity_date"
                             t-options='{"widget": "date"}'/>
        </p>
      </div>
    </xpath>
  </template>
</odoo>

Styling the Line Items Table

CSS — static/src/css/quotation.css
/* ── Section Headers ── */
.o_report_layout .o_section_header td {
    background-color: #1a5c2e;
    color: #ffffff;
    font-weight: 700;
    font-size: 10pt;
    padding: 6px 12px;
    border: none;
}

/* ── Note Lines ── */
.o_report_layout .o_note_line td {
    font-style: italic;
    color: #555555;
    padding: 4px 12px;
    border: none;
}

/* ── Optional Products Section ── */
.o_report_layout .o_optional_products {
    margin-top: 24px;
    border-top: 2px dashed #cccccc;
    padding-top: 16px;
}
.o_report_layout .o_optional_products h3 {
    color: #1a5c2e;
    font-size: 12pt;
}

/* ── Totals ── */
.o_report_layout #total td.text-end {
    font-size: 11pt;
    font-weight: 700;
    color: #1a5c2e;
}
Portal vs. PDF

The quotation portal page and the quotation PDF are two different rendering paths. The portal uses standard web CSS (flexbox works). The PDF uses wkhtmltopdf (flexbox breaks). If you customize the PDF, test both views. Customers interact primarily through the portal; the PDF is for printing and archiving.

04

E-Signature and Online Payment: Close Deals Without Printing a Single Page

Odoo 19 includes built-in e-signature and online payment for quotations. No third-party integrations needed. When enabled, the customer receives a quotation link, reviews the document on the portal, draws or types their signature, and optionally pays a deposit—all in one flow.

Enabling E-Signature and Online Payment

Navigate to Sales → Configuration → Settings:

  • Online Signature — enables the signature pad on the quotation portal page. The customer draws their signature or types their name. The signed PDF is attached to the sales order.
  • Online Payment — adds a "Pay Now" button to the portal page. Requires a payment provider configured under Accounting → Configuration → Payment Providers (Stripe, PayPal, Authorize.net, etc.).

Per-Template Signature and Payment Settings

Both settings can be controlled at the template level, overriding the global default. This lets you require signatures on high-value project quotes but skip them for quick product orders:

Python — Template-level e-signature config
# Template that requires signature but not prepayment
project_template = env['sale.order.template'].create({
    'name': 'Fixed-Price Project',
    'number_of_days': 15,
    'require_signature': True,
    'require_payment': False,
})

# Template that requires both signature and 30% deposit
enterprise_template = env['sale.order.template'].create({
    'name': 'Enterprise License + Implementation',
    'number_of_days': 30,
    'require_signature': True,
    'require_payment': True,
    'prepayment_percent': 30.0,
})

The Customer Experience Flow

When a customer opens the quotation link, the portal page presents a clear flow:

  1. Review — the customer sees the full quotation with line items, sections, notes, and optional products.
  2. Customize — they can add optional products to the order by clicking "Add to Order."
  3. Sign — a signature pad appears at the bottom. The customer draws their signature or types their name.
  4. Pay — if online payment is enabled, the payment form appears after signing. The customer pays the deposit or full amount.
  5. Confirm — the quotation converts to a confirmed sales order. The sales rep is notified.
Legal Validity

Odoo's e-signature captures the signer's name, IP address, timestamp, and browser fingerprint. This meets the requirements for simple electronic signatures under eIDAS (EU), ESIGN Act (US), and equivalent frameworks. For regulated industries requiring qualified electronic signatures (QES), you'll need an external provider like DocuSign or Yousign integrated via API.

05

Expiration Dates, Follow-Up Automation, and Quotation Lifecycle Management

A quotation without an expiration date sits in limbo forever. A quotation with an expiration date but no follow-up automation expires silently. Odoo 19 handles both, but you need to configure the pipeline correctly.

Setting Default Validity Periods

The number_of_days field on the quotation template sets the default validity period. When a sales rep creates a quotation from that template, the Expiration Date field auto-calculates. You can also set a global default under Sales → Configuration → Settings → Default Quotation Validity.

Automated Follow-Up with Scheduled Actions

Python — Scheduled action for quotation follow-up
from odoo import fields, models
from datetime import timedelta


class SaleOrder(models.Model):
    _inherit = 'sale.order'

    def _cron_quotation_follow_up(self):
        """Send follow-up emails for quotations expiring in 3 days."""
        today = fields.Date.today()
        expiring_soon = self.search([
            ('state', '=', 'draft'),
            ('validity_date', '!=', False),
            ('validity_date', '<=', today + timedelta(days=3)),
            ('validity_date', '>=', today),
        ])

        mail_template = self.env.ref(
            'my_module.email_quotation_expiring_soon'
        )
        for order in expiring_soon:
            mail_template.send_mail(order.id)

    def _cron_quotation_expire(self):
        """Cancel quotations past their validity date."""
        expired = self.search([
            ('state', '=', 'draft'),
            ('validity_date', '!=', False),
            ('validity_date', '<', fields.Date.today()),
        ])
        expired.action_cancel()
        # Log the reason for audit trail
        for order in expired:
            order.message_post(
                body='Quotation auto-expired: '
                     'validity date passed without confirmation.',
            )
XML — Scheduled action definitions
<odoo>
  <!-- Follow-up reminder: runs daily -->
  <record id="cron_quotation_follow_up"
          model="ir.cron">
    <field name="name">Quotation: Expiry Reminder</field>
    <field name="model_id"
           ref="sale.model_sale_order"/>
    <field name="state">code</field>
    <field name="code">
      model._cron_quotation_follow_up()
    </field>
    <field name="interval_number">1</field>
    <field name="interval_type">days</field>
    <field name="numbercall">-1</field>
  </record>

  <!-- Auto-expire: runs daily -->
  <record id="cron_quotation_expire"
          model="ir.cron">
    <field name="name">Quotation: Auto Expire</field>
    <field name="model_id"
           ref="sale.model_sale_order"/>
    <field name="state">code</field>
    <field name="code">
      model._cron_quotation_expire()
    </field>
    <field name="interval_number">1</field>
    <field name="interval_type">days</field>
    <field name="numbercall">-1</field>
  </record>
</odoo>
Follow-Up Timing Matters

The most effective follow-up pattern: send a reminder 3 days before expiration ("Your quotation expires Friday—questions before then?") and a second one on the expiration day ("This is the last day to lock in this pricing"). After expiration, send a re-engagement email 7 days later with a fresh quotation link. This three-touch sequence recovers 15-20% of otherwise-lost deals.

06

3 Quotation Builder Mistakes That Cost You Deals in Production

1

Optional Products That Disappear After Quotation Confirmation

You configure optional products on your template. Customers see them on the portal and add some to their order. The quotation is confirmed. Then a sales manager opens the confirmed sales order and the optional products section is gone—only the main order lines remain. They assume the customer didn't want the extras and never follow up. In reality, the customer did add optional products, but they were moved into the main order lines upon confirmation. The "Options" section only exists on draft quotations.

Our Fix

Train sales teams that optional products selected by the customer merge into the main order lines on confirmation. They appear as regular line items, not in a separate section. If you need to track which items were originally optional, add a boolean field x_was_optional on sale.order.line and set it via an onchange or override on action_confirm. This preserves the upsell attribution for sales analytics.

2

E-Signature Enabled Globally But Payment Provider Not Configured

You enable both "Online Signature" and "Online Payment" in Sales settings. The payment provider setup is still in test mode (or not configured at all). The customer signs the quotation, clicks "Pay Now," and sees a blank payment form or a cryptic error. The deal stalls because the customer thinks your system is broken. Worse, the quotation is now in a "signed but not paid" state that confuses your sales pipeline.

Our Fix

Before enabling Online Payment globally, verify that at least one payment provider is fully configured in production mode (not test mode). Test the complete flow: send yourself a quotation, open the portal link in an incognito browser, sign it, and complete payment. If you need signatures but aren't ready for online payment, enable them separately—they're independent settings. Use template-level overrides to disable payment on specific quotation types while keeping signatures.

3

Template Changes Don't Update Existing Draft Quotations

You update a quotation template—add a new product line, change the terms, adjust the validity period. You expect all existing draft quotations based on that template to update. They don't. Quotation templates are applied at creation time. Modifying the template only affects new quotations created from it. Every draft quotation that was already sent to a customer still carries the old terms, old products, and old pricing.

Our Fix

This is by design—you don't want template updates retroactively changing quotations that customers are already reviewing. But if you need to update existing drafts (e.g., a pricing correction), use a server action or script to re-apply the template. The method sale_order._onchange_sale_order_template_id() re-reads the template and regenerates the lines. Run this selectively on specific quotations, not blanket across all drafts, to avoid overwriting manual customizations sales reps have already made.

BUSINESS ROI

What a Properly Configured Quotation Builder Saves Your Business

This isn't a cosmetic exercise. It's a revenue acceleration project:

3-5 daysShorter Sales Cycle

E-signature and online payment eliminate the print-sign-scan loop. Customers confirm deals the same day they receive the quotation.

15-25%Optional Product Attach Rate

Customers add training, support, and extended services when they're presented as one-click options during the buying moment.

70%Less Quoting Time

Templates with pre-configured sections, products, and terms reduce quotation creation from 45 minutes to under 10 minutes per deal.

For a team sending 200 quotations per month with a $25,000 average deal value, a 20% optional product attach rate at $3,000 average option value adds $120,000 in monthly revenue. Combine that with a 3-day shorter sales cycle across 50 closed deals per month, and you're looking at materially improved cash flow from configuration alone—no custom code required.

SEO NOTES

Optimization Metadata

Meta Desc

Complete guide to Odoo 19 quotation templates. Configure dynamic sections, optional products for upselling, PDF cover pages, e-signature, online payment, and automated expiration follow-ups.

H2 Keywords

1. "Building Dynamic Quotation Templates with Sections and Configurable Terms"
2. "Optional Products: The Built-In Upsell Engine That Most Teams Ignore"
3. "E-Signature and Online Payment: Close Deals Without Printing a Single Page"
4. "3 Quotation Builder Mistakes That Cost You Deals in Production"

Your Quotation Should Close the Deal, Not Just List the Price

A well-built quotation does more than enumerate line items. It presents a professional proposal, offers logical upsells, removes friction from the approval process, and creates urgency with clear expiration dates. Every generic-looking quote, every missing signature workflow, and every manual follow-up email is costing you deal velocity and incremental revenue.

If your Odoo quotations still look like the default template, we should fix that. We configure and customize the complete quotation builder—templates, optional products, PDF branding, e-signature, payment integration, and follow-up automation. The project typically takes 3-5 days and pays for itself within the first month of higher close rates and upsell revenue.

Book a Free Quotation Audit