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_serialised_or_batched_item()
|
||||||
self.validate_stock_availablility()
|
self.validate_stock_availablility()
|
||||||
self.validate_return_items_qty()
|
self.validate_return_items_qty()
|
||||||
self.validate_non_stock_items()
|
|
||||||
self.set_status()
|
self.set_status()
|
||||||
self.set_account_for_mode_of_payment()
|
self.set_account_for_mode_of_payment()
|
||||||
self.validate_pos()
|
self.validate_pos()
|
||||||
@ -175,9 +174,11 @@ class POSInvoice(SalesInvoice):
|
|||||||
def validate_stock_availablility(self):
|
def validate_stock_availablility(self):
|
||||||
if self.is_return or self.docstatus != 1:
|
if self.is_return or self.docstatus != 1:
|
||||||
return
|
return
|
||||||
|
|
||||||
allow_negative_stock = frappe.db.get_single_value('Stock Settings', 'allow_negative_stock')
|
allow_negative_stock = frappe.db.get_single_value('Stock Settings', 'allow_negative_stock')
|
||||||
for d in self.get('items'):
|
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:
|
if d.serial_no:
|
||||||
self.validate_pos_reserved_serial_nos(d)
|
self.validate_pos_reserved_serial_nos(d)
|
||||||
self.validate_delivered_serial_nos(d)
|
self.validate_delivered_serial_nos(d)
|
||||||
@ -188,7 +189,7 @@ class POSInvoice(SalesInvoice):
|
|||||||
if allow_negative_stock:
|
if allow_negative_stock:
|
||||||
return
|
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)
|
item_code, warehouse, qty = frappe.bold(d.item_code), frappe.bold(d.warehouse), frappe.bold(d.qty)
|
||||||
if flt(available_stock) <= 0:
|
if flt(available_stock) <= 0:
|
||||||
@ -259,14 +260,6 @@ class POSInvoice(SalesInvoice):
|
|||||||
.format(d.idx, bold_serial_no, bold_return_against)
|
.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):
|
def validate_mode_of_payment(self):
|
||||||
if len(self.payments) == 0:
|
if len(self.payments) == 0:
|
||||||
frappe.throw(_("At least one mode of payment is required for POS invoice."))
|
frappe.throw(_("At least one mode of payment is required for POS invoice."))
|
||||||
@ -506,12 +499,18 @@ class POSInvoice(SalesInvoice):
|
|||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_stock_availability(item_code, warehouse):
|
def get_stock_availability(item_code, warehouse):
|
||||||
if frappe.db.get_value('Item', item_code, 'is_stock_item'):
|
if frappe.db.get_value('Item', item_code, 'is_stock_item'):
|
||||||
|
is_stock_item = True
|
||||||
bin_qty = get_bin_qty(item_code, warehouse)
|
bin_qty = get_bin_qty(item_code, warehouse)
|
||||||
pos_sales_qty = get_pos_reserved_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:
|
else:
|
||||||
|
is_stock_item = False
|
||||||
if frappe.db.exists('Product Bundle', item_code):
|
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):
|
def get_bundle_availability(bundle_item_code, warehouse):
|
||||||
product_bundle = frappe.get_doc('Product Bundle', bundle_item_code)
|
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"],
|
["name as item_code", "item_name", "description", "stock_uom", "image as item_image", "is_stock_item"],
|
||||||
as_dict=1)
|
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_rate, currency = frappe.db.get_value('Item Price', {
|
||||||
'price_list': price_list,
|
'price_list': price_list,
|
||||||
'item_code': item_code
|
'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)
|
), {'warehouse': warehouse}, as_dict=1)
|
||||||
|
|
||||||
if items_data:
|
if items_data:
|
||||||
items_data = filter_service_items(items_data)
|
|
||||||
items = [d.item_code for d in items_data]
|
items = [d.item_code for d in items_data]
|
||||||
item_prices_data = frappe.get_all("Item Price",
|
item_prices_data = frappe.get_all("Item Price",
|
||||||
fields = ["item_code", "price_list_rate", "currency"],
|
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:
|
for item in items_data:
|
||||||
item_code = item.item_code
|
item_code = item.item_code
|
||||||
item_price = item_prices.get(item_code) or {}
|
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 = {}
|
||||||
row.update(item)
|
row.update(item)
|
||||||
@ -144,14 +143,6 @@ def search_for_serial_or_batch_or_barcode_number(search_value):
|
|||||||
|
|
||||||
return {}
|
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):
|
def get_conditions(search_term):
|
||||||
condition = "("
|
condition = "("
|
||||||
condition += """item.name like {search_term}
|
condition += """item.name like {search_term}
|
||||||
|
@ -630,18 +630,24 @@ erpnext.PointOfSale.Controller = class {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async check_stock_availability(item_row, qty_needed, warehouse) {
|
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();
|
frappe.dom.unfreeze();
|
||||||
const bold_item_code = item_row.item_code.bold();
|
const bold_item_code = item_row.item_code.bold();
|
||||||
const bold_warehouse = warehouse.bold();
|
const bold_warehouse = warehouse.bold();
|
||||||
const bold_available_qty = available_qty.toString().bold()
|
const bold_available_qty = available_qty.toString().bold()
|
||||||
if (!(available_qty > 0)) {
|
if (!(available_qty > 0)) {
|
||||||
|
if (is_stock_item) {
|
||||||
frappe.model.clear_doc(item_row.doctype, item_row.name);
|
frappe.model.clear_doc(item_row.doctype, item_row.name);
|
||||||
frappe.throw({
|
frappe.throw({
|
||||||
title: __("Not Available"),
|
title: __("Not Available"),
|
||||||
message: __('Item Code: {0} is not available under warehouse {1}.', [bold_item_code, bold_warehouse])
|
message: __('Item Code: {0} is not available under warehouse {1}.', [bold_item_code, bold_warehouse])
|
||||||
})
|
});
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
} else if (available_qty < qty_needed) {
|
} else if (available_qty < qty_needed) {
|
||||||
frappe.throw({
|
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}.', [bold_item_code, bold_warehouse, bold_available_qty]),
|
||||||
@ -675,8 +681,8 @@ erpnext.PointOfSale.Controller = class {
|
|||||||
},
|
},
|
||||||
callback(res) {
|
callback(res) {
|
||||||
if (!me.item_stock_map[item_code])
|
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;
|
me.item_stock_map[item_code][warehouse] = res.message[0];
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -79,15 +79,21 @@ erpnext.PointOfSale.ItemSelector = class {
|
|||||||
const me = this;
|
const me = this;
|
||||||
// eslint-disable-next-line no-unused-vars
|
// 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, 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;
|
const precision = flt(price_list_rate, 2) % 1 != 0 ? 2 : 0;
|
||||||
|
let indicator_color;
|
||||||
let qty_to_display = actual_qty;
|
let qty_to_display = actual_qty;
|
||||||
|
|
||||||
|
if (item.is_stock_item) {
|
||||||
|
indicator_color = (actual_qty > 10 ? "green" : actual_qty <= 0 ? "red" : "orange");
|
||||||
|
|
||||||
if (Math.round(qty_to_display) > 999) {
|
if (Math.round(qty_to_display) > 999) {
|
||||||
qty_to_display = Math.round(qty_to_display)/1000;
|
qty_to_display = Math.round(qty_to_display)/1000;
|
||||||
qty_to_display = qty_to_display.toFixed(1) + 'K';
|
qty_to_display = qty_to_display.toFixed(1) + 'K';
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
indicator_color = '';
|
||||||
|
qty_to_display = '';
|
||||||
|
}
|
||||||
|
|
||||||
function get_item_image_html() {
|
function get_item_image_html() {
|
||||||
if (!me.hide_images && item_image) {
|
if (!me.hide_images && item_image) {
|
||||||
|
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