Create Custom Web Forms in Odoo 18: Step-by-Step Guide
By Braincuber Team
Published on February 3, 2026
Odoo's Website builder is powerful, but sometimes you need more than just drag-and-drop blocks. You might need a completely custom form to capture specific data directly into a custom module. Whether it's a "Job Application" form, a "Vendor Registration" portal, or a "Feedback" collector, the process is the same.
In this guide, we'll build a Job Application Form. Candidates will be able to submit their Name, Email, Phone, and formatted Resume URL directly from your website, creating a record in the Odoo backend automatically.
Create Model
Define the database table to store submission data.
Add Menu
Create a navigation link on your website.
Build Controller
Handle the page rendering and form submission logic.
Design Template
Create the front-end XML form with Bootstrap styling.
Step 1: Create the Data Model
First, we need a place to store the incoming applications. We'll define a simple model job.application.form.
from odoo import models, fields
class JobApplicationForm(models.Model):
_name = 'job.application.form'
_description = 'Job Application Submission'
name = fields.Char(string='Applicant Name', required=True)
email = fields.Char(string='Email Address', required=True)
phone = fields.Char(string='Phone Number')
linkedin_url = fields.Char(string='LinkedIn Profile')
position = fields.Selection([
('developer', 'Developer'),
('designer', 'Designer'),
('manager', 'Product Manager')
], string='Position Applied For')
Step 2: Add a Website Menu
We want users to find this form easily. We'll add a menu item "Careers" that points to our custom URL /careers/apply.
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="menu_job_application" model="website.menu">
<field name="name">Careers</field>
<field name="url">/careers/apply</field>
<field name="parent_id" ref="website.main_menu"/>
<field name="sequence" type="int">50</field>
</record>
</odoo>
Make sure to add 'website' to the depends list in your __manifest__.py file, otherwise the website.main_menu reference will fail.
Step 3: Create the Controller
The controller handles two things: displaying the form (GET request) and processing the data (POST request).
from odoo import http
from odoo.http import request
class JobApplicationController(http.Controller):
@http.route('/careers/apply', type='http', auth='public', website=True)
def apply_page(self, **kwargs):
# Render the XML template
return request.render('my_module.apply_form_template')
@http.route('/careers/submit', type='http', methods=['POST'], auth='public', website=True, csrf=True)
def submit_application(self, **post):
# Create record in backend
# sudo() is needed because public users don't have write access
request.env['job.application.form'].sudo().create({
'name': post.get('name'),
'email': post.get('email'),
'phone': post.get('phone'),
'linkedin_url': post.get('linkedin'),
'position': post.get('position'),
})
# Redirect to a thank you page (or reuse the same page with a success message)
return request.redirect('/careers/thank-you')
@http.route('/careers/thank-you', type='http', auth='public', website=True)
def thank_you_page(self, **kwargs):
return request.render('my_module.application_success_template')
Step 4: Design the Form Template
We'll use Odoo's QWeb engine to define the HTML structure. We can leverage standard Bootstrap classes for styling.
<template id="apply_form_template" name="Job Application Form">
<t t-call="website.layout">
<div id="wrap" class="oe_structure">
<div class="container py-5">
<div class="row justify-content-center">
<div class="col-lg-8">
<div class="card shadow-sm border-0">
<div class="card-body p-5">
<h2 class="text-center mb-4">Join Our Team</h2>
<form action="/careers/submit" method="post" enctype="multipart/form-data">
<!-- CRITICAL: CSRF Token for security -->
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
<div class="mb-3">
<label for="name" class="form-label">Full Name *</label>
<input type="text" class="form-control" name="name" required="1"/>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label for="email" class="form-label">Email Address *</label>
<input type="email" class="form-control" name="email" required="1"/>
</div>
<div class="col-md-6 mb-3">
<label for="phone" class="form-label">Phone Number</label>
<input type="tel" class="form-control" name="phone"/>
</div>
</div>
<div class="mb-3">
<label for="position" class="form-label">Position *</label>
<select class="form-select" name="position">
<option value="developer">Developer</option>
<option value="designer">Designer</option>
<option value="manager">Product Manager</option>
</select>
</div>
<div class="mb-3">
<label for="linkedin" class="form-label">LinkedIn Profile URL</label>
<input type="url" class="form-control" name="linkedin" placeholder="https://linkedin.com/in/..."/>
</div>
<div class="d-grid gap-2 mt-4">
<button type="submit" class="btn btn-primary btn-lg">Submit Application</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
</t>
</template>
Never forget the csrf_token hidden input! Odoo's secure controllers require this to prevent Cross-Site Request Forgery attacks. Without it, your form submission will return a 400 Bad Request error.
Once you install the module, navigate to /careers/apply. You should see your new form. Submissions will instantly appear in your job.application.form backend view.
Frequently Asked Questions
In a public web form, the user making the request is usually the unauthenticated 'Public User', who has very limited access rights. `sudo()` allows the code to run with Superuser privileges to create the record in the backend without getting an Access Denied error.
Yes! By wrapping your content in `
First, add `enctype='multipart/form-data'` to the `
In your controller's `apply_page` method, you can pass `request.env.user` to the template. In the XML, you can use `t-att-value='user.name'` on the input fields to set the default value to the logged-in user's name.
The **Cross-Site Request Forgery (CSRF)** token is a security measure. It ensures that the form submission is coming from your actual website and not a malicious third-party site executing actions on behalf of the user. Odoo controllers with `csrf=True` (default) verify this token automatically.
