diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py index 0d15bd75ad3..bb1a9b36214 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py @@ -282,11 +282,7 @@ class StockReconciliation(StockController): if has_serial_no: sl_entries = self.merge_similar_item_serial_nos(sl_entries) - allow_negative_stock = False - if has_batch_no: - allow_negative_stock = True - - self.make_sl_entries(sl_entries, allow_negative_stock=allow_negative_stock) + self.make_sl_entries(sl_entries, allow_negative_stock=self.has_negative_stock_allowed()) if has_serial_no and sl_entries: self.update_valuation_rate_for_serial_no() @@ -457,10 +453,7 @@ class StockReconciliation(StockController): sl_entries = self.merge_similar_item_serial_nos(sl_entries) sl_entries.reverse() - allow_negative_stock = cint( - frappe.db.get_single_value("Stock Settings", "allow_negative_stock") - ) - self.make_sl_entries(sl_entries, allow_negative_stock=allow_negative_stock) + self.make_sl_entries(sl_entries, allow_negative_stock=self.has_negative_stock_allowed()) def merge_similar_item_serial_nos(self, sl_entries): # If user has put the same item in multiple row with different serial no @@ -574,6 +567,7 @@ class StockReconciliation(StockController): from erpnext.stock.stock_ledger import get_valuation_rate sl_entries = [] + for row in self.items: if voucher_detail_no != row.name: continue @@ -619,10 +613,18 @@ class StockReconciliation(StockController): sl_entries.append(new_sle) if sl_entries: - self.make_sl_entries(sl_entries, allow_negative_stock=True) - if frappe.db.exists("Repost Item Valuation", {"voucher_no": self.name, "status": "Queued"}): + self.make_sl_entries(sl_entries, allow_negative_stock=self.has_negative_stock_allowed()) + if not frappe.db.exists("Repost Item Valuation", {"voucher_no": self.name, "status": "Queued"}): self.repost_future_sle_and_gle(force=True) + def has_negative_stock_allowed(self): + allow_negative_stock = cint(frappe.db.get_single_value("Stock Settings", "allow_negative_stock")) + + if all(d.batch_no and flt(d.qty) == flt(d.current_qty) for d in self.items): + allow_negative_stock = True + + return allow_negative_stock + def get_batch_qty_for_stock_reco( item_code, warehouse, batch_no, posting_date, posting_time, voucher_no diff --git a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py index 1d8b72cec9a..df6777bbe4c 100644 --- a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py @@ -769,8 +769,6 @@ class TestStockReconciliation(FrappeTestCase, StockTestMixin): self.assertEqual(flt(sle[0].qty_after_transaction), flt(50.0)) def test_backdated_stock_reco_entry_with_batch(self): - from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry - item_code = self.make_item( "Test New Batch Item ABCVSD", { @@ -868,6 +866,56 @@ class TestStockReconciliation(FrappeTestCase, StockTestMixin): sr1.load_from_db() self.assertEqual(sr1.difference_amount, 10000) + @change_settings("Stock Settings", {"allow_negative_stock": 0}) + def test_negative_stock_reco_for_batch(self): + from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry + + item_code = self.make_item( + "Test New Batch Item ABCVSD", + { + "is_stock_item": 1, + "has_batch_no": 1, + "batch_number_series": "BNS9.####", + "create_new_batch": 1, + }, + ).name + + warehouse = "_Test Warehouse - _TC" + + # Added 100 Qty, Balace Qty 100 + se = make_stock_entry( + item_code=item_code, + target=warehouse, + qty=100, + basic_rate=100, + posting_date=add_days(nowdate(), -2), + ) + + # Removed 100 Qty, Balace Qty 0 + make_stock_entry( + item_code=item_code, + source=warehouse, + qty=100, + batch_no=se.items[0].batch_no, + basic_rate=100, + posting_date=nowdate(), + ) + + # Remove 100 qty, Balace Qty -100 + sr = create_stock_reconciliation( + item_code=item_code, + warehouse=warehouse, + qty=0, + rate=0, + batch_no=se.items[0].batch_no, + posting_date=add_days(nowdate(), -1), + posting_time="11:00:00", + do_not_submit=True, + ) + + # Check if Negative Stock is blocked + self.assertRaises(frappe.ValidationError, sr.submit) + def create_batch_item_with_batch(item_name, batch_id): batch_item_doc = create_item(item_name, is_stock_item=1) @@ -891,7 +939,7 @@ def insert_existing_sle(warehouse, item_code="_Test Item"): posting_time="02:00", item_code=item_code, target=warehouse, - qty=10, + qty=15, basic_rate=700, ) diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 0c3056cc705..d8284af6047 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -635,7 +635,7 @@ class update_entries_after(object): def reset_actual_qty_for_stock_reco(self, sle): doc = frappe.get_cached_doc("Stock Reconciliation", sle.voucher_no) - doc.recalculate_current_qty(sle.voucher_detail_no, sle.creation, sle.actual_qty > 0) + doc.recalculate_current_qty(sle.voucher_detail_no, sle.creation, sle.actual_qty >= 0) if sle.actual_qty < 0: sle.actual_qty = ( @@ -643,9 +643,6 @@ class update_entries_after(object): * -1 ) - if abs(sle.actual_qty) == 0.0: - sle.is_cancelled = 1 - def validate_negative_stock(self, sle): """ validate negative stock for entries current datetime onwards