POS UI: Customizing the Cashier Screen With OWL
The POS interface is a fast, touch-friendly OWL application. Customizing it — adding a button, a screen, a product-card badge — means writing or extending OWL components and registering them in the POS.
import { ProductScreen } from "@point_of_sale/app/screens/product_screen/product_screen";
import { patch } from "@web/core/utils/patch";
// extend an existing POS screen without replacing it
patch(ProductScreen.prototype, {
async onCustomButtonClick() {
this.pos.get_order().add_note("VIP"); // manipulate the current order
}
});
| Technique | Use |
|---|---|
patch(...) | extend a standard POS component's behavior |
| register a component | add a new button/screen/popup |
this.pos | access the POS store (order, products) |
The POS UI is OWL, so it's component-based: screens (product screen, payment screen), popups, and buttons are all OWL components. To customize, you rarely rebuild — you patch an existing component (adding or overriding a method) or register a new one into the POS's registry, so your change layers on top and survives upgrades. The pos store is your gateway: from a component, this.pos gives access to the current order, the product database, the cashier session, and actions like adding lines or notes — that's how a custom button affects the sale. Design for touch and speed: the POS is used on touchscreens at a checkout counter under time pressure — big tap targets, minimal steps, instant feedback. Don't add slow operations to the hot path. Offline-first still applies: UI actions operate on the client-side order and loaded data; anything needing server data must have been preloaded. Practical approach: find the standard component closest to what you want, patch it, and test the full sale flow on a touchscreen-sized viewport. Extending rather than replacing keeps you compatible with Odoo's frequent POS updates.
🏋️ Practical Exercise
- Override a POS OWL component.
- Add a button to a POS screen.
- Patch an existing screen.
- Use the POS store/state.
- Register the component.
🔥 Challenge Exercise
Add a custom button to the POS product screen using OWL and the POS registry, wiring it to an action on the current order. Explain how the POS UI is built from OWL components and how to patch/extend existing screens without breaking upgrades.
- The POS component tree: ProductScreen, OrderWidget, PaymentScreen
- Patching existing POS components with
patch() - Adding a custom button to the ProductScreen control pad
- Creating and showing a popup dialog
- Registering a new screen
POS Component Tree
The POS is a hierarchy of OWL components. Key components:
| Component | Purpose |
|---|---|
| Chrome | Root component — manages which screen is active |
| ProductScreen | Main selling screen with product grid and order |
| OrderWidget | The current order (right panel on ProductScreen) |
| PaymentScreen | Payment method selection and tendering |
| ReceiptScreen | Displays the receipt after payment |
| Numpad | Quantity/price/discount input pad |
| ActionpadWidget | Buttons below the order (New Order, Pay, etc.) |
Patching Existing Components
Use the patch() utility from @web/core/utils/patch to extend an existing POS component without replacing it:
/** @odoo-module **/
import { patch } from '@web/core/utils/patch';
import { ProductScreen } from '@point_of_sale/app/screens/product_screen/product_screen';
patch(ProductScreen.prototype, {
/**
* Override to show loyalty points on product select.
*/
selectProduct(product, options) {
super.selectProduct(product, options);
const order = this.env.pos.get_order();
console.log('Current order lines:', order.orderlines.length);
},
});
patch() merges your methods with the existing component. Call super.method() to invoke the original implementation before or after your code.
Adding a Custom Button
Add a button to the POS control pad by patching ActionpadWidget and extending its template:
<!-- views/pos_templates.xml -->
<template id="LoyaltyButton" inherit_id="point_of_sale.ActionpadWidget">
<xpath expr="//div[hasclass('actionpad')]" position="inside">
<button class="btn btn-secondary loyalty-btn"
t-on-click="onClickLoyalty">
Loyalty
</button>
</xpath>
</template>
/** @odoo-module **/
import { patch } from '@web/core/utils/patch';
import { ActionpadWidget } from '@point_of_sale/app/screens/product_screen/action_pad/action_pad';
patch(ActionpadWidget.prototype, {
async onClickLoyalty() {
const { confirmed, payload } = await this.popup.add(LoyaltyCardPopup, {
title: 'Loyalty Card',
});
if (confirmed) {
this.env.pos.get_order().set_loyalty_card(payload.card_code);
}
},
});
Creating a Popup
A popup is an OWL component registered in the popup registry. It must extend AbstractAwaitablePopup:
/** @odoo-module **/
import { Component, useState } from '@odoo/owl';
import { AbstractAwaitablePopup } from '@point_of_sale/app/popup/abstract_awaitable_popup';
export class LoyaltyCardPopup extends AbstractAwaitablePopup {
static template = 'my_module.LoyaltyCardPopup';
static defaultProps = { title: 'Loyalty Card' };
setup() {
super.setup();
this.state = useState({ cardCode: '' });
}
getPayload() {
return { card_code: this.state.cardCode };
}
}
<templates>
<t t-name="my_module.LoyaltyCardPopup">
<div class="popup">
<header><h3 t-esc="props.title"/></header>
<section>
<div class="input-group">
<label>Card Code</label>
<input type="text" t-model="state.cardCode"
placeholder="Scan or type card code"/>
</div>
</section>
<footer>
<button class="cancel" t-on-click="cancel">Cancel</button>
<button class="confirm" t-on-click="confirm">Apply</button>
</footer>
</div>
</t>
</templates>
Registering JS/CSS in POS Assets
POS JS and XML files must be added to the point_of_sale.assets bundle in your module's __manifest__.py:
'assets': {
'point_of_sale.assets': [
'my_module/static/src/js/loyalty_popup.js',
'my_module/static/src/xml/loyalty_popup.xml',
'my_module/static/src/css/pos_custom.css',
],
}
- Use
patch()to extend existing POS components — never fork the original file - Add buttons via QWeb template inheritance on the target component's template
- Popups extend
AbstractAwaitablePopupand return a payload viagetPayload() - Register all JS and XML files under
'point_of_sale.assets'in__manifest__.py
Interview Questions
- What framework builds the POS UI?
- How do you add a button to a POS screen?
- How do you extend an existing POS component?
- What is the POS store?
- Why prefer patching over copying POS components?
Related Topics
FAQ
OWL uses ES6 class inheritance for creating entirely new components. patch() modifies an existing component's prototype in-place — it's used when you want to extend an existing component that is already registered in the POS and don't want to replace its registration. Use inheritance for brand-new components; use patch() for extending existing ones.
Yes — the popup service's add() method returns a promise, but you don't have to await it. If you don't await, the popup appears and the code continues. The popup will close when the user clicks confirm or cancel, but you won't receive the payload. For most use cases, awaiting is the right choice.
Inject the POS store service: this.env.pos is available in any POS component that is inside the Chrome root. Call this.env.pos.get_order() for the active order. If your component is outside the POS tree, inject the pos service via static serviceDependencies = ['pos'].

