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: