Advertisement
💻 Website Development

Odoo Website Forms – Contact and Custom Forms

Website forms let visitors submit data from public pages — contact requests, lead generation, event registrations, or custom enquiries. Odoo provides a built-in website_form JavaScript widget for simple cases, and the full POST controller pattern for custom logic.

⏱️ 20 min 🎯 Intermediate 📅 Updated 2026

Website Forms: Turning Visitor Input Into Records

Odoo's website builder lets you drop a form on a page that creates a record on submit — a contact request, a job application, a lead. It posts to a built-in generic endpoint that maps fields to a model.

<form action="/website/form/" method="post" data-model_name="crm.lead">
  <input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
  <input type="text"  name="contact_name"  required="1"/>
  <input type="email" name="email_from"    required="1"/>
  <textarea name="description"></textarea>
  <button type="submit">Send</button>
</form>

The data-model_name tells Odoo which model to create; each input's name must match a field on that model. The /website/form/ controller validates and creates the record.

ConcernHandling
CSRFinclude the csrf_token — required, blocks forged posts
Allowed fieldsa model's _website_form_fields whitelist limits what can be set
Spamadd a honeypot/captcha for public forms

Security matters here because the form is public. Odoo only lets website forms write to fields a model explicitly whitelists (via _website_form_fields / the website_form access), so a visitor can't set arbitrary fields like user_id or prices by injecting hidden inputs. Always keep the CSRF token, validate/whitelist server-side, and add anti-spam (captcha) on any public-facing form. For custom target models, you must opt the model and its safe fields into website forms explicitly.

🏋️ Practical Exercise

  1. Add a form snippet to a page.
  2. Map form fields to a model.
  3. Handle the submission.
  4. Add a success message.
  5. Add spam protection (concept).

🔥 Challenge Exercise

Build a public contact/lead form on a website page that creates a record in a backend model on submit, with validation and a thank-you message. Explain how the Website Form snippet maps fields to a model and how submissions are stored.

What you'll learn:
  • The built-in website_form widget and when to use it
  • Writing a custom POST controller for website form submissions
  • CSRF and SPAM protection on public forms
  • Handling file uploads and confirmation redirects

The website_form Widget

For standard forms that create records (leads, job applications, contact messages), the website_form JavaScript widget handles submission, validation, and SPAM protection automatically. Enable it by adding data-model-name to your form tag:

XML (QWeb)
<form id="contact_form"
      class="s_website_form"
      action="/website_form/"
      method="post"
      data-model-name="crm.lead"
      data-success-page="/contactus-thank-you">

  <input type="hidden" name="csrf_token"
         t-att-value="request.csrf_token()"/>

  <div class="form-group">
    <label for="contact_name">Name</label>
    <input type="text" class="form-control"
           name="contact_name" required="required"/>
  </div>

  <div class="form-group">
    <label for="email_from">Email</label>
    <input type="email" class="form-control"
           name="email_from" required="required"/>
  </div>

  <div class="form-group">
    <label for="description">Message</label>
    <textarea class="form-control" name="description" rows="5"/>
  </div>

  <button type="submit" class="btn btn-primary">Send</button>
</form>

The website_form widget posts to /website_form/{model_name} and creates a record using the form field names as model field names. The model must allow website form creation — configure this in Website → Configuration → Form Allowed Models.

Custom POST Controller

For custom logic (multi-step forms, complex validation, multiple model writes), write your own controller:

Python
from odoo import http
from odoo.http import request

class LibraryWebsite(http.Controller):

    @http.route('/books/request', type='http',
                auth='public', website=True, methods=['GET', 'POST'])
    def book_request_form(self, **kw):
        if request.httprequest.method == 'POST':
            return self._handle_book_request(kw)

        books = request.env['library.book'].sudo().search([
            ('website_published', '=', True)
        ])
        return request.render('my_module.book_request_form', {
            'books': books, 'errors': {}, 'values': {},
        })

    def _handle_book_request(self, values):
        errors = {}
        name = (values.get('visitor_name') or '').strip()
        email = (values.get('visitor_email') or '').strip()
        book_id = int(values.get('book_id') or 0)

        if not name:
            errors['visitor_name'] = 'Name is required.'
        if not email or '@' not in email:
            errors['visitor_email'] = 'Valid email required.'
        if not book_id:
            errors['book_id'] = 'Please select a book.'

        if errors:
            books = request.env['library.book'].sudo().search([
                ('website_published', '=', True)
            ])
            return request.render('my_module.book_request_form', {
                'books': books, 'errors': errors, 'values': values,
            })

        request.env['library.book.request'].sudo().create({
            'visitor_name': name,
            'visitor_email': email,
            'book_id': book_id,
        })
        return request.redirect('/books/request/thank-you')

CSRF Protection on Public Forms

All POST forms must include the CSRF token. For public (unauthenticated) pages, the token is still required:

XML (QWeb)
<form method="post" action="/books/request">
  <input type="hidden" name="csrf_token"
         t-att-value="request.csrf_token()"/>
  ...
</form>

Without the CSRF token, Odoo returns HTTP 403. The token is tied to the visitor's session, which Odoo creates automatically even for anonymous visitors.

Advertisement

SPAM Protection

Public forms are targets for bot spam. Odoo's website_form widget adds a honeypot field automatically. For custom forms, implement your own protection:

  • Honeypot field — an invisible input; if filled in, it's a bot:
XML (QWeb)
<!-- Hidden from real users via CSS, filled by bots -->
<input type="text" name="website_url"
       style="display:none" tabindex="-1" autocomplete="off"/>
Python
# In controller: reject if honeypot filled
if values.get('website_url'):
    return request.redirect('/books/request')  # silently ignore bots

File Uploads

For file uploads, add enctype="multipart/form-data" to the form and access files in the controller:

Python
import base64

attachment = request.httprequest.files.get('document')
if attachment and attachment.filename:
    data = attachment.read()
    # Validate size (max 5 MB)
    if len(data) > 5 * 1024 * 1024:
        errors['document'] = 'File must be under 5 MB.'
    else:
        request.env['ir.attachment'].sudo().create({
            'name': attachment.filename,
            'datas': base64.b64encode(data),
            'res_model': 'library.book.request',
            'res_id': book_request.id,
        })
Key takeaways:
  • Use the website_form widget for simple record-creation forms — it handles SPAM and validation automatically
  • For custom logic, write a GET/POST controller with manual validation and a POST/Redirect/GET pattern
  • Always include csrf_token in every POST form
  • Validate file type and size before creating ir.attachment records

Interview Questions

  • How do you add a form to an Odoo website?
  • How are form fields mapped to a model?
  • How is a submission stored?
  • How do you validate website form input?
  • How do you protect a public form from spam?

FAQ

When should I use website_form vs a custom POST controller? +

Use website_form when you're creating a single record (lead, contact message, job application) and standard field mapping is sufficient. Use a custom controller when you need multi-model writes, complex validation, file uploads alongside record creation, or conditional logic based on submitted values.

How do I show a thank-you page after form submission? +

Redirect to a dedicated thank-you URL after successful processing: return request.redirect('/my-form/thank-you'). Create a simple controller and template for that URL. This also prevents duplicate submissions on browser refresh (POST/Redirect/GET pattern).

Can I use reCAPTCHA with Odoo website forms? +

Yes — the google_recaptcha module integrates reCAPTCHA v3 with the website_form widget. Configure the site key in Website → Configuration → Settings. For custom forms, call request.env['ir.recaptcha']._verify_recaptcha_token(token) in your controller.