Compare commits

..

2 Commits

Author SHA1 Message Date
Mihir Kandoi
27a48a7ab8 Merge pull request #57347 from mihir-kandoi/fix-neg-stock-cancel-replay-v15
fix: seed cancelled voucher replay from before its posting datetime (v15)
2026-07-22 13:09:42 +05:30
Mihir Kandoi
1ff297e9da fix: seed cancelled voucher replay from before its posting datetime
On cancel, update_entries_after replays every live SLE at the voucher's
posting datetime, but get_previous_sle_of_current_voucher seeded the
replay with the reversal SLE's creation, which resolves to the bucket's
own closing row. The bucket's net qty got double-counted into every
same-datetime row, so later submissions passed negative stock validation
against inflated balances, and the queued repost then rewrote correct
values with allow_negative_stock forced on, silently creating negative
stock. Backports the missing guard from eca71dce54.
2026-07-22 12:51:39 +05:30
3 changed files with 46 additions and 2 deletions

View File

@@ -4,7 +4,7 @@ import inspect
import frappe
from frappe.utils.user import is_website_user
__version__ = "15.118.0"
__version__ = "15.112.0"
def get_default_company(user=None):

View File

@@ -2751,6 +2751,50 @@ class TestStockEntry(FrappeTestCase):
self.assertEqual(se.process_loss_qty, 50)
@change_settings("Stock Settings", {"allow_negative_stock": 0})
def test_cancel_seeds_replay_from_before_posting_datetime_bucket(self):
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
item = make_item().name
warehouse = create_warehouse("Cancel Replay Warehouse", company="_Test Company")
def stock_entry(qty, posting_date, **kwargs):
return make_stock_entry(
item_code=item,
qty=qty,
company="_Test Company",
posting_date=posting_date,
posting_time="10:00:00",
**kwargs,
)
stock_entry(25, add_days(today(), -3), to_warehouse=warehouse, rate=10)
bucket_date = add_days(today(), -2)
stock_entry(20, bucket_date, from_warehouse=warehouse)
receipt = stock_entry(75, bucket_date, to_warehouse=warehouse, rate=10)
duplicate_issue = stock_entry(20, bucket_date, from_warehouse=warehouse)
frappe.flags.dont_execute_stock_reposts = True
self.addCleanup(frappe.flags.pop, "dont_execute_stock_reposts")
duplicate_issue.reload()
duplicate_issue.cancel()
self.assertEqual(
flt(
frappe.db.get_value(
"Stock Ledger Entry",
{"voucher_no": receipt.name, "is_cancelled": 0},
"qty_after_transaction",
)
),
80,
)
overdraw = stock_entry(100, add_days(today(), -1), from_warehouse=warehouse, do_not_submit=True)
self.assertRaises(NegativeStockError, overdraw.submit)
def make_serialized_item(**args):
args = frappe._dict(args)

View File

@@ -1762,7 +1762,7 @@ def get_previous_sle_of_current_voucher(args, operator="<", exclude_current_vouc
voucher_no = args.get("voucher_no")
voucher_condition = f"and voucher_no != '{voucher_no}'"
elif args.get("creation") and args.get("sle_id"):
elif args.get("creation") and args.get("sle_id") and not args.get("cancelled"):
creation = args.get("creation")
operator = "<="
voucher_condition = f"and creation < '{creation}'"