Your Customers Shouldn't Have to Email You for a Copy of Their Own Invoice
Every time a customer emails your team asking "Can you resend that quote?" or "Where's my latest invoice?" or "What's the status of my order?"—that's a process failure. It means your business is manually handling information that should be self-service. Each of those emails costs 10-15 minutes of staff time, introduces delays, and signals to the customer that your operations aren't mature enough for a portal.
Odoo 19 ships with a fully integrated customer portal that gives your contacts self-service access to quotes, sales orders, invoices, payment history, helpdesk tickets, and shared documents. But the default setup is bare-bones. Most implementations either skip the portal entirely or enable it without configuring it properly—leaving customers with a confusing, half-empty dashboard.
This guide covers everything you need to build a production-ready customer portal in Odoo 19: user management, quote acceptance with online signatures, order tracking, invoice and payment history, helpdesk ticket submission, document sharing, and portal customization with branding and templates.
Portal User Management: Invitations, Access Rights, and Multi-Contact Setup
The portal starts with user accounts. Odoo distinguishes between three user types: internal users (your employees), portal users (your customers/vendors with login access), and public users (anonymous website visitors). Getting the portal user setup right is the foundation for everything else.
Sending Portal Invitations
Portal access is granted per-contact, not per-company. This means if your customer "Acme Corp" has a procurement manager and a finance director, each person gets their own portal login and sees only the documents relevant to them.
# Navigate to the contact record
Contacts > Select contact > Action > "Grant Portal Access"
# Odoo sends an email with a signup link
# The contact creates their password on first login
# Bulk invite: select multiple contacts in list view
Contacts > Select checkboxes > Action > "Grant Portal Access"
# Verify portal users in Settings
Settings > Users > Filter by "Portal" user typeProgrammatic Portal User Creation
For automated onboarding (e.g., after a CRM lead converts to a customer), you can create portal users via code:
from odoo import models, api
class ResPartner(models.Model):
_inherit = "res.partner"
def action_grant_portal_and_notify(self):
"""Grant portal access and send the invitation
email in one step. Called from a server action
or automated workflow."""
wizard = self.env["portal.wizard"].create({
"partner_ids": [
(0, 0, {"partner_id": p.id, "email": p.email})
for p in self
if p.email and not p.user_ids
],
})
wizard.action_apply()
# Each contact now has a portal user
# and receives a signup emailMulti-Contact Access Control
A common requirement: the finance team at your customer's company should see invoices but not helpdesk tickets. The procurement team should see quotes and orders but not invoices. Odoo's default portal gives every portal user access to all documents linked to their parent company. To restrict this, you need portal access rules:
<odoo>
<record id="portal_invoice_personal_rule"
model="ir.rule">
<field name="name">
Portal: Invoices for own contact only
</field>
<field name="model_id"
ref="account.model_account_move"/>
<field name="groups"
eval="[(4, ref('base.group_portal'))]"/>
<field name="domain_force">
[('partner_id', '=', user.partner_id.id)]
</field>
</record>
</odoo> By default, Odoo's portal record rules use ('partner_id', 'child_of', user.commercial_partner_id.id), meaning any portal user linked to "Acme Corp" sees all of Acme's documents. Changing this to ('partner_id', '=', user.partner_id.id) restricts visibility to documents assigned specifically to that contact. Test this thoroughly—some workflows assign documents to the parent company, not the child contact.
Online Quote Acceptance and Digital Signatures Through the Portal
The portal's highest-value feature for sales teams is online quote acceptance. Instead of emailing a PDF, waiting for the customer to print it, sign it, scan it, and email it back, the customer clicks a link, reviews the quote in their browser, and signs it digitally. The sales order is confirmed instantly.
Enabling Online Signatures and Payment
# Enable online signature and payment
Settings > Sales > Quotations & Orders
[x] Online Signature — customers sign to accept
[x] Online Payment — customers pay a deposit/full amount
[x] Quotation Validity — set default expiry (e.g., 30 days)
# Per-quotation overrides (on the SO form):
Sales > Quotations > Select quote
> "Other Info" tab
> Online Signature: Yes/No
> Online Payment: Yes/No
> Prepayment Percentage: e.g., 30%The Customer's Portal Experience
When you send a quotation, the customer receives an email with a portal link (not a PDF attachment). Clicking the link takes them to a branded page where they can:
- Review line items — product descriptions, quantities, unit prices, taxes, and totals rendered in a clean table.
- Download the PDF — a "Print" button generates the QWeb PDF on demand.
- Accept & sign — a signature pad appears. The customer draws or types their signature. On submission, Odoo timestamps the signature, logs the IP address, and confirms the sales order.
- Pay online — if online payment is enabled, a payment button appears with your configured providers (Stripe, PayPal, Adyen, etc.).
- Reject with feedback — the customer can decline the quote with a reason, which is logged on the chatter.
Adding Optional Products to Portal Quotes
Odoo 19 supports optional products on quotations. These appear as upsell suggestions on the portal page. The customer can add them to the order before accepting:
sale_order = self.env["sale.order"].browse(order_id)
# Add optional products that appear on the portal
sale_order.write({
"sale_order_option_ids": [
(0, 0, {
"name": "Extended Warranty - 2 Years",
"product_id": warranty_product.id,
"price_unit": 499.00,
"quantity": 1,
"uom_id": warranty_product.uom_id.id,
}),
(0, 0, {
"name": "Priority Support Package",
"product_id": support_product.id,
"price_unit": 1200.00,
"quantity": 1,
"uom_id": support_product.uom_id.id,
}),
],
})Odoo's digital signature captures a timestamp, the signer's name, and their IP address. For most B2B contexts this constitutes a valid electronic signature under eIDAS (EU) and ESIGN (US). However, if your contracts require qualified electronic signatures (common in government procurement), you'll need a third-party integration with a certified signature provider like DocuSign or Yousign. Odoo's built-in signature is a "simple electronic signature"—legally binding but with a lower evidentiary weight.
Sales Order Tracking and Delivery Status in the Customer Portal
Once a quote is accepted and becomes a confirmed sales order, the portal becomes the customer's order dashboard. They can track the fulfillment status without calling your warehouse team.
What Customers See by Default
| Portal Section | Information Displayed | Requires Module |
|---|---|---|
| Sales Orders | Order reference, date, status (Quotation/Sales Order), line items, total | sale |
| Delivery Orders | Delivery reference, scheduled date, carrier, tracking number, status | sale_stock + delivery |
| Tracking Link | Clickable carrier tracking URL (UPS, FedEx, DHL, etc.) | delivery + carrier connector |
| Purchase Orders (vendor portal) | PO reference, expected date, line items, receipt confirmation | purchase |
Adding Custom Status Fields to the Portal
The default portal shows order status but not custom fields. To expose a x_manufacturing_status field or an estimated ship date, you need to extend the portal template:
<odoo>
<template id="portal_order_custom_fields"
inherit_id="sale.sale_order_portal_template">
<xpath expr="//div[@id='informations']"
position="inside">
<div class="row mb-2"
t-if="sale_order.x_estimated_ship_date">
<div class="col-6">
<strong>Estimated Ship Date:</strong>
</div>
<div class="col-6">
<span t-field="sale_order.x_estimated_ship_date"
t-options='{{"widget": "date"}}'/>
</div>
</div>
<div class="row mb-2"
t-if="sale_order.x_manufacturing_status">
<div class="col-6">
<strong>Production Status:</strong>
</div>
<div class="col-6">
<span t-field=
"sale_order.x_manufacturing_status"/>
</div>
</div>
</xpath>
</template>
</odoo> For tracking links to appear on the portal, your delivery carrier connector must populate the carrier_tracking_ref field on the stock.picking record. Odoo's built-in connectors for UPS, FedEx, DHL, and USPS do this automatically. If you use a regional carrier, you'll need to write a custom connector or manually populate the tracking number—the portal template renders it as a clickable link automatically.
Invoice Access and Online Payment History in the Portal
The invoice section of the portal is where self-service pays for itself. Customers can view all their invoices, download PDFs, see payment status, and pay outstanding balances online—all without contacting your finance team.
Portal Invoice Features
- Invoice list with filters — customers see all invoices sorted by date, with status badges (Draft, Posted, Paid, Overdue). They can filter by date range and search by reference number.
- PDF download — each invoice has a download button that generates the QWeb PDF on demand, using your customized template.
- Online payment — a "Pay Now" button appears on unpaid invoices. The customer selects a payment provider and completes the transaction. The payment is reconciled automatically in Odoo.
- Payment history — the portal shows the payment status, date, and amount for each invoice. Partial payments are displayed with the remaining balance.
- Credit notes — credit notes appear in the same list, clearly labeled, with a link to the original invoice they reference.
Enabling Online Payment on Invoices
# 1. Enable a payment provider
Settings > Payment Providers > e.g., Stripe
> State: Enabled
> "Allow payments from the Customer Portal": [x]
# 2. Enable payment on invoices
Settings > Accounting > Customer Payments
[x] Invoice Online Payment
# 3. The "Pay Now" button now appears on every
# unpaid invoice in the portal
# 4. Partial payments are supported:
# customer can enter a custom amount < invoice totalRestricting Payment Methods per Customer
Some customers should only pay by bank transfer (no credit card). Others should have access to all payment methods. Odoo 19 lets you restrict payment providers at the partner level:
from odoo import models, api
class PaymentProvider(models.Model):
_inherit = "payment.provider"
@api.model
def _get_compatible_providers(
self, company_id, partner_id, amount,
currency_id=None, **kwargs
):
providers = super()._get_compatible_providers(
company_id, partner_id, amount,
currency_id=currency_id, **kwargs
)
partner = self.env["res.partner"].browse(
partner_id
)
# If partner has restricted payment methods,
# filter the provider list
if partner.x_allowed_provider_ids:
providers = providers.filtered(
lambda p: p.id
in partner.x_allowed_provider_ids.ids
)
return providersHelpdesk Ticket Submission and Tracking Through the Portal
The helpdesk portal turns customer support from an email-based guessing game into a structured, trackable process. Customers submit tickets, attach files, track status, and communicate through the portal—all logged in Odoo's helpdesk module.
Enabling the Helpdesk Portal
# 1. Enable the helpdesk portal
Helpdesk > Configuration > Helpdesk Teams
> Select team > "Portal & Website" section
[x] Website Form
[x] Portal Ticket Closing
[x] Portal Ticket Rating
# 2. Configure stages visible on the portal
Helpdesk > Configuration > Stages
> Each stage has "Visible in Portal" toggle
# 3. Customer portal now shows:
# - "New Ticket" button
# - List of their existing tickets
# - Ticket detail with status and messagesCustom Ticket Submission Form
The default portal ticket form is basic: subject, description, and attachments. Most businesses need additional fields like product reference, priority, or category. Here's how to extend it:
<odoo>
<template id="portal_helpdesk_custom_form"
inherit_id=
"helpdesk.portal_create_ticket">
<xpath expr="//div[@id='ticket_subject']"
position="after">
<!-- Product / Serial Number field -->
<div class="mb-3">
<label for="x_product_ref">
Product / Serial Number
</label>
<input type="text"
name="x_product_ref"
class="form-control"
placeholder=
"e.g., SN-2026-04812"/>
</div>
<!-- Priority selector -->
<div class="mb-3">
<label for="priority">Urgency</label>
<select name="priority"
class="form-select">
<option value="0">Normal</option>
<option value="1">High</option>
<option value="2">Urgent</option>
</select>
</div>
</xpath>
</template>
</odoo>Portal Ticket Communication
Once a ticket is submitted, the customer can communicate with your support team directly through the portal. Messages appear in a threaded conversation view, and both sides receive email notifications. Attachments (screenshots, logs, documents) can be uploaded from the portal. Everything is logged in the helpdesk ticket's chatter in the backend—your support team never needs to leave Odoo.
If you configure SLA policies on your helpdesk team, the portal can display the SLA deadline to the customer. This sets expectations upfront: "Your ticket will be resolved within 4 business hours." Enable this in Helpdesk > Configuration > Helpdesk Teams > SLA Policies. The portal shows a progress bar with time remaining when the SLA is active.
Sharing Documents and Files Through the Odoo Portal
Beyond transactional documents (quotes, invoices, tickets), businesses often need to share files with customers: product datasheets, compliance certificates, onboarding materials, training documents, or contract amendments. Odoo 19's Documents module integrates with the portal to provide a secure file-sharing layer.
Setting Up Portal Document Sharing
# Option 1: Share via the Documents app
Documents > Upload file > Share button
> "Share with" > Select portal user/partner
> Set expiry date (optional)
> Copy share link or send email
# Option 2: Attach to a record (SO, Invoice, Ticket)
# Documents attached to records are automatically
# visible on the portal for that record
# Option 3: Use the "Sign" module
# Upload a contract/NDA > Send for signature
# The document appears in the portal under "Signatures"
Sign > Upload Template > Send
> The customer signs from their portalProgrammatic Document Sharing
from odoo import models
class DocumentShare(models.Model):
_inherit = "documents.share"
def share_folder_with_customer(
self, folder_id, partner_id
):
"""Create a portal-accessible share link
for a specific document folder."""
share = self.create({
"folder_id": folder_id,
"partner_id": partner_id,
"type": "ids", # share specific docs
"date_deadline": "2026-12-31",
"action": "downloadonly",
})
# The portal user now sees these documents
# under their "Shared Documents" section
return share.full_urlPortal document shares use token-based authentication—the share URL contains a unique token that grants access. This means anyone with the link can access the files. For sensitive documents, always set an expiry date and consider using the "Login Required" option that forces the user to authenticate before downloading.
Portal Customization: Templates, Branding, and Layout Overrides
The default portal is functional but generic. It uses Odoo's standard website theme with minimal branding. For a professional customer experience, you need to customize the portal's appearance, layout, and navigation to match your brand.
Portal Template Architecture
Odoo's portal pages are rendered by QWeb templates served through HTTP controllers. The inheritance chain is:
| Template | Module | Controls |
|---|---|---|
portal.portal_layout | portal | Overall portal page structure, sidebar navigation |
portal.portal_my_home | portal | Portal dashboard: document count cards |
portal.portal_breadcrumbs | portal | Breadcrumb navigation bar |
sale.sale_order_portal_template | sale | Individual sale order detail page |
account.portal_invoice_page | account | Individual invoice detail page |
Branding the Portal Header and Footer
<odoo>
<template id="portal_layout_branded"
inherit_id="portal.portal_layout">
<xpath expr="//header" position="replace">
<header class="o_portal_navbar">
<nav class="navbar navbar-expand-lg"
style="background-color: #1a5c2e;">
<div class="container">
<a class="navbar-brand" href="/my">
<img src="/my_module/static/img/logo.svg"
alt="Company Logo"
height="40"/>
</a>
<span class="navbar-text text-white">
Customer Portal
</span>
</div>
</nav>
</header>
</xpath>
</template>
</odoo>Adding Custom Dashboard Cards
The portal home page shows count cards for each document type (e.g., "5 Quotations", "12 Invoices"). You can add custom cards for your own models:
from odoo.addons.portal.controllers.portal import (
CustomerPortal,
)
from odoo.http import request
class CustomPortal(CustomerPortal):
def _prepare_home_portal_values(
self, counters
):
values = super()._prepare_home_portal_values(
counters
)
if "project_count" in counters:
values["project_count"] = (
request.env["project.project"]
.search_count([
("partner_id", "=",
request.env.user.partner_id.id),
])
)
return values<odoo>
<template id="portal_my_home_projects"
inherit_id="portal.portal_my_home">
<xpath expr="//div[hasclass('o_portal_docs')]"
position="inside">
<t t-call="portal.portal_docs_entry">
<t t-set="icon" t-value="'/my_module/static/img/project_icon.svg'"/>
<t t-set="title">Projects</t>
<t t-set="url" t-value="'/my/projects'"/>
<t t-set="text">View your active projects</t>
<t t-set="count" t-value="project_count"/>
</t>
</xpath>
</template>
</odoo>Custom CSS for the Portal
/* ── Portal Brand Overrides ── */
.o_portal_navbar {
background-color: #1a5c2e !important;
.navbar-brand img {
max-height: 40px;
}
.navbar-text {
color: #ffffff;
font-weight: 600;
}
}
/* ── Dashboard Cards ── */
.o_portal_docs .o_portal_doc_entry {
border-left: 3px solid #1a5c2e;
transition: transform 0.15s ease;
&:hover {
transform: translateX(4px);
}
}
/* ── Status Badges ── */
.badge-portal-success {
background-color: #1a5c2e;
color: #fff;
}
.badge-portal-warning {
background-color: #e8a317;
color: #fff;
}
.badge-portal-danger {
background-color: #c0392b;
color: #fff;
} Unlike PDF reports (which use wkhtmltopdf), the portal renders in a real browser. This means you can and should use flexbox, CSS grid, and media queries. Over 40% of portal traffic comes from mobile devices. Test your customizations on phone-sized viewports—the default portal is responsive, but custom templates often break the responsive grid by using fixed widths or absolute positioning.
3 Portal Implementation Mistakes That Frustrate Customers and Create Security Risks
Exposing All Company Documents to Every Portal Contact
A company has 8 contacts with portal access. The IT manager submits a helpdesk ticket about a server outage. The procurement manager logs into the portal and sees the IT manager's ticket—including internal server details, IP addresses, and security notes. The default Odoo portal record rules use child_of commercial_partner_id, meaning every portal user from the same company sees every document assigned to any contact at that company.
Audit every ir.rule with domain child_of commercial_partner_id. For sensitive document types (helpdesk tickets, HR documents, contracts), change the rule to ('partner_id', '=', user.partner_id.id). For sales orders and invoices, the company-wide visibility is usually correct. Create separate rules per model based on your business requirements. Always test with two different portal users from the same company.
Not Configuring Email Templates for Portal Notifications
You enable the portal, send invitations, and customers can log in. But they never actually use it because they don't know when new documents are available. The default Odoo installation sends minimal portal notifications. A new invoice is posted? No email. A helpdesk ticket status changes? No notification. The customer logs in once, sees nothing new, and goes back to emailing your team.
Configure automated email templates for every portal-relevant event: quote sent, order confirmed, invoice posted, payment received, delivery shipped, ticket status changed. In Odoo 19, go to Settings > Technical > Email Templates and ensure each template includes a portal link (using {{ object.get_portal_url() }}) instead of a PDF attachment. This drives portal adoption: the email is the notification, the portal is the destination.
Skipping Portal Testing with Real Customer Accounts
Your development team configures the portal, tests it by logging in as an admin user with sudo access, and declares it ready. The first real customer logs in and sees a blank dashboard, broken links, or documents from other companies. Admin users bypass record rules, access rights, and company filters. The portal experience for an admin is fundamentally different from the experience for a real portal user.
Create a dedicated test portal user for every customer company you want to verify. Log in as that user in a private/incognito browser window. Walk through every portal section: dashboard, quotes, orders, invoices, tickets, documents. Verify that the user sees only their own documents, that links work, that PDFs download, and that online payment flows complete. Automate this with Odoo's HttpCase tour tests for regression coverage.
What a Properly Configured Customer Portal Saves Your Business
A customer portal isn't a technology project. It's an operational efficiency multiplier:
When customers can check order status, download invoices, and track deliveries themselves, they stop emailing your team for information they can find in 10 seconds.
Online quote acceptance with digital signatures eliminates the print-sign-scan-email cycle. Customers accept quotes in minutes instead of days.
Structured ticket submission with required fields (product, priority, description) means your support team gets the information they need on the first interaction, not the third email.
For a company with 200 active customers and 50 support interactions per week, a portal that eliminates 70% of status inquiry emails saves 35 hours of staff time per week—nearly a full-time employee. Add faster quote acceptance and you're looking at a measurable revenue acceleration that compounds every quarter.
Optimization Metadata
Complete guide to building a customer portal in Odoo 19. Configure portal users, online quote signatures, order tracking, invoice payments, helpdesk tickets, and branded portal templates.
1. "Portal User Management: Invitations, Access Rights, and Multi-Contact Setup"
2. "Online Quote Acceptance and Digital Signatures Through the Portal"
3. "Invoice Access and Online Payment History in the Portal"
4. "Helpdesk Ticket Submission and Tracking Through the Portal"
5. "Portal Customization: Templates, Branding, and Layout Overrides"