Ad – 728×90
🖥️ POS Development

Odoo POS UI Components – OWL in the Point of Sale

The POS frontend is built with OWL components. Customizing the UI means patching existing components to add behavior, or registering new components as screens, buttons, or popups. Odoo's patching system lets you do this without forking the original code.

⏱️ 25 min 🎯 Advanced 📅 Updated 2026
What you'll learn:
  • 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:

ComponentPurpose
ChromeRoot component — manages which screen is active
ProductScreenMain selling screen with product grid and order
OrderWidgetThe current order (right panel on ProductScreen)
PaymentScreenPayment method selection and tendering
ReceiptScreenDisplays the receipt after payment
NumpadQuantity/price/discount input pad
ActionpadWidgetButtons 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:

JavaScript
/** @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:

XML (QWeb/OWL)
<!-- 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>
JavaScript
/** @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);
        }
    },
});
Ad – 728×90

Creating a Popup

A popup is an OWL component registered in the popup registry. It must extend AbstractAwaitablePopup:

JavaScript
/** @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 };
    }
}
XML (OWL template)
<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:

Python
'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',
    ],
}
Key takeaways:
  • 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 AbstractAwaitablePopup and return a payload via getPayload()
  • 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'].