diff --git a/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py b/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py index 12bc74fe5b4..4fa2b48e1ae 100644 --- a/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py +++ b/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py @@ -38,6 +38,7 @@ def execute(filters=None): columns = get_columns(filters) item_map = get_item_details(filters) iwb_map = get_item_warehouse_batch_map(filters, float_precision) + reserved_stock = get_reserved_stock(filters, iwb_map) data = [] for item in sorted(iwb_map): @@ -63,6 +64,7 @@ def execute(filters=None): ), flt(qty_dict.bal_value, float_precision), item_map[item]["stock_uom"], + flt(reserved_stock.get((item, wh, batch), 0), float_precision), ] ) @@ -85,11 +87,48 @@ def get_columns(filters): _("Valuation Rate") + ":Float:120", _("Balance Value") + ":Currency:120", _("UOM") + "::90", + _("Reserved Stock (Current)") + ":Float:175", ] return columns +def get_reserved_stock(filters, iwb_map): + if not iwb_map: + return {} + + sre = frappe.qb.DocType("Stock Reservation Entry") + sb_entry = frappe.qb.DocType("Serial and Batch Entry") + warehouses = {warehouse for item in iwb_map.values() for warehouse in item} + query = ( + frappe.qb.from_(sre) + .inner_join(sb_entry) + .on(sre.name == sb_entry.parent) + .select( + sre.item_code, + sre.warehouse, + sb_entry.batch_no, + fn.Sum(sb_entry.qty - sb_entry.delivered_qty).as_("reserved_qty"), + ) + .where( + (sre.docstatus == 1) + & (sre.reservation_based_on == "Serial and Batch") + & (sre.status.notin(["Closed", "Delivered"])) + & (sre.item_code.isin(list(iwb_map))) + & (sre.warehouse.isin(warehouses)) + & (sb_entry.batch_no.isnotnull()) + & (sb_entry.qty > sb_entry.delivered_qty) + ) + .groupby(sre.item_code, sre.warehouse, sb_entry.batch_no) + ) + if filters.get("company"): + query = query.where(sre.company == filters.company) + if filters.get("batch_no"): + query = query.where(sb_entry.batch_no == filters.batch_no) + + return {(row.item_code, row.warehouse, row.batch_no): row.reserved_qty for row in query.run(as_dict=True)} + + def get_stock_ledger_entries(filters): entries = [] diff --git a/erpnext/stock/report/batch_wise_balance_history/test_batch_wise_balance_history.py b/erpnext/stock/report/batch_wise_balance_history/test_batch_wise_balance_history.py index d2064f5f628..e04fbe67c9c 100644 --- a/erpnext/stock/report/batch_wise_balance_history/test_batch_wise_balance_history.py +++ b/erpnext/stock/report/batch_wise_balance_history/test_batch_wise_balance_history.py @@ -2,14 +2,18 @@ # See license.txt import frappe +from frappe.utils import today +from erpnext.selling.doctype.sales_order.mapper import make_delivery_note +from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order 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.stock.report.batch_wise_balance_history.batch_wise_balance_history import execute from erpnext.tests.utils import ERPNextTestSuite WH = "Stores - _TC" -# row indexes: 0 item, 1 name, 2 desc, 3 wh, 4 batch, 5 opening, 6 in, 7 out, 8 bal, 9 rate, 10 value, 11 uom +# row indexes: 0 item, 1 name, 2 desc, 3 wh, 4 batch, 5 opening, 6 in, 7 out, 8 bal, +# 9 rate, 10 value, 11 uom, 12 reserved stock class TestBatchWiseBalanceHistory(ERPNextTestSuite): @@ -50,3 +54,36 @@ class TestBatchWiseBalanceHistory(ERPNextTestSuite): self.assertEqual(row[5], 8) # opening carried from 2025 self.assertEqual(row[6], 0) self.assertEqual(row[8], 8) # balance + + @ERPNextTestSuite.change_settings( + "Stock Settings", + { + "enable_stock_reservation": 1, + "auto_reserve_stock": 0, + "auto_reserve_serial_and_batch": 1, + "use_serial_batch_fields": 1, + }, + ) + def test_reserved_stock_after_delivery(self): + posting_date = today() + report_dates = {"from_date": posting_date, "to_date": posting_date} + item = self.make_batch_item() + make_stock_entry(item_code=item, to_warehouse=WH, qty=10, rate=100, posting_date=posting_date) + (row,) = self.run_report(item, **report_dates) + batch = row[4] + self.assertEqual(row[12], 0) + + order = make_sales_order(item_code=item, warehouse=WH, qty=6, transaction_date=posting_date) + order.create_stock_reservation_entries() + self.assertEqual(self.run_report(item, **report_dates)[0][12], 6) + + for delivered_qty, expected_reserved_qty in [(2, 4), (4, 0)]: + delivery = make_delivery_note(order.name) + delivery.set_posting_time = 1 + delivery.posting_date = posting_date + delivery.items[0].qty = delivered_qty + delivery.items[0].use_serial_batch_fields = 1 + delivery.items[0].batch_no = batch + delivery.items[0].serial_and_batch_bundle = None + delivery.insert().submit() + self.assertEqual(self.run_report(item, **report_dates)[0][12], expected_reserved_qty)