diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py index f8516d6932d..aac18100263 100644 --- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py @@ -217,6 +217,7 @@ class POSInvoice(SalesInvoice): self.validate_loyalty_transaction() self.validate_company_with_pos_company() self.validate_full_payment() + self.update_packing_list() if self.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): frappe.throw( - _( - "Row #{}: Stock quantity not enough for Item Code: {} under warehouse {}. Available quantity {}." - ).format(d.idx, item_code, warehouse, available_stock), + _("Row #{}: Stock quantity not enough for Item Code: {} under warehouse {}.").format( + d.idx, item_code, warehouse + ), title=_("Item Unavailable"), ) @@ -867,10 +868,8 @@ def get_bundle_availability(bundle_item_code, warehouse): bundle_bin_qty = 1000000 for item in product_bundle.items: 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( "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): + """ + 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_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 = ( frappe.qb.from_(p_inv) .from_(p_item) - .select(Sum(p_item.stock_qty).as_("stock_qty")) + .select(Sum(p_item[qty_column]).as_("stock_qty")) .where( (p_inv.name == p_item.parent) & (IfNull(p_inv.consolidated_invoice, "") == "")