Advertisement
🖥️ 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

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
    }
});
TechniqueUse
patch(...)extend a standard POS component's behavior
register a componentadd a new button/screen/popup
this.posaccess 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

  1. Override a POS OWL component.
  2. Add a button to a POS screen.
  3. Patch an existing screen.
  4. Use the POS store/state.
  5. 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.

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);
        }
    },
});
Advertisement

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

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?

FAQ

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'].