Skip to content

Commit aaa079f

Browse files
committed
[ADD] pos_second_uom: Added support for Second Unit of Measure in POS
- Added a new field in product form to set a second UoM - Backend constraint to prevent primary and secondary UoM being the same - Loaded second UoM info into POS - Added custom Add Quantity button in POS control panel(before customer button) - Clicking the button opens a popup to enter quantity in second UoM - POS converts the quantity based on UoM ratio - Added validations in popup: - Show error if quantity is zero or negative or empty input - Optimized validation with translatable error message - UI improvements with conditional visibility and consistent UX
1 parent f7e46c4 commit aaa079f

File tree

10 files changed

+201
-0
lines changed

10 files changed

+201
-0
lines changed

Diff for: pos_second_uom/__init__.py

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from . import models

Diff for: pos_second_uom/__manifest__.py

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{
2+
'name': 'POS Second UOM',
3+
'version': '1.0',
4+
'summary': 'Allows products to have a second unit of measure in POS',
5+
'description': """
6+
Adds the ability to set and use a second unit of measure for products in the Point of Sale.
7+
This is useful for businesses that sell products in multiple units.
8+
The second UOM is displayed and managed directly within the POS interface for easier sales handling.
9+
""",
10+
'author': 'Raghav Agiwal',
11+
'depends': ['point_of_sale'],
12+
'data': [
13+
'views/product_template_view.xml',
14+
],
15+
'assets': {
16+
'point_of_sale._assets_pos': [
17+
'pos_second_uom/static/src/**/*',
18+
],
19+
},
20+
'installable': True,
21+
'application': True,
22+
'auto_install': True,
23+
'license': 'LGPL-3'
24+
}

Diff for: pos_second_uom/models/__init__.py

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
from . import product
2+
from . import product_template

Diff for: pos_second_uom/models/product.py

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
from odoo import api, models
2+
3+
4+
class Product(models.Model):
5+
_inherit = 'product.product'
6+
7+
@api.model
8+
def _load_pos_data_fields(self, config_id):
9+
pos_fields = super()._load_pos_data_fields(config_id)
10+
pos_fields.extend(['pos_second_uom_id'])
11+
return pos_fields

Diff for: pos_second_uom/models/product_template.py

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
from odoo import api, fields, models
2+
from odoo.exceptions import ValidationError
3+
4+
5+
class ProductTemplate(models.Model):
6+
_inherit = 'product.template'
7+
8+
pos_second_uom_id = fields.Many2one(
9+
'uom.uom',
10+
string="POS Second UoM",
11+
domain="[('category_id', '=', uom_category_id)]"
12+
)
13+
14+
uom_category_id = fields.Many2one(
15+
'uom.category',
16+
string="UoM Category",
17+
related='uom_id.category_id',
18+
)
19+
20+
@api.constrains('uom_id', 'pos_second_uom_id')
21+
def _check_secondary_uom_not_same_as_primary(self):
22+
if self.pos_second_uom_id and self.uom_id == self.pos_second_uom_id:
23+
raise ValidationError(
24+
f"Configuration Error:\n\n"
25+
f'Product "{self.name}" has "{self.uom_id.name}" set as both primary and secondary Unit of Measure.\n'
26+
f'These must be different. Please select a different second UoM for POS operations.'
27+
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/** @odoo-module **/
2+
3+
import { AddQuantityDialog } from "./dialog_box/add_quantity_dialog";
4+
import { ControlButtons } from "@point_of_sale/app/screens/product_screen/control_buttons/control_buttons";
5+
import { useService } from "@web/core/utils/hooks";
6+
import { patch } from "@web/core/utils/patch";
7+
8+
patch(ControlButtons.prototype, {
9+
setup() {
10+
super.setup();
11+
this.dialog = useService("dialog");
12+
this.pos = this.env.services.pos;
13+
},
14+
15+
get secondUomName() {
16+
const line = this.pos.get_order().get_selected_orderline();
17+
return line?.get_product()?.pos_second_uom_id?.name;
18+
},
19+
20+
get shouldShowAddQtyButton() {
21+
const order = this.pos.get_order();
22+
const line = order?.get_selected_orderline();
23+
return !!line?.get_product()?.pos_second_uom_id;
24+
},
25+
26+
onAddQuantity() {
27+
const order = this.pos.get_order();
28+
const line = order.get_selected_orderline();
29+
30+
if (!line || !line.get_product()?.pos_second_uom_id) {
31+
return;
32+
}
33+
34+
this.dialog.add(AddQuantityDialog, {
35+
product: line.get_product(),
36+
orderline: line,
37+
secondUomName: this.secondUomName,
38+
});
39+
},
40+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<templates id="template" xml:space="preserve">
3+
<t t-name="pos_second_uom.ControlButtons"
4+
t-inherit="point_of_sale.ControlButtons"
5+
t-inherit-mode="extension">
6+
<xpath expr="//SelectPartnerButton" position="before">
7+
<button
8+
t-if="shouldShowAddQtyButton"
9+
t-attf-class="{{ buttonClass }}"
10+
t-on-click="onAddQuantity"
11+
title="Add quantity using Secondary UoM"
12+
>
13+
<i class="fa fa-plus"/> Add Quantity
14+
</button>
15+
</xpath>
16+
</t>
17+
</templates>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
/** @odoo-module **/
2+
3+
import { Component, useRef } from "@odoo/owl";
4+
import { usePos } from "@point_of_sale/app/store/pos_hook";
5+
import { AlertDialog } from "@web/core/confirmation_dialog/confirmation_dialog";
6+
import { useService } from "@web/core/utils/hooks";
7+
import { _t } from "@web/core/l10n/translation";
8+
import { Dialog } from "@web/core/dialog/dialog";
9+
10+
export class AddQuantityDialog extends Component {
11+
static template = "pos_second_uom.AddQuantityDialog";
12+
static components = { Dialog };
13+
static props = {
14+
close: { type: Function },
15+
product: { type: Object },
16+
orderline: { type: Object },
17+
secondUomName: { type: String, optional: true },
18+
};
19+
20+
setup() {
21+
this.pos = usePos();
22+
this.input_qty = useRef("quantityInput");
23+
this.uom_ratio = this.computeUomRatio(this.props.product);
24+
this.dialog = useService("dialog");
25+
}
26+
27+
computeUomRatio(product) {
28+
if (product.uom_id && product.pos_second_uom_id) {
29+
return product.uom_id.factor / product.pos_second_uom_id.factor;
30+
}
31+
return 1;
32+
}
33+
34+
confirm() {
35+
const quantity = parseFloat(this.input_qty.el.value);
36+
if (!quantity || quantity <= 0) {
37+
this.pos.dialog.add(AlertDialog, {
38+
title: _t("Error"),
39+
body: _t("Oops!!!!! Quantity cannot be negative, zero, or empty. Please enter a valid positive number !!"),
40+
});
41+
return;
42+
}
43+
this.props.orderline.set_quantity(quantity * this.uom_ratio);
44+
this.props.close();
45+
}
46+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<templates xml:space="preserve">
3+
<t t-name="pos_second_uom.AddQuantityDialog">
4+
<Dialog size="'sm'" title="'Enter Quantity in Second UoM'">
5+
<t t-set-slot="default">
6+
<div class="mb-3">
7+
<label for="quantityInput" class="form-label">
8+
Enter Quantity in <b><t t-out="props.secondUomName"/></b>
9+
</label>
10+
<input type="number" min="0.01" step="0.01" class="form-control"
11+
id="quantityInput" placeholder="Enter quantity" t-ref="quantityInput"/>
12+
</div>
13+
</t>
14+
<t t-set-slot="footer">
15+
<button class="btn btn-primary" t-on-click="confirm">Confirm</button>
16+
<button class="btn btn-secondary" t-on-click="props.close">Cancel</button>
17+
</t>
18+
</Dialog>
19+
</t>
20+
</templates>

Diff for: pos_second_uom/views/product_template_view.xml

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<odoo>
3+
<record id="view_product_template_form_inherit_uom" model="ir.ui.view">
4+
<field name="name">product.template.form.inherit.uom</field>
5+
<field name="model">product.template</field>
6+
<field name="inherit_id" ref="product.product_template_form_view"/>
7+
<field name="arch" type="xml">
8+
<xpath expr="//field[@name='pos_categ_ids']" position="after">
9+
<field name="pos_second_uom_id"/>
10+
</xpath>
11+
</field>
12+
</record>
13+
</odoo>

0 commit comments

Comments
 (0)