diff --git a/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py b/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py index 5b5356433d9..e7abb0be3ea 100644 --- a/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py +++ b/erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py @@ -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"]: diff --git a/erpnext/stock/doctype/stock_reservation_entry/test_stock_reservation_entry.py b/erpnext/stock/doctype/stock_reservation_entry/test_stock_reservation_entry.py index b4dbd759ec4..1c863852b7a 100644 --- a/erpnext/stock/doctype/stock_reservation_entry/test_stock_reservation_entry.py +++ b/erpnext/stock/doctype/stock_reservation_entry/test_stock_reservation_entry.py @@ -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", {