diff --git a/erpnext/stock/deprecated_serial_batch.py b/erpnext/stock/deprecated_serial_batch.py index 69443e3a608..6c30b087de8 100644 --- a/erpnext/stock/deprecated_serial_batch.py +++ b/erpnext/stock/deprecated_serial_batch.py @@ -211,6 +211,7 @@ class DeprecatedBatchNoValuation: .select( sle.batch_no, Sum(sle.actual_qty).as_("batch_qty"), + Sum(sle.stock_value_difference).as_("batch_value"), ) .where( (sle.item_code == self.sle.item_code) @@ -227,9 +228,24 @@ class DeprecatedBatchNoValuation: if self.sle.name: query = query.where(sle.name != self.sle.name) + # Moving Average items with no Use Batch wise Valuation but want to use batch wise valuation + moving_avg_item_non_batch_value = False + if valuation_method := self.get_valuation_method(self.sle.item_code): + if valuation_method == "Moving Average" and not frappe.db.get_single_value( + "Stock Settings", "do_not_use_batchwise_valuation" + ): + query = query.where(batch.use_batchwise_valuation == 0) + moving_avg_item_non_batch_value = True + batch_data = query.run(as_dict=True) for d in batch_data: self.available_qty[d.batch_no] += flt(d.batch_qty) + if moving_avg_item_non_batch_value: + self.non_batchwise_balance_qty[d.batch_no] += flt(d.batch_qty) + self.non_batchwise_balance_value[d.batch_no] += flt(d.batch_value) + + if moving_avg_item_non_batch_value: + return for d in batch_data: if self.available_qty.get(d.batch_no): @@ -327,9 +343,24 @@ class DeprecatedBatchNoValuation: query = query.where(bundle.voucher_type != "Pick List") + # Moving Average items with no Use Batch wise Valuation but want to use batch wise valuation + moving_avg_item_non_batch_value = False + if valuation_method := self.get_valuation_method(self.sle.item_code): + if valuation_method == "Moving Average" and not frappe.db.get_single_value( + "Stock Settings", "do_not_use_batchwise_valuation" + ): + query = query.where(batch.use_batchwise_valuation == 0) + moving_avg_item_non_batch_value = True + batch_data = query.run(as_dict=True) for d in batch_data: self.available_qty[d.batch_no] += flt(d.batch_qty) + if moving_avg_item_non_batch_value: + self.non_batchwise_balance_qty[d.batch_no] += flt(d.batch_qty) + self.non_batchwise_balance_value[d.batch_no] += flt(d.batch_value) + + if moving_avg_item_non_batch_value: + return if not self.last_sle: return @@ -337,3 +368,8 @@ class DeprecatedBatchNoValuation: for batch_no in self.available_qty: self.non_batchwise_balance_value[batch_no] = flt(self.last_sle.stock_value) self.non_batchwise_balance_qty[batch_no] = flt(self.last_sle.qty_after_transaction) + + def get_valuation_method(self, item_code): + from erpnext.stock.utils import get_valuation_method + + return get_valuation_method(item_code) 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 64563625297..51b939c343d 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 @@ -358,6 +358,136 @@ class TestSerialandBatchBundle(FrappeTestCase): self.assertFalse(json.loads(sle.stock_queue or "[]")) self.assertEqual(flt(sle.stock_value), 0.0) + def test_old_moving_avg_item_with_without_batchwise_valuation(self): + frappe.flags.ignore_serial_batch_bundle_validation = True + frappe.flags.use_serial_and_batch_fields = True + batch_item_code = "Old Batch Item Valuation 2" + make_item( + batch_item_code, + { + "has_batch_no": 1, + "batch_number_series": "TEST-OLD2-BAT-VAL-.#####", + "create_new_batch": 1, + "is_stock_item": 1, + "valuation_method": "Moving Average", + }, + ) + + non_batchwise_val_batches = [ + "TEST-OLD2-BAT-VAL-00001", + "TEST-OLD2-BAT-VAL-00002", + "TEST-OLD2-BAT-VAL-00003", + "TEST-OLD2-BAT-VAL-00004", + ] + + for batch_id in non_batchwise_val_batches: + if not frappe.db.exists("Batch", batch_id): + batch_doc = frappe.get_doc( + { + "doctype": "Batch", + "batch_id": batch_id, + "item": batch_item_code, + "use_batchwise_valuation": 0, + } + ).insert(ignore_permissions=True) + + self.assertTrue(batch_doc.use_batchwise_valuation) + batch_doc.db_set( + { + "use_batchwise_valuation": 0, + "batch_qty": 20, + } + ) + + qty_after_transaction = 0 + balance_value = 0 + i = 0 + for batch_id in non_batchwise_val_batches: + i += 1 + qty = 20 + valuation = 100 * i + qty_after_transaction += qty + balance_value += qty * valuation + + doc = frappe.get_doc( + { + "doctype": "Stock Ledger Entry", + "posting_date": today(), + "posting_time": nowtime(), + "batch_no": batch_id, + "incoming_rate": valuation, + "qty_after_transaction": qty_after_transaction, + "stock_value_difference": valuation * qty, + "stock_value": balance_value, + "balance_value": balance_value, + "valuation_rate": balance_value / qty_after_transaction, + "actual_qty": qty, + "item_code": batch_item_code, + "warehouse": "_Test Warehouse - _TC", + } + ) + + doc.set_posting_datetime() + doc.flags.ignore_permissions = True + doc.flags.ignore_mandatory = True + doc.flags.ignore_links = True + doc.flags.ignore_validate = True + doc.submit() + doc.reload() + + frappe.flags.ignore_serial_batch_bundle_validation = False + frappe.flags.use_serial_and_batch_fields = False + + se = make_stock_entry( + item_code=batch_item_code, + target="_Test Warehouse - _TC", + qty=30, + rate=355, + use_serial_batch_fields=True, + ) + + se = make_stock_entry( + item_code=batch_item_code, + source="_Test Warehouse - _TC", + qty=70, + use_serial_batch_fields=True, + ) + + sle = frappe.db.get_value( + "Stock Ledger Entry", + {"item_code": batch_item_code, "is_cancelled": 0, "voucher_no": se.name}, + ["qty_after_transaction", "stock_value"], + as_dict=True, + ) + + self.assertEqual(flt(sle.stock_value), 14000.0) + self.assertEqual(flt(sle.qty_after_transaction), 40.0) + + se = make_stock_entry( + item_code=batch_item_code, + target="_Test Warehouse - _TC", + qty=10, + rate=200, + use_serial_batch_fields=True, + ) + + se = make_stock_entry( + item_code=batch_item_code, + source="_Test Warehouse - _TC", + qty=50, + use_serial_batch_fields=True, + ) + + sle = frappe.db.get_value( + "Stock Ledger Entry", + {"item_code": batch_item_code, "is_cancelled": 0, "voucher_no": se.name}, + ["qty_after_transaction", "stock_value"], + as_dict=True, + ) + + self.assertEqual(flt(sle.stock_value), 0.0) + self.assertEqual(flt(sle.qty_after_transaction), 0.0) + def test_old_serial_no_valuation(self): from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt diff --git a/erpnext/stock/doctype/stock_settings/stock_settings.py b/erpnext/stock/doctype/stock_settings/stock_settings.py index 0ea1738b60e..69e626db1ba 100644 --- a/erpnext/stock/doctype/stock_settings/stock_settings.py +++ b/erpnext/stock/doctype/stock_settings/stock_settings.py @@ -110,6 +110,22 @@ class StockSettings(Document): self.validate_auto_insert_price_list_rate_if_missing() self.change_precision_for_for_sales() self.change_precision_for_purchase() + self.validate_do_not_use_batchwise_valuation() + + def validate_do_not_use_batchwise_valuation(self): + doc_before_save = self.get_doc_before_save() + if not doc_before_save: + return + + if not frappe.get_all("Serial and Batch Bundle", filters={"docstatus": 1}, limit=1, pluck="name"): + return + + if doc_before_save.do_not_use_batchwise_valuation and not self.do_not_use_batchwise_valuation: + frappe.throw( + _("Cannot disable {0} as it may lead to incorrect stock valuation.").format( + frappe.bold(_("Do Not Use Batchwise Valuation")) + ) + ) def validate_warehouses(self): warehouse_fields = ["default_warehouse", "sample_retention_warehouse"]