In Odoo, a One2many field allows the inclusion of sections and notes, which help users group related records into meaningful categories. Notes offer a way to add additional information or comments within the context of specific records. For example, in a sale order line, users can use sections and notes to better organize and manage items.
In this blog, we'll explore how to add sections and notes to a custom model. To begin, we'll create a new model and include a One2many field.
from odoo import models, fields, _
from datetime import datetime
class WarrantyRequest(models.Model):
_name = 'warranty.request'
_description = 'Warranty Request'
_inherit = ['mail.thread', 'mail.activity.mixin']
partner_id = fields.Many2one('res.partner',
string='Customer')
date = fields.Date(string='Date', default=datetime.today())
name = fields.Char(string="Sequence Number", readonly=True,
required=True, copy=False, default=_('New'))
state = fields.Selection(
[('draft', 'Draft'), ('to_approve', 'To approve'),
('approved', 'Approved'), ('cancelled', 'Cancelled')],
string='Status', default='draft', clickable=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')
And then add the warranty. request.line model,
from odoo import _, api, fields, models
from odoo.exceptions import UserError
class WarrantyRequestLine(models.Model):
_name = "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 quantity IS NOT NULL))",
"Missing required fields on accountable warranty request line."),
('non_accountable_null_fields',
"CHECK(display_type IS NULL OR (product_id IS NULL AND quantity = 0))",
"Forbidden values on non-accountable warranty request line"),
]
warranty_id = fields.Many2one('warranty.request')
sequence = fields.Integer(
string="Sequence",
help="Gives the sequence order when displaying a list of sale quote lines.",
default=10)
product_id = fields.Many2one(
comodel_name='product.product',
check_company=True)
name = fields.Text(
string="Description",
translate=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',
required=True,
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 option in self:
option.product_uom_id = option.product_id.uom_id
@api.model_create_multi
def create(self, vals_list):
for vals in vals_list:
if vals.get('display_type', self.default_get(['display_type'])['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 sale quote line. Instead you should delete the current line and create a new line of the proper type."))
return super().write(values)
And then add the corresponding XML file,
<record id="warranty_request_form_view" model="ir.ui.view">
<field name="name">warranty.request.form.view</field>
<field name="model">warranty.request</field>
<field name="arch" type="xml">
<form>
<sheet>
<group>
<group>
<field name='partner_id' required="True"/>
</group>
<group>
<field name='date'/>
<field name='warranty_period'/>
<field name='warranty_expire_date'/>
</group>
</group>
<notebook>
<page id="product" name="Product">
<field name="warranty_line_ids" widget="section_and_note_one2many">
<list string="Product Lines" editable="bottom">
<control>
<create name="add_product_control"
string="Add product"/>
<create name="add_section_control" string="Add a section"
context="{'default_display_type': 'line_section'}"/>
<create name="add_note_control" string="Add a note"
context="{'default_display_type': 'line_note'}"/>
</control>
<field name="display_type" column_invisible="True"/>
<field name="sequence" widget="handle"/>
<field name="product_id" required="not display_type"/>
<field name="name"/>
<field name="product_uom_qty"/>
<field name="product_uom_id"
required="not display_type"/>
</list>
</field>
</page>
<page id="product" name="Description">
<field name="description"
readonly="state != 'draft'"/>
</page>
</notebook>
</sheet>
<chatter/>
</form>
</field>
</record>
It results in the following view: By default, we get the ‘Add a line’ control button.
To include the section and note in the one2many model, follow these steps:
1. Incorporate the display type field into the one2many field model.
Include the display type field in the one2many field model. The model should include the 'name' field for the section and note, along with the display_type field, as shown below:
display_type = fields.Selection(
selection=[
('line_section', "Section"),
('line_note', "Note"),
],
default=False)
name = fields.Char('Description')
2. Add the create and write function as follows:
@api.model_create_multi
def create(self, vals_list):
for vals in vals_list:
if vals.get('display_type', self.default_get(['display_type'])['display_type']):
vals.update(product_id=False, product_uom_qty=0, product_uom_id=False)
return super().create(vals_list)
It ensures that when display_type is set, product_id is set to False and quantity is 0; this enforces the rule that lines with display_type must not have values for product or quantity.
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 sale quote line. Instead you should delete the current line and create a new line of the proper type."))
return super().write(values)
It guarantees that when display_type is set, product_id is set to False and quantity is set to 0; This enforces the rule that lines with display_type should not contain values for product or quantity.
3. Defines SQL constraints
This ensures data integrity by enforcing that a warranty request line is either a display line (with display_type set) or an accountable line (with product_id and quantity set), but not a combination of both.
_sql_constraints = [
('accountable_required_fields',
"CHECK(display_type IS NOT NULL OR (product_id IS NOT NULL AND quantity IS NOT NULL))",
"Missing required fields on accountable warranty request line."),
('non_accountable_null_fields',
"CHECK(display_type IS NULL OR (product_id IS NULL AND quantity = 0))",
"Forbidden values on non-accountable warranty request line"),
]
The non_accountable_null_fields constraint guarantees that for lines with display_type set, the product_id must be empty, and the quantity must be zero.
The non_accountable_null_fields constraint ensures that when display_type is set, the product_id must remain empty, and the quantity must be set to zero.
4. Add the ‘section_and_note_one2many’ widget and control section in the one2many tree view.
a) Add the ‘section_and_note_one2many’ widget in the field:
<field name="warranty_line_ids" widget="section_and_note_one2many">
b) Add control tag inside tree tag:
These controls enable users to quickly add new records with preset contexts for sections and notes.
<control>
<create name="add_product_control" string="Add a product"/>
<create name="add_section_control" string="Add a section"
context="{'default_display_type': 'line_section'}"/>
<create name="add_note_control" string="Add a note"
context="{'default_display_type': 'line_note'}"/>
</control>
* Product Control: Without any specific context, this will generate a standard product line.
* Section Control: Sets the display_type to "line_section" by default.
* Note Control: Sets the display_type to "line_note" by default.
c) Include the display_type field in the view
The display_type field is added to the view but is hidden in the list/tree view.
<field name="display_type" column_invisible="1"/>
d) Change the required condition for the mandatory field.
<field name="product_id"
required="not display_type"/>
<field name="quantity"
required="not display_type"/>
Ensure that the product_id and quantity fields are required only when display_type is not set.
In the One2many tree view, users can add both sections and notes, as shown in the screenshot below.
In Odoo, the use of sections and notes within the One2Many field provides various advantages that enhance organization, user experience, and overall efficiency. Sections allow users to easily separate and identify different categories or data sets within a form or view, improving information readability. Meanwhile, notes offer a way to include additional details or comments directly related to specific records.
To read more about How to Add a Section & Note for a Custom Module in Odoo 17, refer to our blog How to Add a Section & Note for a Custom Module in Odoo 17.