Skip to main content
iVentureTeam

so_line_field

The Sales Order Item column on Odoo timesheets is rendered by so_line_field, a many2one variant that quietly records whether a human picked the item. Here is what it actually does.

Siddharth JambukiyaSiddharth JambukiyaOdoo Techno-Functional Consultant
August 18, 2026Updated August 18, 20266 min read
Technical nameso_line_field
Field typesmany2one
Viewslist, form, kanban (timesheet views)
Modulesale_timesheet, installed with Sales + Timesheets
Used in core8 occurrences in sale_timesheet, on the timesheet list, form and grid views of account.analytic.line
VersionsOdoo 20.0, Odoo 19.0, Odoo 18.0, Odoo 17.0, Odoo 16.0
No-code setupNo. This widget is set in view XML; Studio has no equivalent
Alternativesmany2one, timesheet_uom

What the SO Item field does

On a billable timesheet line, the Sales Order Item column decides which order line the logged hours are invoiced against. Odoo could render it with the plain many2one widget, and visually you would not see any difference. so_line_field exists for one reason that is invisible on screen: it records that a human made the choice.

The component wraps the standard Many2One and overrides only the update handler. When the value changes to a different sales order item and the record's is_so_line_edited flag is not yet set, the widget sets that flag to true in the same edit. Everything else, the dropdown, the search, the create options, comes straight from the many2one base via buildM2OFieldDescription.

There is also a small readonly nicety added in 19.0: when the field is empty and readonly, the template prints the field's falsy value label in muted text, which is why non-billable lines read as a label rather than an empty cell.

What this means for your team

Timesheet-to-invoice mapping is where services companies leak revenue. Odoo computes a default sales order item for each timesheet line, but project leads reassign hours between order lines all the time: a support block here, a development package there. The business problem is what happens afterwards, when the project or task changes and the defaults are recomputed. Should the system overwrite the line a human deliberately set?

The is_so_line_edited flag this widget maintains is Odoo's answer: it marks the line as manually assigned so later recomputations can leave it alone. For your team this means two things. First, corrections made by a project manager stick. Second, if you build custom timesheet views and use the plain many2one widget instead of this one, that protection silently disappears, and users will report that "Odoo keeps changing my sales order item". That ticket is almost always a missing widget, not a bug.

Supported options in Odoo 19

Verified against so_line_field.js in the Odoo 19.0 sale_timesheet module and the many2one_field.js base it composes through buildM2OFieldDescription. The widget declares no options of its own; everything below is inherited from the many2one descriptor, including two entries that exist only in extractM2OFieldProps and appear in no documentation.

OptionTypeWhat it does
no_openbooleanDisables opening the selected sales order item in a dialog or breadcrumb from the field. Core sets it on the timesheet list view.
no_createbooleanRemoves every creation path from the dropdown. Core sets it everywhere this widget appears, since order lines should come from quotations, not timesheets.
no_quick_createbooleanRemoves only the inline Create "typed text" entry; the Create and Edit dialog remains available.
no_create_editbooleanRemoves only the Create and Edit dialog entry; inline quick-create remains available.
search_thresholdnumberMinimum characters typed before the dropdown searches. Without it, the search fires on focus, which is wasteful on large order line tables.
placeholder_fieldfield nameA char field on the timesheet line supplying a dynamic placeholder. Core uses the static placeholder attribute ("Non-billable") instead.
can_scan_barcodebooleanEnables barcode scanning for the relation on mobile. Read in extractM2OFieldProps only; it appears in no options panel or documentation.
create_name_fieldfield nameWhich field on the related model receives the typed text on quick-create. Also read only in extractM2OFieldProps; irrelevant in core usage since creation is disabled.(default: name)

The last two entries are undocumented. can_scan_barcode and create_name_field are read in the many2one's extractProps but declared nowhere, so they inherit into every many2one-based widget, this one included. Also note the two permission switches that are attributes, not options: can_create and can_write accept Python expressions on the field element and gate creation and editing before any option is considered.

Working examples

How core uses it (timesheet list)

<field name="is_so_line_edited" column_invisible="True"/>
<field name="so_line" widget="so_line_field"
       options="{'no_create': True, 'no_open': True}"
       placeholder="Non-billable"
       invisible="not allow_billable"/>

Three details worth copying: the hidden is_so_line_edited column that receives the widget's flag write, no_create so users cannot invent order lines from a timesheet, and the placeholder that labels empty lines as non-billable.

Reusing it on a custom timesheet view

<field name="is_so_line_edited" invisible="1"/>
<field name="so_line" widget="so_line_field"
       options="{'no_create': True, 'search_threshold': 2}"/>

The flag field must be present, even invisible, wherever you place the widget. search_threshold is worth adding on databases with thousands of order lines so the dropdown does not query on every focus.

How the is_so_line_edited flag really works

The entire delta over a plain many2one is one condition in the update handler, and reading it precisely avoids two wrong assumptions.

The flag is set once, never cleared. The widget writes is_so_line_edited: true only when the flag is currently false and the new item differs from the old one. Nothing in the widget ever resets it to false, so clearing the manual mark is a server-side or data operation, not something a user can do from this field.

The flag write happens in the same record update as the value. Both go through record.update(), so they save together. That is also why the flag field has to be loaded in the view: updating a field the view does not know about is not possible, and core marks it column_invisible rather than leaving it out.

The widget does not compute anything. Which sales order item a timesheet line gets by default is decided by the server. The widget's job ends at flagging the manual override so those computations can respect it.

Version compatibility

VersionStatusNotes
Odoo 20.0In developmentNot released. Internal rewrite only on the development branch; behavior unchanged. See below.
Odoo 19.0VerifiedRebuilt on buildM2OFieldDescription; adds the readonly falsy value label display. All options verified against the shipped source.
Odoo 18.0VerifiedSame behavior; implemented as a many2OneField spread with an update wrapper.
Odoo 17.0VerifiedSame behavior and flag logic; the file also registered a so_line_one2many companion used by the old gantt view.
Odoo 16.0VerifiedWidget present with the same name; registered directly as a component class in the pre-descriptor style.

Upgrade note. The registration name has been stable since 16.0, so view XML carries over. JavaScript patches do not: 16.0 registered the component class directly, 17.0 and 18.0 spread many2OneField, and 19.0 rebuilds it on buildM2OFieldDescription with the new Many2One component, so any patch targeting the old class structure needs rewriting after an Odoo migration.

What is changing in Odoo 20

Odoo 20 is expected at Odoo Experience in Brussels, 24 to 26 September 2026. The notes below are read from the public development branch, which is unstable and can change until release; we re-verify this page against the shipped version.

For this widget the development branch shows no behavior change. The file is rewritten onto the new Owl props system (useProps and many2OneFieldProps replace the static props spread), the update handler and the is_so_line_edited logic are untouched, and the registration name stays so_line_field. Views carry over; JavaScript patches against the component class will need review.

Common problems and fixes

SymptomCause and fix
Odoo keeps overwriting the sales order item a user pickedA custom view renders so_line with the plain many2one widget, so is_so_line_edited is never set and recomputation treats the line as untouched. Use widget="so_line_field" and include is_so_line_edited invisible in the view.
Changing the item throws an error about is_so_line_editedThe widget updates that field on the record, but the view does not load it. Add <field name="is_so_line_edited" column_invisible="True"/> next to the so_line field, as core does.
The Sales Order Item column is missing entirelyThe line's project is not billable; core hides the field when allow_billable is false. Enable billing on the project; the column returns automatically.
Users create stray sales order items from timesheetsThe widget was reused without no_create, so the inherited many2one creation paths are active. Add options="{'no_create': True}" as every core usage does.
Empty cells show "Non-billable" and users think it is a valueThat is the placeholder, plus the 19.0 readonly fallback that prints a label for empty values in muted text. No fix needed; train users that muted text means no linked order item.

SO Item field vs the alternatives

WidgetBest forKey difference
so_line_fieldThe sales order item on billable timesheet linesFlags manual edits via is_so_line_edited so recomputes keep them
many2oneAny other relation without edit trackingIdentical UI, but never writes the manual-edit flag
timesheet_uomThe duration column on the same timesheet linesDispatches to the company's encoding unit widget instead of a relation

The practical test: if the many2one you are rendering is a timesheet's sales order item, use this widget so manual assignments are flagged. For any other relation, the plain many2one is the right base.

Frequently asked questions

What does the so_line_field widget do in Odoo?+
It renders the Sales Order Item many2one on timesheet lines and, whenever a user manually changes the value, writes is_so_line_edited = true on the record in the same update. Odoo's server logic can then distinguish manual assignments from computed defaults. Visually it behaves exactly like the standard many2one.
Why does my custom timesheet view lose manual sales order item choices?+
Because it renders so_line with the plain many2one widget. Only so_line_field sets the manual-edit flag, and without the flag the line looks computed. Switch the widget and include is_so_line_edited invisible in the view.
Which options does so_line_field support?+
Everything the many2one supports, because it inherits the full descriptor through buildM2OFieldDescription: no_open, no_create, no_quick_create, no_create_edit, search_threshold, placeholder_field, plus the undocumented can_scan_barcode and create_name_field read in extractProps.
Can I clear the is_so_line_edited flag from the form?+
Not through this widget. It only ever sets the flag to true; nothing in the component resets it. Clearing it requires a server action, an automated rule, or a data fix.
Does so_line_field work outside timesheets?+
Technically it renders any many2one, but its one special behavior writes to a field literally named is_so_line_edited, which only exists on timesheet lines. On other models the update would fail, so use the plain many2one there.
Is the widget available in Odoo 16, 17 and 18?+
Yes, registered under the same name since 16.0, so view XML migrates cleanly. The internal implementation changed in every major version (component class in 16, descriptor spread in 17 and 18, buildM2OFieldDescription in 19), which matters only if you patched the JavaScript.
What is the Non-billable text on empty lines?+
Core sets placeholder="Non-billable" on the field, and since 19.0 the widget's template also shows the field's falsy value label in muted text when the line is readonly and empty. It signals that the timesheet line is not linked to any sales order item.

Billing hours that never reach an invoice?

Timesheet-to-sales-order mapping is the plumbing your services revenue flows through. We tune billable project setups, protect manual assignments, and build custom timesheet views that keep the invoicing logic intact, across Odoo 16 to 19.

Book a free consultation

How this page was produced

Every statement on this page was read from the Odoo 19.0 source: so_line_field.js in sale_timesheet, its template so_line_field.xml, and the many2one_field.js base whose descriptor it inherits, then cross-checked against the widget's actual usage in hr_timesheet_views.xml. Version differences were confirmed by diffing the same file on the 16.0, 17.0, 18.0 and master branches. The Odoo 20 section reads the unreleased development branch and is marked as such. Spotted something off? Tell us and we will correct it.