Skip to main content
iVentureTeam

so_template_line_o2m

Quotation templates gained optional sections in Odoo 19, and this widget is what keeps them honest: move a line into one and its quantity becomes zero, move it out and the quantity comes back.

Siddharth JambukiyaSiddharth JambukiyaOdoo Techno-Functional Consultant
August 27, 2026Updated August 27, 20267 min read
Technical nameso_template_line_o2m
Field typesone2many
Viewsform, with an embedded list
Modulesale_management, the quotation templates app
Used in core1 occurrence, the Lines tab of the quotation template form in sale_management
VersionsOdoo 20.0, Odoo 19.0
No-code setupNo. It is a view XML widget tied to the quotation template line model
Alternativesso_line_field, section_and_note_text, product_label_section_and_note_field, one2many

What the quotation template lines does

A quotation template is a pre-built quote: sections, products, quantities. Odoo 19 added the idea that a whole section can be optional, meaning the customer sees it as an upsell rather than as part of the price. The convention for that is simple, a product line inside an optional section carries a quantity of zero, and this widget is what enforces the convention while the template is being edited.

It builds on the same section-and-note list that sale orders and invoices use, so sections, notes, subsections and drag-to-reorder all work the way people expect. What it adds is the optional flag, a way to toggle it on a section, coloring for those sections, and the quantity bookkeeping that has to follow every structural change.

That bookkeeping is the real content of the widget. A line can become optional or stop being optional in four different ways: by being created under a section, by being inserted from a dropdown, by the section itself being toggled, or by being dragged. Each of those paths has to end with the right quantity, and each is handled explicitly.

What this means for your team

Optional sections are a pricing tactic with a real conversion effect. A quote that shows the core scope at a firm price, with clearly marked add-ons underneath, is easier to approve than one bundled price, and it gives the buyer something to say yes to beyond the total. Templates make that structure repeatable across a sales team instead of dependent on whoever writes the quote.

What makes it work operationally is that the zero quantity is not a manual discipline. A salesperson dragging a line into the optional block does not have to remember to zero it, and a line pulled back into scope does not stay at zero and silently drop out of the total. Both are the kind of mistake that is invisible until the customer signs a quote missing a line item.

The thing to brief a sales team on is that the quantity rewrite is automatic and does overwrite what they typed. A line moved into an optional section loses its quantity, and moving it back gives it one, not whatever it had before. For quantities that matter, set them after the line is in its final position.

Supported options in Odoo 19

The widget declares no options of its own. It spreads the section-and-note one2many descriptor, so the options below are inherited from it, along with the attribute it reads. Core switches on only one of them. Read from the widget's source and the shared section-and-note field, Odoo 19.0.

OptionTypeWhat it does
subsectionsbooleanInherited from the section-and-note list. Enables a second level of section nesting. This is the option core turns on for quotation templates.(default: false)
hide_pricesbooleanInherited, and read in the base extractor without being declared. Hides price columns in the embedded list.(default: false)
hide_compositionbooleanInherited and undeclared in the same way. Hides the composition display of combo or kit lines.(default: false)
aggregated_fieldsstring (attribute)Inherited. A comma-separated list of fields to aggregate on section rows. Written as an attribute on the field element, not in options.

The optional-section behavior is not configurable. The field name it toggles, the zero quantity it writes and the reset to one are all hardcoded, and the widget reads the quantity field by name. This is a widget built for one model, unlike the generic section list it extends.

Working examples

The core usage

<field name="sale_order_template_line_ids"
       widget="so_template_line_o2m"
       options="{'subsections': True}">
  <form> ... </form>
</field>

Subsections are the only inherited option core turns on here. Everything else about optional sections comes from the widget.

Hiding prices in a variant of the list

<field name="line_ids" widget="so_template_line_o2m"
       options="{'subsections': True, 'hide_prices': True}"/>

hide_prices and hide_composition are inherited from the shared section list and are read in its extractor without being declared.

The fields the behavior depends on

<!-- on the child model -->
<field name="is_optional"/>       <!-- toggled on sections -->
<field name="product_uom_qty"/>  <!-- rewritten to 0 or 1 -->
<field name="display_type"/>     <!-- distinguishes structural rows -->

All three names are written literally in the widget. A model that calls them something else will render but not behave.

The quantity bookkeeping behind a drag

The interesting code is the drag-and-drop handler, because it is the only path where the widget cannot know in advance what changed. Before the drop is applied it builds a map of every row whose optional state might be affected, recording what that state is right now. After the drop it recomputes each one and issues a quantity update only where the state actually flipped: zero where a line became optional, one where a line stopped being optional and was still sitting at zero.

Which rows go into that map depends on what moved. Moving a plain line only affects that line. Moving a section is more work: dragging it upwards means every row it carries with it plus everything it passed on the way, and dragging it downwards means every row under every section between the old and new positions, with overlapping rows skipped so nothing is counted twice.

Toggling a section's optional flag is the simpler path and takes a different route: it issues one update for the section itself and one for each row underneath, setting the quantity to zero when turning optional on, and restoring it to the row's existing quantity or one when turning it off. Note the asymmetry, turning optional off restores a value, turning it on discards one.

Two smaller behaviors round it out. New lines created under an optional section get a default quantity of zero through the create context rather than through a later update, which avoids a visible flicker. And rows in an optional section are given the primary text color, which is how the section reads as optional at a glance.

Version compatibility

VersionStatusNotes
Odoo 20.0In developmentNot released. The development branch adds section quantity ratios and a unit of measure column; optional sections are unchanged.
Odoo 19.0VerifiedFirst version. Verified against the shipped source.
Odoo 18.0Not availableWidget does not exist. Template lines used the generic section-and-note list.
Odoo 17.0Not availableWidget does not exist.
Odoo 16.0Not availableWidget does not exist.

Upgrade note. The widget is new in Odoo 19, together with optional sections on quotation templates. On Odoo 18 and earlier the template lines used the generic section-and-note list, so an upgrade gains the feature without any view change. Custom quotation template views that replaced the lines field should switch the widget name to pick up the new behavior.

What is changing in Odoo 20

Odoo 20 is expected at Odoo Experience in Brussels, 24 to 26 September 2026. The development branch is unstable and the notes below can still change; we re-verify the page after release.

The widget grows considerably. The development branch adds a quantity-ratio adjustment across a section, so changing a section-level quantity scales the lines under it, and publishes that helper plus the optional check into the environment for child components to call.

A unit of measure column appears in the section column set, and the renderer hides it when the underlying field is optional and not switched on, so the column does not occupy width it is not using.

The optional-section behavior described on this page is unchanged, and core still uses the widget on the quotation template form.

Common problems and fixes

SymptomCause and fix
A line I moved lost its quantityIt was dropped into an optional section, and the widget writes a quantity of zero for those lines. Set the quantity after the line is in its final position, or move it out of the optional section.
A line came back with quantity one, not its old valueLeaving an optional section resets a zero quantity to one; the previous value is not remembered. Re-enter the quantity after moving the line out.
Toggling a section optional did not update its linesThe rows under it are identified by position, so a structural row in between can end the section early. Check the section boundaries and the display type of the rows underneath.
Optional sections are not colored differentlyThe row class depends on the optional flag being loaded and true on the section. Make sure the flag field exists on the model and is part of the list.
The widget renders but nothing is optionalThe child model does not carry the field names the widget writes literally. Use it only on the quotation template line model, or align your model's field names.
Subsections are not availableThe inherited subsections option is off by default. Add options="{'subsections': True}" as the core view does.

Quotation template lines vs the alternatives

WidgetBest forKey difference
so_template_line_o2mQuotation templates that separate core scope from optional add-onsAdds optional sections to the section list and rewrites line quantities whenever the structure changes
so_line_fieldIndividual sale order line cellsA cell widget rather than a list renderer
section_and_note_textThe text cell inside a sectioned listRenders section and note rows as free text, no quantity logic
product_label_section_and_note_fieldThe product cell in sectioned document linesHandles the product label and description, not the section structure
one2manyAn ordinary list of child recordsNo sections, no optional flag and no quantity bookkeeping

For any other sectioned document, the generic section-and-note list is the right base: sale orders, invoices and purchase orders all use it directly. This widget only makes sense on a model that has an optional flag and a quantity field with the names it expects. What is worth copying, if you build something similar, is the before-and-after state map around the drop handler rather than the widget itself.

Frequently asked questions

What makes a section optional in a quotation template?+
A flag on the section row. The widget toggles it and then rewrites the quantity of every line under that section: zero when the section becomes optional, and back to one when it stops being optional.
Why did my line's quantity change to zero?+
Because it moved into an optional section. That happens on drag and drop, on creation under the section and on inserting a line from the dropdown, all handled explicitly by the widget.
Does moving a line out restore its old quantity?+
No. If the quantity is zero when the line leaves an optional section it is set to one. The previous value is not stored anywhere.
Which options does it support?+
None of its own. It inherits the section-and-note list's options, of which core enables subsections, plus the undeclared hide_prices and hide_composition keys and the aggregated fields attribute.
Can I use it on another model?+
Not usefully. It writes the optional flag, the quantity field and the display type by literal name, so it only behaves correctly on quotation template lines.

Quotes that close faster, built once as a template

Optional sections, tiered scopes and templated pricing turn quoting from a writing task into a choice the customer makes. We configure Odoo Sales templates and the approval flow around them so your team quotes the same way every time.

Book a free consultation

How this page was produced

The optional-section toggle, the create and insert contexts, the drag-and-drop state map and the quantity rules were read from sale_order_template_line_field.js on the Odoo 19.0 branch, and the inherited option set from the shared section-and-note field in the account module. The usage and the enabled option come from sale_management/views/sale_order_template_views.xml. The Odoo 20 notes come from a full comparison against the public development branch. Spotted an error? Tell us and we will correct the page.