Zahid Anwar
December 11, 2024
This repository demonstrates advanced Odoo 18 Point of Sale (POS) customization techniques, covering the following 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!
your_app/
├── __init__.py
├── __manifest__.py
├── controllers/
│ └── __init__.py
│ └── validate_employee_discount.py
├── models/
│ └── __init__.py
│ └── hr_employee.py
│ └── product_tamplate.py
├── views/
│ └── hr_employee.xml
│ └── product_template.xml
├── static/
│ └── src/
│ └── app/
│ └── custom_button/
│ └── custom_button.xml
│ └── custom_button.js
│ └── custom_popup/
│ └── text_input_popup.xml
│ └── text_input_popup.xml
└── README.md
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."}
employee_id = request.env['hr.employee'].sudo().search([('cnic','=',cnic)])
if len(employee_id) > 0:
return {
"success": True,
"message": "Validation successful!",
}
else:
return {"success": False, "message": f"No Employee Found with given CNIC: {cnic}"}
<?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>
/**@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.price_for_employee || line.product_id.lst_price); // Update the price
line.set_unit_price(line.product_id.price_for_employee || line.product_id.lst_price); // 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;
}
},
});
},
});
<?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>
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();
}
}
}
from odoo import api, fields, models, _
from odoo.exceptions import UserError
class HrEmployee(models.Model):
_inherit = 'hr.employee'
cnic = fields.Char('CNIC')
from odoo import api, fields, models, _
from odoo.exceptions import UserError
class ProductTemplate(models.Model):
_inherit = 'product.template'
price_for_employee = fields.Float('Price For Employee')
@api.model
def _load_pos_data_fields(self, config_id):
fields = super()._load_pos_data_fields(config_id)
fields += ['price_for_employee']
return fields
class ProductProduct(models.Model):
_inherit = 'product.product'
@api.model
def _load_pos_data_fields(self, config_id):
fields = super()._load_pos_data_fields(config_id)
fields += ['price_for_employee']
return fields
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<record id="view_hr_employee_form_inherit_pos_custom" model="ir.ui.view">
<field name="name">hr.employee.form.inherit.pos.custom</field>
<field name="model">hr.employee</field>
<field name="inherit_id" ref="hr.view_employee_form"/>
<field name="arch" type="xml">
<xpath expr="//notebook" position="inside">
<page string="Additional Info">
<group>
<group>
<field name="cnic"/>
</group>
</group>
</page>
</xpath>
</field>
</record>
</odoo>
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<record id="view_product_template_form_inherit_pos_custom" model="ir.ui.view">
<field name="name">product.template.form.inherit.pos.custom</field>
<field name="model">product.template</field>
<field name="inherit_id" ref="product.product_template_only_form_view"/>
<field name="arch" type="xml">
<xpath expr="//notebook" position="inside">
<page string="Additional Info">
<group>
<group>
<field name="price_for_employee"/>
</group>
</group>
</page>
</xpath>
</field>
</record>
</odoo>
{
'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','hr'],
'data': [
'views/product_template.xml',
'views/hr_employee.xml',
],
'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
}
That’s it! You’ve added the custom buttons in point of sale!