Friday, August 21, 2026
10 changes · master
Code cleanup and technical improvements
The portal rating message display was updated internally to use a newer supported approach in Odoo's interface framework. This keeps the portal experience compatible with upcoming platform changes without changing what users see, and a new test helps prevent regressions.
Original PR description
Replaced `useLayoutEffect` with `useEffect` because
`useLayoutEffect` is deprecated in OWL3.
`richBodyRef` was already a `signal.ref()` from the t-ref migration
base. `useEffect` auto-tracks that signal read, so it re-runs
whenever the element enters/leaves the DOM — matching the old
`() => [this.richBodyRef()]` dep exactly. The null-guard
`if (el) {` handles the eager pre-mount call (no element yet).
When commenting out the useLayoutEffect there was no error,
the code we refactored had NO TEST coverage. A test was written
to ensure our fix was correct, and it was tested against the
previous useLayoutEffect:
- Passed with previous useLayoutEffect.
- Failed with previous useLayoutEffect commented.
- Passed with our OWL3 replacement.
see runbot build, green even with the useLayoutEffect commented out: https://runbot.odoo.com/runbot/batch/2681594/build/120349135This update modernizes how the web interface keeps navigation items current, reducing the risk of menus or keyboard navigation starting with missing items. It preserves existing behavior while making the underlying navigation logic more reliable for future platform updates.
Original PR description
Split out of #282959, which is now the ~10 purely mechanical `useLayoutEffect` → `useOnChange` sites. `useNavigation` is the one that is not mechanical, so it gets its own PR. The two touch no files…
Split out of #282959, which is now the ~10 purely mechanical `useLayoutEffect` → `useOnChange` sites. `useNavigation` is the one that is not mechanical, so it gets its own PR. The two touch no files in common. ### Why this one is different Everywhere else in the migration the dependency list is nothing but refs, which makes the swap timing-neutral: a ref signal is written during the DOM patch, so the effect's microtask fires straight after it with the patched DOM already in place, where the shim's `onPatched` ran it. `useNavigation` is the only hook in the series that hands state back to the caller for a lifecycle hook to read: ``` time (down) useLayoutEffect useOnChange -------------------- ---------------------- -------------------- patch DOM - effect queued onMounted (hook) update() -> items not registered onMounted (consumer) reads items[0]: ok reads items[0]: [] microtask - update() -> items ``` `onMounted` never runs again, so the missed `setActive()` is lost rather than merely delayed. This is what broke the 7 `navigation_hook` tests on the first attempt. ### What this does instead The first version of this fixed the ordering by calling `navigator.update()` from an extra `onMounted` inside the hook. @seb-odoo's review comment on #282959 was that the actual problem is that `Navigator` is not reactive, and that is right: `items` was a plain field reassigned by `update()`, sitting outside the reactive graph, so *something* had to push a refresh in at the right moment. So `items` is derived instead, and the ordering question disappears rather than being answered: - `_elements` — a computed over `options.getItems()`, memoised on a shallow comparison. Every signal that callback reads (the container ref, an `isOpen()` state) is tracked, so the list re-derives on its own. - `_items` — a computed over it that reconciles `NavigationItem`s, reusing the instance of an element that is still there so its listeners survive. - `items` — a getter over that. Reading it from anywhere, including a consumer's own `onMounted`, derives against the DOM as it is at that moment. - `update()` — now just an invalidation, for the DOM, which is the one input that cannot notify us. It still syncs eagerly, so `update(); this.items` keeps working for `dropdown.js`. - `_syncActiveItem()` — the active-item reconciliation, `onUpdated` and `shouldFocusFirstItem` move out of the derivation, because they move focus and a plain read of `items` must not do that. Idempotent per derivation, which is what the hand-rolled `didUpdate` flag bought. ### Blast radius `navigation.js` only, plus one test. No consumer changes: `items`, `update()` and `onUpdated` keep their signatures, and enterprise never constructs a `Navigator` nor calls `update()`. Nothing reads `items` during a render — every read across both repos is a hotkey callback or an event handler. That matters now that `NavigationItem` construction, which attaches listeners and writes `ariaSelected`, happens inside a computed. It is a constraint the code satisfies today rather than one it enforces. The destroy test stepped on `update()`, which no longer runs on mount at all, so it steps on `_syncActiveItem` instead. ### For review - `onUpdated` and `shouldFocusFirstItem` now fire from an effect (a microtask after patch) for signal-driven changes, where `useLayoutEffect` fired them inside `onPatched`. Explicit `update()` callers are unaffected. This is the same class of timing shift that broke the tests the first time, so it is the thing to look at hardest. - `activeItem` / `activeItemIndex` are still plain fields. Making those reactive is the other half of "properly reactive" and is deliberately not in here. Draft until CI has had a go at it.
Point of Sale customer displays now communicate with terminals more directly and only when a display is actually connected. This reduces unnecessary background activity, improves reliability after startup or reload, and makes future display-related changes easier to maintain.
Original PR description
pos*: point_of_sale, pos_loyalty, pos_hr, pos_online_payment, pos_stock, l10n_in_pos, l10n_id_pos Refactor the customer display communication to make it more efficient, decouple the terminal and…
pos*: point_of_sale, pos_loyalty, pos_hr, pos_online_payment, pos_stock,
l10n_in_pos, l10n_id_pos
Refactor the customer display communication to make it more efficient,
decouple the terminal and display sides, and avoid unnecessary requests
when no display is connected.
- Replace the effect on `pos_service` with targeted listeners on order-related events.
- Split `customerDisplayService` into two OWL plugins: `CustomerDisplayTerminalPlugin` for the PoS terminal and `CustomerDisplayPlugin` for the customer display.
- Centralize payload generation through `GeneratePrinterData` and remove the deprecated `CustomerDisplayPosAdapter`.
- Replace `device_uuid` with a `device_identifier` generated by `DeviceIdentifierSequence`.
- Use a registration route to track connected customer displays: displays announce themselves with `ADD` and `REMOVE`, while terminals use `PING` to discover displays after startup or reload.
- Skip payload generation and requests when no customer display is connected.
- Refactor customer display tests to exercise the real application flow and assert against system-generated payloads.
```mermaid
flowchart LR
POS([PosStore])
UI([Customer display UI])
subgraph Terminal["CustomerDisplayTerminalPlugin — app/plugins"]
direction TB
initT["init({ identifier, models, scale, bus, ... })"]
sendOrder["sendOrder(order)"]
build["_buildDisplayPayload()"]
send["send(payload)"]
sendOrder --> build --> send
end
subgraph DisplaySide["CustomerDisplayPlugin — customer_display"]
direction TB
initD["init({ bus })"]
onData["_onDataReceived()"]
data[("data — signal")]
onData --> data
end
POS -->|initCustomerDisplay| initT
POS -->|order or screen changed| sendOrder
UI -->|mounted| initD
send -->|"update_customer_display — bus"| onData
send -.->|"BroadcastChannel — only if the request fails"| onData
data -->|render| UI
```
Task-5911881This internal cleanup makes mail record lists follow the same structure as related records. It improves consistency for future maintenance without changing how users interact with Odoo.
Original PR description
Before this commit, the record list holds its proxy get and set traps inline in its constructor and its localIds in a `data` own property. The record holds the same logic on its internal, `RecordInternal`. This commit moves the traps to `RecordListInternal.proxyGet` and `proxySet` and the localIds to `RecordListInternal.data`, so that the list and the record have the same shape. Note that the set trap thereby runs untracked: the untrackFunctions list of `RecordListInternal` already names `proxySet`, like the record's. This commit also stores the record list on its internal and drops the record list parameter of the internal methods: every caller passed the list whose internal it calls, so the parameter only repeated `this.recordList`.
The mail module now manages record lists through a dedicated internal mechanism, making updates easier to track consistently. This is a behind-the-scenes refactor intended to preserve behavior while improving maintainability and future performance reliability.
Original PR description
Before this commit, a record list keeps its records in a plain `RecordListInternal.data` array and gets its reactivity from the owl proxy wrapping the list, so a read has to travel through that proxy to register the observer, and every mutator routes its writes through `recordList._proxy` for the observers to hear them. This commit moves the array into its own signal behind a `data` accessor of the internal, whose reads return the array as an owl proxy, so a read observes the list from any receiver and the list needs no owl proxy of its own: `_proxyInternal` merges into `_proxy`, and the `gettingField` flag and the mutators' routing through `recordList._proxy` go with it. Note that the granularity stays per key: a replacement notifies through the signal, an in-place mutation through the proxy's key atoms. One solution could have been `signal.Array`, but it holds one atom for the whole list, so any write re-runs every reader (owl issue 1991).
This update replaces an older internal screen update mechanism with the newer approach required by the next version of Odoo’s web framework. It helps keep several user interface areas maintainable and ready for future upgrades without changing expected business workflows.
Original PR description
Changes `useLayoutEffect` to `useOnChange` for all `useLayoutEffect` with **only references** as dependency list. WHY: Because useLayoutEffect is deprecated in OWL3 NOTE: Because every dependency list contains nothing but refs, it makes these timing-neutral by construction. A ref signal is written during the DOM patch, so the effect's microtask fires straight after it with the patched DOM already in place -- where the shim's onPatched ran it. `initialRun: false` drops the setup-time run where the ref is still null The untrack() in six dep lists had to go. useOnChange turns the dependency function into a computed, and untrack leaves that computed with no source, so the effect would fire once and never again.
Point of Sale customer display updates are now handled through a centralized communication component instead of depending on broader service activity. This should reduce unnecessary processing and make connected display and scale integrations more consistent across supported POS setups.
Original PR description
pos*: pos_iot, pos_mobile, l10n_eu_iot_scale_cert Customer display previously relied on an effect on `pos_service` to dispatch updates, which was inefficient and unnecessarily dependent on the full service lifecycle while only order data was required. This commit refactors and centralizes the customer display logic: - Replace effect-based updates with targeted event listeners on order-related events to reduce overhead and improve performance - Make `customerDisplayService` the single source of truth for all customer display communication (send/receive) - Standardize payload generation using `GeneratePrinterData` And remove deprecated `CustomerDisplayPosAdapter` - Replace `device_uuid` with `device_identifier` generated via `DeviceIdentifierSequence` for consistent device identification Task-5911881 Related PR: - https://github.com/odoo/odoo/pull/257781
The Knowledge article template picker was updated to use the newer supported behavior in the underlying interface framework. This keeps the feature compatible with upcoming platform changes and adds test coverage to help prevent regressions.
Original PR description
Replaces `useLayoutEffect` with a `useEffect` because useLayoutEffect is deprecated in OWL3 Note: I added a test because when the useLayoutEffect was commented out no test broke. see: https://runbot.odoo.com/runbot/batch/2596681/build/114854277
The barcode inventory screens were updated to use a newer internal framework method, replacing an approach that is being retired. This helps keep the stock barcode module compatible with the next Odoo web framework version without changing day-to-day user behavior.
Original PR description
Changes `useLayoutEffect` to `useOnChange` for all `useLayoutEffect` with *untracked* *references* as dependency list. WHY: Because useLayoutEffect is deprecated in OWL3 NOTE: Because every dependency list contains nothing but refs, it makes these timing-neutral by construction. A ref signal is written during the DOM patch, so the effect's microtask fires straight after it with the patched DOM already in place -- where the shim's onPatched ran it. `initialRun: false` drops the setup-time run where the ref is still null The untrack() in six dep lists had to go. useOnChange turns the dependency function into a computed, and untrack leaves that computed with no source, so the effect would fire once and never again. Community PR: https://github.com/odoo/odoo/pull/282959
The Knowledge app comments panel was updated to use a newer internal mechanism, replacing deprecated code without changing how users interact with it. A new automated test was added to help ensure this area continues to work reliably in future updates.
Original PR description
Replaces the deprecated `useLayoutEffect` with `useOnChange` (the only difference between both is useOnChange triggers a bit earlier, which is irrelevant here because we don't access the DOM) This feature was not tested, so a new HOOT test is introduced.