Point of Sale: Why It's Offline-First
The Odoo POS is architecturally different from the rest of Odoo: it's a JavaScript (OWL) app that loads its data up front and runs in the browser, so a cashier can keep ringing up sales even if the network drops. Orders sync back to the server when the connection returns.
| Phase | What happens |
|---|---|
| Load | products, prices, taxes, customers cached in the browser |
| Sell | cart, totals, payments computed locally in JS — no server call per click |
| Sync | completed orders pushed to the server (queued if offline) |
Why this design: a checkout line can't freeze because the WiFi hiccuped, and it must feel instant — so the POS avoids a server round-trip on every scan or key press. Everything needed to complete a sale is preloaded and processed client-side.
What it means for developers: extending the POS is mostly front-end work — OWL components and JS models, not Python views. When you add a field the POS needs, you must tell it to load that field (via the POS's loaded-models config) or it won't be in the browser's cached data. And because computation happens locally, business logic like custom pricing often has to be implemented in JS on the POS side, not only in Python — a common surprise for backend-focused Odoo devs. Orders reconcile server-side after the fact.
🏋️ Practical Exercise
- Note that POS runs largely in the browser (offline-capable).
- Identify the POS JS assets.
- Note the local order model.
- Understand how orders sync to the backend.
- Open a POS session.
🔥 Challenge Exercise
Explain the Point of Sale architecture: a JavaScript/OWL frontend that runs offline in the browser, keeping orders locally and syncing to the backend when online. Contrast POS’s client-heavy model with standard backend views.
- The two-layer architecture: Python backend + OWL frontend
- How
pos.configandpos.sessionwork - How data is loaded into the browser at session open
- How orders are created, stored, and synced
- The communication layer: RPC calls from POS to server
Two-Layer Architecture
Odoo POS has a clear separation between two layers:
| Layer | Technology | Location |
|---|---|---|
| Backend (server) | Python + PostgreSQL | Odoo server |
| Frontend (client) | OWL + JavaScript | Browser (cashier's device) |
When a cashier opens a POS session, the browser loads all necessary data from the server in one batch. After that, the POS works with local JavaScript objects. The server is only contacted for specific operations: real-time card payments, loyalty program lookups, and closing the session.
pos.config – Shop Configuration
pos.config is the static configuration of a POS shop. One business can have multiple configs (e.g., one for each register or each shop location). Key fields:
name— display name ("Cash Register 1")payment_method_ids— accepted payment methods (cash, card, etc.)pricelist_id— default pricelist for this POSproduct_configurator_ids— which product categories to showmodule_account— whether to generate invoices at checkout
Custom modules that add settings to the POS extend pos.config with new fields.
pos.session – An Active Session
pos.session represents one working session on one pos.config. A session has states:
| State | Meaning |
|---|---|
| opening_control | Cashier enters opening cash amount |
| opened | Active — orders can be processed |
| closing_control | Cashier counts cash before closing |
| closed | Session closed, payments posted to accounting |
Closing a session posts all session payments as bank/cash journal entries and marks all session orders as done. The session's statement_ids link to accounting journal items.
Data Loading at Session Open
When the POS loads, it calls /pos/ui which returns a JSON payload containing everything the frontend needs. The _get_pos_ui_data() method (on pos.config) controls what is loaded:
# What gets sent to the browser at session open (simplified):
{
'pos.session': [session data],
'pos.config': [config data],
'product.product': [all products visible in POS],
'res.partner': [all customers],
'account.tax': [all taxes],
'pos.payment.method': [payment methods],
'pos.category': [product categories],
# ... more models
}
To add your custom model's data to this payload, override _get_pos_ui_data() in your Python extension of pos.config.
Order Lifecycle
A POS order goes through these stages:
- New order — created as a JavaScript object in the browser
- Lines added — product lines with qty, price, tax computed locally
- Payment — payment lines added; total validated
- Order finalized — sent to server via
/pos/create_from_ui - Server creates records —
pos.order+pos.order.line+account.payment
If the internet connection drops after step 3, the order is stored locally (IndexedDB) and synced when connectivity is restored.
RPC Communication
The POS frontend calls the server using JSON-RPC. Key endpoints:
| Endpoint | Purpose |
|---|---|
| /pos/ui | Load initial session data |
| /pos/create_from_ui | Submit completed orders |
| /pos/web_read_models | Fetch additional model data on demand |
| /web/dataset/call_kw | Generic ORM calls (search, read, write) |
From JavaScript, use this.env.services.orm.call(model, method, args) to call Python model methods from the POS frontend.
- POS is an OWL single-page app that loads all data at session open and operates offline-capable
pos.config= static shop settings;pos.session= one working session- Data is loaded via
_get_pos_ui_data()— override it to include your custom model - Completed orders are synced to the server via
/pos/create_from_ui
Interview Questions
- How is the Odoo POS architected?
- Why can POS work offline?
- Where do POS orders live before syncing?
- What frontend technology does POS use?
- How does POS sync with the backend?
Related Topics
FAQ
Yes — once the session is loaded, the POS can process orders offline. Completed orders are stored in the browser's IndexedDB and synced to the server when connectivity is restored. However, some features require connectivity: real-time card payments via payment terminals, loyalty programs with server-side validation, and loading new products added after session open.
Override _get_pos_ui_data() on pos.config in Python, add your model's data to the returned dict, and register the model in JavaScript using the POS model loading system. The model's data will be available as this.env.pos.models in the frontend.
pos.order represents one customer transaction — it has a session, a cashier, payment lines, and a total. pos.order.line represents one product item within that order — it has a product, quantity, price, and tax. One order has many order lines. After closing, orders and lines are archived but remain visible in Odoo's backend for reporting.

