Odoo Extension Techniques: Component, Template, and View Inheritance

Component Inheritance with OWL

/** @odoo-module */

import { BasePopup } from "@point_of_sale/app/popup/base_popup";
import { TranslationService } from "@web/core/l10n/translation";
import { lifecycleHooks, elementRef, reactiveState, cleanup } from "@odoo/owl";
import { LineItem } from "@point_of_sale/app/components/line_item/line_item";
import { CartDisplay } from "@point_of_sale/app/components/cart_display/cart_display";
import { objectUtils } from "@web/core/utils/objects";
import { serviceHook } from "@web/core/utils/hooks";
import { posStore } from "@point_of_sale/app/store/pos_store";
import { createEnvironment } from "@web/env";

export class RFIDScannerPopup extends BasePopup {
    static template = "custom_module.RFIDScannerPopup";
    static components = {
        LineItem,
        CartDisplay,
    };

    static defaultProps = {
        confirmLabel: TranslationService._t("Confirm"),
        cancelLabel: TranslationService._t("Cancel"),
        resetLabel: TranslationService._t("Reset All"),
        popupTitle: "RFID Scanner Interface",
    };

    static props = {
        scannedData: Object,
        currencyFormatter: Function,
    };

    get itemCount() {
        let count = 0;
        this.props.scannedData.forEach(item => {
            count += Number.parseFloat(item.quantity);
        });
        return count.toFixed(2);
    }

    setup() {
        super.setup();
        this.dataService = serviceHook("orm");
        this.pointOfSale = posStore();
        const appEnvironment = createEnvironment();
        
        console.log('POS configuration:', this.pointOfSale.configuration)

        cleanup(this.componentCleanup);
        lifecycleHooks.onMount(this.componentMounted);
    }

    async processRFIDTag(tagIdentifier) {
        if (this.scannedTags.has(tagIdentifier)) {
            return;
        }
        this.scannedTags.add(tagIdentifier);
        let productCode = this.extractProductCode(tagIdentifier);
        productCode = productCode.productCode;
        console.log("Product code:", productCode);
        
        try {
            const maxResults = 30;
            const productIdentifiers = await this.dataService.call(
                "product.product",
                "search",
                [
                    [
                        ["barcode", "=", productCode],
                    ],
                ],
                {
                    offset: 0,
                    limit: maxResults,
                }
            );
            console.log("Product IDs:", productIdentifiers);
            
            if (productIdentifiers.length > 0) {
                const productItem = await this.pointOfSale.database.getProductById(productIdentifiers[0]);
                console.log("Product:", productItem);
                
                if (productItem) {
                    let productInfo = {
                        'attributes': [],
                        'discountPercentage': '0',
                        'lotTracking': false,
                        'unitPrice': productItem.listPrice.toFixed(2),
                        'originalPrice': productItem.listPrice.toFixed(2),
                        'productDescription': productItem.displayName,
                        'productId': productIdentifiers[0],
                        'quantity': "1.00",
                        'measurementUnit': productItem.unitOfMeasure[1],
                        'pricePerUnit': productItem.listPrice.toFixed(2),
                        'undeletable': true,
                        'rfidTag': tagIdentifier,
                        "productDetails": productItem,
                    };
                    console.log("Product info:", productInfo);
                    this.props.scannedData.push(productInfo);
                }
            }
        } catch (error) {
            if (error instanceof ConnectionLostError || error instanceof ConnectionAbortedError) {
                return this.popupManager.add(OfflineErrorPopup, {
                    title: TranslationService._t("Network Issue"),
                    body: TranslationService._t(
                        "Unable to load product data due to network connectivity problems."
                    ),
                });
            } else {
                throw error;
            }
        }
    }

    componentMounted() {
        this.scannedTags = new Set();
        this.connectionTimer = null;
        this.initializeWebSocket(
            this.pointOfSale.configuration.rfid_endpoint, 
            this.pointOfSale.configuration.rfid_client,
            this.pointOfSale.configuration.rfid_location,
            this.pointOfSale.configuration.rfid_user,
            this.pointOfSale.configuration.rfid_device
        );
        console.log('Component mounted');
    }

    componentCleanup() {
        clearInterval(this.connectionTimer);
        this.terminateReading();
        this.webSocket.close();
        console.log('Component cleanup');
    }

    getResultData() {
        this.props.currencyFormatter(this.props.scannedData);
        return "";
    }
}

Tmeplate Inheritance

<?xml version="1.0" encoding="UTF-8" ?>
<template id="CustomClearIcon" inherit="point_of_sale_template" xml:space="preserve">
    <t t-inherit="point_of_sale.LineItem" t-inherit-mode="extension">
        <xpath expr="//div[hasclass('product-price')]" position="inside">
            <t t-if="!item.undeletable">
                <i style="margin-left:6px;cursor:pointer;color:red;" id="remove_icon" class="fa fa-trash" t-on-click="removeItem"/>
            </t>
        </xpath>
    </t>
</template>

View Inheritance

<?xml version="1.0" encoding="utf-8"?>
<odoo>
    <data>
        <record id="custom_pos_config_view" model="ir.ui.view">
            <field name="name">custom.pos.config</field>
            <field name="model">pos.config</field>
            <field name="inherit_id" ref="point_of_sale.pos_config_view_form"/>
            <field eval="9" name="priority"/>
            <field name="arch" type="xml">
                <xpath expr="//div[@groups='base.group_system']" position="before">
                    <group>
                        <group>
                            <field name="custom_field" />
                        </group>
                        <group>
                        </group>
                    </group>
                </xpath>
            </field>
        </record>
    </data>
</odoo>

Module Manifest Configuration

These extension resources are configured in the module manifest file. View definitions are placed in the data section, while OWL components and web templates are included in the assets section:

{
    'depends': ['base', 'point_of_sale'],
    'assets': {
        'point_of_sale.assets': [
            'custom_module/static/src/**/*',
        ],
    },
    'data': [
        'views/pos_config_custom.xml',
    ],
}

Tags: Odoo OWL Components View Inheritance Template Extension POS Customization

Posted on Mon, 21 Sep 2026 16:18:28 +0000 by ritter