Merge pull request #49108 from LewisMojica/develop

fix(pos): include Product Bundle components in reserved qty to preven…
This commit is contained in:
Diptanil Saha
2025-08-14 18:47:26 +05:30
committed by GitHub

View File

@@ -217,6 +217,7 @@ class POSInvoice(SalesInvoice):
self.validate_loyalty_transaction() self.validate_loyalty_transaction()
self.validate_company_with_pos_company() self.validate_company_with_pos_company()
self.validate_full_payment() self.validate_full_payment()
self.update_packing_list()
if self.coupon_code: if self.coupon_code:
from erpnext.accounts.doctype.pricing_rule.utils import validate_coupon_code from erpnext.accounts.doctype.pricing_rule.utils import validate_coupon_code
@@ -410,9 +411,9 @@ class POSInvoice(SalesInvoice):
) )
elif is_stock_item and flt(available_stock) < flt(d.stock_qty): elif is_stock_item and flt(available_stock) < flt(d.stock_qty):
frappe.throw( frappe.throw(
_( _("Row #{}: Stock quantity not enough for Item Code: {} under warehouse {}.").format(
"Row #{}: Stock quantity not enough for Item Code: {} under warehouse {}. Available quantity {}." d.idx, item_code, warehouse
).format(d.idx, item_code, warehouse, available_stock), ),
title=_("Item Unavailable"), title=_("Item Unavailable"),
) )
@@ -867,10 +868,8 @@ def get_bundle_availability(bundle_item_code, warehouse):
bundle_bin_qty = 1000000 bundle_bin_qty = 1000000
for item in product_bundle.items: for item in product_bundle.items:
item_bin_qty = get_bin_qty(item.item_code, warehouse) item_bin_qty = get_bin_qty(item.item_code, warehouse)
item_pos_reserved_qty = get_pos_reserved_qty(item.item_code, warehouse)
available_qty = item_bin_qty - item_pos_reserved_qty
max_available_bundles = available_qty / item.qty max_available_bundles = item_bin_qty / item.qty
if bundle_bin_qty > max_available_bundles and frappe.get_value( if bundle_bin_qty > max_available_bundles and frappe.get_value(
"Item", item.item_code, "is_stock_item" "Item", item.item_code, "is_stock_item"
): ):
@@ -893,13 +892,49 @@ def get_bin_qty(item_code, warehouse):
def get_pos_reserved_qty(item_code, warehouse): def get_pos_reserved_qty(item_code, warehouse):
"""
Calculate total quantity reserved for the given item and warehouse.
Includes:
- Direct sales of the item in submitted POS Invoices
- Sales of the item as a component of a Product Bundle
Excludes consolidated invoices (already merged into Sales Invoices via
POS Closing Entry). Used to reflect near real-time availability in the
POS UI and to prevent overselling while multiple sessions may be active.
"""
pinv_item_reserved_qty = get_pos_reserved_qty_from_table("POS Invoice Item", item_code, warehouse)
packed_item_reserved_qty = get_pos_reserved_qty_from_table("Packed Item", item_code, warehouse)
reserved_qty = pinv_item_reserved_qty + packed_item_reserved_qty
return reserved_qty
def get_pos_reserved_qty_from_table(child_table, item_code, warehouse):
"""
Get the total reserved quantity for a given item in POS Invoices
from a specific child table.
Args:
child_table (str): Name of the child table to query
(e.g., "POS Invoice Item", "Packed Item").
item_code (str): The Item Code to filter by.
warehouse (str): The Warehouse to filter by.
Returns:
float: The total reserved quantity for the item in the given
warehouse from submitted, unconsolidated POS Invoices.
"""
p_inv = frappe.qb.DocType("POS Invoice") p_inv = frappe.qb.DocType("POS Invoice")
p_item = frappe.qb.DocType("POS Invoice Item") p_item = frappe.qb.DocType(child_table)
qty_column = "qty" if child_table == "Packed Item" else "stock_qty"
reserved_qty = ( reserved_qty = (
frappe.qb.from_(p_inv) frappe.qb.from_(p_inv)
.from_(p_item) .from_(p_item)
.select(Sum(p_item.stock_qty).as_("stock_qty")) .select(Sum(p_item[qty_column]).as_("stock_qty"))
.where( .where(
(p_inv.name == p_item.parent) (p_inv.name == p_item.parent)
& (IfNull(p_inv.consolidated_invoice, "") == "") & (IfNull(p_inv.consolidated_invoice, "") == "")