feat(pos): multiple item prices (#33005)
* fix(pos): multiple item prices
feat: show uom with product price
feat: multiple item (variant) depending on uom
Signed-off-by: Sabu Siyad <hello@ssiyad.com>
* feat(pos): consider uom for new item
Signed-off-by: Sabu Siyad <hello@ssiyad.com>
* feat(pos): uom based stock display
Signed-off-by: Sabu Siyad <hello@ssiyad.com>
* use `//` instead of `math.floor()`
Signed-off-by: Sabu Siyad <hello@ssiyad.com>
* feat(pos): uom by barcode
Signed-off-by: Sabu Siyad <hello@ssiyad.com>
* fix: replace `is not` with `!=`
Signed-off-by: Sabu Siyad <hello@ssiyad.com>
* fix(pos): barcode_info `next()`: fallback
Signed-off-by: Sabu Siyad <hello@ssiyad.com>
* chore: format
Signed-off-by: Sabu Siyad <hello@ssiyad.com>
* Update erpnext/selling/page/point_of_sale/point_of_sale.py
Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com>
* chore: un-ignore unused local variable
Signed-off-by: Sabu Siyad <hello@ssiyad.com>
---------
Signed-off-by: Sabu Siyad <hello@ssiyad.com>
Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com>
Co-authored-by: Deepesh Garg <deepeshgarg6@gmail.com>
diff --git a/erpnext/selling/page/point_of_sale/point_of_sale.py b/erpnext/selling/page/point_of_sale/point_of_sale.py
index 999ddc2..158ac1d 100644
--- a/erpnext/selling/page/point_of_sale/point_of_sale.py
+++ b/erpnext/selling/page/point_of_sale/point_of_sale.py
@@ -17,45 +17,79 @@
def search_by_term(search_term, warehouse, price_list):
result = search_for_serial_or_batch_or_barcode_number(search_term) or {}
- item_code = result.get("item_code") or search_term
- serial_no = result.get("serial_no") or ""
- batch_no = result.get("batch_no") or ""
- barcode = result.get("barcode") or ""
+ item_code = result.get("item_code", search_term)
+ serial_no = result.get("serial_no", "")
+ batch_no = result.get("batch_no", "")
+ barcode = result.get("barcode", "")
- if result:
- item_info = frappe.db.get_value(
- "Item",
- item_code,
- [
- "name as item_code",
- "item_name",
- "description",
- "stock_uom",
- "image as item_image",
- "is_stock_item",
- ],
- as_dict=1,
- )
+ if not result:
+ return
- item_stock_qty, is_stock_item = get_stock_availability(item_code, warehouse)
- price_list_rate, currency = frappe.db.get_value(
- "Item Price",
- {"price_list": price_list, "item_code": item_code},
- ["price_list_rate", "currency"],
- ) or [None, None]
+ item_doc = frappe.get_doc("Item", item_code)
- item_info.update(
+ if not item_doc:
+ return
+
+ item = {
+ "barcode": barcode,
+ "batch_no": batch_no,
+ "description": item_doc.description,
+ "is_stock_item": item_doc.is_stock_item,
+ "item_code": item_doc.name,
+ "item_image": item_doc.image,
+ "item_name": item_doc.item_name,
+ "serial_no": serial_no,
+ "stock_uom": item_doc.stock_uom,
+ "uom": item_doc.stock_uom,
+ }
+
+ if barcode:
+ barcode_info = next(filter(lambda x: x.barcode == barcode, item_doc.get("barcodes", [])), None)
+ if barcode_info and barcode_info.uom:
+ uom = next(filter(lambda x: x.uom == barcode_info.uom, item_doc.uoms), {})
+ item.update(
+ {
+ "uom": barcode_info.uom,
+ "conversion_factor": uom.get("conversion_factor", 1),
+ }
+ )
+
+ item_stock_qty, is_stock_item = get_stock_availability(item_code, warehouse)
+ item_stock_qty = item_stock_qty // item.get("conversion_factor")
+ item.update({"actual_qty": item_stock_qty})
+
+ price = frappe.get_list(
+ doctype="Item Price",
+ filters={
+ "price_list": price_list,
+ "item_code": item_code,
+ },
+ fields=["uom", "stock_uom", "currency", "price_list_rate"],
+ )
+
+ def __sort(p):
+ p_uom = p.get("uom")
+
+ if p_uom == item.get("uom"):
+ return 0
+ elif p_uom == item.get("stock_uom"):
+ return 1
+ else:
+ return 2
+
+ # sort by fallback preference. always pick exact uom match if available
+ price = sorted(price, key=__sort)
+
+ if len(price) > 0:
+ p = price.pop(0)
+ item.update(
{
- "serial_no": serial_no,
- "batch_no": batch_no,
- "barcode": barcode,
- "price_list_rate": price_list_rate,
- "currency": currency,
- "actual_qty": item_stock_qty,
+ "currency": p.get("currency"),
+ "price_list_rate": p.get("price_list_rate"),
}
)
- return {"items": [item_info]}
+ return {"items": [item]}
@frappe.whitelist()
@@ -121,33 +155,43 @@
as_dict=1,
)
- if items_data:
- items = [d.item_code for d in items_data]
- item_prices_data = frappe.get_all(
+ # return (empty) list if there are no results
+ if not items_data:
+ return result
+
+ for item in items_data:
+ uoms = frappe.get_doc("Item", item.item_code).get("uoms", [])
+
+ item.actual_qty, _ = get_stock_availability(item.item_code, warehouse)
+ item.uom = item.stock_uom
+
+ item_price = frappe.get_all(
"Item Price",
- fields=["item_code", "price_list_rate", "currency"],
- filters={"price_list": price_list, "item_code": ["in", items]},
+ fields=["price_list_rate", "currency", "uom"],
+ filters={
+ "price_list": price_list,
+ "item_code": item.item_code,
+ "selling": True,
+ },
)
- item_prices = {}
- for d in item_prices_data:
- item_prices[d.item_code] = d
+ if not item_price:
+ result.append(item)
- for item in items_data:
- item_code = item.item_code
- item_price = item_prices.get(item_code) or {}
- item_stock_qty, is_stock_item = get_stock_availability(item_code, warehouse)
+ for price in item_price:
+ uom = next(filter(lambda x: x.uom == price.uom, uoms), {})
- row = {}
- row.update(item)
- row.update(
+ if price.uom != item.stock_uom and uom and uom.conversion_factor:
+ item.actual_qty = item.actual_qty // uom.conversion_factor
+
+ result.append(
{
- "price_list_rate": item_price.get("price_list_rate"),
- "currency": item_price.get("currency"),
- "actual_qty": item_stock_qty,
+ **item,
+ "price_list_rate": price.get("price_list_rate"),
+ "currency": price.get("currency"),
+ "uom": price.uom or item.uom,
}
)
- result.append(row)
return {"items": result}
diff --git a/erpnext/selling/page/point_of_sale/pos_controller.js b/erpnext/selling/page/point_of_sale/pos_controller.js
index 595b919..c442774 100644
--- a/erpnext/selling/page/point_of_sale/pos_controller.js
+++ b/erpnext/selling/page/point_of_sale/pos_controller.js
@@ -542,12 +542,12 @@
if (!this.frm.doc.customer)
return this.raise_customer_selection_alert();
- const { item_code, batch_no, serial_no, rate } = item;
+ const { item_code, batch_no, serial_no, rate, uom } = item;
if (!item_code)
return;
- const new_item = { item_code, batch_no, rate, [field]: value };
+ const new_item = { item_code, batch_no, rate, uom, [field]: value };
if (serial_no) {
await this.check_serial_no_availablilty(item_code, this.frm.doc.set_warehouse, serial_no);
@@ -649,6 +649,7 @@
const is_stock_item = resp[1];
frappe.dom.unfreeze();
+ const bold_uom = item_row.stock_uom.bold();
const bold_item_code = item_row.item_code.bold();
const bold_warehouse = warehouse.bold();
const bold_available_qty = available_qty.toString().bold()
@@ -664,7 +665,7 @@
}
} else if (is_stock_item && available_qty < qty_needed) {
frappe.throw({
- message: __('Stock quantity not enough for Item Code: {0} under warehouse {1}. Available quantity {2}.', [bold_item_code, bold_warehouse, bold_available_qty]),
+ message: __('Stock quantity not enough for Item Code: {0} under warehouse {1}. Available quantity {2} {3}.', [bold_item_code, bold_warehouse, bold_available_qty, bold_uom]),
indicator: 'orange'
});
frappe.utils.play_sound("error");
diff --git a/erpnext/selling/page/point_of_sale/pos_item_cart.js b/erpnext/selling/page/point_of_sale/pos_item_cart.js
index e7dd211..12cc629 100644
--- a/erpnext/selling/page/point_of_sale/pos_item_cart.js
+++ b/erpnext/selling/page/point_of_sale/pos_item_cart.js
@@ -609,7 +609,7 @@
if (item_data.rate && item_data.amount && item_data.rate !== item_data.amount) {
return `
<div class="item-qty-rate">
- <div class="item-qty"><span>${item_data.qty || 0}</span></div>
+ <div class="item-qty"><span>${item_data.qty || 0} ${item_data.uom}</span></div>
<div class="item-rate-amount">
<div class="item-rate">${format_currency(item_data.amount, currency)}</div>
<div class="item-amount">${format_currency(item_data.rate, currency)}</div>
@@ -618,7 +618,7 @@
} else {
return `
<div class="item-qty-rate">
- <div class="item-qty"><span>${item_data.qty || 0}</span></div>
+ <div class="item-qty"><span>${item_data.qty || 0} ${item_data.uom}</span></div>
<div class="item-rate-amount">
<div class="item-rate">${format_currency(item_data.rate, currency)}</div>
</div>
diff --git a/erpnext/selling/page/point_of_sale/pos_item_selector.js b/erpnext/selling/page/point_of_sale/pos_item_selector.js
index b5eb048..ec67bdf 100644
--- a/erpnext/selling/page/point_of_sale/pos_item_selector.js
+++ b/erpnext/selling/page/point_of_sale/pos_item_selector.js
@@ -78,7 +78,7 @@
get_item_html(item) {
const me = this;
// eslint-disable-next-line no-unused-vars
- const { item_image, serial_no, batch_no, barcode, actual_qty, stock_uom, price_list_rate } = item;
+ const { item_image, serial_no, batch_no, barcode, actual_qty, uom, price_list_rate } = item;
const precision = flt(price_list_rate, 2) % 1 != 0 ? 2 : 0;
let indicator_color;
let qty_to_display = actual_qty;
@@ -118,7 +118,7 @@
return (
`<div class="item-wrapper"
data-item-code="${escape(item.item_code)}" data-serial-no="${escape(serial_no)}"
- data-batch-no="${escape(batch_no)}" data-uom="${escape(stock_uom)}"
+ data-batch-no="${escape(batch_no)}" data-uom="${escape(uom)}"
data-rate="${escape(price_list_rate || 0)}"
title="${item.item_name}">
@@ -128,7 +128,7 @@
<div class="item-name">
${frappe.ellipsis(item.item_name, 18)}
</div>
- <div class="item-rate">${format_currency(price_list_rate, item.currency, precision) || 0}</div>
+ <div class="item-rate">${format_currency(price_list_rate, item.currency, precision) || 0} / ${uom}</div>
</div>
</div>`
);
diff --git a/erpnext/selling/page/point_of_sale/pos_past_order_summary.js b/erpnext/selling/page/point_of_sale/pos_past_order_summary.js
index 40165c3..be75bd6 100644
--- a/erpnext/selling/page/point_of_sale/pos_past_order_summary.js
+++ b/erpnext/selling/page/point_of_sale/pos_past_order_summary.js
@@ -94,7 +94,7 @@
get_item_html(doc, item_data) {
return `<div class="item-row-wrapper">
<div class="item-name">${item_data.item_name}</div>
- <div class="item-qty">${item_data.qty || 0}</div>
+ <div class="item-qty">${item_data.qty || 0} ${item_data.uom}</div>
<div class="item-rate-disc">${get_rate_discount_html()}</div>
</div>`;