diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py
index d4825b87212..9f2d6431fba 100644
--- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py
+++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py
@@ -20,6 +20,7 @@ from erpnext.accounts.party import get_due_date, get_party_account
from erpnext.controllers.queries import item_query as _item_query
from erpnext.controllers.sales_and_purchase_return import get_sales_invoice_item_from_consolidated_invoice
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
+from erpnext.stock.stock_ledger import is_negative_stock_allowed
class POSInvoice(SalesInvoice):
@@ -395,32 +396,66 @@ class POSInvoice(SalesInvoice):
for d in self.get("items"):
if not d.serial_and_batch_bundle:
- available_stock, is_stock_item, is_negative_stock_allowed = get_stock_availability(
- d.item_code, d.warehouse
- )
+ if frappe.db.exists("Product Bundle", d.item_code):
+ (
+ availability,
+ is_stock_item,
+ is_negative_stock_allowed,
+ ) = get_product_bundle_stock_availability(d.item_code, d.warehouse, d.stock_qty)
+
+ else:
+ availability, is_stock_item, is_negative_stock_allowed = get_stock_availability(
+ d.item_code, d.warehouse
+ )
if is_negative_stock_allowed:
continue
- item_code, warehouse, _qty = (
- frappe.bold(d.item_code),
- frappe.bold(d.warehouse),
- frappe.bold(d.qty),
- )
- if is_stock_item and flt(available_stock) <= 0:
- frappe.throw(
- _("Row #{}: Item Code: {} is not available under warehouse {}.").format(
- d.idx, item_code, warehouse
- ),
- title=_("Item Unavailable"),
- )
- elif is_stock_item and flt(available_stock) < flt(d.stock_qty):
- frappe.throw(
- _("Row #{}: Stock quantity not enough for Item Code: {} under warehouse {}.").format(
- d.idx, item_code, warehouse
- ),
- title=_("Item Unavailable"),
- )
+ if isinstance(availability, list):
+ error_msgs = []
+ for item in availability:
+ if flt(item["available"]) < flt(item["required"]):
+ error_msgs.append(
+ _("
Packed Item {0}: Required {1}, Available {2}").format(
+ frappe.bold(item["item_code"]),
+ frappe.bold(flt(item["required"], 2)),
+ frappe.bold(flt(item["available"], 2)),
+ )
+ )
+
+ if error_msgs:
+ frappe.throw(
+ _(
+ "Row #{0}: Bundle {1} in warehouse {2} has insufficient packed items:
"
+ ).format(
+ d.idx,
+ frappe.bold(d.item_code),
+ frappe.bold(d.warehouse),
+ "
".join(error_msgs),
+ ),
+ title=_("Insufficient Stock for Product Bundle Items"),
+ )
+
+ else:
+ item_code, warehouse = frappe.bold(d.item_code), frappe.bold(d.warehouse)
+ if is_stock_item and flt(availability) <= 0:
+ frappe.throw(
+ _("Row #{0}: Item {1} has no stock in warehouse {2}.").format(
+ d.idx, item_code, warehouse
+ ),
+ title=_("Item Out of Stock"),
+ )
+ elif is_stock_item and flt(availability) < flt(d.stock_qty):
+ frappe.throw(
+ _("Row #{0}: Item {1} in warehouse {2}: Available {3}, Needed {4}.").format(
+ d.idx,
+ item_code,
+ warehouse,
+ frappe.bold(flt(availability, 2)),
+ frappe.bold(flt(d.stock_qty, 2)),
+ ),
+ title=_("Insufficient Stock"),
+ )
def validate_is_pos_using_sales_invoice(self):
self.invoice_type_in_pos = frappe.db.get_single_value("POS Settings", "invoice_type")
@@ -858,8 +893,6 @@ class POSInvoice(SalesInvoice):
@frappe.whitelist()
def get_stock_availability(item_code, warehouse):
- from erpnext.stock.stock_ledger import is_negative_stock_allowed
-
if frappe.db.get_value("Item", item_code, "is_stock_item"):
is_stock_item = True
bin_qty = get_bin_qty(item_code, warehouse)
@@ -876,6 +909,26 @@ def get_stock_availability(item_code, warehouse):
return 0, is_stock_item, False
+def get_product_bundle_stock_availability(item_code, warehouse, item_qty):
+ is_stock_item = True
+ bundle = frappe.get_doc("Product Bundle", item_code)
+ availabilities = []
+ for bundle_item in bundle.items:
+ if frappe.get_value("Item", bundle_item.item_code, "is_stock_item"):
+ bin_qty = get_bin_qty(bundle_item.item_code, warehouse)
+ reserved_qty = get_pos_reserved_qty(bundle_item.item_code, warehouse)
+ available = bin_qty - reserved_qty
+ availabilities.append(
+ {
+ "item_code": bundle_item.item_code,
+ "required": bundle_item.qty * item_qty,
+ "available": available,
+ }
+ )
+
+ return availabilities, is_stock_item, is_negative_stock_allowed(item_code=item_code)
+
+
def get_bundle_availability(bundle_item_code, warehouse):
product_bundle = frappe.get_doc("Product Bundle", bundle_item_code)