Odoo POS is an outlier in the suite. Almost every other Odoo application is a server-rendered form with synchronous RPC calls. POS is a single-page browser app that loads all its data at session open, runs locally including during network outages, and syncs to the server when it reconnects. That architecture makes it robust for retail environments and complex to extend.
This guide covers what you need to know to configure POS correctly, understand the data model, and build extensions that work within the offline constraint.
The session model#
A POS session maps to pos.session. Sessions go through three states:
new_session → opening_control → open → closing_control → closedOpening control shows the opening cash count. The cashier enters the actual cash in drawer; Odoo records the expected amount from the previous session's closing.
Closing control summarizes the session: total sales by payment method, expected vs. counted cash, any discrepancy. Validating the closing creates the journal entries, reconciles the cash, and posts the session invoice if configured.
One session per POS configuration (pos.config) at a time. Multiple terminals running the same POS config share the same session - useful for a store with several checkout lanes all posting to the same end-of-day journal entry.
Loading data at session open#
When a session opens, the POS app calls /pos/models/load (or the older /web/dataset/call_kw pattern in earlier versions) to download all the data it needs: products, taxes, pricelists, payment methods, customers, and more. This request is the single most important performance factor for POS startup.
If startup is slow, the most common cause is too many active products. POS loads every product with available_in_pos = True. Deactivate products that are not sold at the counter. For a store with 50,000 SKUs in the ERP, only the 2,000 counter-sellable items should have the flag.
Payment methods and journals#
Payment methods in POS are pos.payment.method records. Each method is linked to an accounting journal. The journal type determines how the payment is accounted:
| Journal type | Use case |
|---|---|
| Cash | Physical cash; shows in cash count at close |
| Bank | Card terminals, bank transfer |
| Misc | Internal settlements (e.g., employee accounts) |
For integrated card terminals (Stripe, Adyen, Worldline), Odoo provides dedicated payment method modules (pos_adyen, pos_stripe, etc.). These add a terminal configuration to the payment method and handle the payment request/response over a secondary connection while the POS UI waits.
Split payment - a single order can be paid with multiple methods. Odoo tracks each payment line in pos.payment. The journal entry at session close aggregates all payments per method.
Product configuration for POS#
Three fields on product.template matter for POS:
| Field | Effect |
|---|---|
available_in_pos | Includes the product in POS data load |
pos_categ_ids | POS category for the on-screen product grid |
to_weigh_with_scale | Triggers barcode scale integration |
POS categories (pos.category) are distinct from internal product categories (product.category). They exist solely for the POS touchscreen layout. Configure a category tree that matches how your cashiers think, not how the accounting team categorizes inventory.
Variants and attributes#
Product variants (size, color) are fully supported in POS. When a product with variants is selected, POS shows an attribute picker before adding to the order. For products with many variant combinations, this can be slow - consider flattening high-velocity products into separate product.template records if speed matters more than catalog tidiness.
Pricelists#
POS supports pricelists if you enable Pricelists in POS configuration. Once enabled, you can assign a pricelist to each customer. The pricelist is evaluated client-side in the browser; Odoo does not make a server call per order line. This means:
- All pricelists and their rules are downloaded at session open (adds to load time).
- Pricelist rules based on quantity breaks work correctly.
- Rules that call Python code (custom pricelists) are not supported in POS - the browser cannot run server-side Python.
If you have complex pricing logic that requires server-side computation, you need a custom /pos/get_price endpoint and a POS UI patch to call it per order line (and handle the offline case).
Loyalty programs and gift cards#
Odoo 16+ unified loyalty programs across POS, eCommerce, and Sales into loyalty.program. A program has:
- Triggers - conditions that activate the program (minimum order amount, specific products)
- Rewards - what the customer gets (discount, free product, points)
In POS, loyalty points are tracked on the customer (res.partner). When the customer is identified at checkout (barcode scan or search), their point balance is shown and applicable rewards are offered.
Gift cards work through the same framework. A gift card is a loyalty.card with a monetary balance. At checkout, the cashier scans the gift card barcode; Odoo applies the balance as a payment (creating a pos.payment line against the gift card journal).
Extending POS: the OWL component model#
POS UI is built on Odoo's OWL (Odoo Web Library) component framework. Extending it requires understanding two layers:
1. Patching existing components#
OWL extensions use the patch utility to override methods on existing components:
import { patch } from "@web/core/utils/patch";
import { ProductScreen } from "@point_of_sale/app/screens/product_screen/product_screen";
patch(ProductScreen.prototype, {
async _clickProduct(event) {
// Custom logic before adding to order
const product = event.detail;
if (product.requires_age_verification) {
const verified = await this.showAgeVerificationDialog();
if (!verified) return;
}
await super._clickProduct(event);
},
});Patches are applied at module load time. Because POS runs as a bundled app, your custom module must be included in the POS app asset bundle:
<template id="assets" inherit_id="point_of_sale._assets_pos">
<xpath expr="." position="inside">
<script type="module" src="/my_module/static/src/js/product_screen_patch.js"/>
</xpath>
</template>2. Adding new screens and popups#
New screens inherit from AbstractScreen, new popups from AbstractPopup. Odoo POS uses a centralized popup registry - you register your popup class and invoke it with showPopup('MyPopup', props).
3. Communicating with the backend#
For operations that require a server call (creating a record, checking inventory), use the POS orm service:
const result = await this.env.services.orm.call(
'my.model',
'my_method',
[arg1, arg2],
{ kwarg: value }
);Critical constraint: any feature that uses this pattern becomes unavailable when the network is offline. Design your extension so it degrades gracefully - show a warning, disable the button, or queue the call for reconnection. Do not block the order flow on a server call.
Developer tips#
Use the POS config for custom settings. Add custom fields to pos.config and include them in the session data load (_get_pos_ui_pos_config override). This avoids a round trip per session for your feature's settings.
Test offline mode explicitly. In Chrome DevTools → Network, throttle to Offline. Confirm your extension handles the disconnected state. Offline errors that crash the POS UI are severe in a retail environment.
Avoid heavy ORM calls in session data load. Methods hooked into _get_pos_ui_* are called synchronously during session open. A 500 ms query means every cashier waits 500 ms longer at shift start. Profile with ?debug=1 and the developer mode network tab.
Journal entries for custom payment methods. If you add a custom payment method (e.g., a house account), you must configure the reconciliation correctly. Unreconciled lines in the POS cash journal cause closing to fail.
For the website eCommerce side of the Odoo retail stack, see the eCommerce configuration guide. For inventory and fulfillment that POS orders trigger, see the inventory configuration guide.

