Merge pull request #29556 from nextchamp-saqib/nemesis189-pos-service-items
feat: Allowing non stock items in POS
This commit is contained in:
commit
4055cc76a7
@ -42,7 +42,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()
|
||||
@ -175,9 +174,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)
|
||||
@ -188,7 +189,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:
|
||||
@ -259,14 +260,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."))
|
||||
@ -506,12 +499,18 @@ 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, is_stock_item
|
||||
|
||||
|
||||
def get_bundle_availability(bundle_item_code, warehouse):
|
||||
product_bundle = frappe.get_doc('Product Bundle', bundle_item_code)
|
||||
|
@ -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
|
||||
@ -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"],
|
||||
@ -112,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)
|
||||
@ -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['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}
|
||||
|
@ -630,18 +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.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 @@ 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] = {};
|
||||
me.item_stock_map[item_code][warehouse] = res.message[0];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -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() {
|
||||
|
53
erpnext/tests/test_point_of_sale.py
Normal file
53
erpnext/tests/test_point_of_sale.py
Normal file
@ -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)
|
Loading…
x
Reference in New Issue
Block a user