Skip to main content
iVentureTeam

hr_holidays_radio_image

The little row of illustrations on a Time Off type is a radio group in disguise. It builds each image path from the option's own label, and it is gone on the Odoo 20 development branch.

Siddharth JambukiyaSiddharth JambukiyaOdoo Techno-Functional Consultant
August 27, 2026Updated August 27, 20267 min read
Technical namehr_holidays_radio_image
Field typesmany2one, selection
Viewsform
Modulehr_holidays, the Time Off app
Used in core1 occurrence, the Time Off type form in hr_holidays
VersionsOdoo 19.0, Odoo 18.0, Odoo 17.0
No-code setupNo. Studio cannot set this widget, and it depends on assets shipped inside the module
Alternativesimage_radio, hr_homeworking_radio_image, radio, image

What the time off icon picker does

Each Time Off type in Odoo carries a cover image: the small illustration that identifies Paid Time Off, Sick Time Off or Parental Time Off wherever the type appears. Choosing that image on the type's form is not a file upload and not a dropdown. It is a row of pictures, and you click one.

Underneath, it is the ordinary radio widget. The only thing this widget changes is the template: instead of a label next to each radio input, it renders an image, and the source of that image is the option's own label. The label goes straight into a URL under the module's icons folder.

In core the field is a many2one to ir.attachment, restricted by domain to attachments whose model is the Time Off type and whose field is this one. The eight shipped icons are seeded as URL-type attachment records whose names are the SVG file names, which is what makes label-as-file-name work.

What this means for your team

Icons on time off types are not decoration. They appear on the Time Off dashboard where employees pick what to request, and a recognizable picture removes a class of mistake that plain text does not: requesting sick leave when you meant unpaid, or the wrong one of three similarly named allowances. For a company with more than a handful of types, that is worth the two minutes of configuration.

The practical constraint is that the choice is limited to what the module ships. Adding your own illustration means adding an attachment record with the right model and field, and putting a matching image where the widget expects to find it. That is a small technical task, not a configuration screen, and it is worth knowing before promising a client branded leave icons.

The larger planning point is the Odoo 20 direction. On the development branch the Time Off type model itself is folded into work entry types, and this picker, its icon attachments and the form section that held it all disappear together. If leave type icons are part of a design you are committing to, treat them as version-bound.

Supported options in Odoo 19

The widget declares no options of its own; it registers the base radio descriptor with a different template. The inherited option is listed below with what it actually does here, which is nothing. Read from radio_image_field.js, radio_image_field.xml and radio_field.js, Odoo 19.0.

OptionTypeWhat it does
horizontalbooleanInherited from the radio widget and <strong>inert here</strong>. The replaced template lays the images out with fixed flex-wrap classes and never reads the orientation prop. Core's own view passes it regardless.(default: false)

The image path is a convention, not a configuration. The template writes /hr_holidays/static/src/img/icons/ followed by the option's label, with no option to change either half. A custom choice therefore has to be named exactly like an image file that exists at that path, and the label is what users would otherwise read, so the two requirements are permanently coupled.

Working examples

The core usage

<field name="icon_id" widget="hr_holidays_radio_image"
       class="o_time_off_icon_types d-flex flex-wrap"
       options="{'horizontal': true}"/>

Straight from the Time Off type form. The horizontal option is passed and then ignored by the replaced template, so core ships a no-op here.

The field it sits on

# hr_leave_type.py
icon_id = fields.Many2one('ir.attachment', string='Cover Image',
    domain="[('res_model', '=', 'hr.leave.type'),
             ('res_field', '=', 'icon_id')]")

The domain is what limits the row to the shipped icons. Widen it and the widget will happily try to render whatever comes back.

Adding your own icon

<record id="icon_custom" model="ir.attachment">
  <field name="name">Study_Leave.svg</field>
  <field name="res_model">hr.leave.type</field>
  <field name="res_field">icon_id</field>
  <field name="public" eval="True"/>
  <field name="type">url</field>
  <field name="url">/hr_holidays/static/src/img/icons/Study_Leave.svg</field>
</record>

This mirrors how core seeds its eight icons. The record's name has to match the file name, because the widget builds the image URL from it.

Why the icons are named like files

The JavaScript is four meaningful lines: subclass the radio field, point it at a different template, register it with the unchanged radio descriptor. Everything interesting is in the template.

The base radio widget exposes its choices as pairs. For a selection field the pair is value and label; for a many2one it is id and display name, fetched by the widget's own search. The template here takes the second element of that pair and drops it into an image source under the module's icons folder. So on a many2one, the record's display name becomes the file name, which is exactly why the shipped attachments are called Paid_Time_Off.svg rather than Paid Time Off.

Read-only mode is written differently from edit mode and is the fragile half. It guards against no value at all, then finds the matching pair and reads its second element. If the stored value is not in the fetched list, for example because the field's domain changed or the attachment was deleted, that search returns nothing and reading from it raises. Edit mode has no such problem, since it simply renders the whole list.

The inherited horizontal option is the other verified detail. The base template consumes an orientation prop; this template does not, laying the images out with fixed flex-wrap classes instead. The extractor still produces the prop, so the option is accepted, passed and ignored, and core's own view passes it. Its sibling in the HR module, hr_homeworking_radio_image, has exactly the same dead option.

Version compatibility

VersionStatusNotes
Odoo 20.0In developmentNot released. The widget, its template and the whole Time Off type form are absent from the development branch.
Odoo 19.0VerifiedVerified against the shipped source. Same template and behavior as Odoo 18.
Odoo 18.0VerifiedIdentical behavior; only the class syntax differs from Odoo 17.
Odoo 17.0VerifiedFirst version. Same template, same icon folder convention.
Odoo 16.0Not availableWidget does not exist.

Upgrade note. The widget behaves identically from Odoo 17 through Odoo 19, so no view work is needed within that range. It does not exist in Odoo 16. The one thing to check on any upgrade is custom icon attachments: they are ordinary data records, so a module that seeds them keeps working, but attachments created by hand in a database can be missed by a migration and leave the picker showing fewer choices than before.

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

The widget is deleted. Its JavaScript and template files are absent from the development branch, and so is its only usage, because the Time Off type form is gone: on that branch hr_leave_type.py and hr_leave_type_views.xml no longer exist and the model is folded into work entry types, which the same module now extends.

The icons go with it. The data file that seeded the eight cover-image attachments is also absent, and the field itself does not reappear on the replacement model. This is not a rename, it is a removal, and it follows the same path taken by the sibling hr_homeworking_radio_image widget, which is also deleted on that branch.

Anything in a custom module that references this widget, the Time Off type icons, or the type form section that holds them should be treated as work to be redone rather than migrated.

Common problems and fixes

SymptomCause and fix
Broken image placeholders instead of iconsAn option's label does not match a file in the module's icons folder, because the widget builds the URL from the label. Name the attachment exactly like the image file, including the extension and capitalization.
The picker shows no choicesThe field's domain matches no attachment records, usually because the seed data was not loaded. Update the hr_holidays module so the icon attachment records are recreated.
The form errors when opening an existing recordThe stored value is not among the fetched choices, and the read-only branch reads from the result of that lookup without guarding it. Restore or recreate the missing attachment, or clear the field on the affected records.
The horizontal option changes nothingExpected. The replaced template ignores the orientation prop entirely. Adjust layout with the class attribute on the field instead.
Custom icons do not appear for portal or public usersThe attachment record is not marked public, so the image URL is not served. Set public to True on the attachment, as core does for its eight icons.
Missing widget error after upgradingThe database is on a version where the widget does not exist, Odoo 16 or the Odoo 20 development branch. Remove the widget attribute, or move the choice to a widget that still exists in that version.

Time off icon picker vs the alternatives

WidgetBest forKey difference
hr_holidays_radio_imagePicking a Time Off cover image from the icons the module shipsRadio group whose image URLs are built from each option's label, inside a fixed module folder
image_radioA visual choice that is not tied to the Time Off moduleGeneral-purpose image radio without a hardcoded asset folder
hr_homeworking_radio_imagePicking a work location in the HR appSame idea with font icons instead of images, and a hardcoded whitelist of three values
radioPlain labelled choicesText labels, and its horizontal option actually works
imageUploading an arbitrary pictureFree upload rather than choosing from a fixed set

If you want a visual picker that is not tied to the Time Off module, the general-purpose image_radio widget is the closer match, since it does not hardcode a folder. If the goal is simply to make choices scannable rather than beautiful, an icon or badge presentation on a selection field costs far less and survives version changes better, which given this widget's fate on the development branch is a real consideration.

Frequently asked questions

Where do the Time Off icons come from?+
They are eight SVG files shipped in hr_holidays/static/src/img/icons/, seeded as URL-type attachment records whose names match the file names. The field's domain limits the picker to exactly those records.
How do I add a custom Time Off icon?+
Create an ir.attachment with res_model set to the Time Off type, res_field set to the icon field, public true, type url, and a name identical to the image file name. The widget builds the image URL from that name.
Why does the horizontal option do nothing?+
Because this widget replaces the base radio template, and the replacement never reads the orientation prop. The option is still accepted and forwarded, which is why core's own view passes it without effect.
Can I use it on a selection field?+
Technically yes, since the base radio widget supports selection and many2one alike. Every selection label would then have to be a file name in the module's icons folder, which is why core uses a many2one to attachments instead.
Does the widget still exist in Odoo 20?+
Not on the development branch. The widget files, the Time Off type form and the icon seed data are all absent, because the Time Off type model is folded into work entry types. Treat any custom use of it as work to redo.

Planning an HR upgrade past Odoo 19?

The Time Off data model is being reshaped in the next release, and cover icons are the visible tip of it. We map that kind of change against your customizations before it becomes an upgrade weekend, as part of Odoo migration work from 16 and 17 onward.

Book a free consultation

How this page was produced

The template's URL construction, the read-only lookup and the inert layout option were read from radio_image_field.js, radio_image_field.xml and the base radio_field.js on the Odoo 19.0 branch. The field definition, its domain and the eight seeded attachments come from hr_leave_type.py, ir_attachment_data.xml and hr_leave_type_views.xml in the same branch. The Odoo 20 statement is based on the absence of all of those files on the public development branch, checked file by file. Spotted an error? Tell us and we will correct the page.