diff --git a/erpnext/accounts/doctype/pos_closing_entry/test_pos_closing_entry.py b/erpnext/accounts/doctype/pos_closing_entry/test_pos_closing_entry.py index 7ce87af95c5..71ffafa9020 100644 --- a/erpnext/accounts/doctype/pos_closing_entry/test_pos_closing_entry.py +++ b/erpnext/accounts/doctype/pos_closing_entry/test_pos_closing_entry.py @@ -238,9 +238,6 @@ class TestPOSClosingEntry(IntegrationTestCase): pos_inv2.payments[0].amount = pos_inv2.grand_total pos_inv2.submit() - batch_qty = frappe.db.get_value("Batch", batch_no, "batch_qty") - self.assertEqual(batch_qty, 10) - batch_qty_with_pos = get_batch_qty(batch_no, "_Test Warehouse - _TC", item_code) self.assertEqual(batch_qty_with_pos, 0.0) @@ -270,9 +267,6 @@ class TestPOSClosingEntry(IntegrationTestCase): pcv_doc.reload() pcv_doc.cancel() - batch_qty = frappe.db.get_value("Batch", batch_no, "batch_qty") - self.assertEqual(batch_qty, 10) - batch_qty_with_pos = get_batch_qty(batch_no, "_Test Warehouse - _TC", item_code) self.assertEqual(batch_qty_with_pos, 0.0) diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index 44f0ab21514..7b435e43732 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -924,9 +924,11 @@ class StockController(AccountsController): row.db_set(dimension.source_fieldname, sl_dict[dimension.target_fieldname]) def make_sl_entries(self, sl_entries, allow_negative_stock=False, via_landed_cost_voucher=False): + from erpnext.stock.serial_batch_bundle import update_batch_qty 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) def make_gl_entries_on_cancel(self): cancel_exchange_gain_loss_journal(frappe._dict(doctype=self.doctype, name=self.name)) diff --git a/erpnext/stock/doctype/batch/batch.py b/erpnext/stock/doctype/batch/batch.py index 1e1e6c8ee26..ae77672b730 100644 --- a/erpnext/stock/doctype/batch/batch.py +++ b/erpnext/stock/doctype/batch/batch.py @@ -455,10 +455,14 @@ def get_available_batches(kwargs): batches = get_auto_batch_nos(kwargs) for batch in batches: - if batch.get("batch_no") not in batchwise_qty: - batchwise_qty[batch.get("batch_no")] = batch.get("qty") + key = batch.get("batch_no") + if kwargs.get("based_on_warehouse"): + key = (batch.get("batch_no"), batch.get("warehouse")) + + if key not in batchwise_qty: + batchwise_qty[key] = batch.get("qty") else: - batchwise_qty[batch.get("batch_no")] += batch.get("qty") + batchwise_qty[key] += batch.get("qty") return batchwise_qty 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 8dc9d5d95b9..63cabc1eda3 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 @@ -101,7 +101,10 @@ class SerialandBatchBundle(Document): self.set_is_outward() self.calculate_total_qty() self.set_warehouse() - self.set_incoming_rate() + + if self.voucher_type != "Stock Entry" or not self.voucher_no or self.docstatus == 1: + self.set_incoming_rate() + self.calculate_qty_and_amount() def allow_existing_serial_nos(self): @@ -1026,7 +1029,6 @@ class SerialandBatchBundle(Document): self.set_purchase_document_no() def on_submit(self): - self.validate_batch_inventory() self.validate_serial_nos_inventory() def set_purchase_document_no(self): @@ -1053,25 +1055,9 @@ class SerialandBatchBundle(Document): self.validate_batch_inventory() def validate_batch_inventory(self): - if ( - self.voucher_type in ["Purchase Invoice", "Purchase Receipt"] - and frappe.db.get_value(self.voucher_type, self.voucher_no, "docstatus") == 1 - ): - return - - if self.voucher_type in ["Sales Invoice", "Delivery Note"] and self.type_of_transaction == "Inward": - return - if not self.has_batch_no: return - if ( - self.voucher_type == "Stock Reconciliation" - and self.type_of_transaction == "Outward" - and frappe.db.get_value("Stock Reconciliation Item", self.voucher_detail_no, "qty") > 0 - ): - return - batches = [d.batch_no for d in self.entries if d.batch_no] if not batches: return diff --git a/erpnext/stock/serial_batch_bundle.py b/erpnext/stock/serial_batch_bundle.py index f4d862b583c..c6089284bee 100644 --- a/erpnext/stock/serial_batch_bundle.py +++ b/erpnext/stock/serial_batch_bundle.py @@ -302,9 +302,6 @@ class SerialBatchBundle: ): self.set_batch_no_in_serial_nos() - if self.item_details.has_batch_no == 1: - self.update_batch_qty() - if self.sle.is_cancelled and self.sle.serial_and_batch_bundle: self.cancel_serial_and_batch_bundle() @@ -410,26 +407,6 @@ class SerialBatchBundle: .where(sn_table.name.isin(serial_nos)) ).run() - def update_batch_qty(self): - from erpnext.stock.doctype.batch.batch import get_available_batches - - batches = get_batch_nos(self.sle.serial_and_batch_bundle) - if not self.sle.serial_and_batch_bundle and self.sle.batch_no: - 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()), - "consider_negative_batches": 1, - } - ) - ) - - for batch_no in batches: - frappe.db.set_value("Batch", batch_no, "batch_qty", batches_qty.get(batch_no, 0)) - def get_serial_nos(serial_and_batch_bundle, serial_nos=None): if not serial_and_batch_bundle: @@ -1258,3 +1235,53 @@ def get_serial_nos_batch(serial_nos): as_list=1, ) ) + + +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) + 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_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) + + 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 throw_negative_batch_validation(batch_no, warehouse, 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) + ), + title=_("Negative Batch Quantity"), + ) + + +def get_distinct_batches(voucher_type, voucher_no): + bundles = frappe.get_all( + "Serial and Batch Bundle", + filters={"voucher_no": voucher_no, "voucher_type": voucher_type}, + pluck="name", + ) + if not bundles: + return + + return frappe.get_all( + "Serial and Batch Entry", + filters={"parent": ("in", bundles), "batch_no": ("is", "set")}, + group_by="batch_no", + pluck="batch_no", + )