# Odoo 19 to 20 migration: a guide for custom modules

> A broad release touching almost every app, where the sharpest edges for custom modules are removed controller routes and reworked business-logic methods rather than a single dramatic rewrite.

Odoo 20 was released on 24 September 2026. This guide covers Odoo 19 → 20, **Community Edition**, built from the released version; Enterprise modules are not included in these counts. It counts 32,680 verified changes, including 6,762 in country localisation modules, which the per-area figures leave out.

Approach this one workflow by workflow rather than file by file. Start by cataloguing every place your module talks to Odoo over HTTP — the JSON-RPC routes your JavaScript calls and any controller you've subclassed — because that is where several public endpoints have simply disappeared and a silent 404 in the browser is the most likely first symptom you'll hit. Once the frontend touchpoints are mapped, move inward to the Python overrides on core models.

The areas most likely to bite are the eCommerce cart and delivery flow, Discuss channels, and the calendar sync hooks — all of which shed public methods that custom modules commonly lean on.

## What needs the closest look

### Trimming tracebacks off access-denied errors is gone

*Removed method — `AccessDenied.suppress_traceback`*

This helper used to strip the traceback from an AccessDenied exception before it was shown to a user, so sensitive internal detail didn't leak into the UI. If your module caught AccessDenied and called this before re-raising or displaying it, that call no longer exists — drop it and rely on the framework's own handling, or format the message yourself.

Removed in Odoo 20 — it was defined as:

```python
def suppress_traceback(self):
```

### The bus 'missed notifications' check has been removed

*Removed method — `BusController.has_missed_notifications`*

This public route let the client ask whether a given notification id still existed, so the longpolling bus could detect gaps after a reconnect. If your frontend code (or a customised bus service) pings this endpoint, it will now fail; align with however the current bus service detects missed messages instead of calling it directly.

Removed in Odoo 20 — it was defined as:

```python
@route("/bus/has_missed_notifications", type="jsonrpc", auth="public")
def has_missed_notifications(self, last_notification_id):
```

### Generic calendar credential check dropped

*Removed method — `CalendarController.check_calendar_credentials`*

This was the base hook that sync providers overrode to verify a user's calendar connection. It's gone from the controller, and the Google-specific version has gone too, so any module that called the check-credentials route or overrode this method needs to move to the current provider integration points.

Removed in Odoo 20 — it was defined as:

```python
@http.route('/calendar/check_credentials', type='jsonrpc', auth='user')
def check_calendar_credentials(self):
```

### Cart quantity endpoint removed

*Removed method — `Cart.cart_quantity`*

The shop used this route to report the current number of items in the cart, reading from the session when available. If your storefront JavaScript or a snippet fetches cart quantity from this URL, switch to reading it from the cart data the current templates already expose rather than the dedicated call.

Present in Odoo 19, **removed in 20**:

```python
@route(
    route='/shop/cart/quantity',
    type='jsonrpc',
    auth='public',
    methods=['POST'],
    website=True
)
def cart_quantity(self):
    if 'website_sale_cart_quantity' not in request.session:
        return request.cart.cart_quantity
    return request.session['website_sale_cart_quantity']
```

### 'Clear cart' route removed

*Removed method — `Cart.clear_cart`*

This endpoint emptied the cart by unlinking all order lines in one shot. Any 'empty cart' button or custom checkout flow that posted here must be reworked — remove the lines through the supported cart operations instead of hitting this route.

Present in Odoo 19, **removed in 20**:

```python
@route(
    route='/shop/cart/clear',
    type='jsonrpc',
    auth='public',
    website=True
)
def clear_cart(self):
    request.cart.order_line.unlink()
```

### Direct 'join channel' route removed

*Removed method — `ChannelController.discuss_channel_join`*

This let a public visitor join a Discuss channel by id, creating their membership on the fly. If your chat or livechat customisation triggered a join through this endpoint, you'll need to route the action through the current channel join mechanism instead.

Removed in Odoo 20 — it was defined as:

```python
@http.route("/discuss/channel/join", methods=["POST"], type="jsonrpc", auth="public")
@add_guest_to_context
def discuss_channel_join(self, channel_id):
```

### Fetching channel members over its own route is gone

*Removed method — `ChannelController.discuss_channel_members`*

This endpoint returned the members of a channel that the client didn't already know about, in batches. Custom Discuss front-ends that lazily loaded member lists via this call should now get member data through the store payloads the channel already provides.

Removed in Odoo 20 — it was defined as:

```python
@http.route("/discuss/channel/members", methods=["POST"], type="jsonrpc", auth="public", readonly=True)
@add_guest_to_context
def discuss_channel_members(self, channel_id, known_member_ids):
```

## Also worth checking

The removals above are part of a wider pruning of public controller methods, so group your review by the app the endpoints live in.

- **Point of Sale & eCommerce (Website Sale):** Beyond the cart routes already covered, the delivery pickup-location flow lost both its lookup and its set-location endpoints — if you customise click-and-collect or third-party carriers, this needs a close look. The HTML editor's video-URL helper is also gone, which matters only if a snippet or website builder extension resolved embedded video data through it.

- **Discuss & Livechat:** Alongside the join and members routes, the channel messages and pins endpoints and a livechat attachment upload route have been removed, and a CORS chatbot email-validation route dropped. Custom messaging widgets and embedded chatbots are the ones to check here; give ordinary Discuss usage only a quick pass.

- **Calendar:** Both the generic and Google credential checks are gone, so any sync provider you maintain needs revisiting rather than a light touch.

- **Import/Export:** A couple of export helpers that described field metadata and grafted subfields into export trees have been removed — relevant only if you built a custom export or extended the export UI.

- **Localisations & IoT:** The Peru portal state-info helper, an Egyptian ETA crypto-library getter and USB controller, and IoT box version/ngrok helpers were removed; these are narrow and only concern you if you work in those specific stacks.

Beyond these, 18,188 smaller internal changes exist — only relevant if you override Odoo's private helpers or compute methods.

## Your migration checklist

1. Inventory every JSON-RPC route your JavaScript calls and every controller you subclass, then check each against the removed endpoints above.
2. Fix the eCommerce cart and delivery flows first — cart quantity, clear cart, and pickup locations — since these break the storefront visibly.
3. Review Discuss and Livechat customisations for the removed join, members, messages, pins, attachment and chatbot routes, moving to the current store/channel mechanisms.
4. Revisit any calendar sync provider you maintain, replacing calls to the dropped credential-check hooks.
5. Check localisation, IoT and export overrides only if your module touches those areas, and remove the AccessDenied traceback call wherever you used it.
6. Run your full test suite on Odoo 20 and exercise every affected workflow end to end before go-live.
