From 62b83bb483e56906421df5cdd03b41101cc25e0b Mon Sep 17 00:00:00 2001 From: habar-odoo Date: Wed, 3 Jun 2026 10:37:12 +0530 Subject: [PATCH] [ADD] pos_second_uom: allow selling products in secondary unit of measure - Add a secondary UoM field on the product form, filtered by the same category. - Introduce a new button in the POS interface to input quantity in the secondary UoM via a popup. - Automatically convert and update the order line quantity based on the UoM ratio upon confirmation. This provides a quick customization for specific client requirements without implementing a fully generic framework. --- pos_second_uom/__init__.py | 1 + pos_second_uom/__manifest__.py | 18 ++++++++ pos_second_uom/models/__init__.py | 2 + pos_second_uom/models/product_product.py | 11 +++++ pos_second_uom/models/product_template.py | 44 ++++++++++++++++++ .../static/src/js/control_buttons.js | 45 +++++++++++++++++++ .../static/src/xml/control_button.xml | 22 +++++++++ .../views/product_template_view.xml | 14 ++++++ 8 files changed, 157 insertions(+) create mode 100644 pos_second_uom/__init__.py create mode 100644 pos_second_uom/__manifest__.py create mode 100644 pos_second_uom/models/__init__.py create mode 100644 pos_second_uom/models/product_product.py create mode 100644 pos_second_uom/models/product_template.py create mode 100644 pos_second_uom/static/src/js/control_buttons.js create mode 100644 pos_second_uom/static/src/xml/control_button.xml create mode 100644 pos_second_uom/views/product_template_view.xml diff --git a/pos_second_uom/__init__.py b/pos_second_uom/__init__.py new file mode 100644 index 00000000000..0650744f6bc --- /dev/null +++ b/pos_second_uom/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/pos_second_uom/__manifest__.py b/pos_second_uom/__manifest__.py new file mode 100644 index 00000000000..8bc6d7099cf --- /dev/null +++ b/pos_second_uom/__manifest__.py @@ -0,0 +1,18 @@ +{ + "name": "POS Second UoM", + "category": "Point of Sale", + "depends": ["product", "uom", "point_of_sale"], + "author": "habar", + "data": [ + "views/product_template_view.xml", + ], + "assets": { + "point_of_sale._assets_pos": [ + "pos_second_uom/static/src/js/control_buttons.js", + "pos_second_uom/static/src/xml/control_button.xml", + ], + }, + "installable": True, + "application": False, + "license": "LGPL-3", +} diff --git a/pos_second_uom/models/__init__.py b/pos_second_uom/models/__init__.py new file mode 100644 index 00000000000..049669dd0fe --- /dev/null +++ b/pos_second_uom/models/__init__.py @@ -0,0 +1,2 @@ +from . import product_template +from . import product_product diff --git a/pos_second_uom/models/product_product.py b/pos_second_uom/models/product_product.py new file mode 100644 index 00000000000..88d43925ee0 --- /dev/null +++ b/pos_second_uom/models/product_product.py @@ -0,0 +1,11 @@ +from odoo import api, models + + +class ProductProduct(models.Model): + _inherit = "product.product" + + @api.model + def _load_pos_data_fields(self, config_id): + fields_list = super()._load_pos_data_fields(config_id) + fields_list.append("pos_second_uom_id") + return fields_list diff --git a/pos_second_uom/models/product_template.py b/pos_second_uom/models/product_template.py new file mode 100644 index 00000000000..e08e5c92a97 --- /dev/null +++ b/pos_second_uom/models/product_template.py @@ -0,0 +1,44 @@ +from odoo import _, api, fields, models +from odoo.exceptions import ValidationError + + +class ProductTemplate(models.Model): + _inherit = 'product.template' + + pos_second_uom_id = fields.Many2one( + 'uom.uom', + string="POS Second Unit of Measure" + ) + pos_second_uom_domain_ids = fields.Many2many('uom.uom', compute="_compute_pos_second_uom_domain_ids", string="All Pos Second Uom") + + @api.depends("uom_id") + def _compute_pos_second_uom_domain_ids(self): + for rec in self: + if not rec.uom_id: + rec.pos_second_uom_domain_ids = self.env['uom.uom'].search([]) + continue + + root_uom = rec.uom_id + while root_uom.relative_uom_id: + root_uom = root_uom.relative_uom_id + + compatible_uoms = self.env['uom.uom'].search([ + '|', ('id', '=', root_uom.id), ('parent_path', 'like', f'%{root_uom.id}%') + ]) + + rec.pos_second_uom_domain_ids = compatible_uoms + + @api.constrains('pos_second_uom_id', 'uom_id') + def _check_uom_compatibility(self): + for record in self: + if record.pos_second_uom_id and record.uom_id: + root_main = record.uom_id + while root_main.relative_uom_id: + root_main = root_main.relative_uom_id + + root_second = record.pos_second_uom_id + while root_second.relative_uom_id: + root_second = root_second.relative_uom_id + + if root_main.id != root_second.id: + raise ValidationError(_("Selected Second UoM must be from the same unit hierarchy family!")) diff --git a/pos_second_uom/static/src/js/control_buttons.js b/pos_second_uom/static/src/js/control_buttons.js new file mode 100644 index 00000000000..514bc11028e --- /dev/null +++ b/pos_second_uom/static/src/js/control_buttons.js @@ -0,0 +1,45 @@ +import { ControlButtons } from "@point_of_sale/app/screens/product_screen/control_buttons/control_buttons"; +import { NumberPopup } from "@point_of_sale/app/components/popups/number_popup/number_popup"; +import { makeAwaitable } from "@point_of_sale/app/utils/make_awaitable_dialog"; +import { patch } from "@web/core/utils/patch"; + +patch(ControlButtons.prototype, { + displaySecondUomButton() { + const line = this.currentOrder?.getSelectedOrderline(); + return line?.product_id?.pos_second_uom_id; + }, + + async clickSecondUomButton() { + const line = this.currentOrder?.getSelectedOrderline(); + + if (!line) { + return; + } + + const secondUom = line.product_id.pos_second_uom_id; + const mainUom = line.product_id.uom_id; + + const qty = await makeAwaitable( + this.dialog, + NumberPopup, + { + title: `Enter ${secondUom.name} Quantity`, + startingValue: 0, + } + ); + + if (qty === null || qty === undefined) { + return; + } + + const enteredQty = parseFloat(qty); + + if (isNaN(enteredQty)) { + return; + } + + const convertedQty = (enteredQty * secondUom.factor) / mainUom.factor; + + line.setQuantity(convertedQty); + }, +}); diff --git a/pos_second_uom/static/src/xml/control_button.xml b/pos_second_uom/static/src/xml/control_button.xml new file mode 100644 index 00000000000..e097562387d --- /dev/null +++ b/pos_second_uom/static/src/xml/control_button.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + diff --git a/pos_second_uom/views/product_template_view.xml b/pos_second_uom/views/product_template_view.xml new file mode 100644 index 00000000000..7a2920c1a39 --- /dev/null +++ b/pos_second_uom/views/product_template_view.xml @@ -0,0 +1,14 @@ + + + + product.template.form.pos.second.uom + product.template + + + + + + + + +