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>
This commit is contained in:
Sabu Siyad 2023-01-28 10:28:31 +05:30 committed by GitHub
parent 3598bcc9a8
commit 9e50aa4833
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 105 additions and 60 deletions

View File

@ -17,45 +17,79 @@ from erpnext.stock.utils import scan_barcode
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 @@ def get_items(start, page_length, price_list, item_group, pos_profile, search_te
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}

View File

@ -542,12 +542,12 @@ erpnext.PointOfSale.Controller = class {
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 @@ erpnext.PointOfSale.Controller = class {
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 @@ erpnext.PointOfSale.Controller = class {
}
} 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");

View File

@ -609,7 +609,7 @@ erpnext.PointOfSale.ItemCart = class {
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 @@ erpnext.PointOfSale.ItemCart = class {
} 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>

View File

@ -78,7 +78,7 @@ erpnext.PointOfSale.ItemSelector = class {
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 @@ erpnext.PointOfSale.ItemSelector = class {
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 @@ erpnext.PointOfSale.ItemSelector = class {
<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>`
);

View File

@ -94,7 +94,7 @@ erpnext.PointOfSale.PastOrderSummary = class {
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>`;