How to Add Section and Note in Custom Odoo 19 Module: Complete Guide
In Odoo, sections and notes within a One2many field provide significant advantages that enhance organization, user experience, and overall efficiency. Sections allow users to easily separate and identify different categories or data groupings within a form, drastically improving readability. Meanwhile, notes offer a way to include additional details, terms, or comments directly related to specific records in the list. This complete step by step beginner guide walks through implementing sections and notes in a custom Odoo 19 module using the display_type field, SQL constraints, and the section_and_note_one2many widget.
What You'll Learn:
- How to create a parent model with a One2many field for warranty requests
- How to add the display_type field to support sections and notes in line items
- How to implement SQL constraints for data integrity
- How to override create and write methods for type safety
- How to configure the section_and_note_one2many widget with control buttons in XML views
Understanding Sections and Notes in Odoo 19
Sections and notes are visual elements that can be inserted into list views within Odoo forms. A section acts as a header or group separator that helps organize related line items under a common label. A note provides an inline text area for additional information, comments, or terms relevant to the lines that follow. Both elements are fully integrated with Odoo's drag-and-drop sequencing, making them powerful tools for creating professional, well-organized form layouts.
The key to implementing sections and notes lies in the display_type field, a selection field that Odoo uses internally on models like sale.order.line and purchase.order.line. It can have three values: False for standard product lines, line_section for section headers, and line_note for notes. By adding this field to your custom model and using the appropriate widget, you unlock the same functionality natively available in Odoo's core modules.
Visual Grouping
Sections act as bold category headers within the list view, separating related line items under clearly labeled groups for improved readability and organization.
Inline Notes
Notes provide inline text areas directly within the list for additional details, terms, or comments without requiring separate fields or popup windows.
Drag-and-Drop Sequencing
Sections, notes, and product lines all support drag-and-drop reordering using the sequence field with the handle widget, giving users full control over list organization.
Data Integrity
SQL constraints and Python method overrides enforce strict rules: sections and notes cannot have product data, and standard lines cannot exist without product information, preventing data corruption.
Step 1: Creating the Parent Model
We start by defining our main model that will hold the One2many relationship. For this tutorial, we create a Warranty Request model that tracks customer warranty claims with product lines, sections, and notes.
from odoo import api, fields, models, _
from odoo.exceptions import UserError
class WarrantyRequest(models.Model):
_name = 'warranty.request'
_description = 'Warranty Request'
_inherit = ['mail.thread', 'mail.activity.mixin']
partner_id = fields.Many2one('res.partner', string='Customer', required=True)
date = fields.Date(string='Date', default=fields.Date.context_today)
name = fields.Char(string="Sequence Number", readonly=True,
required=True, copy=False,
default=lambda self: _('New'))
state = fields.Selection(
[('draft', 'Draft'), ('to_approve', 'To Approve'),
('approved', 'Approved'), ('cancelled', 'Cancelled')],
string='Status', default='draft', tracking=True)
warranty_period = fields.Selection(
[('3months', '3 Months'), ('6month', '6 Months'),
('1year', '1 Year')],
string='Warranty Period', default='3months')
warranty_expire_date = fields.Date(string="Expiry Date")
warranty_line_ids = fields.One2many(
'warranty.request.line', 'warranty_id')
description = fields.Html(string='Description')
The parent model includes standard fields such as customer, date, sequence number, status, warranty period, and expiry date. The warranty_line_ids One2many field links to the line model where sections and notes will be implemented. The model also inherits from mail.thread and mail.activity.mixin for built-in messaging and activity tracking.
Step 2: Creating the Line Model with display_type
The line model is where the section and note functionality lives. We add the display_type selection field and SQL constraints to ensure data integrity. This model will store individual warranty line items that can be product lines, section headers, or notes.
class WarrantyRequestLine(models.Model):
_name = "warranty.request.line"
_description = "Warranty Request Line"
_order = 'warranty_id, sequence, id'
_sql_constraints = [
('accountable_required_fields',
"CHECK(display_type IS NOT NULL OR (product_id IS NOT NULL "
"AND product_uom_qty > 0))",
"Missing required fields on accountable warranty request line."),
('non_accountable_null_fields',
"CHECK(display_type IS NULL OR (product_id IS NULL "
"AND product_uom_qty = 0))",
"Forbidden values on non-accountable warranty request line"),
]
warranty_id = fields.Many2one(
'warranty.request', string="Warranty Reference",
ondelete="cascade")
sequence = fields.Integer(
string="Sequence",
help="Gives the sequence order when displaying list of lines.",
default=10)
product_id = fields.Many2one(
comodel_name='product.product',
check_company=True)
name = fields.Text(
string="Description",
translate=True, required=True)
product_uom_id = fields.Many2one(
'uom.uom', 'Unit of Measure',
compute='_compute_product_uom_id', store=True,
readonly=False, precompute=True)
product_uom_qty = fields.Float(
string='Quantity',
digits='Product Unit of Measure',
default=1)
display_type = fields.Selection([
('line_section', "Section"),
('line_note', "Note")], default=False)
@api.depends('product_id')
def _compute_product_uom_id(self):
for line in self:
if line.product_id:
line.product_uom_id = line.product_id.uom_id
Understanding display_type
The display_type field is a selection field that categorizes each line. A value of False (default) means it is a standard product line. line_section renders a bold section header that spans the full width. line_note renders an inline editable text area for additional information. This is the same field used internally by Odoo's sale.order.line and purchase.order.line models.
Understanding the SQL Constraints
The two SQL constraints enforce data integrity at the database level:
accountable_required_fields ensures that when display_type is NULL (meaning it is a standard product line), the product_id must be set and the quantity must be greater than 0. This prevents users from creating product lines without the necessary product data.
non_accountable_null_fields ensures that when display_type is set (to either line_section or line_note), the product_id must be NULL and the quantity must be 0. This prevents users from accidentally attaching product information to section headers or notes.
Step 3: Adding the create and write Overrides
We override the create and write methods to enforce business logic. The create method automatically clears product-related fields when a section or note is being created. The write method prevents users from changing the type of an existing line, which would cause data integrity issues.
@api.model_create_multi
def create(self, vals_list):
for vals in vals_list:
if vals.get('display_type',
self.default_get(['display_type'])
.get('display_type')):
vals.update({
'product_id': False,
'product_uom_qty': 0,
'product_uom_id': False
})
return super().create(vals_list)
def write(self, values):
if 'display_type' in values and self.filtered(
lambda line: line.display_type !=
values.get('display_type')):
raise UserError(_(
"You cannot change the type of a line. "
"Instead you should delete the current line "
"and create a new line of the proper type."))
return super().write(values)
Why the write Override Matters
Without the write override, a user could change a product line into a section or vice versa after data has been entered. This would leave orphaned product data on a section header or empty required fields on a product line. The override raises a UserError explaining that the line must be deleted and recreated with the proper type, ensuring clean data integrity.
Step 4: Configuring the XML View
The XML view definition brings everything together. We define the form view, tree view, action, and menu items. The key elements are the section_and_note_one2many widget on the One2many field and the control buttons inside the list view.
warranty.request.form.view
warranty.request
Understanding the Widget and Control Buttons
The widget="section_and_note_one2many" attribute on the warranty_line_ids field tells Odoo to handle the list rows dynamically, rendering section headers with bold styling and notes with editable text areas that span the full width.
The <control> element inside the <list> defines the buttons that appear at the top of the editable list. Each button uses a specific context to set the default_display_type for new lines:
Add Product
Creates a standard product line with display_type=False. The product_id and quantity fields become required.
Add a Section
Creates a section header with display_type='line_section'. The name field is rendered as a bold section title.
Add a Note
Creates a note with display_type='line_note'. The name field is rendered as an inline editable text area.
The fields in the list view also use special attributes. The column_invisible="True" on display_type hides it from the list columns since the widget handles the visual rendering. The required="not display_type" attribute on product_id and product_uom_id makes these fields dynamically required only when adding a product line, not when adding sections or notes.
Widget Feature: section_and_note_text
The widget="section_and_note_text" on the name field works alongside the section_and_note_one2many widget. When display_type is line_section, the name field renders as a bold, non-editable label. When display_type is line_note, it renders as an editable text area that spans the full width of the list.
Complete Module Structure
| File | Purpose |
|---|---|
| __manifest__.py | Module declaration with dependencies and data files |
| models/__init__.py | Imports warranty_request.py |
| models/warranty_request.py | Parent and line model definitions with display_type, SQL constraints, method overrides |
| views/warranty_request_views.xml | Form view with section_and_note_one2many widget and control buttons |
| security/ir.model.access.csv | Access rights for the new models |
With these steps complete, your custom Odoo 19 module will feature natively styled, drag-and-droppable sections and notes, bringing professional organization right to your users' fingertips. The same pattern used in Odoo's own sales and purchase modules is now available in your custom warranty request module.
Frequently Asked Questions
What is the display_type field and why is it needed?
display_type is a selection field that categorizes each line as a standard product (False), a section header (line_section), or a note (line_note). It is required because the section_and_note_one2many widget uses this field to determine how to render each row in the list view.
Can users change a product line to a section after creating it?
No. The write method override prevents type changes and raises a UserError explaining that the line must be deleted and recreated with the proper type. This prevents orphaned product data on section headers or empty required fields on product lines.
Is the section_and_note_one2many widget only for the warranty module?
No. This widget is a standard Odoo 19 widget that works with any model that has a proper display_type field and appropriate SQL constraints. It is the same widget used by Odoo's own sale.order.line and purchase.order.line models.
Are sections and notes included in the line sequence ordering?
Yes. The model uses _order = 'warranty_id, sequence, id' and the sequence field uses the handle widget, so sections, notes, and product lines can all be drag-and-dropped into any order within the list. Sections maintain their grouping position relative to nearby product lines.
What happens if I remove the SQL constraints from the model?
Without SQL constraints, the database would allow inconsistent data such as product lines without product information or section headers with product data. The constraints enforce these rules at the database level, preventing data corruption even if bugs exist in the UI or API-level validation.
Need Help with Odoo Custom Module Development?
Our Odoo development experts can help you build custom modules with sections and notes, implement advanced list view widgets, configure SQL constraints and method overrides, and optimize your Odoo instance for your specific business workflows.
About the author
Founder & Odoo Practice Lead, Braincuber Technologies
Founder of Braincuber. Has scoped and shipped 500+ Odoo implementations for US mid-market and global brands. Takes every founder call personally — no SDR layer between buyers and the people building the system.
