diff --git a/erpnext/stock/doctype/batch/batch.js b/erpnext/stock/doctype/batch/batch.js
index 77e4e560acf..a485f849639 100644
--- a/erpnext/stock/doctype/batch/batch.js
+++ b/erpnext/stock/doctype/batch/batch.js
@@ -54,7 +54,12 @@ frappe.ui.form.on("Batch", {
frappe.call({
method: "erpnext.stock.doctype.batch.batch.get_batch_qty",
- args: { batch_no: frm.doc.name, item_code: frm.doc.item, for_stock_levels: for_stock_levels },
+ args: {
+ batch_no: frm.doc.name,
+ item_code: frm.doc.item,
+ for_stock_levels: for_stock_levels,
+ consider_negative_batches: 1,
+ },
callback: (r) => {
if (!r.message) {
return;
@@ -71,7 +76,7 @@ frappe.ui.form.on("Batch", {
// show
(r.message || []).forEach(function (d) {
- if (d.qty > 0) {
+ if (d.qty != 0) {
$(`
${d.warehouse}
${d.qty}
diff --git a/erpnext/stock/doctype/batch/batch.py b/erpnext/stock/doctype/batch/batch.py
index 3882e5b2424..1e1e6c8ee26 100644
--- a/erpnext/stock/doctype/batch/batch.py
+++ b/erpnext/stock/doctype/batch/batch.py
@@ -218,6 +218,7 @@ def get_batch_qty(
posting_time=None,
ignore_voucher_nos=None,
for_stock_levels=False,
+ consider_negative_batches=False,
):
"""Returns batch actual qty if warehouse is passed,
or returns dict of qty by warehouse if warehouse is None
@@ -243,6 +244,7 @@ def get_batch_qty(
"batch_no": batch_no,
"ignore_voucher_nos": ignore_voucher_nos,
"for_stock_levels": for_stock_levels,
+ "consider_negative_batches": consider_negative_batches,
}
)
diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py
index 9e23270ca71..85c74480e7d 100644
--- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py
+++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py
@@ -139,8 +139,8 @@ class StockReconciliation(StockController):
"voucher_type": self.doctype,
"voucher_no": self.name,
"voucher_detail_no": row.name,
- "qty": row.current_qty,
- "type_of_transaction": "Outward",
+ "qty": row.current_qty * -1,
+ "type_of_transaction": "Outward" if row.current_qty > 0 else "Inward",
"company": self.company,
"is_rejected": 0,
"serial_nos": get_serial_nos(row.current_serial_no)
@@ -1367,6 +1367,7 @@ def get_stock_balance_for(
posting_date=posting_date,
posting_time=posting_time,
for_stock_levels=True,
+ consider_negative_batches=True,
)
or 0
)
diff --git a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py
index a3673063a48..48a27a25962 100644
--- a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py
+++ b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py
@@ -1330,6 +1330,84 @@ class TestStockReconciliation(FrappeTestCase, StockTestMixin):
self.assertEqual(stock_value_difference, 1500.00 * -1)
+ def test_stock_reco_for_negative_batch(self):
+ from erpnext.stock.doctype.batch.batch import get_batch_qty
+ from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
+
+ item_code = self.make_item(
+ "Test Item For Negative Batch",
+ {
+ "is_stock_item": 1,
+ "has_batch_no": 1,
+ "create_new_batch": 1,
+ "batch_number_series": "TEST-BATCH-NB-.###",
+ },
+ ).name
+
+ warehouse = "_Test Warehouse - _TC"
+
+ se = make_stock_entry(
+ posting_date="2024-11-01",
+ posting_time="11:00",
+ item_code=item_code,
+ target=warehouse,
+ qty=10,
+ basic_rate=100,
+ )
+
+ batch_no = get_batch_from_bundle(se.items[0].serial_and_batch_bundle)
+
+ se = make_stock_entry(
+ posting_date="2024-11-01",
+ posting_time="11:00",
+ item_code=item_code,
+ source=warehouse,
+ qty=10,
+ basic_rate=100,
+ use_serial_batch_fields=1,
+ batch_no=batch_no,
+ )
+
+ sles = frappe.get_all(
+ "Stock Ledger Entry",
+ filters={"voucher_no": se.name, "is_cancelled": 0},
+ )
+
+ # intentionally setting negative qty
+ doc = frappe.get_doc("Stock Ledger Entry", sles[0].name)
+ doc.db_set(
+ {
+ "actual_qty": -20,
+ "qty_after_transaction": -10,
+ }
+ )
+
+ sabb_doc = frappe.get_doc("Serial and Batch Bundle", doc.serial_and_batch_bundle)
+ for row in sabb_doc.entries:
+ row.db_set("qty", -20)
+
+ batch_qty = get_batch_qty(batch_no, warehouse, item_code, consider_negative_batches=True)
+ self.assertEqual(batch_qty, -10)
+
+ sr = create_stock_reconciliation(
+ posting_date="2024-11-02",
+ posting_time="11:00",
+ item_code=item_code,
+ warehouse=warehouse,
+ use_serial_batch_fields=1,
+ batch_no=batch_no,
+ qty=0,
+ rate=100,
+ do_not_submit=True,
+ )
+
+ self.assertEqual(sr.items[0].current_qty, -10)
+ sr.submit()
+ sr.reload()
+
+ self.assertTrue(sr.items[0].current_serial_and_batch_bundle)
+ self.assertFalse(sr.items[0].serial_and_batch_bundle)
+
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/serial_batch_bundle.py b/erpnext/stock/serial_batch_bundle.py
index 85adb0348d9..f4d862b583c 100644
--- a/erpnext/stock/serial_batch_bundle.py
+++ b/erpnext/stock/serial_batch_bundle.py
@@ -418,7 +418,13 @@ class SerialBatchBundle:
batches = frappe._dict({self.sle.batch_no: self.sle.actual_qty})
batches_qty = get_available_batches(
- frappe._dict({"item_code": self.item_code, "batch_no": list(batches.keys())})
+ frappe._dict(
+ {
+ "item_code": self.item_code,
+ "batch_no": list(batches.keys()),
+ "consider_negative_batches": 1,
+ }
+ )
)
for batch_no in batches: