diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index e9386db73c07454d8eb747f686bf2ea74d0b8a09..d93c4a73fbfa608b8de77f119ba71634a803b415 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -1070,7 +1070,9 @@ class StockController(AccountsController): from erpnext.stock.stock_ledger import make_sl_entries make_sl_entries(sl_entries, allow_negative_stock, via_landed_cost_voucher) - update_batch_qty(self.doctype, self.name, via_landed_cost_voucher=via_landed_cost_voucher) + update_batch_qty( + self.doctype, self.name, self.docstatus, via_landed_cost_voucher=via_landed_cost_voucher + ) def make_gl_entries_on_cancel(self, from_repost=False): if not from_repost: 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 a770ac9544583c5ddfc146dc0c847a95c14f614a..beefb990533178690eee6a0a035207a671b57c14 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 @@ -222,7 +222,12 @@ class TestSerialandBatchBundle(IntegrationTestCase): ).insert(ignore_permissions=True) self.assertTrue(batch_doc.use_batchwise_valuation) - batch_doc.db_set("use_batchwise_valuation", 0) + batch_doc.db_set( + { + "use_batchwise_valuation": 0, + "batch_qty": 30, + } + ) stock_queue = [] qty_after_transaction = 0 diff --git a/erpnext/stock/serial_batch_bundle.py b/erpnext/stock/serial_batch_bundle.py index 77253d98f72b566069674db8f9c6631d22eef476..5bdc0ba4223c078230cc4feadbdb58b5386f4a2b 100644 --- a/erpnext/stock/serial_batch_bundle.py +++ b/erpnext/stock/serial_batch_bundle.py @@ -1378,40 +1378,40 @@ def get_serial_nos_batch(serial_nos): ) -def update_batch_qty(voucher_type, voucher_no, via_landed_cost_voucher=False): - from erpnext.stock.doctype.batch.batch import get_available_batches - - batches = get_distinct_batches(voucher_type, voucher_no) +def update_batch_qty(voucher_type, voucher_no, docstatus, via_landed_cost_voucher=False): + batches = get_batchwise_qty(voucher_type, voucher_no) if not batches: return precision = frappe.get_precision("Batch", "batch_qty") - batch_data = get_available_batches( - frappe._dict({"batch_no": batches, "consider_negative_batches": 1, "based_on_warehouse": True}) - ) - batchwise_qty = defaultdict(float) + for batch, qty in batches.items(): + current_qty = get_batch_current_qty(batch) + current_qty += flt(qty, precision) * (-1 if docstatus == 2 else 1) + + if not via_landed_cost_voucher and current_qty < 0: + throw_negative_batch_validation(batch, current_qty) - for (batch_no, warehouse), qty in batch_data.items(): - if not via_landed_cost_voucher and flt(qty, precision) < 0: - throw_negative_batch_validation(batch_no, warehouse, qty) + frappe.db.set_value("Batch", batch, "batch_qty", current_qty) - batchwise_qty[batch_no] += qty - for batch_no in batches: - qty = flt(batchwise_qty.get(batch_no, 0), precision) - frappe.db.set_value("Batch", batch_no, "batch_qty", qty) +def get_batch_current_qty(batch): + doctype = frappe.qb.DocType("Batch") + query = frappe.qb.from_(doctype).select(doctype.batch_qty).where(doctype.name == batch).for_update() + batch_qty = query.run() + return flt(batch_qty[0][0]) if batch_qty else 0.0 -def throw_negative_batch_validation(batch_no, warehouse, qty): + +def throw_negative_batch_validation(batch_no, qty): frappe.throw( - _("The Batch {0} has negative quantity {1} in warehouse {2}. Please correct the quantity.").format( - bold(batch_no), bold(qty), bold(warehouse) + _("The Batch {0} has negative quantity {1}. Please correct the quantity.").format( + bold(batch_no), bold(qty) ), title=_("Negative Batch Quantity"), ) -def get_distinct_batches(voucher_type, voucher_no): +def get_batchwise_qty(voucher_type, voucher_no): bundles = frappe.get_all( "Serial and Batch Bundle", filters={"voucher_no": voucher_no, "voucher_type": voucher_type}, @@ -1420,9 +1420,15 @@ def get_distinct_batches(voucher_type, voucher_no): if not bundles: return - return frappe.get_all( + batches = frappe.get_all( "Serial and Batch Entry", filters={"parent": ("in", bundles), "batch_no": ("is", "set")}, + fields=["batch_no", "SUM(qty) as qty"], group_by="batch_no", - pluck="batch_no", + as_list=1, ) + + if not batches: + return frappe._dict({}) + + return frappe._dict(batches)