File Upload in Website Forms Odoo 18
By Braincuber Team
Published on February 4, 2026
File uploads on website forms require special handling. Unlike backend binary fields with automatic widget support, website forms need explicit file input handling, base64 encoding, and attachment creation. The process involves multipart form encoding, CSRF protection, and proper linking between attachments and your target model.
This tutorial builds a complete document upload feature for a Support Ticket system. Visitors submit tickets through a website form, attaching screenshots or documents. The controller processes the upload, creates an ir.attachment record, and links it to the ticket. All files appear in the backend for support staff to review.
What You'll Build: A Support Ticket form with file upload capability. Files are encoded, stored as attachments, and linked to ticket records—viewable in both frontend confirmation and backend ticket view.
How File Uploads Work in Odoo
multipart/form-data
Forms must use enctype="multipart/form-data" to transmit files. Without this, file inputs send only the filename, not the actual content.
Base64 Encoding
Odoo stores binary data as base64. Read the file with file.read() and encode with base64.b64encode() before saving.
ir.attachment Model
Odoo's built-in attachment model stores files with metadata. Link attachments to records using res_model and res_id fields.
Module Structure
Implementation Steps
Create the Support Ticket Model
Define a model with a Many2many field linking to ir.attachment.
from odoo import models, fields, api
class SupportTicket(models.Model):
_name = 'support.ticket'
_description = 'Support Ticket'
_order = 'create_date desc'
name = fields.Char(string='Ticket Number', readonly=True, copy=False)
subject = fields.Char(string='Subject', required=True)
email = fields.Char(string='Email', required=True)
description = fields.Text(string='Description')
priority = fields.Selection([
('low', 'Low'),
('medium', 'Medium'),
('high', 'High'),
('urgent', 'Urgent'),
], string='Priority', default='medium')
state = fields.Selection([
('new', 'New'),
('in_progress', 'In Progress'),
('resolved', 'Resolved'),
('closed', 'Closed'),
], string='Status', default='new')
# Attachment field - key for file uploads
attachment_ids = fields.Many2many(
'ir.attachment',
string='Attachments',
help='Files attached to this ticket'
)
attachment_count = fields.Integer(
compute='_compute_attachment_count',
string='Attachment Count'
)
@api.depends('attachment_ids')
def _compute_attachment_count(self):
for ticket in self:
ticket.attachment_count = len(ticket.attachment_ids)
@api.model_create_multi
def create(self, vals_list):
for vals in vals_list:
if not vals.get('name'):
vals['name'] = self.env['ir.sequence'].next_by_code(
'support.ticket'
) or 'New'
return super().create(vals_list)
Key Field: The attachment_ids Many2many field links tickets to ir.attachment records. Multiple files can be attached to a single ticket.
Create the Backend View
Add the attachments field to the ticket form view using the many2many_binary widget.
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<!-- Sequence for ticket numbers -->
<record id="seq_support_ticket" model="ir.sequence">
<field name="name">Support Ticket Sequence</field>
<field name="code">support.ticket</field>
<field name="prefix">TKT-</field>
<field name="padding">5</field>
</record>
<!-- Form View -->
<record id="support_ticket_form_view" model="ir.ui.view">
<field name="name">support.ticket.form</field>
<field name="model">support.ticket</field>
<field name="arch" type="xml">
<form>
<header>
<field name="state" widget="statusbar"
statusbar_visible="new,in_progress,resolved,closed"/>
</header>
<sheet>
<div class="oe_title">
<h1><field name="name"/></h1>
</div>
<group>
<group>
<field name="subject"/>
<field name="email"/>
<field name="priority"/>
</group>
</group>
<notebook>
<page string="Description">
<field name="description"/>
</page>
<page string="Attachments">
<!-- many2many_binary widget for file management -->
<field name="attachment_ids" widget="many2many_binary"/>
</page>
</notebook>
</sheet>
</form>
</field>
</record>
<!-- Tree View -->
<record id="support_ticket_tree_view" model="ir.ui.view">
<field name="name">support.ticket.tree</field>
<field name="model">support.ticket</field>
<field name="arch" type="xml">
<tree>
<field name="name"/>
<field name="subject"/>
<field name="email"/>
<field name="priority"/>
<field name="attachment_count"/>
<field name="state" widget="badge"
decoration-info="state == 'new'"
decoration-warning="state == 'in_progress'"
decoration-success="state == 'resolved'"/>
</tree>
</field>
</record>
<!-- Action and Menu -->
<record id="action_support_tickets" model="ir.actions.act_window">
<field name="name">Support Tickets</field>
<field name="res_model">support.ticket</field>
<field name="view_mode">tree,form</field>
</record>
<menuitem id="menu_support_root" name="Support"/>
<menuitem id="menu_support_tickets"
name="Tickets"
parent="menu_support_root"
action="action_support_tickets"/>
</odoo>
Widget Choice: The many2many_binary widget provides a user-friendly upload/download interface in backend forms. It handles display, deletion, and addition of files automatically.
Create the Website Form Template
Build an HTML form with file input and proper encoding.
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<!-- Support Ticket Form Page -->
<template id="support_ticket_form_page">
<t t-call="website.layout">
<div id="wrap" class="container py-5">
<div class="row justify-content-center">
<div class="col-lg-8">
<div class="card shadow-lg border-0">
<div class="card-header bg-primary text-white py-4">
<h2 class="mb-0">
<i class="fa fa-ticket me-2"/>
Submit Support Ticket
</h2>
<p class="mb-0 mt-2 opacity-75">
Describe your issue and attach relevant files
</p>
</div>
<div class="card-body p-4">
<!--
CRITICAL: enctype="multipart/form-data"
Without this, files won't be transmitted
-->
<form action="/support/submit"
method="POST"
enctype="multipart/form-data">
<!-- CSRF Token -->
<input type="hidden" name="csrf_token"
t-att-value="request.csrf_token()"/>
<div class="mb-3">
<label class="form-label">
Email Address <span class="text-danger">*</span>
</label>
<input type="email" name="email"
class="form-control" required="required"
placeholder="your@email.com"/>
</div>
<div class="mb-3">
<label class="form-label">
Subject <span class="text-danger">*</span>
</label>
<input type="text" name="subject"
class="form-control" required="required"
placeholder="Brief description of issue"/>
</div>
<div class="mb-3">
<label class="form-label">Priority</label>
<select name="priority" class="form-select">
<option value="low">Low</option>
<option value="medium" selected="selected">Medium</option>
<option value="high">High</option>
<option value="urgent">Urgent</option>
</select>
</div>
<div class="mb-3">
<label class="form-label">
Description <span class="text-danger">*</span>
</label>
<textarea name="description" class="form-control"
rows="5" required="required"
placeholder="Please describe your issue in detail..."></textarea>
</div>
<!-- File Upload Field -->
<div class="mb-4">
<label class="form-label">
<i class="fa fa-paperclip me-1"/>
Attach File (Screenshot, Document)
</label>
<input type="file" name="attachment"
class="form-control"
accept=".pdf,.doc,.docx,.png,.jpg,.jpeg,.gif"/>
<small class="text-muted">
Accepted: PDF, DOC, DOCX, PNG, JPG, GIF (Max 10MB)
</small>
</div>
<div class="d-grid">
<button type="submit" class="btn btn-primary btn-lg">
<i class="fa fa-paper-plane me-2"/>
Submit Ticket
</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
</t>
</template>
<!-- Thank You Page -->
<template id="support_ticket_thanks">
<t t-call="website.layout">
<div id="wrap" class="container py-5">
<div class="row justify-content-center">
<div class="col-lg-6 text-center">
<div class="card shadow border-0 p-5">
<div class="mb-4">
<i class="fa fa-check-circle text-success"
style="font-size: 80px;"/>
</div>
<h2 class="mb-3">Ticket Submitted!</h2>
<p class="text-muted mb-2">
Your ticket number is:
</p>
<h3 class="text-primary mb-4">
<t t-esc="ticket.name"/>
</h3>
<p class="text-muted mb-4">
We'll respond to <strong><t t-esc="ticket.email"/></strong>
as soon as possible.
</p>
<a href="/" class="btn btn-primary">
<i class="fa fa-home me-2"/>Return to Homepage
</a>
</div>
</div>
</div>
</div>
</t>
</template>
</odoo>
Create the Controller with File Handling
Process the form submission, encode the file, and create attachment records.
import base64
from odoo import http
from odoo.http import request
class SupportTicketController(http.Controller):
@http.route('/support', type='http', auth='public', website=True)
def support_form(self):
"""Render the support ticket form."""
return request.render(
'support_ticket_upload.support_ticket_form_page'
)
@http.route('/support/submit', type='http', auth='public',
website=True, methods=['POST'], csrf=True)
def submit_ticket(self, **post):
"""Handle ticket submission with file upload."""
# Create the ticket record first
ticket_vals = {
'email': post.get('email'),
'subject': post.get('subject'),
'description': post.get('description'),
'priority': post.get('priority', 'medium'),
}
ticket = request.env['support.ticket'].sudo().create(ticket_vals)
# Process file upload if present
uploaded_file = post.get('attachment')
if uploaded_file and uploaded_file.filename:
# Read file content and encode to base64
file_content = uploaded_file.read()
file_data = base64.b64encode(file_content)
# Create attachment record
attachment = request.env['ir.attachment'].sudo().create({
'name': uploaded_file.filename,
'type': 'binary',
'datas': file_data,
'res_model': 'support.ticket',
'res_id': ticket.id,
})
# Link attachment to ticket using Command.link()
ticket.sudo().write({
'attachment_ids': [(4, attachment.id)],
})
# Render thank you page with ticket reference
return request.render(
'support_ticket_upload.support_ticket_thanks',
{'ticket': ticket}
)
File Processing Steps: (1) Check if file exists and has filename, (2) Read binary content with file.read(), (3) Encode with base64.b64encode(), (4) Create ir.attachment with res_model and res_id, (5) Link to ticket using (4, id) command.
Create Module Init Files
Set up proper Python imports for the module.
from . import models from . import controllers
from . import support_ticket
from . import main
Create the Module Manifest
Define module metadata and dependencies.
{
'name': 'Support Ticket with File Upload',
'version': '18.0.1.0.0',
'category': 'Website',
'summary': 'Submit support tickets with file attachments via website',
'description': """
Allow website visitors to submit support tickets
with file attachments. Files are stored as
ir.attachment records linked to tickets.
""",
'depends': ['website', 'base'],
'data': [
'security/ir.model.access.csv',
'views/support_ticket_views.xml',
'views/website_form.xml',
],
'installable': True,
'application': True,
'license': 'LGPL-3',
}
Set Up Security Access
Grant permissions for the new model.
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink access_support_ticket_user,support.ticket.user,model_support_ticket,base.group_user,1,1,1,0 access_support_ticket_manager,support.ticket.manager,model_support_ticket,base.group_system,1,1,1,1
Conclusion
Website file uploads in Odoo 18 require explicit handling—multipart encoding on the form, base64 conversion in the controller, and proper attachment linking. The ir.attachment model provides standardized storage with automatic file handling. Use Many2many fields with many2many_binary widget for backend display and (4, id) commands to link attachments to records. This pattern works for any model—job applications, expense claims, project documents, or support tickets.
Key Takeaways: Use enctype="multipart/form-data" on forms. Encode with base64.b64encode(file.read()). Create ir.attachment with res_model and res_id. Link using (4, attachment.id). Use many2many_binary widget in backend views.
