fix(stock): ignore future stock during batch reservation (#58303)

This commit is contained in:
Mihir Kandoi
2026-08-19 18:20:26 +05:30
committed by GitHub
parent cfca71782e
commit 478a2f4f4b
2 changed files with 82 additions and 3 deletions

View File

@@ -9,9 +9,9 @@ from frappe import _
from frappe.model.document import Document
from frappe.query_builder import Case
from frappe.query_builder.functions import Max, Min, Sum
from frappe.utils import cint, flt, nowdate, nowtime, parse_json
from frappe.utils import cint, flt, get_datetime, now_datetime, nowdate, nowtime, parse_json
from erpnext.stock.utils import get_or_make_bin, get_stock_balance
from erpnext.stock.utils import get_combine_datetime, get_or_make_bin, get_stock_balance
class StockReservationEntry(Document):
@@ -298,11 +298,13 @@ class StockReservationEntry(Document):
self.reservation_based_on = "Serial and Batch"
self.sb_entries.clear()
kwargs = frappe._dict(
{
"item_code": self.item_code,
"warehouse": self.warehouse,
"qty": abs(self.reserved_qty) or 0,
"posting_datetime": self.get_voucher_posting_datetime(),
"based_on": based_on
or frappe.get_single_value("Stock Settings", "pick_serial_and_batch_based_on"),
}
@@ -341,6 +343,37 @@ class StockReservationEntry(Document):
},
)
def get_voucher_posting_datetime(self):
reservation_datetime = now_datetime()
meta = frappe.get_meta(self.voucher_type)
if meta.has_field("posting_datetime"):
if posting_datetime := frappe.db.get_value(
self.voucher_type, self.voucher_no, "posting_datetime"
):
return min(get_datetime(posting_datetime), reservation_datetime)
for date_field, time_field in (
("posting_date", "posting_time"),
("transaction_date", "transaction_time"),
):
if not meta.has_field(date_field):
continue
fields = [date_field]
if meta.has_field(time_field):
fields.append(time_field)
values = frappe.db.get_value(self.voucher_type, self.voucher_no, fields, as_dict=True)
if not values or not values.get(date_field):
continue
posting_datetime = get_combine_datetime(
values.get(date_field), values.get(time_field) or "23:59:59.999999"
)
return min(posting_datetime, reservation_datetime)
return reservation_datetime
def validate_reservation_based_on_serial_and_batch(self) -> None:
"""Validates `Reserved Qty`, `Serial and Batch Nos` when `Reservation Based On` is `Serial and Batch`."""
if self.voucher_type in ["Work Order", "Subcontracting Order"]:

View File

@@ -4,7 +4,7 @@
from random import randint
import frappe
from frappe.utils import flt, today
from frappe.utils import add_days, flt, today
from erpnext.selling.doctype.sales_order.mapper import create_pick_list, make_delivery_note
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
@@ -717,6 +717,52 @@ class TestStockReservationEntry(ERPNextTestSuite):
# Test - 9: After Delivery Note cancellation, SB Entry Delivered Qty should be `0`.
self.assertEqual(sb_entry.delivered_qty, 0)
@ERPNextTestSuite.change_settings(
"Stock Settings",
{
"allow_negative_stock": 0,
"enable_stock_reservation": 1,
"auto_reserve_serial_and_batch": 1,
"pick_serial_and_batch_based_on": "LIFO",
},
)
def test_auto_reserve_batch_ignores_future_stock(self) -> None:
item = make_batch_item()
voucher_date = add_days(today(), -1)
available_batch = frappe.get_doc(doctype="Batch", item=item.name).insert().name
make_stock_entry(
item_code=item.name,
qty=1,
to_warehouse=self.warehouse,
rate=100,
batch_no=available_batch,
posting_date=voucher_date,
posting_time="23:59:00",
)
future_batch = frappe.get_doc(doctype="Batch", item=item.name).insert().name
make_stock_entry(
item_code=item.name,
qty=1,
to_warehouse=self.warehouse,
rate=100,
batch_no=future_batch,
posting_date=add_days(today(), 1),
posting_time="00:01:00",
)
so = make_sales_order(
item_code=item.name,
warehouse=self.warehouse,
qty=1,
transaction_date=voucher_date,
)
so.db_set("transaction_time", None)
so.create_stock_reservation_entries()
self.assertSetEqual(get_reserved_batch_nos(so.name), {available_batch})
@ERPNextTestSuite.change_settings(
"Stock Settings",
{