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 0ff773cd6e7..0491498b1b2 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 @@ -1344,6 +1344,47 @@ class TestStockLedgerEntry(ERPNextTestSuite, 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().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_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 7f6cc176373..23e99899068 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -497,7 +497,9 @@ class update_entries_after: self.data = frappe._dict() - if not self.repost_doc or not self.args.get("item_wh_wise_last_posted_sle"): + if (not self.repost_doc or not self.args.get("item_wh_wise_last_posted_sle")) and not self.args.get( + "cancelled" + ): self.initialize_previous_data(self.args) self.build() @@ -805,10 +807,23 @@ 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: sle["timestamp"] = sle.posting_datetime self.process_sle(sle) + def seed_previous_sle_for_cancellation(self, anchor_sle): + key = (anchor_sle.item_code, anchor_sle.warehouse) + if key in self.prev_sle_dict: + return + + 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[key] = prev_sle + def get_future_entries_to_fix(self): # includes current entry! args = self.data[self.args.warehouse].previous_sle or frappe._dict(