mirror of
https://github.com/frappe/erpnext.git
synced 2026-09-19 03:17:55 +00:00
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:
@@ -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)
|
||||
)
|
||||
|
||||
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):
|
||||
args = frappe._dict(args)
|
||||
|
||||
@@ -1825,7 +1825,15 @@ class update_entries_after:
|
||||
|
||||
|
||||
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")
|
||||
|
||||
query = (
|
||||
@@ -1958,14 +1966,19 @@ def get_stock_ledger_entries(
|
||||
frappe.db.escape(f"%\n{serial_no}\n%"),
|
||||
)
|
||||
|
||||
if not previous_sle.get("posting_date"):
|
||||
previous_sle["posting_datetime"] = "1900-01-01 00:00:00"
|
||||
else:
|
||||
posting_time = previous_sle.get("posting_time")
|
||||
if not posting_time:
|
||||
posting_time = "00:00:00"
|
||||
if not previous_sle.get("posting_datetime"):
|
||||
# Derive only when the caller has not supplied the stored posting_datetime. Re-deriving it
|
||||
# would shift the boundary for rows whose stored value differs from posting_date + posting_time.
|
||||
if not previous_sle.get("posting_date"):
|
||||
previous_sle["posting_datetime"] = "1900-01-01 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"):
|
||||
conditions += " and name!=%(name)s"
|
||||
|
||||
Reference in New Issue
Block a user