Welcome to the world of Odoo 18 POS OWL development! If you’re looking to transform your Odoo Point of Sale experience, you’ve come to the right place. This guide will walk you through the essentials of customizing the Odoo POS using the powerful OWL (Odoo Web Library) framework. We’ll explore how to modify the look and feel, extend functionalities, and create new components, turning your POS into a tailored solution that perfectly fits your business needs. Get ready to unlock the full potential of your Odoo 18 POS OWL system.
Your Ultimate Customization Guide!
source code : https://github.com/alferjay/odoo-owl-tutorial/tree/pos_inheritance_v2/custom-addons/owl
This tutorial is not for absolute beginners; a foundational understanding of JavaScript, XML, and general Odoo module structure will be beneficial. We’ll cover everything from basic UI tweaks like changing logos and colors to more advanced JavaScript extensions for adding new features and overriding existing methods.
Understanding the Odoo 18 POS OWL Structure: The Foundation of Customization
Before diving into coding, it’s crucial to understand the underlying structure of an Odoo 18 POS OWL module and the key files you’ll be interacting with.
Setting Up Your Custom Odoo 18 POS OWL Module
First, you’ll need a custom Odoo module. Let’s call it my_custom_pos.
- Create the Module Directory:
Create a new directory namedmy_custom_poswithin your Odooaddonspath. - The
__manifest__.pyFile:
Next, create an__manifest__.pyfile insidemy_custom_pos. This file defines your module’s metadata.
# my_custom_pos/__manifest__.py
{
'name': 'My Custom POS Enhancements for Odoo 18 POS OWL',
'version': '1.0',
'summary': 'Customizations for the Odoo 18 Point of Sale using OWL.',
'author': 'Your Name',
'category': 'Point of Sale',
'depends': ['point_of_sale'], # Crucial dependency
'assets': {
'point_of_sale.assets': [
# SCSS files for styling
'my_custom_pos/static/src/scss/pos.scss',
'my_custom_pos/static/src/scss/favorite_products.scss',
# XML files for OWL templates
'my_custom_pos/static/src/xml/chrome.xml',
'my_custom_pos/static/src/xml/payment_screen.xml',
'my_custom_pos/static/src/xml/favorite_products.xml',
# JavaScript files for OWL components and logic
'my_custom_pos/static/src/js/payment_screen.js',
'my_custom_pos/static/src/js/favorite_products.js',
'my_custom_pos/static/src/js/pos_global_state.js',
],
},
'installable': True,
'application': False,
'auto_install': False,
}
- Important: The
point_of_sale.assetskey is where you register all your static assets (JS, XML, SCSS) that need to be loaded by the POS. - The
__init__.pyFile:
Then, create an__init__.pyfile inmy_custom_posto make it a Python package. If you add Python models, import them here.# my_custom_pos/__init__.py from . import modelsAnd an__init__.pyinsidemy_custom_pos/models:# my_custom_pos/models/__init__.py from . import product_product
Exploring Key JavaScript Files in the Native Odoo 18 POS OWL System
The Odoo POS JavaScript codebase is organized into several key files within the point_of_sale module (typically found under addons/point_of_sale/static/src/js/ in older versions, and app/ subdirectories in Odoo 17+ for OWL components). Understanding their roles is vital for Odoo 18 POS OWL development:
chrome.js(or its equivalent inapp/chrome/): This is often the root component of the POS application. It handles the main layout, including headers, and manages the overall application state.registry.js(often part ofapp/store/registries.js): Odoo POS uses a registry system to manage and extend components, popups, screens, and more. This allows for modularity and easier customization without directly altering core files. You’ll frequently useRegistries.Component.add()orRegistries.Component.extend().models.js(often part ofapp/store/models.jsorpos_store.js): This file (or its new equivalent) defines the data models used within the POS, such as how orders, products, and customers are structured and handled on the client-side. It also includes logic for RPC calls to the backend.db.js(often part ofapp/store/db.js): The POS database (this.env.pos.dborthis.pos.db) is an in-memory JavaScript database that stores data fetched from the server (products, categories, customers, etc.) for quick offline access. This file manages the initialization and interaction with this local database.- Screen Components (e.g.,
ProductScreen.js,PaymentScreen.jsinapp/screens/): Each major view in the POS (like the product selection screen or payment screen) is an OWL component.
Core XML Structure in Odoo 18 POS OWL Applications
OWL components use XML templates to define their structure. Key native XML files include:
chrome.xml(orpos_app.xmlinapp/chrome/templates/): This template defines the main layout of the POS, including the header, content area, and potentially placeholders for notifications and popups.- Screen Templates (e.g.,
ProductScreen.xml,PaymentScreen.xmlinapp/screens/respective subdirectories): Each screen component has an associated XML template defining its visual structure. For instance,ProductScreen.xmltypically outlines the left pane (order widget, numpad) and the right pane (product list).
The OWL Component Lifecycle and Essential Concepts for Odoo 18 POS OWL
When building with Odoo 18 POS OWL, you’ll constantly use these core OWL concepts:
PosComponent: Most custom POS components will extendPosComponent(from@point_of_sale/app/store/pos_storeor a similar path). This base component provides access to the POS environment (this.env.posorthis.pos).setup(): This is the constructor equivalent in OWL components. Importantly, always callsuper.setup(...args)at the beginning of yoursetupmethod to ensure the parent component initializes correctly.static template: This static property in your JS component class links to its XML template (e.g.,static template = 'my_custom_pos.MyCustomButton';).useState(initialState): Imported from@odoo/owl, this hook makes your component’s state reactive. Any changes to state variables will trigger a re-render of the component.useService(serviceName): This hook allows you to access various Odoo services likeorm(for RPC calls),popup,notification, andpositself.useListener(eventName, selector?, callback): This hook attaches event listeners to elements within your component’s template (or globally if no selector).trigger(eventName, detail?): Used to dispatch custom events that other components (often parents) can listen for.
Customizing the Look and Feel with CSS and XML in Odoo 18 POS OWL
Let’s start with some visual customizations. These often involve SCSS for styling and XML for template modifications.
Personalizing Your Odoo 18 POS OWL Interface: Changing the POS Logo
The POS logo is usually defined in the chrome.xml (or equivalent main app) template.
- Create an
imgDirectory:
Insidemy_custom_pos/static/src/, create animgdirectory. Place your new logo (e.g.,my_logo.png) here. - Inherit
chrome.xml:
Createmy_custom_pos/static/src/xml/chrome.xml:
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<t t-name="point_of_sale.Chrome" t-inherit="point_of_sale.Chrome" t-inherit-mode="extension">
<xpath expr="//img[hasclass('pos-logo')]" position="attributes">
<attribute name="src">/my_custom_pos/static/src/img/my_logo.png</attribute>
</xpath>
</t>
</templates>
- This code finds the
imgtag with the classpos-logoand changes itssrcattribute to your new logo.
Modifying Background Colors and Styles with SCSS in Your Odoo 18 POS OWL System
You can add custom styles using SCSS.
- Create
pos.scss:
Createmy_custom_pos/static/src/scss/pos.scss. - Add Styles:
For example, to change the header background and the product list background:
// my_custom_pos/static/src/scss/pos.scss
// Targeting the POS header
// Inspect your POS to find the correct class for the header bar.
// It might be '.pos .pos-header' or a more specific class.
.pos .pos-header {
background-color: #333A40; // A dark green example
// You might need to adjust text color for readability
color: white;
.pos-logo {
height: 80% !important; // Adjust logo height if needed
margin: 5px;
}
}
// Targeting the product list area (right pane in ProductScreen)
// Inspect to find the correct class. It could be '.pos .rightpane' or similar.
.pos .product-screen .rightpane { // Example selector
background-color: #FFFFFF; // Make product list background white
}
// Example: Darkening the order list (left pane)
.pos .product-screen .leftpane { // Example selector
background-color: #333A40;
color: #F0F0F0;
.orderline {
color: #D0D0D0;
.product-name {
color: #FFFFFF;
}
.price{
color: #FFFFFF;
}
}
.order-summary {
.entry {
color: #FFFFFF;
}
}
.numpad button {
background-color: #4A525A;
color: white;
&:hover {
background-color: #5A626A;
}
}
.actionpad button {
background-color: #00A09D; // Odoo green for action buttons
color: white;
&:hover {
background-color: #00B0AD;
}
}
}
- Note: Always inspect the live POS interface with your browser’s developer tools to find the exact CSS classes you need to target. Classes can change between Odoo versions.
Efficiently Rearranging Elements in the Odoo 18 POS OWL Layout
Let’s say you want to swap the order of the left and right panes on the ProductScreen. This is often controlled by CSS Flexbox properties.
- Target Panes in SCSS:
In yourmy_custom_pos/static/src/scss/pos.scss:
// my_custom_pos/static/src/scss/pos.scss
// Assuming .product-screen uses display: flex for .leftpane and .rightpane
.pos .product-screen {
// This is an example; the parent container of leftpane and rightpane
// needs to be a flex container for 'order' to work.
// display: flex; // Ensure the parent is a flex container if not already
.leftpane {
order: 2; // Move left pane to the second position
}
.rightpane {
order: 1; // Move right pane to the first position
}
}
- This demonstrates how CSS
ordercan rearrange flex items. The success of this depends on the existing CSS structure ofProductScreen.
Extending POS Functionality with JavaScript and OWL in Odoo 18
Now for the more dynamic customizations using JavaScript and the Odoo 18 POS OWL framework.
Integrating a Custom Button into the Payment Screen of Your Odoo 18 POS OWL
Let’s add a new button to the PaymentScreen.
- Create the Button Component (JS):
Createmy_custom_pos/static/src/js/my_custom_payment_button.js:
// my_custom_pos/static/src/js/my_custom_payment_button.js
odoo.define('my_custom_pos.MyCustomPaymentButton', function(require) {
'use strict';
const { PosComponent } = require("@point_of_sale/app/store/pos_store");
const { Registries } = require("@point_of_sale/app/store/registries");
const { useService } = require("@web/core/utils/hooks");
class MyCustomPaymentButton extends PosComponent {
setup() {
super.setup();
this.notification = useService("notification");
}
async onClick() {
// Custom logic for the button click
console.log("Custom Payment Button Clicked!");
this.notification.add("Custom button was pressed!", { type: 'info' });
// You can call other methods or services here
// For example, this.env.pos.showScreen('ProductScreen');
}
}
MyCustomPaymentButton.template = 'my_custom_pos.MyCustomPaymentButton';
Registries.Component.add(MyCustomPaymentButton);
return MyCustomPaymentButton;
});
- Create the Button Template (XML):
Createmy_custom_pos/static/src/xml/my_custom_payment_button_template.xml(and register it in__manifest__.py‘s assets, or combine it with other XML files):
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<t t-name="my_custom_pos.MyCustomPaymentButton" owl="1">
<button class="btn btn-primary control-button" t-on-click="onClick">
Next (Custom)
<i class="fa fa-arrow-right" role="img" aria-label="Next"/>
</button>
</t>
</templates>
- Self-correction: It’s better to put this button template inside
payment_screen.xmlor a dedicated button XML file for clarity, then ensure that file is in the manifest. Let’s assume we’ll put it intopayment_screen.xmlfor now. - Inherit
PaymentScreen.xmlto Add the Button:
Modify/Createmy_custom_pos/static/src/xml/payment_screen.xml:
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<t t-name="my_custom_pos.MyCustomPaymentButton" owl="1">
<button class="btn btn-lg btn-primary control-button" style="right: 150px; bottom: 0px; font-size: 1.2em;" t-on-click="onClick">
Next (Custom)
<i class="fa fa-arrow-right" role="img" aria-label="Next"/>
</button>
</t>
<t t-name="point_of_sale.PaymentScreen" t-inherit="point_of_sale.PaymentScreen" t-inherit-mode="extension">
<xpath expr="//div[hasclass('paymentmethods-container')]" position="inside">
<MyCustomPaymentButton />
</xpath>
</t>
</templates>
- Note: The
xpathexpression is critical. You need to find a stable element in the originalPaymentScreen.xmlto append your button to. Using<MyCustomPaymentButton />directly works because we registered it. - The JavaScript file for the Payment Screen (if you need to add methods called by the button):
If yourMyCustomPaymentButtonneeds to call methods on its parentPaymentScreen, you’d extendPaymentScreen.js. For the current button that only logs and notifies, this step isn’t strictly needed yet, but for the sake of demonstrating method overriding:
Createmy_custom_pos/static/src/js/payment_screen.js:
// my_custom_pos/static/src/js/payment_screen.js
odoo.define('my_custom_pos.PaymentScreen', function(require) {
'use_strict';
const PaymentScreen = require("@point_of_sale/app/screens/payment_screen/payment_screen");
const Registries = require("@point_of_sale/app/store/registries");
const MyCustomPosPaymentScreen = (OriginalPaymentScreen) =>
class extends OriginalPaymentScreen {
setup() {
super.setup();
console.log("Inherited Payment Screen Setup Correctly!");
}
// Example: Overriding an existing method or adding a new one
async addNewPaymentLine({ detail: paymentMethod }) {
console.log("Inherited addNewPaymentLine for method:", paymentMethod.name);
// Always call super if you are overriding and want original functionality
const result = await super.addNewPaymentLine(...arguments);
// Add custom logic after original method execution
console.log("Custom logic after adding payment line.");
return result;
}
// Method for our new button (if it needs to interact with screen state)
async goNext() {
console.log("Go Next method called from custom button on Payment Screen!");
// Example: Navigate back to product screen
// this.pos.showScreen('ProductScreen');
}
};
Registries.Component.extend(PaymentScreen, MyCustomPosPaymentScreen);
return PaymentScreen;
});
Advanced Customization: Creating a “Favorite Products” Section in Your Odoo 18 POS OWL Interface
This is a more involved example that combines XML, SCSS, and JavaScript, including fetching data.
- Design the XML Layout:
Createmy_custom_pos/static/src/xml/favorite_products.xml:
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<t t-name="my_custom_pos.FavoriteProductsSection" owl="1">
<div class="favorite-products-container">
<h3>Favorite Products</h3>
<div class="favorite-products-list">
<t t-if="favoriteProducts.length === 0">
<p>No favorite products configured.</p>
</t>
<t t-foreach="favoriteProducts" t-as="product" t-key="product.id">
<div class="product favorite-product-item" t-on-click="() => this.clickFavoriteProduct(product)">
<div class="product-img">
<img t-att-src="getProductImage(product)" t-att-alt="product.display_name"/>
</div>
<div class="product-name">
<t t-esc="product.display_name"/>
</div>
</div>
</t>
</div>
</div>
</t>
<t t-name="point_of_sale.ProductScreen" t-inherit="point_of_sale.ProductScreen" t-inherit-mode="extension">
<xpath expr="//div[hasclass('rightpane')]" position="inside">
<FavoriteProductsSection />
</xpath>
</t>
</templates>
- Style with SCSS:
Createmy_custom_pos/static/src/scss/favorite_products.scss://
// my_custom_pos/static/src/scss/favorite_products.scss
.favorite-products-container {
padding: 10px;
background-color: #f9f9f9;
border-top: 1px solid #ddd;
min-height: 150px; // Ensure it has some height
margin-top: 10px;
h3 {
margin-top: 0;
margin-bottom: 10px;
font-size: 1.2em;
color: #555;
}
.favorite-products-list {
display: flex;
flex-wrap: wrap; // Allow items to wrap
gap: 10px; // Space between items
overflow-x: auto; // Allow horizontal scrolling if needed, or use flex-wrap
.favorite-product-item {
flex: 0 0 100px; // Flex-grow, flex-shrink, flex-basis
border: 1px solid #ccc;
border-radius: 3px;
padding: 5px;
text-align: center;
cursor: pointer;
background-color: white;
box-shadow: 0 1px 2px rgba(0,0,0,0.1);
&:hover {
box-shadow: 0 2px 4px rgba(0,0,0,0.15);
}
.product-img {
img {
max-width: 80px;
max-height: 80px;
object-fit: contain;
}
}
.product-name {
font-size: 0.9em;
margin-top: 5px;
height: 2.5em; // Ensure consistent height for names
overflow: hidden;
}
}
}
}
- Create the Python Model Method (Backend Logic):
Createmy_custom_pos/models/product_product.py:
# my_custom_pos/models/product_product.py
from odoo import models, fields, api
class ProductProduct(models.Model):
_inherit = 'product.product'
# You could add a boolean field 'is_favorite' or use product tags.
# For this example, we'll use product tags.
# Ensure you have a product tag named 'Favorite' in your Odoo backend.
@api.model
def get_pos_favorite_products(self):
"""Returns IDs of products tagged as 'Favorite' and available in POS."""
# Find the 'Favorite' product tag
favorite_tag = self.env['product.tag'].search([('name', '=', 'Favorite')], limit=1)
if not favorite_tag:
return []
products = self.search([
('available_in_pos', '=', True),
('product_tag_ids', 'in', favorite_tag.id)
])
return products.ids # Return only IDs as POS will load full data from its DB
- Create the JavaScript Logic for Favorite Products and Extend
PosGlobalState:pos_global_state.js(To load favorite products on POS start):
Createmy_custom_pos/static/src/js/pos_global_state.js:
// my_custom_pos/static/src/js/pos_global_state.js
odoo.define('my_custom_pos.PosGlobalState', function(require) {
'use strict';
const { PosGlobalState } = require("@point_of_sale/app/store/pos_store");
const Registries = require("@point_of_sale/app/store/registries");
const MyCustomPosGlobalState = (OriginalPosGlobalState) =>
class extends OriginalPosGlobalState {
constructor() {
super(...arguments);
this.favoriteProductIds = []; // Initialize
}
async _loadNewData() {
await super._loadNewData(...arguments);
// Fetch favorite product IDs
const fav_ids = await this.env.services.rpc({
model: 'product.product',
method: 'get_pos_favorite_products',
args: [[]], // Pass context if needed
kwargs: { context: this.env.session.user_context },
});
this.favoriteProductIds = fav_ids;
console.log("Favorite Product IDs loaded:", this.favoriteProductIds);
}
getFavoriteProducts() {
// Get full product objects from the POS DB using the loaded IDs
return this.favoriteProductIds.map(id => this.db.get_product_by_id(id)).filter(p => p);
}
};
Registries.Model.extend(PosGlobalState, MyCustomPosGlobalState);
return PosGlobalState;
});
favorite_products.js(Component logic):
Createmy_custom_pos/static/src/js/favorite_products.js:
// my_custom_pos/static/src/js/favorite_products.js
odoo.define('my_custom_pos.FavoriteProductsSection', function(require) {
'use strict';
const { PosComponent } = require("@point_of_sale/app/store/pos_store");
const Registries = require("@point_of_sale/app/store/registries");
const { useState, onWillStart, useComponent } = require("@odoo/owl");
class FavoriteProductsSection extends PosComponent {
setup() {
super.setup();
// this.state = useState({ products: [] });
// No need for useState here if we directly access this.env.pos.getFavoriteProducts()
// which itself might be reactive or fetched once.
// onWillStart is not ideal here because PosGlobalState already loads it.
// We'll fetch them from the already populated PosGlobalState
}
get favoriteProducts() {
// Access the method from the extended PosGlobalState
return this.env.pos.getFavoriteProducts() || [];
}
getProductImage(product) {
return `/web/image/product.product/${product.id}/image_128`;
}
async clickFavoriteProduct(product) {
// Logic similar to clicking a product in the main list
this.env.pos.addProductToCurrentOrder(product);
}
}
FavoriteProductsSection.template = 'my_custom_pos.FavoriteProductsSection';
Registries.Component.add(FavoriteProductsSection);
return FavoriteProductsSection;
});
- Install/Upgrade Your Module:
After adding these files and updating__manifest__.py, install (or upgrade) yourmy_custom_posmodule in Odoo. Then, refresh your POS interface.
This detailed “Favorite Products” example shows how to interact with the backend, modify global POS state, and render new UI elements within an existing screen in an Odoo 18 POS OWL application.
Navigating Between Screens in Your Odoo 18 POS OWL Application
To switch between different POS screens (e.g., from a custom screen back to ProductScreen):
// Inside an OWL component method
this.env.pos.showScreen('ProductScreen');
// or to show a custom screen you registered:
// this.env.pos.showScreen('MyCustomScreenName');
Creating Custom Screens and Popups in Odoo 18 POS OWL (Brief Overview)
- Custom Screens:
- Define a JS class extending
PosComponent. - Create its corresponding XML template.
- Register it using
Registries.Component.add('YourScreenName', YourScreenClass);. - Navigate to it using
this.env.pos.showScreen('YourScreenName');.
- Define a JS class extending
- Custom Popups:
- Define a JS class extending
AbstractAwaitablePopup(from@point_of_sale/app/utils/popUp/abstract_awaitable_popup). - Implement
getPayload()to return data when the popup closes. - Register it:
Registries.Component.add('YourPopupName', YourPopupClass);. - Show it:
const result = await this.env.services.popup.add('YourPopupName', { title: 'My Popup', body: 'Hello!' });.
- Define a JS class extending
Best Practices and Key Takeaways for Odoo 18 POS OWL Development
super.setup(): Always callsuper.setup(...args)first in yoursetup()method when extending components.- Consistent Naming: Ensure your JS module names (
odoo.define), static template names in JS, andt-nameattributes in XML are consistent. patchUtility: For more complex modifications to existing components, especially their internal structures or getters/setters that are not easily extensible via class inheritance, Odoo provides apatchutility (@web/core/utils/patch). This allows targeted modifications.- RPC vs. Local DB (
this.env.pos.db):- Use
this.env.pos.dbfor frequently accessed data that’s loaded once (products, categories). - Use RPC calls (
this.env.services.rpc) for actions requiring real-time backend interaction or data not typically stored in the POS DB.
- Use
onWillStart()Hook: Use this asynchronous hook in your components to perform any pre-rendering setup, like fetching data via RPC, before the component is first rendered.- Developer Tools: Your browser’s developer tools are indispensable for inspecting elements, debugging JavaScript, and understanding the existing structure of the Odoo 18 POS OWL application.
Conclusion: Empowering Your Odoo 18 POS OWL Experience
Customizing the Odoo 18 POS OWL interface offers incredible flexibility to tailor the Point of Sale to your specific operational workflows. By understanding the core concepts, file structures, and leveraging the power of OWL components, services, and hooks, you can significantly enhance user experience and efficiency.
This guide has provided a comprehensive walkthrough of common customization tasks. Remember to explore the official Odoo documentation and community resources for even deeper insights.
Happy Odoo 18 POS OWL coding!
For further official Odoo development resources, visit the Odoo Developer Documentation.
Discover more from teguhteja.id
Subscribe to get the latest posts sent to your email.

