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:
parent
3598bcc9a8
commit
9e50aa4833
@ -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}
|
||||
|
||||
|
@ -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");
|
||||
|
@ -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>
|
||||
|
@ -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>`
|
||||
);
|
||||
|
@ -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>`;
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user