diff --git a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py index d1e5c5d345f..c913af3301a 100644 --- a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py @@ -956,6 +956,58 @@ class TestStockReconciliation(FrappeTestCase, StockTestMixin): self.assertRaises(frappe.ValidationError, sr.save) + @change_settings("Stock Settings", {"allow_negative_stock": 0}) + def test_backdated_stock_reco_for_batch_item_dont_have_future_sle(self): + # Step - 1: Create a Batch Item + from erpnext.stock.doctype.item.test_item import make_item + + item = make_item( + properties={ + "is_stock_item": 1, + "has_batch_no": 1, + "create_new_batch": 1, + "batch_number_series": "TEST-BATCH-.###", + } + ).name + + # Step - 2: Create Opening Stock Reconciliation + sr1 = create_stock_reconciliation( + item_code=item, + warehouse="_Test Warehouse - _TC", + qty=10, + purpose="Opening Stock", + posting_date=add_days(nowdate(), -2), + ) + + # Step - 3: Create Stock Entry (Material Receipt) + from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry + + se1 = make_stock_entry( + item_code=item, + target="_Test Warehouse - _TC", + qty=100, + ) + + # Step - 4: Create Stock Entry (Material Issue) + make_stock_entry( + item_code=item, + source="_Test Warehouse - _TC", + qty=100, + batch_no=se1.items[0].batch_no, + purpose="Material Issue", + ) + + # Step - 5: Create Stock Reconciliation (Backdated) after the Stock Reconciliation 1 (Step - 2) + sr2 = create_stock_reconciliation( + item_code=item, + warehouse="_Test Warehouse - _TC", + qty=5, + batch_no=sr1.items[0].batch_no, + posting_date=add_days(nowdate(), -1), + ) + + self.assertEqual(sr2.docstatus, 1) + def create_batch_item_with_batch(item_name, batch_id): batch_item_doc = create_item(item_name, is_stock_item=1) diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 84bcb99f738..bdae87c7ee7 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -1617,27 +1617,33 @@ def is_negative_with_precision(neg_sle, is_batch=False): return qty_deficit < 0 and abs(qty_deficit) > 0.0001 -def get_future_sle_with_negative_qty(args): - return frappe.db.sql( - """ - select - qty_after_transaction, posting_date, posting_time, - voucher_type, voucher_no - from `tabStock Ledger Entry` - where - item_code = %(item_code)s - and warehouse = %(warehouse)s - and voucher_no != %(voucher_no)s - and timestamp(posting_date, posting_time) >= timestamp(%(posting_date)s, %(posting_time)s) - and is_cancelled = 0 - and qty_after_transaction < 0 - order by timestamp(posting_date, posting_time) asc - limit 1 - """, - args, - as_dict=1, +def get_future_sle_with_negative_qty(sle): + SLE = frappe.qb.DocType("Stock Ledger Entry") + query = ( + frappe.qb.from_(SLE) + .select( + SLE.qty_after_transaction, SLE.posting_date, SLE.posting_time, SLE.voucher_type, SLE.voucher_no + ) + .where( + (SLE.item_code == sle.item_code) + & (SLE.warehouse == sle.warehouse) + & (SLE.voucher_no != sle.voucher_no) + & ( + CombineDatetime(SLE.posting_date, SLE.posting_time) + >= CombineDatetime(sle.posting_date, sle.posting_time) + ) + & (SLE.is_cancelled == 0) + & (SLE.qty_after_transaction < 0) + ) + .orderby(CombineDatetime(SLE.posting_date, SLE.posting_time)) + .limit(1) ) + if sle.voucher_type == "Stock Reconciliation" and sle.batch_no: + query = query.where(SLE.batch_no == sle.batch_no) + + return query.run(as_dict=True) + def get_future_sle_with_negative_batch_qty(args): return frappe.db.sql(