Merge pull request #59088 from frappe/mergify/bp/version-16-hotfix/pr-58998

fix(stock): use stored posting_datetime when reposting stock ledger entries (backport #58998)
This commit is contained in:
Sudharsanan Ashok
2026-09-17 18:44:36 +05:30
committed by GitHub
2 changed files with 139 additions and 8 deletions

View File

@@ -1594,6 +1594,124 @@ class TestStockLedgerEntry(ERPNextTestSuite, StockTestMixin):
item_code=item_code, source=warehouse, qty=470.84, rate=100, posting_date=add_days(today(), -1) item_code=item_code, source=warehouse, qty=470.84, rate=100, posting_date=add_days(today(), -1)
) )
def test_repost_with_diverged_posting_datetime(self):
"""Reposting must use the stored posting_datetime, not one rebuilt from posting_date and posting_time."""
from erpnext.stock.stock_ledger import get_sle_against_current_voucher
raw = make_item().name
fg = make_item().name
warehouse = "_Test Warehouse - _TC"
clash_time = "09:04:25.784069"
make_stock_entry(
item_code=fg,
to_warehouse=warehouse,
qty=123,
rate=2,
posting_date="2026-08-20",
posting_time="10:00:00",
)
make_stock_entry(
item_code=fg,
to_warehouse=warehouse,
qty=500,
rate=2,
posting_date="2026-08-22",
posting_time="10:00:00",
)
make_stock_entry(
item_code=raw,
to_warehouse=warehouse,
qty=500,
rate=2,
posting_date="2026-08-21",
posting_time="10:00:00",
)
# issue posted first, then a repack producing the same item at the same posting time
make_stock_entry(
item_code=fg,
from_warehouse=warehouse,
qty=500,
posting_date="2026-08-24",
posting_time=clash_time,
)
repack = make_stock_entry(
item_code=raw, source=warehouse, qty=500, rate=2, purpose="Repack", do_not_save=True
)
repack.set_posting_time = 1
repack.posting_date = "2026-08-24"
repack.posting_time = clash_time
repack.append(
"items", {"item_code": fg, "t_warehouse": warehouse, "qty": 500, "conversion_factor": 1}
)
repack.save()
repack.submit()
make_stock_entry(
item_code=fg, from_warehouse=warehouse, qty=8, posting_date="2026-08-25", posting_time="10:00:00"
)
# mimic a repair that backdates the stored value and leaves posting_time untouched
incoming = frappe.db.get_value(
"Stock Ledger Entry",
{"voucher_no": repack.name, "item_code": fg, "actual_qty": (">", 0)},
["name", "posting_date", "posting_time", "creation"],
as_dict=True,
)
frappe.db.set_value(
"Stock Ledger Entry",
incoming.name,
"posting_datetime",
"2026-08-24 09:04:25.784068",
update_modified=False,
)
# the row must stay reachable through its own voucher
found = get_sle_against_current_voucher(
frappe._dict(
{
"item_code": fg,
"warehouse": warehouse,
"posting_date": incoming.posting_date,
"posting_time": incoming.posting_time,
"creation": incoming.creation,
"name": incoming.name,
}
)
)
self.assertEqual([d.name for d in found], [incoming.name])
riv = frappe.get_doc(
{
"doctype": "Repost Item Valuation",
"based_on": "Item and Warehouse",
"item_code": raw,
"warehouse": warehouse,
"posting_date": "2026-08-21",
"posting_time": "00:00:00",
"company": "_Test Company",
"allow_negative_stock": 1,
}
).insert()
riv.submit()
# the backdated row is replayed instead of contributing a stale balance
sle = frappe.qb.DocType("Stock Ledger Entry")
balances = (
frappe.qb.from_(sle)
.select(sle.qty_after_transaction)
.where((sle.item_code == fg) & (sle.warehouse == warehouse) & (sle.is_cancelled == 0))
.orderby(sle.posting_datetime)
.orderby(sle.creation)
).run(pluck=True)
self.assertEqual([flt(b) for b in balances], [123.0, 623.0, 1123.0, 623.0, 615.0])
self.assertEqual(
flt(frappe.db.get_value("Bin", {"item_code": fg, "warehouse": warehouse}, "actual_qty")), 615.0
)
def create_repack_entry(**args): def create_repack_entry(**args):
args = frappe._dict(args) args = frappe._dict(args)

View File

@@ -1825,7 +1825,15 @@ class update_entries_after:
def get_sle_against_current_voucher(kwargs): def get_sle_against_current_voucher(kwargs):
kwargs["posting_datetime"] = get_combine_datetime(kwargs.posting_date, kwargs.posting_time) # Match on the row's stored posting_datetime. Re-deriving it from posting_date and posting_time
# makes rows whose stored value differs unmatchable, so the voucher gets reposted against nothing.
if kwargs.get("name") and not kwargs.get("posting_datetime"):
kwargs["posting_datetime"] = frappe.db.get_value(
"Stock Ledger Entry", kwargs.get("name"), "posting_datetime"
)
if not kwargs.get("posting_datetime"):
kwargs["posting_datetime"] = get_combine_datetime(kwargs.posting_date, kwargs.posting_time)
doctype = frappe.qb.DocType("Stock Ledger Entry") doctype = frappe.qb.DocType("Stock Ledger Entry")
query = ( query = (
@@ -1958,14 +1966,19 @@ def get_stock_ledger_entries(
frappe.db.escape(f"%\n{serial_no}\n%"), frappe.db.escape(f"%\n{serial_no}\n%"),
) )
if not previous_sle.get("posting_date"): if not previous_sle.get("posting_datetime"):
previous_sle["posting_datetime"] = "1900-01-01 00:00:00" # Derive only when the caller has not supplied the stored posting_datetime. Re-deriving it
else: # would shift the boundary for rows whose stored value differs from posting_date + posting_time.
posting_time = previous_sle.get("posting_time") if not previous_sle.get("posting_date"):
if not posting_time: previous_sle["posting_datetime"] = "1900-01-01 00:00:00"
posting_time = "00:00:00" else:
posting_time = previous_sle.get("posting_time")
if not posting_time:
posting_time = "00:00:00"
previous_sle["posting_datetime"] = get_combine_datetime(previous_sle["posting_date"], posting_time) previous_sle["posting_datetime"] = get_combine_datetime(
previous_sle["posting_date"], posting_time
)
if operator in (">", "<=") and previous_sle.get("name"): if operator in (">", "<=") and previous_sle.get("name"):
conditions += " and name!=%(name)s" conditions += " and name!=%(name)s"