diff --git a/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py b/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py index 6aea0ef5337..d917d5914bf 100644 --- a/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py +++ b/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py @@ -1357,6 +1357,81 @@ class TestStockLedgerEntry(FrappeTestCase, StockTestMixin): # receipt2 now sits on a zero base -> 10 (not 0 from a double shift, nor a negative-stock error). self.assertEqual(qty_after(receipt2), 10) + def test_cancel_shifts_same_timestamp_delivery_notes(self): + item = make_item("Test Shifts Same Timestamp Multiple DNs").name + warehouse = "_Test Warehouse - _TC" + posting_date = today() + posting_time = "10:00:00" + + make_stock_entry( + item_code=item, + to_warehouse=warehouse, + qty=100, + rate=10, + posting_date=posting_date, + posting_time="09:00:00", + ) + + dns = [] + for i in range(5): + dns.append( + create_delivery_note( + item_code=item, + warehouse=warehouse, + qty=20, + rate=10 * i, + posting_date=posting_date, + posting_time=posting_time, + ) + ) + time.sleep(1) + + dn = dns[2] + dn.cancel() + + expected_qty_after_transaction_of_dns3 = 40 + qty_after_transaction_of_dns3 = frappe.db.get_value( + "Stock Ledger Entry", + {"voucher_no": dns[3].name, "is_cancelled": 0}, + "qty_after_transaction", + ) + + self.assertEqual(expected_qty_after_transaction_of_dns3, qty_after_transaction_of_dns3) + + def test_cancel_updates_bin_stock_value_when_no_sle_shares_timestamp(self): + item = make_item("Test Cancel Bin Stock Value Lone Timestamp").name + warehouse = "_Test Warehouse - _TC" + posting_date = today() + + make_stock_entry( + item_code=item, + to_warehouse=warehouse, + qty=100, + rate=10, + posting_date=posting_date, + posting_time="09:00:00", + ) + + dn = create_delivery_note( + item_code=item, + warehouse=warehouse, + qty=20, + rate=10, + posting_date=posting_date, + posting_time="10:00:00", + ) + + dn.cancel() + + # Nothing else sits on the delivery note's posting datetime, so the cancellation leaves no + # live SLE to reprocess. The bin must still fall back to the receipt's stock value. + bin_qty, bin_stock_value = frappe.db.get_value( + "Bin", {"item_code": item, "warehouse": warehouse}, ["actual_qty", "stock_value"] + ) + + self.assertEqual(bin_qty, 100) + self.assertEqual(bin_stock_value, 1000) + def test_get_next_stock_reco_respects_creation_order(self): # A stock reco sharing the exact posting timestamp of the current entry must only count as the # "next" reco when it was created after that entry. A reco created before it actually precedes diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index ddaaf702c86..a2cce420e3a 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -799,9 +799,18 @@ class update_entries_after: def process_sle_against_current_timestamp(self): sl_entries = get_sle_against_current_voucher(self.args) + if self.args.get("cancelled") and sl_entries: + self.seed_previous_sle_for_cancellation(sl_entries[0]) for sle in sl_entries: self.process_sle(sle) + def seed_previous_sle_for_cancellation(self, anchor_sle): + args = frappe._dict(anchor_sle) + args["sle_id"] = args.name + prev_sle = get_previous_sle_of_current_voucher(args) + if prev_sle: + self.prev_sle_dict[(anchor_sle.item_code, anchor_sle.warehouse)] = prev_sle + def get_future_entries_to_fix(self): # includes current entry! args = self.data[self.args.warehouse].previous_sle or frappe._dict(