How to Add a One2Many Field in Odoo 18 Website Forms (Complete Guide)

Learn how to create and display a One2Many field in Odoo 18 Website forms. Step-by-step tutorial for developers working with Odoo web module.

Jul 16, 2025 - 13:14
 7
How to Add a One2Many Field in Odoo 18 Website Forms (Complete Guide)


?How to Create a One2Many Field in Odoo 18 Website Forms: Full Guide

Odoo for small business owners provides powerful tools to streamline operations especially when it comes to handling internal processes like inventory and request tracking. One such functionality is the One2Many field, which enables the creation of dynamic forms where one main record can link to multiple related records.

This is particularly useful in scenarios like submitting a material request with multiple items directly from the website.


? Understanding One2Many Relationships in Odoo

In Odoo, a One2Many field allows multiple child records (from a secondary model) to be associated with a single parent record. To configure this, you'll specify the related model and the inverse field that links back to the main model.

Model Example:

from odoo import models, fields

class MaterialRequest(models.Model):
    _name = 'material.request'
    _description = 'Material Request'

    employee_id = fields.Many2one('res.users', string="Requested By", required=True)
    date = fields.Date(string="Request Date", required=True)
    material_order_ids = fields.One2many('material.order', 'request_id', string='Materials')


class MaterialOrder(models.Model):
    _name = 'material.order'
    _description = 'Material Order Line'

    request_id = fields.Many2one('material.request', string='Material Request')
    material = fields.Many2one('product.product', string='Material', required=True)
    operation_id = fields.Many2one('stock.picking.type', string='Internal Transfer', required=True)
    quantity = fields.Float(string='Quantity', required=True)
    source = fields.Many2one('stock.location', string='Source Location')
    destination = fields.Many2one('stock.location', string='Destination Location')

Here, MaterialRequest acts as the parent model, and MaterialOrder is the child model containing multiple order lines linked via a Many2one field (request_id).


? Displaying One2Many Fields on the Website

To let users submit material requestsincluding multiple materialsthrough the website, follow these steps:


1. Create a Website Menu Item

Add the following XML snippet to your module to register a new menu item:

<record id="material_request_website_menu" model="website.menu">
    <field name="name">Material Request</field>
    <field name="url">/material_request</field>
    <field name="parent_id" ref="website.main_menu"/>
    <field name="sequence" type="int">90</field>
</record>

2. Build the Web Form Template

This XML template renders a form where users can input material request details and add/remove material lines dynamically.

<template id="web_machine_request_template">
    <t t-call="website.layout">
        <div id="wrap">
            <div class="container">
                <h3>Online Material Request</h3>
                <form enctype="multipart/form-data" class="o_mark_required">
                    <input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
                    <!-- User Selection -->
                    <select name="customer" class="form-control" required="1">
                        <t t-foreach="customer" t-as="cust">
                            <option t-att-value="cust['id']"><t t-esc="cust['name']"/></option>
                        </t>
                    </select>
                    <!-- Date -->
                    <input type="date" name="date" class="form-control" required="1"/>

                    <!-- Material Lines Table -->
                    <table class="table" id="material_table">
                        <thead>
                            <tr>
                                <th>Material *</th>
                                <th>Quantity *</th>
                                <th>Operation *</th>
                                <th>Source *</th>
                                <th>Destination *</th>
                                <th></th>
                            </tr>
                        </thead>
                        <tbody>
                            <tr class="material_order_line">
                                <!-- Dynamic Dropdowns -->
                                <!-- Filled by controller -->
                            </tr>
                        </tbody>
                    </table>
                    <button type="button" class="btn add_total_project">Add Material</button>
                    <button type="button" class="btn btn-primary custom_create">Create Request</button>
                </form>
            </div>
        </div>
    </t>
</template>

3. Controller to Render Template

Define a controller to serve the data (products, customers, operations, locations) to the template:

@http.route('/material_request', auth='public', website=True)
def material_request(self):
    products = request.env['product.product'].sudo().search([])
    customer = request.env['res.users'].sudo().search([])
    locations = request.env['stock.location'].sudo().search([])
    operations = request.env['stock.picking.type'].sudo().search([])
    return request.render('website_one2many.web_machine_request_template', {
        'products': products,
        'customer': customer,
        'locations': locations,
        'operations': operations,
    })

4. JavaScript to Handle Form Logic

Use a custom JS file to add/remove rows and handle submission via RPC:

/** @odoo-module **/
import publicWidget from "@web/legacy/js/public/public_widget";
import { rpc } from '@web/core/network/rpc';

publicWidget.registry.MaterialRequest = publicWidget.Widget.extend({
    selector: "#wrap",
    events: {
        'click .add_total_project': '_onClickAddMaterial',
        'click .remove_line': '_onClickRemoveLine',
        'click .custom_create': '_onClickSubmit',
    },

    _onClickAddMaterial: function () {
        let $newRow = $('#material_table tbody tr:first').clone();
        $newRow.find('input, select').val('');
        $('#material_table tbody').append($newRow);
    },

    _onClickRemoveLine: function (ev) {
        if ($('#material_table tbody tr').length > 1) {
            $(ev.target).closest('tr').remove();
        } else {
            alert("At least one material line is required.");
        }
    },

    _onClickSubmit: async function (ev) {
        ev.preventDefault();
        let employee_id = $('#customer').val();
        let date = $('#date').val();
        let material_order_ids = [];

        $('#material_table tbody tr').each(function () {
            material_order_ids.push({
                'material': $(this).find('select[name="product"]').val(),
                'quantity': $(this).find('input[name="quantity"]').val(),
                'operation_id': $(this).find('select[name="operation"]').val(),
                'source': $(this).find('select[name="source"]').val(),
                'destination': $(this).find('select[name="destination"]').val()
            });
        });

        try {
            let result = await rpc('/material/submit', {
                employee_id, date, material_order_ids
            });
            alert('Material Request Submitted Successfully');
        } catch (error) {
            alert('Submission Failed');
        }
    }
});

5. Backend Controller to Handle Submission

This controller receives data and dynamically builds One2Many lines:

@http.route('/material/submit', type='json', auth='public', website=True)
def request_submit(self, **post):
    model_name = 'material.request'
    model_fields = request.env['ir.model.fields'].sudo().search([('model', '=', model_name)])
    values = {}
    
    for key, val in post.items():
        field = model_fields.filtered(lambda f: f.name == key)
        if not field:
            continue
        if field.ttype == 'many2one':
            val = int(val) if val else False
        elif field.ttype == 'one2many':
            relation_fields = request.env['ir.model.fields'].sudo().search([('model', '=', field.relation)])
            one2many_lines = []
            for line in val:
                line_data = {}
                for sub_key, sub_val in line.items():
                    sub_field = relation_fields.filtered(lambda f: f.name == sub_key)
                    if sub_field:
                        if sub_field.ttype == 'many2one':
                            sub_val = int(sub_val) if sub_val else False
                        elif sub_field.ttype in ['integer', 'float']:
                            sub_val = float(sub_val) if sub_val else 0
                        elif sub_field.ttype == 'boolean':
                            sub_val = str(sub_val).lower() in ['true', '1', 'yes']
                    line_data[sub_key] = sub_val
                one2many_lines.append((0, 0, line_data))
            val = one2many_lines
        values[key] = val

    record = request.env[model_name].sudo().create(values)
    return {'success': True, 'record_id': record.id}

? Conclusion

This method enables a seamless experience for users submitting data on the website while leveraging Odoo's One2Many relationships in the backend. Whether you're managing inventory, requests, or custom business flows, this setup makes Odoo for small business a flexible and scalable solution.


Book an implementation consultant today.