Merge pull request #29556 from nextchamp-saqib/nemesis189-pos-service-items
feat: Allowing non stock items in POS
diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py
index f66abdc..97d34e0 100644
--- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py
+++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py
@@ -42,7 +42,6 @@
self.validate_serialised_or_batched_item()
self.validate_stock_availablility()
self.validate_return_items_qty()
- self.validate_non_stock_items()
self.set_status()
self.set_account_for_mode_of_payment()
self.validate_pos()
@@ -175,9 +174,11 @@
def validate_stock_availablility(self):
if self.is_return or self.docstatus != 1:
return
-
allow_negative_stock = frappe.db.get_single_value('Stock Settings', 'allow_negative_stock')
for d in self.get('items'):
+ is_service_item = not (frappe.db.get_value('Item', d.get('item_code'), 'is_stock_item'))
+ if is_service_item:
+ return
if d.serial_no:
self.validate_pos_reserved_serial_nos(d)
self.validate_delivered_serial_nos(d)
@@ -188,7 +189,7 @@
if allow_negative_stock:
return
- available_stock = get_stock_availability(d.item_code, d.warehouse)
+ available_stock, is_stock_item = get_stock_availability(d.item_code, d.warehouse)
item_code, warehouse, qty = frappe.bold(d.item_code), frappe.bold(d.warehouse), frappe.bold(d.qty)
if flt(available_stock) <= 0:
@@ -259,14 +260,6 @@
.format(d.idx, bold_serial_no, bold_return_against)
)
- def validate_non_stock_items(self):
- for d in self.get("items"):
- is_stock_item = frappe.get_cached_value("Item", d.get("item_code"), "is_stock_item")
- if not is_stock_item:
- if not frappe.db.exists('Product Bundle', d.item_code):
- frappe.throw(_("Row #{}: Item {} is a non stock item. You can only include stock items in a POS Invoice.")
- .format(d.idx, frappe.bold(d.item_code)), title=_("Invalid Item"))
-
def validate_mode_of_payment(self):
if len(self.payments) == 0:
frappe.throw(_("At least one mode of payment is required for POS invoice."))
@@ -506,12 +499,18 @@
@frappe.whitelist()
def get_stock_availability(item_code, warehouse):
if frappe.db.get_value('Item', item_code, 'is_stock_item'):
+ is_stock_item = True
bin_qty = get_bin_qty(item_code, warehouse)
pos_sales_qty = get_pos_reserved_qty(item_code, warehouse)
- return bin_qty - pos_sales_qty
+ return bin_qty - pos_sales_qty, is_stock_item
else:
+ is_stock_item = False
if frappe.db.exists('Product Bundle', item_code):
- return get_bundle_availability(item_code, warehouse)
+ return get_bundle_availability(item_code, warehouse), is_stock_item
+ else:
+ # Is a service item
+ return 0, is_stock_item
+
def get_bundle_availability(bundle_item_code, warehouse):
product_bundle = frappe.get_doc('Product Bundle', bundle_item_code)
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 db5b20e..993c61d 100644
--- a/erpnext/selling/page/point_of_sale/point_of_sale.py
+++ b/erpnext/selling/page/point_of_sale/point_of_sale.py
@@ -24,7 +24,7 @@
["name as item_code", "item_name", "description", "stock_uom", "image as item_image", "is_stock_item"],
as_dict=1)
- item_stock_qty = get_stock_availability(item_code, warehouse)
+ 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
@@ -99,7 +99,6 @@
), {'warehouse': warehouse}, as_dict=1)
if items_data:
- items_data = filter_service_items(items_data)
items = [d.item_code for d in items_data]
item_prices_data = frappe.get_all("Item Price",
fields = ["item_code", "price_list_rate", "currency"],
@@ -112,7 +111,7 @@
for item in items_data:
item_code = item.item_code
item_price = item_prices.get(item_code) or {}
- item_stock_qty = get_stock_availability(item_code, warehouse)
+ item_stock_qty, is_stock_item = get_stock_availability(item_code, warehouse)
row = {}
row.update(item)
@@ -144,14 +143,6 @@
return {}
-def filter_service_items(items):
- for item in items:
- if not item['is_stock_item']:
- if not frappe.db.exists('Product Bundle', item['item_code']):
- items.remove(item)
-
- return items
-
def get_conditions(search_term):
condition = "("
condition += """item.name like {search_term}
diff --git a/erpnext/selling/page/point_of_sale/pos_controller.js b/erpnext/selling/page/point_of_sale/pos_controller.js
index ce74f6d..56aa24f 100644
--- a/erpnext/selling/page/point_of_sale/pos_controller.js
+++ b/erpnext/selling/page/point_of_sale/pos_controller.js
@@ -630,18 +630,24 @@
}
async check_stock_availability(item_row, qty_needed, warehouse) {
- const available_qty = (await this.get_available_stock(item_row.item_code, warehouse)).message;
+ const resp = (await this.get_available_stock(item_row.item_code, warehouse)).message;
+ const available_qty = resp[0];
+ const is_stock_item = resp[1];
frappe.dom.unfreeze();
const bold_item_code = item_row.item_code.bold();
const bold_warehouse = warehouse.bold();
const bold_available_qty = available_qty.toString().bold()
if (!(available_qty > 0)) {
- frappe.model.clear_doc(item_row.doctype, item_row.name);
- frappe.throw({
- title: __("Not Available"),
- message: __('Item Code: {0} is not available under warehouse {1}.', [bold_item_code, bold_warehouse])
- })
+ if (is_stock_item) {
+ frappe.model.clear_doc(item_row.doctype, item_row.name);
+ frappe.throw({
+ title: __("Not Available"),
+ message: __('Item Code: {0} is not available under warehouse {1}.', [bold_item_code, bold_warehouse])
+ });
+ } else {
+ return;
+ }
} else if (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]),
@@ -675,8 +681,8 @@
},
callback(res) {
if (!me.item_stock_map[item_code])
- me.item_stock_map[item_code] = {}
- me.item_stock_map[item_code][warehouse] = res.message;
+ me.item_stock_map[item_code] = {};
+ me.item_stock_map[item_code][warehouse] = res.message[0];
}
});
}
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 a30bcd7..1177615 100644
--- a/erpnext/selling/page/point_of_sale/pos_item_selector.js
+++ b/erpnext/selling/page/point_of_sale/pos_item_selector.js
@@ -79,14 +79,20 @@
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 indicator_color = actual_qty > 10 ? "green" : actual_qty <= 0 ? "red" : "orange";
const precision = flt(price_list_rate, 2) % 1 != 0 ? 2 : 0;
-
+ let indicator_color;
let qty_to_display = actual_qty;
- if (Math.round(qty_to_display) > 999) {
- qty_to_display = Math.round(qty_to_display)/1000;
- qty_to_display = qty_to_display.toFixed(1) + 'K';
+ if (item.is_stock_item) {
+ indicator_color = (actual_qty > 10 ? "green" : actual_qty <= 0 ? "red" : "orange");
+
+ if (Math.round(qty_to_display) > 999) {
+ qty_to_display = Math.round(qty_to_display)/1000;
+ qty_to_display = qty_to_display.toFixed(1) + 'K';
+ }
+ } else {
+ indicator_color = '';
+ qty_to_display = '';
}
function get_item_image_html() {
diff --git a/erpnext/tests/test_point_of_sale.py b/erpnext/tests/test_point_of_sale.py
new file mode 100644
index 0000000..df2dc8b
--- /dev/null
+++ b/erpnext/tests/test_point_of_sale.py
@@ -0,0 +1,53 @@
+# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
+# MIT License. See license.txt
+
+
+from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profile
+from erpnext.selling.page.point_of_sale.point_of_sale import get_items
+from erpnext.stock.doctype.item.test_item import make_item
+from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
+from erpnext.tests.utils import ERPNextTestCase
+
+
+class TestPointOfSale(ERPNextTestCase):
+ def test_item_search(self):
+ """
+ Test Stock and Service Item Search.
+ """
+
+ pos_profile = make_pos_profile()
+ item1 = make_item("Test Search Stock Item", {"is_stock_item": 1})
+ make_stock_entry(
+ item_code="Test Search Stock Item",
+ qty=10,
+ to_warehouse="_Test Warehouse - _TC",
+ rate=500,
+ )
+
+ result = get_items(
+ start=0,
+ page_length=20,
+ price_list=None,
+ item_group=item1.item_group,
+ pos_profile=pos_profile.name,
+ search_term="Test Search Stock Item",
+ )
+ filtered_items = result.get("items")
+
+ self.assertEqual(len(filtered_items), 1)
+ self.assertEqual(filtered_items[0]["item_code"], item1.item_code)
+ self.assertEqual(filtered_items[0]["actual_qty"], 10)
+
+ item2 = make_item("Test Search Service Item", {"is_stock_item": 0})
+ result = get_items(
+ start=0,
+ page_length=20,
+ price_list=None,
+ item_group=item2.item_group,
+ pos_profile=pos_profile.name,
+ search_term="Test Search Service Item",
+ )
+ filtered_items = result.get("items")
+
+ self.assertEqual(len(filtered_items), 1)
+ self.assertEqual(filtered_items[0]["item_code"], item2.item_code)