orm Service
The most used service. Wraps Odoo's JSON-RPC ORM calls with a clean async API.
/** @odoo-module **/
import { useService } from "@web/core/utils/hooks";
setup() {
this.orm = useService("orm");
}
// ── searchRead: search + read in one call ─────────────────────────
const partners = await this.orm.searchRead(
"res.partner", // model
[["is_company", "=", true], ["active", "=", true]], // domain
["id", "name", "email", "phone"], // fields to fetch
{ limit: 20, offset: 0, order: "name asc" } // options
);
// ── read: fetch specific records by IDs ──────────────────────────
const [order] = await this.orm.read(
"sale.order",
[this.props.orderId],
["name", "partner_id", "amount_total", "state"]
);
// ── write: update records ─────────────────────────────────────────
await this.orm.write(
"res.partner",
[partnerId],
{ name: "New Name", phone: "+1234567890" }
);
// ── create: create a record ───────────────────────────────────────
const newId = await this.orm.create(
"sale.order",
{ partner_id: 5, order_line: [[0, 0, { product_id: 12, product_uom_qty: 2 }]] }
);
// ── unlink: delete records ────────────────────────────────────────
await this.orm.unlink("res.partner", [id1, id2]);
// ── call: custom Python method on model ──────────────────────────
const result = await this.orm.call(
"sale.order", // model
"action_confirm", // method
[[orderId]], // positional args (first arg is always ids list)
{ raise_if_nothing: true } // keyword args
);
notification Service
setup() {
this.notification = useService("notification");
}
// Simple message
this.notification.add("Record saved successfully!");
// With type
this.notification.add("Invalid email address", { type: "warning" });
this.notification.add("Could not connect to server", { type: "danger" });
this.notification.add("Payment received", { type: "success" });
// Sticky — stays until manually closed
this.notification.add("Long background task started…", {
type: "info",
sticky: true,
});
// With action buttons
this.notification.add("New sale order created", {
type: "success",
buttons: [
{
name: "View Order",
primary: true,
onClick: () => this.action.doAction({
type: "ir.actions.act_window",
res_model: "sale.order",
res_id: newOrderId,
views: [[false, "form"]],
}),
},
],
});
action Service
setup() {
this.action = useService("action");
}
// Open a form view for a specific record
this.action.doAction({
type: "ir.actions.act_window",
res_model: "res.partner",
res_id: partnerId,
views: [[false, "form"]],
target: "current", // "current" | "new" (dialog) | "fullscreen"
});
// Open a list view with a domain
this.action.doAction({
type: "ir.actions.act_window",
res_model: "sale.order",
views: [[false, "list"], [false, "form"]],
domain: [["state", "in", ["draft", "sent"]]],
name: "Draft Orders",
});
// Trigger by XML ID
this.action.doAction("sale.action_quotations_with_onboarding");
// Client action
this.action.doAction({ type: "ir.actions.client", tag: "my_addon.dashboard" });
// Switch current view type
this.action.switchView("kanban");
dialog Service
import { ConfirmationDialog } from "@web/core/confirmation_dialog/confirmation_dialog";
setup() {
this.dialog = useService("dialog");
}
// Built-in confirmation dialog
confirmDelete() {
this.dialog.add(ConfirmationDialog, {
title: "Delete Record",
body: "This action cannot be undone. Are you sure?",
confirm: async () => {
await this.orm.unlink("sale.order", [this.props.orderId]);
this.notification.add("Deleted successfully", { type: "success" });
},
cancel: () => {},
});
}
// Custom dialog component
openCustomDialog() {
this.dialog.add(MyCustomDialog, {
// Props passed to the dialog component
orderId: this.state.selectedId,
onSave: (data) => this.handleSave(data),
});
}
user Service
setup() {
this.user = useService("user");
}
// Properties
const uid = this.user.userId; // res.users ID (number)
const name = this.user.name; // "Administrator"
const lang = this.user.lang; // "en_US"
const tz = this.user.tz; // "Europe/London"
const isAdmin = this.user.isAdmin; // true / false
const companies = this.user.allowedCompanies;
// Check group membership
const isSalesManager = await this.user.hasGroup("sales_team.group_sale_manager");
const isAccountant = await this.user.hasGroup("account.group_account_user");
Writing a Custom Service
/** @odoo-module **/
import { registry } from "@web/core/registry";
// Services receive { env } and optionally dependencies from other services
const myAnalyticsService = {
dependencies: ["orm", "user"],
start(env, { orm, user }) {
// Runs once when the service is initialized
const sessionId = crypto.randomUUID();
return {
// Public API of the service
track(eventName, payload = {}) {
console.log(`[Analytics] ${eventName}`, {
userId: user.userId,
sessionId,
timestamp: Date.now(),
...payload,
});
// In production: POST to /web/dataset/call_kw or external API
},
setPage(pageName) {
this.track("page_view", { page: pageName });
},
};
},
};
// Register in the services category
registry.category("services").add("my_analytics", myAnalyticsService);
// Usage in any component:
// this.analytics = useService("my_analytics");
// this.analytics.track("button_click", { button: "confirm_order" });
📋 Summary
- orm:
searchRead,read,write,create,unlink,call— all return Promises. - notification:
add(message, { type, sticky, buttons })— types: info, success, warning, danger. - action:
doAction(action | xmlid),switchView(type)— navigate to any Odoo view or client action. - dialog:
add(DialogComponent, props)— opens any component as a modal; use built-inConfirmationDialogfor confirmations. - user:
userId,name,lang,isAdmin,hasGroup(xmlid). - Custom services: register under
registry.category("services")with astart(env, deps)method that returns the public API.
🏋️ Exercise
Build a QuickActionsPanel component using multiple services together:
- Use
userservice to show the logged-in user's name and hide admin-only buttons when!user.isAdmin. - A "Confirm All Quotes" button: uses
orm.callto call a Python method, then shows a successnotification. - A "View Pipeline" button: uses
action.doActionto open the CRM pipeline kanban view. - A "Delete Selected" button: uses
dialog.add(ConfirmationDialog)before callingorm.unlink. - Add a custom service
audit_logthat logs user actions to the console. Call it from each button click.
FAQ
Use orm service for standard model operations (read, write, create, unlink, searchRead, call). Use rpc service only for non-ORM endpoints: custom controllers, JSON-RPC calls to routes you wrote, or third-party API calls through Odoo. The orm service handles authentication, session cookies, and error formatting automatically — the rpc service is lower-level.
Yes — declare dependencies in the dependencies array of your service definition. Odoo resolves them and passes them as the second argument to start(env, { dep1, dep2 }). Circular dependencies will throw at startup. This is how orm internally uses the rpc service, and how the action service uses router.