Ir al contenido

Portal del cliente en Odoo: listas, formularios y descargas

Controladores, templates QWeb y permisos por partner
26 de junio de 2026 por
Portal del cliente en Odoo: listas, formularios y descargas
Atemi, Aitor Atencia

Odoo · Portal · Seguridad

El portal del cliente es la cara visible de tu ERP para usuarios externos. Aquí un error de permisos no es un bug: es una fuga de datos. La regla es simple y absoluta: cada consulta se filtra por el partner del usuario. Veamos cómo añadir listas, fichas y descargas al portal sin abrir agujeros.

Logo Odoo Portal seguro
En el portal, cada registro mostrado debe pertenecer al partner autenticado.

Lista

Menú /my con contador

from odoo import http
from odoo.http import request
from odoo.addons.portal.controllers.portal import CustomerPortal

class DiagnosticPortal(CustomerPortal):

    def _prepare_home_portal_values(self, counters):
        values = super()._prepare_home_portal_values(counters)
        if 'diagnostic_count' in counters:
            partner = request.env.user.partner_id
            values['diagnostic_count'] = request.env[
                'diagnostic.test'].search_count(
                    [('partner_id', '=', partner.id)])
        return values

Heredar de CustomerPortal integra tu modelo en la página /my con su tarjeta y contador, igual que pedidos o facturas.

Listado

Página de listado filtrada

    @http.route('/my/diagnostics', type='http',
                auth='user', website=True)
    def portal_diagnostics(self, **kw):
        partner = request.env.user.partner_id
        tests = request.env['diagnostic.test'].search(
            [('partner_id', '=', partner.id)])
        return request.render(
            'mi_modulo.portal_my_diagnostics',
            {'tests': tests, 'page_name': 'diagnostics'})

Ficha

Detalle con check_access y sudo

    @http.route('/my/diagnostic/<int:test_id>',
                type='http', auth='user', website=True)
    def portal_diagnostic_page(self, test_id, access_token=None, **kw):
        try:
            test_sudo = self._document_check_access(
                'diagnostic.test', test_id, access_token)
        except (AccessError, MissingError):
            return request.redirect('/my')
        return request.render(
            'mi_modulo.portal_diagnostic_page',
            {'test': test_sudo})

_document_check_access (heredado de portal) valida que el partner tiene acceso o que el access_token es válido, y devuelve el registro en sudo de forma controlada. Es el patrón estándar: nunca hagas browse(test_id) directo desde una URL.

Descarga

Servir un PDF protegido

    @http.route('/my/diagnostic/<int:test_id>/pdf',
                type='http', auth='user', website=True)
    def portal_diagnostic_pdf(self, test_id, access_token=None, **kw):
        test_sudo = self._document_check_access(
            'diagnostic.test', test_id, access_token)
        pdf, _ = request.env['ir.actions.report'].sudo()._render_qweb_pdf(
            'mi_modulo.action_report_diagnostic', [test_sudo.id])
        headers = [('Content-Type', 'application/pdf'),
                   ('Content-Length', len(pdf))]
        return request.make_response(pdf, headers=headers)

Mismo guard antes de generar el PDF. Enlaza con la entrada de informes QWeb de esta serie: el portal solo añade la capa de control de acceso sobre el render ya existente.

Checklist

Antes de publicar al portal

✅ ComprobaciónPor qué
Toda ruta usa auth='user'Identidad real del partner
Filtrado por partner_idNo listar datos ajenos
_document_check_access en detalleEvita IDOR por URL
Record rule grupo portalSegunda capa a nivel ORM
Probar con dos clientesConfirmar aislamiento real

Resumen

Hereda de CustomerPortal, usa auth='user' en todas las rutas y filtra siempre por partner_id. Para fichas y descargas, _document_check_access es obligatorio: cierra el IDOR clasico. Defiende en profundidad combinando filtrado en el controlador con record rules del grupo portal, y prueba el aislamiento con dos clientes distintos antes de publicar.

en Odoo
Survey en Odoo: tests online y resultados conectados a tus modelos
Vincular survey.user_input a modelos propios y actualizar estados