All posts
Integrator8 min read

Odoo portal users: granting customers access to quotations, orders, invoices, and project tasks

Odoo's portal lets customers log in and view their own documents - quotes, orders, invoices, delivery tracking, project tasks, and helpdesk tickets - without needing a full internal user licence. This guide covers setup, access control, and customization of the portal experience.

Odoo's customer portal is an underused feature. When configured correctly, it lets customers view their own quotes, confirm orders, download invoices, track deliveries, and follow project progress - all without a paid internal user licence. It removes the need for email attachments and "can you send me a copy of that invoice?" support requests.

This guide covers how portal access works, how to grant it, what customers can see, and how to extend the portal for custom modules.

What is a portal user?#

A portal user is an Odoo user in the base.group_portal security group. Portal users:

  • Log in at /web/login with an email and password
  • Are redirected to /my (the My Account portal) rather than the backend
  • Can access their own records via the portal but cannot enter the backend
  • Are billed as external users (no full-user licence cost in most Odoo plans)

The key difference from a public (unauthenticated) user: portal users are authenticated and can see records where they are the partner (customer or contact). Public users see only content explicitly marked as public.

Granting portal access#

Portal access is granted per-contact, not per-company. To give a contact portal access:

  1. Open the contact form (Contacts → select partner)
  2. Click Action → Grant Portal Access
  3. Select the contact and click Apply

Odoo sends an invitation email with a link to set a password. The contact is now a portal user.

You can also grant portal access from a sale order or invoice using the Share / Portal Access button on the chatter. This is useful when you want to give access for a specific document immediately.

Removing portal access: go to Settings → Users → Portal Users, find the user, and click Revoke Portal Access. This removes the user from group_portal and prevents further login without deleting the record.

What portal users can see#

By default, an authenticated portal user at /my sees:

ModuleWhat they see
SalesQuotations and orders where they are the customer
InvoicingInvoices and credit notes (can download PDF)
PurchaseNothing by default - vendors need a different setup
InventoryDelivery orders linked to their sale orders
ProjectTasks and milestones on projects where portal access is enabled
HelpdeskTheir submitted tickets
WebsitePublic pages + their portal pages

Each item in the list is controlled by a portal.mixin on the model and a dedicated portal route in the relevant module. If a module does not include portal.mixin support, its records are not accessible via the portal out of the box.

The portal.mixin#

Odoo provides a mixin to add portal access to any model. For standard models that already inherit it (sale.order, account.move, project.task, etc.), no additional work is needed. For custom models, add the mixin:

python
from odoo import models

class CustomCertificate(models.Model):
    _name = 'my_module.certificate'
    _inherit = ['portal.mixin', 'mail.thread']
    _description = 'Quality Certificate'

    def _compute_access_url(self):
        for record in self:
            record.access_url = f'/my/certificate/{record.id}'

The mixin adds:

  • access_url: the portal URL for the record
  • access_token: a random token that allows access without login (for share links)
  • _compute_access_url(): override to define your record's portal URL

Sharing documents without a portal login#

For customers who do not have a portal account, you can share a document via a signed URL. Every portal.mixin record has an access_token field. When the URL includes ?access_token=, Odoo grants read access to that specific record without requiring login.

The Share button in the chatter (on sale orders, invoices, etc.) generates this URL and sends it by email. The recipient clicks the link and sees the document in a read-only portal view - no account required.

Tokens can be reset (Reset Token action on the record) if you need to invalidate a shared link.

Portal controllers and routes#

Portal pages are served by controllers inheriting from CustomerPortal. Each module registers its own routes. For example, the sale module adds:

python
class CustomerPortal(CustomerPortal):

    @route(['/my/orders', '/my/orders/page/<int:page>'], auth='user')
    def portal_my_orders(self, page=0, **kw):
        # ...builds the order list and returns a portal template

The auth='user' decorator requires an authenticated user. auth='public' allows unauthenticated access; auth='none' disables authentication entirely (used for truly public pages).

To add a custom section to the portal My Account page, inherit the portal.my_home template and add an entry:

xml
<template id="portal_my_home_certificate" inherit_id="portal.portal_my_home" priority="30">
  <xpath expr="//div[hasclass('o_portal_docs')]" position="inside">
    <t t-call="portal.portal_docs_entry">
      <t t-set="title">My Certificates</t>
      <t t-set="url">/my/certificates</t>
      <t t-set="placeholder_count">certificate_count</t>
    </t>
  </xpath>
</template>

The placeholder_count value is passed by the controller via the portal home data method - override _prepare_home_portal_values to include the count:

python
def _prepare_home_portal_values(self, counters):
    values = super()._prepare_home_portal_values(counters)
    partner = request.env.user.partner_id
    if 'certificate_count' in counters:
        values['certificate_count'] = request.env['my_module.certificate'].search_count([
            ('partner_id', '=', partner.id),
        ])
    return values

Security: what portal users can access#

Portal users operate with very limited rights. The group_portal group has no model access by default - access is granted through ir.model.access lines and record rules specifically for portal users.

The record rules are the critical layer. A standard portal record rule looks like:

xml
<record id="portal_certificate_rule" model="ir.rule">
  <field name="name">Certificate: portal users see their own</field>
  <field name="model_id" ref="model_my_module_certificate"/>
  <field name="domain_force">[('partner_id', '=', user.partner_id.id)]</field>
  <field name="groups" eval="[(4, ref('base.group_portal'))]"/>
  <field name="perm_read" eval="True"/>
  <field name="perm_write" eval="False"/>
  <field name="perm_create" eval="False"/>
  <field name="perm_unlink" eval="False"/>
</record>

This gives portal users read-only access to certificates where they are the partner. Without this rule, portal users see nothing - even if the controller fetches the records with sudo(), the view template rendering may fail on related fields if the user cannot read them.

The access_token bypass: when a record is accessed via its access_token URL, Odoo temporarily elevates the request to superuser for the record fetch. This allows sharing documents with non-portal contacts. Be careful with what you expose on those pages - do not display fields from related models that the recipient should not see.

Portal for project tasks and customer collaboration#

The Project module's portal feature is frequently used for client-facing project status. To enable it:

  1. Open the project
  2. Go to Project Settings → Visibility → Portal users and all internal users
  3. Add your client contact as a follower on the project or on specific tasks

Once visibility is set, the client sees the project's tasks at /my/projects and can follow comments via chatter on tasks where they are followers. They can post messages but cannot create tasks, reassign, or change stages unless you explicitly grant write access in the portal record rules.

Customizing the My Account page#

The My Account page at /my/account shows the portal user's personal information (name, email, phone, address). They can edit these fields and the changes write back to res.partner.

To add custom fields to My Account:

  1. Add fields to res.partner in your module
  2. Inherit portal.portal_my_account to add the fields to the form
  3. Override the account controller method to process the posted values

This is a common request for B2C portals: adding a VAT number, a preferred delivery address, or a customer reference field that flows into orders automatically.

Common mistakes#

Granting portal access to the wrong contact: if a company has multiple contacts, granting portal access to the company contact (not the individual) means the portal user can see all orders for that company. Granting access to the individual contact restricts the view to records where that specific person is the partner. Decide which behavior you want before setup.

Missing model access for group_portal: adding a record rule is not enough. You also need a ir.model.access line granting group_portal read access on the model, or the rule will never be evaluated.

Exposing sensitive related fields on token-accessible pages: when a page is accessible via access_token without login, any data you render is effectively public for anyone with the URL. Avoid rendering invoice amounts, partner emails, or financial data on these pages unless the business explicitly accepts the exposure.

Not testing with a real portal user: developers test in superuser mode and assume everything works. Always create a test portal user and verify the portal experience end-to-end before go-live.


For security groups and access rights in custom modules, see the Odoo security model guide. For the website public-facing store, see the Odoo eCommerce product page guide.

Try ERPeek on your own Odoo module - ask questions, scaffold tests, and explore your codebase in plain language.

Get started free