Skip to main content
iVentureTeam

accrual_levels

Accrual plans are the hardest thing to configure in Odoo Time Off, so Odoo stopped showing them as a list. accrual_levels draws them as a timeline you read like a sentence.

Siddharth JambukiyaSiddharth JambukiyaOdoo Techno-Functional Consultant
August 27, 2026Updated August 27, 20266 min read
Technical nameaccrual_levels
Field typesone2many
Viewsform
Modulehr_holidays, the Time Off app
Used in core1 occurrence, the accrual plan form in hr_holidays
VersionsOdoo 20.0, Odoo 19.0
No-code setupNo. It is a bespoke view widget tied to the accrual plan data model
Alternativesone2many, section_one2many, x2many_buttons

What the accrual milestones widget does

An accrual plan is a set of milestones: after so long, an employee starts earning so much time off, at a given frequency, with rules about what happens to unused days. Written as a table of technical columns it is close to unreadable, and it is exactly the configuration people get wrong.

This widget replaces that table. It renders each milestone as a card on a vertical timeline, with a label on the left saying when the milestone starts and a sentence on the right saying what it does. The sentence is assembled in the browser from the milestone's fields, so it always describes the real configuration rather than a stored description.

Cards are clickable and open the milestone in a full dialog. A trash icon on each row removes it, and a button at the bottom of the timeline adds a new one. There is no inline editing at all: the timeline is a reader, and every change goes through the dialog.

What this means for your team

Accrual rules are where time off policy meets payroll, and a misconfigured plan is expensive in both directions. Employees who accrue too fast take leave they have not earned; employees who accrue too slowly complain, and the correction is retroactive. Anything that makes the rule readable before it goes live pays for itself.

The sentence rendering is the whole value here. A reviewer who does not know Odoo can read a card and confirm the policy, which means the HR lead can sign off on the configuration rather than approving a screen they do not understand. In a rollout, that shortens the accrual conversation from several meetings to one.

Where it needs care is the save prompt. Because milestones open in a dialog, the plan itself can have unsaved edits when you click a card, and the widget stops to ask what to do. Users who dismiss that dialog without reading it are the usual source of an accrual plan that looks right on screen and behaves differently in practice.

Supported options in Odoo 19

This widget declares no options and no supported types. Its descriptor carries only a component, a field dependency and a related-field injection, all listed below and read from accrual_levels.js, Odoo 19.0.

OptionTypeWhat it does
carryover_dayfield dependencyThe one field the widget declares as a dependency, so it is loaded even when the form does not show it. Used to build the carry-over date sentence.(since Odoo 19.0)
idrelated field on the milestoneInjected into the related relation so each row exposes its database id, which the widget needs for the server read and for the New badge.(since Odoo 19.0)

It reads more fields than it declares. The descriptor declares a dependency on carryover_day only, but the component also reads carryover_date and carryover_month from the record to build the carry-over sentence. The core accrual plan form loads all three as visible fields, so this is invisible in practice; on a custom form that omits them, the carry-over line will not render correctly.

Working examples

The core usage

<field name="level_ids" widget="accrual_levels"
       invisible="not level_ids"/>

No subview and no options. The invisible guard lets the form show an empty-state panel with its own create button when the plan has no milestones yet.

The fields the sentences need

<field name="carryover_date"/>
<field name="carryover_day"/>
<field name="carryover_month"/>
<field name="level_ids" widget="accrual_levels"/>

Only the first of these is declared as a dependency by the widget. Keep all three in any custom form.

The empty state core pairs with it

<div class="o_view_nocontent" invisible="level_ids">
  <button name="action_create_accrual_plan_level"
          type="object" string="Create a milestone"/>
</div>

The widget hides itself when the relation is empty, so the form supplies its own guidance instead of an empty timeline.

Why the timeline reads fields your subview never loaded

The data path is unusual. A relational field widget normally renders from the records the view already loaded. This one uses a record observer to call a plain read on the milestone model for the current ids, and renders from that result. The practical consequence is that the timeline shows server-side values, including fields the subview never declared, which is what lets the sentences reference a dozen fields without a subview at all.

The same observer tracks which ids were present when the form opened. Any milestone whose id was not in that first set is rendered with a New badge and a highlighted marker, so after creating one you can see which card you just added. It is a small touch that only makes sense once you know the widget keeps that original id list.

Editing goes through a two-step. Clicking a card checks whether the plan record is dirty. If it is, a confirmation dialog offers to save the changes first or to keep the old version, and only then calls a server method that returns the action for the milestone form. Creating uses a different server method with no id. Both are opened with the plan's id in context, and the form reloads the whole root record when the dialog closes.

Deleting is deliberately not symmetric with that. The trash icon removes the record from the loaded relation in the browser and nothing else; the deletion is only written when the plan itself is saved. So a user who deletes a milestone and then discards the form gets it back, which is the safer default for policy data.

One more detail worth knowing for translations: the day and month names in the sentences are produced by the client date library using the user's language from context, not by the server, so they follow the interface language rather than the record's language.

Version compatibility

VersionStatusNotes
Odoo 20.0In developmentNot released. Behavior unchanged on the development branch; only formatting and reactive-state helpers differ.
Odoo 19.0VerifiedFirst version. Verified against the shipped source.
Odoo 18.0Not availableWidget does not exist. Milestones were edited in a conventional embedded list.
Odoo 17.0Not availableWidget does not exist.
Odoo 16.0Not availableWidget does not exist.

Upgrade note. The widget is new in Odoo 19. On Odoo 18 and earlier, accrual plan milestones were edited through a conventional embedded list, so there is nothing to migrate in view XML; a database upgraded from 18 simply gains the timeline with the standard Time Off views.

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 this may still change; we re-verify the page after release.

No behavior change. The development branch differs only in formatting and in the migration from the older reactive-state helper to the new one. The server methods it calls, the field dependency, the save prompt and the local delete are all identical.

Common problems and fixes

SymptomCause and fix
The timeline does not appear at allThe relation is empty and the core view hides the field in that case. Use the Create a milestone button in the empty-state panel; the timeline appears with the first record.
The carry-over sentence is wrong or missingA custom form does not load carryover_date or carryover_month, neither of which the widget declares as a dependency. Add both fields to the form, invisible if needed.
A confirmation dialog appears when clicking a milestoneThe plan record has unsaved changes and the widget is asking whether to save them before opening the dialog. Read the choice carefully. Keeping the old version discards the plan edits you just made.
A deleted milestone came backDeleting only removes the row locally; the change is written when the plan is saved. Save the accrual plan after deleting.
Day or month names are in the wrong languageThey are formatted in the browser using the interface language from context. Change the user's language; the record's language does not drive this text.
The New badge stays on an old milestoneThe badge marks ids that were not present when the form was first loaded. Reload the form; the id set is recaptured.

Accrual milestones widget vs the alternatives

WidgetBest forKey difference
accrual_levelsReading and editing time off accrual milestones as plain sentencesGenerates a timeline from a second server read, with all editing pushed into a dialog
one2manyAn ordinary editable list of child recordsShows raw columns; no generated sentences and no save prompt
section_one2manyChild lists that need section groupingStill a list, with structural rows rather than rendered rules
x2many_buttonsA compact relation rendered as buttonsMinimal display, no per-row description

There is no configuration choice to make here: the widget is bound to the accrual plan model and its server methods. What is worth borrowing is the pattern. When a relation encodes rules rather than data, rendering each row as a generated sentence and pushing editing into a dialog is usually kinder than a wide editable list, and this widget is the cleanest example of it in core.

Frequently asked questions

Can I edit a milestone directly on the timeline?+
No. Every card opens the milestone in a dialog. The timeline is a reader, and the only in-place actions are delete and add.
Why does Odoo ask to save before opening a milestone?+
Because the milestone form is a separate dialog backed by a server action. If the plan itself has unsaved changes, the widget offers to save them first so the dialog opens against a consistent record.
Does deleting a milestone remove it immediately?+
No. The row is removed from the loaded relation in the browser, and the deletion is only written when you save the accrual plan. Discarding the form restores it.
Does the widget take any options?+
None. It declares no supported options and no supported types, only a component, one field dependency and one injected related field.
Which Odoo versions have it?+
Odoo 19 onwards. Earlier versions edited accrual milestones in a conventional embedded list, so there is nothing to migrate in view XML.

Accrual rules nobody on your team can read?

Leave policies that only the person who configured them understands are a payroll risk waiting for a resignation letter. We set up and document Odoo Time Off accrual plans so HR can verify them without opening a developer tool.

Book a free consultation

How this page was produced

The data read, the New-badge id tracking, the save prompt, the local delete and the undeclared field reads were taken from accrual_levels.js and accrual_levels.xml on the Odoo 19.0 branch, with the usage read from hr_holidays/views/hr_leave_accrual_views.xml. Version coverage comes from the absence of the file on 16.0, 17.0 and 18.0 and a full comparison against the public development branch. Spotted an error? Tell us and we will correct the page.