- 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
Frequently Asked Questions
How is patching different from class inheritance in OWL?
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.
Can I show a popup without waiting for the result?
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.
How do I access the current order from any POS component?
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'].