Skip to main content
iVentureTeam

account_json_checkboxes

The checkbox rows on Odoo's invoice Send wizard are not boolean fields: account_json_checkboxes renders a whole JSON dict as a group of checkboxes, each with its own label, lock and help icon.

Siddharth JambukiyaSiddharth JambukiyaOdoo Techno-Functional Consultant
August 25, 2026Updated August 25, 20266 min read
Technical nameaccount_json_checkboxes
Field typesjson
Viewsform (wizard)
Also registered asjson_checkboxes, the canonical name since 19
Moduleweb since 19 (moved from account); template namespace still account.*
Used in core2 occurrences in 1 module: sending_method_checkboxes and extra_edi_checkboxes on the invoice send wizard
VersionsOdoo 20.0, Odoo 19.0, Odoo 18.0
No-code setupNo. Requires a json field and view XML; Studio does not expose it
Alternativesmany2many_checkboxes, boolean, checkbox

What the JSON checkboxes widget does

The Send wizard for invoices offers a changing set of choices: email, download, EDI channels that depend on the customer's country and configuration. Modeling every possible choice as a boolean field would mean schema changes each time a localization adds a channel. Odoo's solution is one fields.Json whose compute builds the choice set per record, and this widget to render it.

Each key of the dict becomes a checkbox. The entry controls everything about it: checked is the state, label the caption, readonly locks that single box while others stay editable, and question_circle, when present, renders a help icon whose tooltip is the string itself. Layout is a compact inline row by default, or one box per line with the stacked option.

State handling is live in both directions. Ticking a box mutates a local reactive copy and pushes the whole dict back to the record after a 100 millisecond debounce, so rapid clicking produces one update instead of five. And because the component observes the record, a server recompute, say, picking a different customer whose country enables another EDI channel, replaces the checkbox set on the fly.

What this means for your team

The wizard this widget serves is one of the most-clicked screens in Odoo invoicing, and its design carries a lesson for anyone extending the system: when the option set is dynamic, put it in data, not in fields. A JSON dict computed per record means new sending channels, compliance toggles or feature flags appear without database migrations, and old ones vanish just as cleanly.

The per-key readonly flag is the underrated part. It lets the server enforce policy inside the choice set, a mandated e-invoicing channel arrives pre-checked and locked, while optional channels stay clickable. Users see the full picture including what they cannot change, which beats hiding the locked options and fielding why is EDI missing tickets.

We reuse this widget for configuration matrices in custom Odoo builds, checklists of integrations, per-partner document options, dynamic consent boxes, because it ships with core, needs zero JavaScript, and the entire UI is driven from one compute method. The only cost is that values live inside a JSON blob, so reporting on individual choices takes a lateral read rather than a plain field filter.

Supported options in Odoo 19

Verified against json_checkboxes_field.js in the Odoo 19.0 web module. One option is declared; everything else is controlled per key inside the JSON value, which is the real configuration surface.

OptionTypeWhat it does
stackedbooleanDeclared in supportedOptions since 19. When set, each checkbox renders as a block on its own line instead of the inline row. The help text in the source spells out exactly this and nothing more.(default: false)(since Odoo 19.0)

Name your widget by version. account_json_checkboxes works everywhere from 18 onward but is formally a deprecated alias since 19, with a source comment scheduling its removal. New 19+ views should use json_checkboxes; 18 databases have no choice, the alias is the only name.

Working examples

How core uses it (invoice Send wizard)

<field name="sending_method_checkboxes" widget="account_json_checkboxes"/>
<field name="extra_edi_checkboxes" widget="account_json_checkboxes"
       invisible="not extra_edi_checkboxes"/>

The value shape the widget renders

{
    "email": {"checked": true, "label": "by Email"},
    "peppol": {
        "checked": true,
        "label": "by Peppol",
        "readonly": true,
        "question_circle": "Required for this customer's country"
    }
}

A custom json field, stacked vertically

<field name="feature_checkboxes"
       widget="json_checkboxes"
       options="{'stacked': True}"/>

On the Python side, read the ticked keys the way core does: iterate the dict and keep keys whose entry has checked truthy. The wizard's sending_methods compute is exactly that loop.

A web widget with an account accent

The move that left a namespace fossil. The widget was born in Odoo 18 inside the account module. Odoo 19 promoted it to web, renamed it json_checkboxes, and kept account_json_checkboxes as a second registration of the same descriptor with the comment // TODO: remove in saas~19.1. The QWeb template, however, kept its original name: account.JsonCheckboxes, an account-namespaced template shipped by the web module. Harmless, but a giveaway of the file's origin, and a warning for anyone building template inheritance: inherit account.JsonCheckboxes, not a web-namespaced guess.

Two 18-era quirks were quietly fixed. In 18 the checkbox's disabled state was computed from the per-key readonly alone, so making the whole field readonly did not lock the boxes; 19 adds props.readonly || to the check. And 18's descriptor declared supportedTypes: ["jsonb"], a type that does not exist in Odoo's field system, corrected to json in 19. Neither ever broke the core wizard, which loads the field editable and json-typed anyway.

Reactivity details. The component seeds a reactive copy from the record, so it mutates locally, then debounces the write-back by 100 ms. The record observer merges server values into that same copy with Object.assign, which means keys removed by a recompute are merged over but new and changed keys arrive live. The Odoo 20 branch swaps the state primitive for the new proxy-based API and typed props with no behavioral change, and the alias registration, TODO comment included, is still there.

Version compatibility

VersionStatusNotes
Odoo 20.0VerifiedNot released. Reactive-proxy and typed-props migration only; the deprecated alias is still registered. Re-verified after launch.
Odoo 19.0VerifiedComponent moved to web as json_checkboxes with this name kept as an alias; stacked option and field-level readonly handling added.
Odoo 18.0Partial / changedIntroduced in the account module under this name only. No stacked option, field-level readonly not applied to the boxes, and a nonexistent jsonb type declared.
Odoo 17.0Not availableThe widget does not exist; the earlier mass-send wizard had no JSON checkbox UI.
Odoo 16.0Not availableThe widget does not exist.

What is changing in Odoo 20

Odoo 20 is expected at Odoo Experience in Brussels, 24 to 26 September 2026. As always, the notes below are read from the public development branch, which is unstable until feature freeze; we re-verify against the shipped release.

No functional change is visible. The development branch migrates the component to the new reactive-proxy and typed-props APIs, a framework-wide sweep, while the option, the value contract, the debounce and the template all stay as in 19.

The deprecated alias outlives its eviction notice. account_json_checkboxes is still registered on the branch, TODO comment intact, despite the comment scheduling removal for saas 19.1. Views using the old name will keep working in 20 as things stand, but the polite move during any version migration is to switch to json_checkboxes and stop depending on a name the source explicitly wants to delete.

Common problems and fixes

SymptomCause and fix
Checkboxes do not lock when the whole field is readonly on Odoo 18The 18 template computed disabled from the per-key readonly only. Set readonly: true per key in the compute, or upgrade; 19 respects the field state.
Nothing renders at allThe field value is falsy or not a dict of entry dicts. Return {} instead of False from the compute and check the {key: {checked, label}} shape.
A help icon shows no tooltip textquestion_circle must be a non-empty string; it doubles as the tooltip content. Put the help sentence directly in the question_circle value.
Rapid clicks only save the final stateUpdates are debounced by 100 ms and coalesced into one record update. Expected; the last state wins and nothing is lost.
Widget not found warning on Odoo 17 or earlierThe widget first shipped in 18. Backport the component or model the choices as boolean fields on older versions.
Unsure which widget name to use in new viewsTwo names register the same descriptor since 19. Use json_checkboxes on 19+; account_json_checkboxes is a deprecated alias the source plans to remove.

JSON checkboxes widget vs the alternatives

WidgetBest forKey difference
account_json_checkboxesDynamic, per-record choice sets stored in one json fieldWhole checkbox group driven by a JSON dict, with per-key labels, locks and help tooltips
many2many_checkboxesChoice sets that are records of a related modelOne checkbox per related record; the set comes from a name_search, not a computed dict
booleanA single fixed yes/no on the modelOne real field per choice, visible to filters and reports but frozen in the schema
checkboxNothing, in practiceA phantom widget name with no registered component; the type fallback renders the default boolean

The decision is about where the option set lives. Fixed sets of real fields want boolean widgets; records want many2many_checkboxes; computed, per-record sets serialized as JSON want this widget.

Frequently asked questions

What is the difference between account_json_checkboxes and json_checkboxes?+
Same component, two names. Odoo 19 moved the widget from account to web and renamed it json_checkboxes, keeping account_json_checkboxes as a backward-compatibility alias with a source comment scheduling its removal in saas 19.1. On Odoo 18 only the account name exists.
What JSON shape does the widget expect?+
A dict of dicts: each key maps to {"checked": bool, "label": str} plus optional "readonly": true to lock that single box and "question_circle": "help text" to add a tooltip icon. The whole dict is written back on every change.
How do I read the selected values server side?+
Iterate the dict and collect keys whose entry has a truthy checked, exactly what the send wizard's sending_methods compute does. There is no magic accessor; it is a plain JSON field.
Can I lock individual checkboxes while leaving others editable?+
Yes, with "readonly": true inside that key's entry. Core uses it for sending methods that are mandatory for a customer's configuration. Field-level readonly locks everything, on Odoo 19 and later.
Why do my checkboxes update by themselves when I change the customer?+
The component observes the record, and the choice fields are computed. When a recompute returns a different dict, the observer merges it into the displayed state immediately. That live refresh is the reason the wizard can tailor channels per customer without reloading.
Does the widget work on regular forms outside wizards?+
Yes. It only needs a fields.Json value in the right shape and works on any form view. Core happens to use it on a wizard, but nothing about the component is wizard-specific.

Dynamic configuration screens without schema migrations?

Choice sets that change per record, per country or per integration do not belong in hardcoded boolean fields. We design JSON-driven configuration UIs on core widgets like this one, keeping your Odoo 16 to 19 database clean and upgrade-friendly.

Plan my configuration UI

How this page was produced

Verified by reading json_checkboxes_field.js and its template in the Odoo 19.0 web module, the 18.0 predecessor in account for the history rows, the fields.Json definitions in the send wizard model, and both view usages. The checkbox flows, per-key readonly and the debounce were exercised on a clean Odoo 19 database, where the screenshot was captured. Odoo 20 findings come from diffing the same file against the public development branch. Corrections welcome via our contact page.