Zalino Logo white

Odoo POS 18 – How to Add Custom Discount Button, RPC Call In odoo Point of Sale

Odoo POS Dashboard Image
Odoo POS 18, how to add custom employee discount button by using RPC call

In this version, we will go a step further and learn about:

  • Creating custom popups and buttons with enhanced functionality.
  • Capturing and utilizing the data entered by the user in a popup.
  • Making RPC calls from the POS interface to interact with the backend.
  • Implementing robust exception handling for error management.
  • Accessing and processing orders and order lines programmatically.
  • Applying discounts to order lines dynamically.
  • Exploring advanced techniques to extend and customize POS features.

These enhancements will empower you to tailor the POS system according to unique business requirements. By the end of this version, you will gain in-depth knowledge to develop and manage custom POS functionalities seamlessly.

Stay tuned as we dive into practical examples, step-by-step tutorials, and tips for optimizing your POS workflows. Let’s get started!

Directory Structure of your custom app

your_app/
├── __init__.py
├── __manifest__.py
├── controllers/
│         └── __init__.py
│         └── validate_employee_discount.py
├── static/
│         └── src/
│                 └── app/
│                         └── custom_button/
│                                 └── custom_button.xml
│                                 └── custom_button.js
│                         └── custom_popup/
│                                 └── text_input_popup.xml
│                                 └── text_input_popup.xml
└── README.md

Add The xml code inside validate_employee_discount.py
from odoo import http
from odoo.http import request, Response
from datetime import datetime, timedelta

class ValidateEmpDiscount(http.Controller):

@http.route('/pos/validate_emp_discount', type='json', auth='user', csrf=False, methods=["POST"])
def validate_emp_discount(self, cnic):
if not cnic.isdigit() or len(cnic) != 13:
return {"success": False, "message": "Invalid CNIC. Must be 13 digits."}
if cnic == '1234567891234':
return {
"success": True,
"message": "Validation successful!",
}
else:
return {"success": False, "message": f"No Employee Found with given CNIC: {cnic}"}
Add The xml code inside custom_button.xml
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<t t-name="pos_custom.ControlButtons" t-inherit="point_of_sale.ControlButtons" t-inherit-mode="extension">

<xpath expr=”//button[@class=’btn btn-light btn-lg flex-shrink-0 ms-auto’]” position=”before”>
<button class=”btn btn-light btn-lg flex-shrink-0 ms-auto”
t-on-click=”() => this.onClickPopupSingleField()”>Custom Button</button>
</xpath>

<xpath expr=”//t[@t-if=’props.showRemainingButtons’]/div/OrderlineNoteButton” position=”after”>
<button class=”btn btn-secondary btn-lg py-5″ t-on-click=”() => this.onClickPopupSingleField()”>
<i class=”fa fa-pencil-square me-1″ role=”img” aria-label=”Custom Alert” title=”Custom Alert”/>Custom
Button
</button>
</xpath>

</t>
</templates>

Add the js code to handle click event inside custom_button.js
your_app>static>src>app>custom_button>custom_button.js:

/**@odoo-module **/
import { AlertDialog } from "@web/core/confirmation_dialog/confirmation_dialog";
import { _t } from "@web/core/l10n/translation";
import { ControlButtons } from "@point_of_sale/app/screens/product_screen/control_buttons/control_buttons";
import { patch } from "@web/core/utils/patch";
import { TextInputPopup } from "@pos_custom/app/custom_popup/text_input_popup";

import { rpc } from "@web/core/network/rpc";


patch(ControlButtons.prototype, {
    async onClickPopupSingleField() {
        this.dialog.add(TextInputPopup, {

            title: _t("To Apply Discount, Enter CNIC"),
            placeholder: _t("Enter CNIC"),
            getPayload: async (code) => {
                let cnic = code.trim();

                // Validation Logic
                if (cnic.length !== 13 || isNaN(cnic)) {
                    this.dialog.add(AlertDialog, {
                        title: _t("CNIC ERROR"),
                        body: _t("CNIC must be a 13-digit number without dashes"),
                    });
                    return;
                }

                const order = this.pos.get_order();
                const orderLines = order.get_orderlines();
                try {
                    const result = await rpc(
                        "/pos/validate_emp_discount",
                        { cnic },
                    );
                    if (result.success) {
                        orderLines.forEach(line => {
                            line.set_unit_price(line.product_id.lst_price - (line.product_id.lst_price* 5/100)); // Update the price
                        });
//                        order.cnic = cnic;
                    } else {
                         this.dialog.add(AlertDialog, {
                            title: _t("ERROR"),
                            body: _t(result.message),
                        });
                        return;
                    }
                } catch (error) {
                    this.dialog.add(AlertDialog, {
                            title: _t("SYSTEM ERROR"),
                            body: _t("Currently, some services are unavailable. Please wait for a while and try again. If the issue persists for an extended period, please contact the administrator."),
                        });
                        return;
                }

            },
        });
    },
});

    
Add The xml code inside text_input_popup.xml
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<t t-name="pos_custom.TextInputPopup">
<Dialog title="props.title">
<t t-foreach="props.buttons" t-as="button" t-key="button_index">
<button t-on-click="() => this.buttonClick(button)" type="button"
t-attf-class="btn btn-lg me-2 mb-2 toggle-button {{button.isSelected? 'btn-primary' : 'btn-secondary'}} lh-lg">
<t t-esc="button.label"/>
</button>
</t>
<textarea t-att-rows="props.rows" class="form-control form-control-lg mx-auto" type="text"
t-model="state.inputValue" t-ref="input" t-att-placeholder="props.placeholder"
t-on-keydown="onKeydown"/>
<t t-set-slot="footer">
<button class="btn btn-primary btn-lg lh-lg o-default-button" t-on-click="confirm">Apply</button>
<button class="btn btn-secondary btn-lg lh-lg o-default-button" t-on-click="close">Discard</button>
</t>
</Dialog>
</t>
</templates>
Add the js code to handle click event inside text_input_popup.js
your_app>static>src>app>custom_popup>text_input_popup.js:

import { Component, onMounted, useRef, useState } from "@odoo/owl";
import { Dialog } from "@web/core/dialog/dialog";

export class TextInputPopup extends Component {
    static template = "pos_custom.TextInputPopup";
    static components = { Dialog };
    static props = {
        title: String,
        buttons: { type: Array, optional: true },
        startingValue: { type: String, optional: true },
        placeholder: { type: String, optional: true },
        rows: { type: Number, optional: true },
        getPayload: Function,
        close: Function,
    };
    static defaultProps = {
        startingValue: "",
        placeholder: "",
        rows: 1,
        buttons: [],
    };

    setup() {
        this.state = useState({ inputValue: this.props.startingValue });
        this.inputRef = useRef("input");
        onMounted(this.onMounted);
    }
    onMounted() {
        this.inputRef.el.focus();
        this.inputRef.el.select();
    }
    confirm() {
        this.props.getPayload(this.state.inputValue);
        this.props.close();
    }

    close() {
        this.props.close();
    }

    buttonClick(button) {
        console.log('Button click');
        const lines = this.state.inputValue.split("\n").filter((line) => line !== "");
        if (lines.includes(button.label)) {
            this.state.inputValue = lines.filter((line) => line !== button.label).join("\n");
            button.isSelected = false;
        } else {
            this.state.inputValue = lines.join("\n");
            this.state.inputValue += (lines.length > 0 ? "\n" : "") + button.label;
            button.isSelected = true;
        }
    }

    onKeydown(ev) {
        if (this.props.rows === 1 && ev.key.toUpperCase() === "ENTER") {
            this.confirm();
        }
    }
}


    
Finally Add the reference in __manifest__.py
add the file reference in manifest file to load the assets in pos:

{
    'name': 'POS Custom',
    'version': '1.0.0',
    'category': 'Point of Sale',
    'summary': "POS Custom, Odoo18, "
               "Odoo Apps",
    'description': "Design for better styles",
    'author': 'Zahid Anwar',
    'company': 'zalino',
    'maintainer': 'Nil',
    'website': 'https://www.zalinotech.com',
    'depends': ['base', 'point_of_sale'],
    'assets': {
        'point_of_sale._assets_pos': [
            'pos_custom/static/src/app/custom_popup/text_input_popup.js',
            'pos_custom/static/src/app/custom_popup/text_input_popup.xml',

            'pos_custom/static/src/app/custom_button/custom_button.js',
            'pos_custom/static/src/app/custom_button/custom_button.xml',
        ],
    },
    'license': 'AGPL-3',
    'installable': True,
    'auto_install': False,
    'application': False
}

    
Restart the service
Once all is done. Now restart the odoo service and upgrade the app. (if assets not loaded properly clear the cache of browser)

That’s it! You’ve added the custom buttons in point of sale!

🔗 Useful Links:
👉 Visit Our Website: https://zalinotech.com
👉 GitHub: https://github.com/ZalinoTech/OdooPos18/tree/Version04

Leave a Reply

Your email address will not be published. Required fields are marked *