How to Create a Custom Screen in POS in Odoo 18
By Braincuber Team
Published on January 17, 2026
The Point of Sale module is one of Odoo's most interactive interfaces—and sometimes, the built-in screens just aren't enough. Maybe you need a loyalty points dashboard, a custom customer lookup, or a special order summary. Whatever the use case, Odoo 18's OWL framework makes it possible to inject entirely new screens into the POS interface.
In this hands-on tutorial, we'll build a Customer Loyalty Dashboard screen that displays points, tier status, and rewards. You'll learn how to create the XML template, write the JavaScript component, patch the control buttons to add a trigger, and wire everything up in the manifest. By the end, you'll have a reusable pattern for adding any custom screen to Odoo POS.
What You'll Build: A custom POS screen triggered by a button in the control panel. The screen will display customer loyalty information with a close button to return to the product screen.
Why Custom POS Screens?
Specialized Workflows
Display information specific to your business—loyalty programs, custom product configurators, or kitchen display integration.
Customer Engagement
Show customers their rewards, membership status, or personalized recommendations right at checkout.
Reporting Dashboards
Give cashiers quick access to daily sales stats, pending orders, or inventory alerts without leaving the POS.
Third-Party Integration
Display data from external APIs—delivery tracking, CRM notes, or inventory from another system.
Module Structure
Here's how our custom POS module will be organized:
pos_loyalty_screen/ ├── static/ │ └── src/ │ ├── js/ │ │ ├── loyalty_screen.js # Screen component │ │ └── loyalty_button.js # Button patch │ └── xml/ │ ├── loyalty_screen.xml # Screen template │ └── loyalty_button.xml # Button template ├── __init__.py └── __manifest__.py
Step 1: Create the Screen Template
The XML template defines the visual layout. We're building a loyalty dashboard with customer info, points balance, and tier status:
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<t t-name="pos_loyalty_screen.LoyaltyScreen">
<div class="screen h-100 bg-100">
<!-- Header with back button -->
<div class="top-content d-flex align-items-center p-3 bg-white border-bottom">
<button class="btn btn-secondary" t-on-click="closeScreen">
<i class="fa fa-arrow-left me-2"/>Back to POS
</button>
<h2 class="ms-3 mb-0">Customer Loyalty Dashboard</h2>
</div>
<!-- Main content area -->
<div class="content p-4">
<div class="row">
<!-- Customer Info Card -->
<div class="col-md-4">
<div class="card shadow-sm">
<div class="card-body text-center">
<div class="rounded-circle bg-primary text-white d-inline-flex
align-items-center justify-content-center mb-3"
style="width:80px;height:80px;font-size:2rem">
<t t-esc="customerInitials"/>
</div>
<h4 t-esc="customerName"/>
<p class="text-muted" t-esc="customerEmail"/>
</div>
</div>
</div>
<!-- Points Card -->
<div class="col-md-4">
<div class="card shadow-sm bg-success text-white">
<div class="card-body text-center">
<h6 class="text-uppercase">Points Balance</h6>
<h1 class="display-4" t-esc="loyaltyPoints"/>
<p>Available to redeem</p>
</div>
</div>
</div>
<!-- Tier Card -->
<div class="col-md-4">
<div class="card shadow-sm bg-warning">
<div class="card-body text-center">
<h6 class="text-uppercase">Current Tier</h6>
<h2 t-esc="loyaltyTier"/>
<p t-esc="tierBenefits"/>
</div>
</div>
</div>
</div>
</div>
</div>
</t>
</templates>
Step 2: Create the JavaScript Component
The JavaScript component handles the screen's logic, data, and lifecycle. It extends OWL's Component class and registers itself in the POS screens registry:
/** @odoo-module */
import { Component } from "@odoo/owl";
import { registry } from "@web/core/registry";
import { usePos } from "@point_of_sale/app/store/pos_hook";
export class LoyaltyScreen extends Component {
static template = "pos_loyalty_screen.LoyaltyScreen";
setup() {
this.pos = usePos();
}
// Computed properties for the template
get customerName() {
const partner = this.pos.get_order()?.get_partner();
return partner?.name || "Guest Customer";
}
get customerEmail() {
const partner = this.pos.get_order()?.get_partner();
return partner?.email || "No email on file";
}
get customerInitials() {
const name = this.customerName;
if (name === "Guest Customer") return "G";
const parts = name.split(" ");
return parts.map(p => p[0]).join("").substring(0, 2).toUpperCase();
}
get loyaltyPoints() {
// In production, fetch from loyalty program
const partner = this.pos.get_order()?.get_partner();
return partner?.loyalty_points || 0;
}
get loyaltyTier() {
const points = this.loyaltyPoints;
if (points >= 1000) return "Platinum";
if (points >= 500) return "Gold";
if (points >= 100) return "Silver";
return "Bronze";
}
get tierBenefits() {
const tier = this.loyaltyTier;
const benefits = {
Platinum: "20% off all purchases",
Gold: "15% off all purchases",
Silver: "10% off all purchases",
Bronze: "5% off all purchases"
};
return benefits[tier];
}
closeScreen() {
this.pos.showScreen("ProductScreen");
}
}
// Register the screen
registry.category("pos_screens").add("LoyaltyScreen", LoyaltyScreen);
Step 3: Add the Trigger Button
We need a way to open our screen. We'll patch the ControlButtons component to add a new button:
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<t t-name="pos_loyalty_screen.ControlButtons"
t-inherit="point_of_sale.ControlButtons"
t-inherit-mode="extension">
<xpath expr="//t[@t-if='props.showRemainingButtons']/div/OrderlineNoteButton"
position="after">
<button t-att-class="buttonClass"
t-on-click="openLoyaltyScreen">
<i class="fa fa-star me-1" role="img"
aria-label="Loyalty Dashboard" title="Loyalty Dashboard"/>
Loyalty
</button>
</xpath>
</t>
</templates>
/** @odoo-module */
import { patch } from "@web/core/utils/patch";
import { ControlButtons } from "@point_of_sale/app/screens/product_screen/control_buttons/control_buttons";
patch(ControlButtons.prototype, {
openLoyaltyScreen() {
this.pos.showScreen("LoyaltyScreen");
}
});
Step 4: Configure the Manifest
The manifest registers our assets with the POS bundle:
{
'name': 'POS Loyalty Screen',
'version': '18.0.1.0.0',
'category': 'Point of Sale',
'summary': 'Custom loyalty dashboard screen for POS',
'description': """
Adds a Loyalty button to POS that opens a customer
loyalty dashboard showing points, tier, and benefits.
""",
'depends': ['point_of_sale'],
'data': [],
'assets': {
'point_of_sale._assets_pos': [
'pos_loyalty_screen/static/src/js/loyalty_screen.js',
'pos_loyalty_screen/static/src/js/loyalty_button.js',
'pos_loyalty_screen/static/src/xml/loyalty_screen.xml',
'pos_loyalty_screen/static/src/xml/loyalty_button.xml',
],
},
'installable': True,
'auto_install': False,
'license': 'LGPL-3',
}
Important: The asset bundle point_of_sale._assets_pos is specific to Odoo 18. In earlier versions, the bundle name may differ. Always check the core POS module for the correct bundle name.
Key Concepts Explained
OWL Component Registration
The screen component is registered using registry.category("pos_screens").add(). This makes it available to this.pos.showScreen().
Template Inheritance
We use t-inherit and t-inherit-mode="extension" to add our button to existing ControlButtons without replacing it entirely.
The usePos Hook
usePos() gives access to the POS store—orders, products, partners, and screen navigation methods like showScreen().
Best Practices
Custom POS Screen Tips:
- Always provide a back button: Users need a clear way to return to the main POS interface.
- Use Bootstrap classes: The POS already includes Bootstrap; leverage it for consistent styling.
- Handle missing data: Check for null partners, empty orders, and other edge cases gracefully.
- Keep it fast: Avoid heavy computations or slow API calls in the screen component—POS should be snappy.
Conclusion
Creating custom screens in Odoo 18 POS follows a clear pattern: XML template for layout, JavaScript component for logic, button patch for triggering, and manifest for bundling. Once you understand this structure, you can build any custom interface your business needs—from loyalty dashboards to inventory checks to customer surveys.
Key Takeaway: Create XML template with t-name → Build JS component extending Component → Register in pos_screens registry → Patch ControlButtons to add trigger → Bundle in point_of_sale._assets_pos.
