Way4Tech

Part 5. Services and registries · lesson 20 of 40

What a service is, and the ones you will use every day

After this lesson you can: Explain what a service is, reach one from a component, and name the right service for a given job.

Written against Odoo 19.0 and 18.0. Last checked 2026-10-09.

A component is a short lived thing that renders. A service is a long lived thing that does. Anything outside your component, the server, a notification, the router, the current user, is reached through a service.

import { useService } from "@web/core/utils/hooks";

setup() {
    this.orm = useService("orm");
    this.notification = useService("notification");
}

useService looks the service up by name and returns it. It must be called in setup, like every hook.

Why not just import a module

Three reasons, and they are the reasons dependency injection exists anywhere.

Lifecycle. A service is started once, when the web client boots, and lives until the page is closed. It can hold connections, caches and subscriptions that no component should own.

Replaceability. Because components ask for "orm" by name rather than importing a file, a module can replace the implementation. Tests do exactly this, swapping the real ORM for a mock, and your component is unchanged.

Dependencies. A service declares what it needs and the loader starts things in the right order.

The ones worth knowing

NameWhat it does
ormRead and write records. The one you will use most
notificationThe toast messages in the corner
dialogModal dialogs, including confirmation
actionOpen views, run actions, navigate
routerRead and change the URL
userCurrent user, groups, context
companyActive company and company switching
rpcRaw calls to a controller route
httpPlain HTTP requests
titleThe browser tab title
uiBlocking UI, size and breakpoints
hotkeyKeyboard shortcuts, registered and cleaned up
effectThe rainbow man
fieldField definitions and formatters
namedisplay_name lookups, batched and cached
popoverPopovers anchored to an element
localizationDate, time and number formats for the user

The ORM, notification, dialog and action services account for most real code.

Seeing them live

In developer mode:

odoo.__WOWL_DEBUG__.root.env.services

That is every service currently running, by name. It is the fastest way to check whether a service you expect exists, and to poke at one while developing.

Services in the env

useService("x") is sugar for this.env.services.x with a check that it exists. You will see this.env.services directly in older code and in places that are not components. Prefer useService, because its error when a service is missing is clear, whereas reading a missing key gives you undefined and a failure later.

A first useful one: user

import { user } from "@web/core/user";

setup() {
    this.canApprove = user.hasGroup("account.group_account_manager");
}

Note that in Odoo 17 and later user is imported directly rather than taken from useService("user"), and hasGroup returns a promise:

setup() {
    this.state = useState({ canApprove: false });
    onWillStart(async () => {
        this.state.canApprove = await user.hasGroup("account.group_account_manager");
    });
}

Hiding a button from someone who lacks a group is a convenience, not security. Anyone can call the method from the console. The access rules and record rules on the server are what actually protect the data, and a client side check that is not backed by one is theatre.

The pattern you will write most

import { Component, useState, onWillStart } from "@odoo/owl";
import { useService } from "@web/core/utils/hooks";

export class OverdueInvoices extends Component {
    static template = "owl_course.OverdueInvoices";
    static props = {};

    setup() {
        this.orm = useService("orm");
        this.action = useService("action");
        this.notification = useService("notification");
        this.state = useState({ invoices: [] });

        onWillStart(async () => {
            this.state.invoices = await this.orm.searchRead(
                "account.move",
                [["move_type", "=", "out_invoice"], ["payment_state", "!=", "paid"]],
                ["name", "partner_id", "amount_residual", "invoice_date_due"],
                { limit: 20, order: "invoice_date_due asc" },
            );
        });
    }

    open(id) {
        this.action.doAction({
            type: "ir.actions.act_window",
            res_model: "account.move",
            res_id: id,
            views: [[false, "form"]],
        });
    }
}

Fetch with the ORM, display, open with the action service, report with notifications. Almost every component you write in Odoo is a variation on this.

What is next

The ORM service in full: search, read, write, create, and calling your own Python methods.

Back to the full course