From e79b4abe9d28d0bb6d7683bca150746911a4694f Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Fri, 14 Aug 2026 11:48:55 +0530 Subject: [PATCH] fix: ignore historical negative batch stock in outward validation (backport #58148) (backport #58151) (#58153) fix: ignore historical negative batch stock in outward validation (backport #58148) (#58151) fix: ignore historical negative batch stock in outward validation (#58148) (cherry picked from commit 9239d1c2a3f4d922f44c624425746519bf44c956) (cherry picked from commit c18881b37d5816cdb87ff4f1386500e27e1e145c) Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> Co-authored-by: rohitwaghchaure --- .../serial_and_batch_bundle.py | 5 +- .../test_serial_and_batch_bundle.py | 152 ++++++++++++++++++ 2 files changed, 156 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py index cd141849eed..fc19b46c133 100644 --- a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py +++ b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py @@ -1736,13 +1736,16 @@ class SerialandBatchBundle(Document): ) precision = frappe.get_precision("Serial and Batch Entry", "qty") + posting_datetime = get_datetime(self.posting_datetime) if self.posting_datetime else None for row in batchwise_entries: if row.batch_no in available_qty: available_qty[row.batch_no] += flt(row.qty) else: available_qty[row.batch_no] = flt(row.qty) - if flt(available_qty[row.batch_no], precision) < 0: + if flt(available_qty[row.batch_no], precision) < 0 and ( + not posting_datetime or get_datetime(row.posting_datetime) >= posting_datetime + ): self.throw_negative_batch( row.batch_no, available_qty[row.batch_no], precision, row.posting_datetime ) diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py b/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py index 2202987c83e..3380aaa7108 100644 --- a/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py +++ b/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py @@ -1419,6 +1419,158 @@ class TestSerialandBatchBundle(ERPNextTestSuite): batch_no="LSBRV-BATCH-0001", ) + def _setup_negative_batch_item(self, item_code, batches): + make_item(item_code, properties={"is_stock_item": 1, "has_batch_no": 1}) + for batch_no in batches: + if not frappe.db.exists("Batch", batch_no): + frappe.get_doc( + {"doctype": "Batch", "batch_id": batch_no, "item": item_code, "company": "_Test Company"} + ).insert(ignore_permissions=True) + + def _allow_negative_stock_temporarily(self): + for field in ("allow_negative_stock", "allow_negative_stock_for_batch"): + original = frappe.db.get_single_value("Stock Settings", field) + frappe.db.set_single_value("Stock Settings", field, 1) + self.addCleanup(frappe.db.set_single_value, "Stock Settings", field, original) + + def _disable_negative_stock(self): + frappe.db.set_single_value("Stock Settings", "allow_negative_stock", 0) + frappe.db.set_single_value("Stock Settings", "allow_negative_stock_for_batch", 0) + + def test_historical_negative_batch_stock_does_not_block_outward(self): + from unittest.mock import patch + + from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import ( + BatchNegativeStockError, + SerialandBatchBundle, + ) + + item_code = "Test Hist Neg Batch Item" + ballast_batch, batch_no = "THNB-BALLAST-001", "THNB-BATCH-001" + self._setup_negative_batch_item(item_code, [ballast_batch, batch_no]) + warehouse = "_Test Warehouse - _TC" + + self._allow_negative_stock_temporarily() + make_stock_entry( + item_code=item_code, + qty=1000, + rate=100, + target=warehouse, + use_serial_batch_fields=True, + batch_no=ballast_batch, + posting_date=add_days(today(), -730), + posting_time="10:00:00", + ) + make_stock_entry( + item_code=item_code, + qty=100, + rate=100, + target=warehouse, + use_serial_batch_fields=True, + batch_no=batch_no, + posting_date=add_days(today(), -365), + posting_time="10:00:00", + ) + with patch.object(SerialandBatchBundle, "validate_negative_batch"): + make_stock_entry( + item_code=item_code, + qty=5, + source=warehouse, + use_serial_batch_fields=True, + batch_no=batch_no, + posting_date=add_days(today(), -730), + posting_time="11:00:00", + ) + self._disable_negative_stock() + + make_stock_entry( + item_code=item_code, + qty=10, + source=warehouse, + use_serial_batch_fields=True, + batch_no=batch_no, + ) + + outward = make_stock_entry( + item_code=item_code, + qty=200, + source=warehouse, + use_serial_batch_fields=True, + batch_no=batch_no, + do_not_submit=True, + ) + self.assertRaises(BatchNegativeStockError, outward.submit) + + def test_backdated_outward_cannot_make_future_batch_stock_negative(self): + from erpnext.stock.stock_ledger import NegativeStockError + + item_code = "Test Future Neg Batch Item" + ballast_batch, batch_no = "TFNB-BALLAST-001", "TFNB-BATCH-001" + self._setup_negative_batch_item(item_code, [ballast_batch, batch_no]) + warehouse = "_Test Warehouse - _TC" + + make_stock_entry( + item_code=item_code, + qty=1000, + rate=100, + target=warehouse, + use_serial_batch_fields=True, + batch_no=ballast_batch, + posting_date=add_days(today(), -365), + posting_time="10:00:00", + ) + make_stock_entry( + item_code=item_code, + qty=100, + rate=100, + target=warehouse, + use_serial_batch_fields=True, + batch_no=batch_no, + posting_date=add_days(today(), -365), + posting_time="11:00:00", + ) + make_stock_entry( + item_code=item_code, + qty=90, + source=warehouse, + use_serial_batch_fields=True, + batch_no=batch_no, + posting_date=add_days(today(), -180), + posting_time="10:00:00", + ) + make_stock_entry( + item_code=item_code, + qty=60, + rate=100, + target=warehouse, + use_serial_batch_fields=True, + batch_no=batch_no, + posting_date=add_days(today(), -30), + posting_time="10:00:00", + ) + + make_stock_entry( + item_code=item_code, + qty=5, + source=warehouse, + use_serial_batch_fields=True, + batch_no=batch_no, + posting_date=add_days(today(), -240), + posting_time="10:00:00", + ) + + backdated = make_stock_entry( + item_code=item_code, + qty=50, + source=warehouse, + use_serial_batch_fields=True, + batch_no=batch_no, + posting_date=add_days(today(), -240), + posting_time="11:00:00", + do_not_submit=True, + ) + self.assertRaises(NegativeStockError, backdated.submit) + def get_batch_from_bundle(bundle): from erpnext.stock.serial_batch_bundle import get_batch_nos