From ec37930404dd1bf4563e4bc2ea4acd767d77aff4 Mon Sep 17 00:00:00 2001 From: Subin Tom Date: Wed, 22 Dec 2021 19:14:47 +0530 Subject: [PATCH 01/10] fix: Allowing non stock items in POS --- .../accounts/doctype/pos_invoice/pos_invoice.py | 15 ++++++++++----- .../selling/page/point_of_sale/point_of_sale.py | 4 ++-- .../selling/page/point_of_sale/pos_controller.js | 16 +++++++++++----- 3 files changed, 23 insertions(+), 12 deletions(-) diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py index 0d6404c3245..e6e92f367c7 100644 --- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py @@ -41,7 +41,7 @@ class POSInvoice(SalesInvoice): self.validate_serialised_or_batched_item() self.validate_stock_availablility() self.validate_return_items_qty() - self.validate_non_stock_items() + # self.validate_non_stock_items() self.set_status() self.set_account_for_mode_of_payment() self.validate_pos() @@ -143,9 +143,11 @@ class POSInvoice(SalesInvoice): 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) @@ -475,9 +477,12 @@ def get_stock_availability(item_code, warehouse): bin_qty = get_bin_qty(item_code, warehouse) pos_sales_qty = get_pos_reserved_qty(item_code, warehouse) return bin_qty - pos_sales_qty - else: - if frappe.db.exists('Product Bundle', item_code): - return get_bundle_availability(item_code, warehouse) + elif frappe.db.exists('Product Bundle', item_code): + return get_bundle_availability(item_code, warehouse) + #To continue the flow considering a service item + elif frappe.db.get_value('Item', item_code, 'is_stock_item') == 0: + return 0 + 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 db5b20e3e19..14201c65ab0 100644 --- a/erpnext/selling/page/point_of_sale/point_of_sale.py +++ b/erpnext/selling/page/point_of_sale/point_of_sale.py @@ -99,7 +99,7 @@ def get_items(start, page_length, price_list, item_group, pos_profile, search_te ), {'warehouse': warehouse}, as_dict=1) if items_data: - items_data = filter_service_items(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"], @@ -146,7 +146,7 @@ def search_for_serial_or_batch_or_barcode_number(search_value): def filter_service_items(items): for item in items: - if not item['is_stock_item']: + if not item.get('is_stock_item'): if not frappe.db.exists('Product Bundle', item['item_code']): items.remove(item) diff --git a/erpnext/selling/page/point_of_sale/pos_controller.js b/erpnext/selling/page/point_of_sale/pos_controller.js index e61a634aaee..0508170dc11 100644 --- a/erpnext/selling/page/point_of_sale/pos_controller.js +++ b/erpnext/selling/page/point_of_sale/pos_controller.js @@ -637,11 +637,17 @@ erpnext.PointOfSale.Controller = class { 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]) - }) + frappe.db.get_value('Item', item_row.item_code, 'is_stock_item').then(({message}) => { + const is_service_item = message.is_stock_item; + console.log('is_service_item', is_service_item); + if (!is_service_item) return; + + 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 if (available_qty < qty_needed) { frappe.show_alert({ message: __('Stock quantity not enough for Item Code: {0} under warehouse {1}. Available quantity {2}.', [bold_item_code, bold_warehouse, bold_available_qty]), From 6b973d658c9b7f77864627c585908f4c7e7e1920 Mon Sep 17 00:00:00 2001 From: Subin Tom <36098155+nemesis189@users.noreply.github.com> Date: Mon, 3 Jan 2022 10:47:13 +0530 Subject: [PATCH 02/10] fix: sider issues --- erpnext/selling/page/point_of_sale/pos_controller.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/erpnext/selling/page/point_of_sale/pos_controller.js b/erpnext/selling/page/point_of_sale/pos_controller.js index 0508170dc11..97d54cecd15 100644 --- a/erpnext/selling/page/point_of_sale/pos_controller.js +++ b/erpnext/selling/page/point_of_sale/pos_controller.js @@ -639,14 +639,13 @@ erpnext.PointOfSale.Controller = class { if (!(available_qty > 0)) { frappe.db.get_value('Item', item_row.item_code, 'is_stock_item').then(({message}) => { const is_service_item = message.is_stock_item; - console.log('is_service_item', is_service_item); if (!is_service_item) return; 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 if (available_qty < qty_needed) { frappe.show_alert({ From 9e20fa85c1823bb8622d7321f561b7d010b7d053 Mon Sep 17 00:00:00 2001 From: Subin Tom Date: Mon, 3 Jan 2022 14:46:53 +0530 Subject: [PATCH 03/10] fix: Removed validate_non_stock_items, filter_service_items methods --- .../doctype/pos_invoice/pos_invoice.py | 20 ++++++------------- .../page/point_of_sale/point_of_sale.py | 9 --------- 2 files changed, 6 insertions(+), 23 deletions(-) diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py index e6e92f367c7..f0ca9c4bf45 100644 --- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py @@ -41,7 +41,6 @@ class POSInvoice(SalesInvoice): 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() @@ -226,14 +225,6 @@ class POSInvoice(SalesInvoice): .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.")) @@ -477,11 +468,12 @@ def get_stock_availability(item_code, warehouse): bin_qty = get_bin_qty(item_code, warehouse) pos_sales_qty = get_pos_reserved_qty(item_code, warehouse) return bin_qty - pos_sales_qty - elif frappe.db.exists('Product Bundle', item_code): - return get_bundle_availability(item_code, warehouse) - #To continue the flow considering a service item - elif frappe.db.get_value('Item', item_code, 'is_stock_item') == 0: - return 0 + else: + if frappe.db.exists('Product Bundle', item_code): + return get_bundle_availability(item_code, warehouse) + else: + # Is a service item + return 0 def get_bundle_availability(bundle_item_code, warehouse): 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 14201c65ab0..b126f8c7579 100644 --- a/erpnext/selling/page/point_of_sale/point_of_sale.py +++ b/erpnext/selling/page/point_of_sale/point_of_sale.py @@ -99,7 +99,6 @@ def get_items(start, page_length, price_list, item_group, pos_profile, search_te ), {'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"], @@ -144,14 +143,6 @@ def search_for_serial_or_batch_or_barcode_number(search_value): return {} -def filter_service_items(items): - for item in items: - if not item.get('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} From ac9a9fb22943a183e9e83077ec428262c4065a15 Mon Sep 17 00:00:00 2001 From: Subin Tom Date: Mon, 17 Jan 2022 19:38:05 +0530 Subject: [PATCH 04/10] fix: removing `get_value` call by returning is_stock_item in `get_stock_availability` method --- .../accounts/doctype/pos_invoice/pos_invoice.py | 10 ++++++---- .../selling/page/point_of_sale/point_of_sale.py | 4 ++-- .../selling/page/point_of_sale/pos_controller.js | 15 ++++++++------- 3 files changed, 16 insertions(+), 13 deletions(-) diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py index f0ca9c4bf45..fd801516e44 100644 --- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py @@ -154,7 +154,7 @@ class POSInvoice(SalesInvoice): 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: @@ -465,15 +465,17 @@ class POSInvoice(SalesInvoice): @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 + return 0, is_stock_item def get_bundle_availability(bundle_item_code, warehouse): 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 b126f8c7579..993c61d5639 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 @@ def search_by_term(search_term, warehouse, price_list): ["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 @@ -111,7 +111,7 @@ def get_items(start, page_length, price_list, item_group, pos_profile, search_te 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) diff --git a/erpnext/selling/page/point_of_sale/pos_controller.js b/erpnext/selling/page/point_of_sale/pos_controller.js index 97d54cecd15..7caf9e7da15 100644 --- a/erpnext/selling/page/point_of_sale/pos_controller.js +++ b/erpnext/selling/page/point_of_sale/pos_controller.js @@ -630,23 +630,24 @@ erpnext.PointOfSale.Controller = class { } 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.db.get_value('Item', item_row.item_code, 'is_stock_item').then(({message}) => { - const is_service_item = message.is_stock_item; - if (!is_service_item) return; - + 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.show_alert({ message: __('Stock quantity not enough for Item Code: {0} under warehouse {1}. Available quantity {2}.', [bold_item_code, bold_warehouse, bold_available_qty]), @@ -681,7 +682,7 @@ erpnext.PointOfSale.Controller = class { 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][warehouse] = res.message[0]; } }); } From 27b35d72e2ba6163c1ea0ba705d018aa7692dc8a Mon Sep 17 00:00:00 2001 From: Subin Tom Date: Mon, 17 Jan 2022 19:53:39 +0530 Subject: [PATCH 05/10] fix: sider fix --- erpnext/selling/page/point_of_sale/pos_controller.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/selling/page/point_of_sale/pos_controller.js b/erpnext/selling/page/point_of_sale/pos_controller.js index 7caf9e7da15..79dc7f71ae0 100644 --- a/erpnext/selling/page/point_of_sale/pos_controller.js +++ b/erpnext/selling/page/point_of_sale/pos_controller.js @@ -681,7 +681,7 @@ erpnext.PointOfSale.Controller = class { }, callback(res) { if (!me.item_stock_map[item_code]) - me.item_stock_map[item_code] = {} + me.item_stock_map[item_code] = {}; me.item_stock_map[item_code][warehouse] = res.message[0]; } }); From 845c02a989553a8375c94dbd0a8a71687fa9ff07 Mon Sep 17 00:00:00 2001 From: Subin Tom Date: Mon, 17 Jan 2022 20:49:11 +0530 Subject: [PATCH 06/10] fix: remove qty indicator from non stock items --- .../page/point_of_sale/pos_item_selector.js | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) 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 496385248c4..68a46a97ccd 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 @@ erpnext.PointOfSale.ItemSelector = class { 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() { From 650d44a714cb94c9afdd80d1c38ce9c1e58cf28c Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Tue, 1 Feb 2022 13:14:28 +0530 Subject: [PATCH 07/10] test: point of sale search --- erpnext/tests/test_point_of_sale.py | 54 +++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 erpnext/tests/test_point_of_sale.py diff --git a/erpnext/tests/test_point_of_sale.py b/erpnext/tests/test_point_of_sale.py new file mode 100644 index 00000000000..e97c78e77aa --- /dev/null +++ b/erpnext/tests/test_point_of_sale.py @@ -0,0 +1,54 @@ +# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors +# MIT License. See license.txt + + +import frappe + +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 Stock Item", {"is_stock_item": 1}) + make_stock_entry( + item_code="Test 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 Stock Item", + ) + filtered_items = result.get("items") + + self.assertEqual(len(filtered_items), 1) + self.assertEqual(filtered_items[0]["item_code"], "Test Stock Item") + self.assertEqual(filtered_items[0]["actual_qty"], 10) + + item2 = make_item("Test 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 Service Item", + ) + filtered_items = result.get("items") + + self.assertEqual(len(filtered_items), 1) + self.assertEqual(filtered_items[0]["item_code"], "Test Service Item") + + frappe.db.rollback() From 2aca54eb383c4e99861e40a6d1d8524ffd66f2ae Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Tue, 1 Feb 2022 13:17:01 +0530 Subject: [PATCH 08/10] chore: remove useless rollback --- erpnext/tests/test_point_of_sale.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/erpnext/tests/test_point_of_sale.py b/erpnext/tests/test_point_of_sale.py index e97c78e77aa..e68f1dc50d0 100644 --- a/erpnext/tests/test_point_of_sale.py +++ b/erpnext/tests/test_point_of_sale.py @@ -50,5 +50,3 @@ class TestPointOfSale(ERPNextTestCase): self.assertEqual(len(filtered_items), 1) self.assertEqual(filtered_items[0]["item_code"], "Test Service Item") - - frappe.db.rollback() From 4e4159ec0668ddecb9684e3c96810b5fca10b29a Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Tue, 1 Feb 2022 14:02:52 +0530 Subject: [PATCH 09/10] chore: remove unused import --- erpnext/tests/test_point_of_sale.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/erpnext/tests/test_point_of_sale.py b/erpnext/tests/test_point_of_sale.py index e68f1dc50d0..e1abf167bd2 100644 --- a/erpnext/tests/test_point_of_sale.py +++ b/erpnext/tests/test_point_of_sale.py @@ -2,8 +2,6 @@ # MIT License. See license.txt -import frappe - 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 bf70feb7c9be7320188d72e26b2ef5d212c7a290 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Tue, 1 Feb 2022 14:44:12 +0530 Subject: [PATCH 10/10] fix: flaky test --- erpnext/tests/test_point_of_sale.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/erpnext/tests/test_point_of_sale.py b/erpnext/tests/test_point_of_sale.py index e1abf167bd2..df2dc8b99a1 100644 --- a/erpnext/tests/test_point_of_sale.py +++ b/erpnext/tests/test_point_of_sale.py @@ -16,9 +16,12 @@ class TestPointOfSale(ERPNextTestCase): """ pos_profile = make_pos_profile() - item1 = make_item("Test Stock Item", {"is_stock_item": 1}) + item1 = make_item("Test Search Stock Item", {"is_stock_item": 1}) make_stock_entry( - item_code="Test Stock Item", qty=10, to_warehouse="_Test Warehouse - _TC", rate=500 + item_code="Test Search Stock Item", + qty=10, + to_warehouse="_Test Warehouse - _TC", + rate=500, ) result = get_items( @@ -27,24 +30,24 @@ class TestPointOfSale(ERPNextTestCase): price_list=None, item_group=item1.item_group, pos_profile=pos_profile.name, - search_term="Test Stock Item", + search_term="Test Search Stock Item", ) filtered_items = result.get("items") self.assertEqual(len(filtered_items), 1) - self.assertEqual(filtered_items[0]["item_code"], "Test Stock Item") + self.assertEqual(filtered_items[0]["item_code"], item1.item_code) self.assertEqual(filtered_items[0]["actual_qty"], 10) - item2 = make_item("Test Service Item", {"is_stock_item": 0}) + 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 Service Item", + search_term="Test Search Service Item", ) filtered_items = result.get("items") self.assertEqual(len(filtered_items), 1) - self.assertEqual(filtered_items[0]["item_code"], "Test Service Item") + self.assertEqual(filtered_items[0]["item_code"], item2.item_code)