fix(stock): value stock entry rows as of the posting date (#59042)

This commit is contained in:
Mihir Kandoi
2026-09-15 12:11:21 +05:30
committed by GitHub
parent 198d40446f
commit f3dcc6180f
3 changed files with 137 additions and 24 deletions

View File

@@ -34,7 +34,7 @@ from erpnext.stock.get_item_details import (
get_default_cost_center,
)
from erpnext.stock.stock_ledger import get_previous_sle, get_valuation_rate
from erpnext.stock.utils import get_incoming_rate
from erpnext.stock.utils import get_combine_datetime, get_incoming_rate
from .services.disassemble import DisassembleStockEntry
from .services.manufacturing import (
@@ -736,6 +736,18 @@ class StockEntry(StockController, SubcontractingInwardController):
raise_error_if_no_rate=raise_error_if_no_rate,
batch_no=d.batch_no,
serial_and_batch_bundle=d.serial_and_batch_bundle,
posting_datetime=get_combine_datetime(self.posting_date, self.posting_time),
creation=self.first_sle_creation,
)
@property
def first_sle_creation(self):
"""Creation of this entry's earliest ledger entry, if it has posted any yet."""
return frappe.db.get_value(
"Stock Ledger Entry",
{"voucher_no": self.name, "voucher_type": self.doctype, "is_cancelled": 0},
"creation",
order_by="creation asc",
)
def _notify_zero_valuation_rate(self, items):

View File

@@ -56,6 +56,19 @@ def get_sle(**args):
)
def stock_entry_row(item_code, qty, **kwargs):
stock_uom = frappe.db.get_value("Item", item_code, "stock_uom")
return {
"item_code": item_code,
"qty": qty,
"transfer_qty": qty,
"uom": stock_uom,
"stock_uom": stock_uom,
"conversion_factor": 1,
**kwargs,
}
class TestStockEntry(ERPNextTestSuite):
def setUp(self):
self.load_test_records("Stock Entry")
@@ -1609,32 +1622,25 @@ class TestStockEntry(ERPNextTestSuite):
make_stock_entry(item_code=rm_item, target="_Test Warehouse - _TC", qty=10, basic_rate=100)
def row(item_code, qty, **kwargs):
stock_uom = frappe.db.get_value("Item", item_code, "stock_uom")
return {
"item_code": item_code,
"qty": qty,
"transfer_qty": qty,
"uom": stock_uom,
"stock_uom": stock_uom,
"conversion_factor": 1,
**kwargs,
}
entry = frappe.new_doc("Stock Entry")
entry.company = "_Test Company"
entry.purpose = "Manufacture"
entry.set_stock_entry_type()
entry.fg_completed_qty = 1
entry.append("items", row(rm_item, 10, s_warehouse="_Test Warehouse - _TC"))
entry.append("items", row(fg_item, 1, t_warehouse="_Test Warehouse 1 - _TC", is_finished_item=1))
entry.append("items", stock_entry_row(rm_item, 10, s_warehouse="_Test Warehouse - _TC"))
entry.append(
"items",
row(scrap_item, 2, t_warehouse="_Test Warehouse 1 - _TC", secondary_item_type="Scrap"),
stock_entry_row(fg_item, 1, t_warehouse="_Test Warehouse 1 - _TC", is_finished_item=1),
)
entry.append(
"items",
row(
stock_entry_row(
scrap_item, 2, t_warehouse="_Test Warehouse 1 - _TC", secondary_item_type="Scrap"
),
)
entry.append(
"items",
stock_entry_row(
manual_item,
1,
t_warehouse="_Test Warehouse 1 - _TC",
@@ -1663,6 +1669,88 @@ class TestStockEntry(ERPNextTestSuite):
manual_row.valuation_type = "% of Component Cost"
self.assertRaises(frappe.ValidationError, entry.save)
@ERPNextTestSuite.change_settings("Stock Reposting Settings", {"item_based_reposting": 0})
def test_repost_values_costed_out_row_as_of_posting_date(self):
from erpnext.stock.doctype.repost_item_valuation.repost_item_valuation import repost_sl_entries
fg_item = make_item(properties={"is_stock_item": 1}).name
rm_item = make_item(properties={"is_stock_item": 1, "valuation_method": "Moving Average"}).name
scrap_item = make_item(properties={"is_stock_item": 1}).name
make_stock_entry(
item_code=rm_item,
target="_Test Warehouse - _TC",
qty=10,
basic_rate=100,
posting_date=add_days(today(), -10),
)
make_stock_entry(
item_code=scrap_item,
target="_Test Warehouse 1 - _TC",
qty=5,
basic_rate=50,
posting_date=add_days(today(), -10),
)
entry = frappe.new_doc("Stock Entry")
entry.company = "_Test Company"
entry.purpose = "Manufacture"
entry.set_stock_entry_type()
entry.set_posting_time = 1
entry.posting_date = add_days(today(), -5)
entry.posting_time = "10:00:00"
entry.fg_completed_qty = 1
entry.append("items", stock_entry_row(rm_item, 10, s_warehouse="_Test Warehouse - _TC"))
entry.append(
"items",
stock_entry_row(fg_item, 1, t_warehouse="_Test Warehouse 1 - _TC", is_finished_item=1),
)
entry.append(
"items",
stock_entry_row(
scrap_item, 2, t_warehouse="_Test Warehouse 1 - _TC", secondary_item_type="Scrap"
),
)
entry.insert()
entry.submit()
self.assertEqual(entry.items[2].basic_rate, 50)
self.assertEqual(entry.items[1].basic_rate, 900)
make_stock_entry(
item_code=scrap_item,
target="_Test Warehouse 1 - _TC",
qty=10,
basic_rate=5000,
posting_date=add_days(today(), -1),
)
make_stock_entry(
item_code=scrap_item,
target="_Test Warehouse 1 - _TC",
qty=10,
basic_rate=9000,
posting_date=add_days(today(), -5),
posting_time="10:00:00",
)
backdated_receipt = make_stock_entry(
item_code=rm_item,
target="_Test Warehouse - _TC",
qty=10,
basic_rate=200,
posting_date=add_days(today(), -8),
)
repost = frappe.db.get_value(
"Repost Item Valuation", {"voucher_no": backdated_receipt.name, "docstatus": 1}, "name"
)
repost_sl_entries(frappe.get_doc("Repost Item Valuation", repost))
entry.load_from_db()
self.assertEqual(entry.items[2].basic_rate, 50)
self.assertEqual(entry.items[1].basic_rate, 1400)
def test_valuation_rate_lookup_without_voucher_no(self):
from erpnext.stock.stock_ledger import get_valuation_rate

View File

@@ -2155,6 +2155,15 @@ def get_sle_by_voucher_detail_no(voucher_detail_no):
)
def get_prior_ledger_condition(table, posting_datetime, creation):
"""Restrict a ledger lookup to the entries that precede a voucher in ledger order."""
if creation:
return (table.posting_datetime < posting_datetime) | (
(table.posting_datetime == posting_datetime) & (table.creation < creation)
)
return table.posting_datetime <= posting_datetime
def get_valuation_rate(
item_code,
warehouse,
@@ -2167,6 +2176,8 @@ def get_valuation_rate(
raise_error_if_no_rate=True,
batch_no=None,
serial_and_batch_bundle=None,
posting_datetime=None,
creation=None,
):
from erpnext.stock.serial_batch_bundle import BatchNoValuation
@@ -2189,6 +2200,9 @@ def get_valuation_rate(
# Comparing against a None voucher_no yields NULL, which filters out every row
query = query.where((table.voucher_no != voucher_no) | (table.voucher_type != voucher_type))
if posting_datetime:
query = query.where(get_prior_ledger_condition(table, posting_datetime, creation))
last_valuation_rate = query.run()
if last_valuation_rate and last_valuation_rate[0][0] is not None:
return flt(last_valuation_rate[0][0])
@@ -2236,6 +2250,11 @@ def get_valuation_rate(
~((sle_entry.voucher_no == voucher_no) & (sle_entry.voucher_type == voucher_type))
)
if posting_datetime:
last_sle_query = last_sle_query.where(
get_prior_ledger_condition(sle_entry, posting_datetime, creation)
)
if last_valuation_rate := last_sle_query.run():
return flt(last_valuation_rate[0][0])
@@ -2671,13 +2690,7 @@ def get_stock_value_difference(
elif voucher_no:
query = query.where(table.voucher_no != voucher_no)
if creation:
query = query.where(
(table.posting_datetime < posting_datetime)
| ((table.posting_datetime == posting_datetime) & (table.creation < creation))
)
else:
query = query.where(table.posting_datetime <= posting_datetime)
query = query.where(get_prior_ledger_condition(table, posting_datetime, creation))
difference_amount = query.run()
return flt(difference_amount[0][0]) if difference_amount else 0