From f06ba0cc36cc30744a9b1acefa72e967ca7628e4 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Fri, 5 Jul 2024 20:14:32 +0530 Subject: [PATCH] fix: provision to enable do not use batch-wise valuation (#42186) fix: provision to enable do not use batchwise valuation --- erpnext/patches.txt | 1 + .../v15_0/do_not_use_batchwise_valuation.py | 15 +++++ erpnext/stock/doctype/batch/batch.py | 4 ++ .../delivery_note/test_delivery_note.py | 2 +- .../stock/doctype/pick_list/test_pick_list.py | 2 +- .../purchase_receipt/test_purchase_receipt.py | 59 +++++++++++++++++++ .../serial_and_batch_bundle.py | 10 ++++ .../test_serial_and_batch_bundle.py | 2 +- .../stock_settings/stock_settings.json | 13 +++- .../doctype/stock_settings/stock_settings.py | 17 ++++++ erpnext/stock/stock_ledger.py | 37 +++++++++++- erpnext/stock/utils.py | 11 +++- 12 files changed, 164 insertions(+), 9 deletions(-) create mode 100644 erpnext/patches/v15_0/do_not_use_batchwise_valuation.py diff --git a/erpnext/patches.txt b/erpnext/patches.txt index c197ab8f73b..fe5a0a1e995 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -373,3 +373,4 @@ erpnext.patches.v15_0.enable_old_serial_batch_fields erpnext.patches.v15_0.update_warehouse_field_in_asset_repair_consumed_item_doctype erpnext.patches.v15_0.update_asset_repair_field_in_stock_entry erpnext.patches.v15_0.update_total_number_of_booked_depreciations +erpnext.patches.v15_0.do_not_use_batchwise_valuation diff --git a/erpnext/patches/v15_0/do_not_use_batchwise_valuation.py b/erpnext/patches/v15_0/do_not_use_batchwise_valuation.py new file mode 100644 index 00000000000..9e95a34acc7 --- /dev/null +++ b/erpnext/patches/v15_0/do_not_use_batchwise_valuation.py @@ -0,0 +1,15 @@ +import frappe + + +def execute(): + valuation_method = frappe.db.get_single_value("Stock Settings", "valuation_method") + if valuation_method in ["FIFO", "LIFO"]: + return + + if frappe.get_all("Batch", filters={"use_batchwise_valuation": 1}, limit=1): + return + + if frappe.get_all("Item", filters={"has_batch_no": 1, "valuation_method": "FIFO"}, limit=1): + return + + frappe.db.set_single_value("Stock Settings", "do_not_use_batchwise_valuation", 1) diff --git a/erpnext/stock/doctype/batch/batch.py b/erpnext/stock/doctype/batch/batch.py index e490badfc40..c539c315747 100644 --- a/erpnext/stock/doctype/batch/batch.py +++ b/erpnext/stock/doctype/batch/batch.py @@ -158,6 +158,10 @@ class Batch(Document): def set_batchwise_valuation(self): if self.is_new(): + if frappe.db.get_single_value("Stock Settings", "do_not_use_batchwise_valuation"): + self.use_batchwise_valuation = 0 + return + self.use_batchwise_valuation = 1 def before_save(self): diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py index 8ee93d09ae8..8a0af0d466d 100644 --- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py @@ -1681,7 +1681,7 @@ class TestDeliveryNote(FrappeTestCase): { "is_stock_item": 1, "has_batch_no": 1, - "batch_no_series": "BATCH-TESTSERIAL-.#####", + "batch_number_series": "BATCH-TESTSERIAL-.#####", "create_new_batch": 1, }, ) diff --git a/erpnext/stock/doctype/pick_list/test_pick_list.py b/erpnext/stock/doctype/pick_list/test_pick_list.py index 3341b1fc6de..b1c03bf8453 100644 --- a/erpnext/stock/doctype/pick_list/test_pick_list.py +++ b/erpnext/stock/doctype/pick_list/test_pick_list.py @@ -648,7 +648,7 @@ class TestPickList(FrappeTestCase): def test_picklist_for_batch_item(self): warehouse = "_Test Warehouse - _TC" item = make_item( - properties={"is_stock_item": 1, "has_batch_no": 1, "batch_no_series": "PICKLT-.######"} + properties={"is_stock_item": 1, "has_batch_no": 1, "batch_number_series": "PICKLT-.######"} ).name # create batch diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index 35ead1464e2..54b16284b16 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -3210,6 +3210,65 @@ class TestPurchaseReceipt(FrappeTestCase): lcv.save().submit() return lcv + def test_do_not_use_batchwise_valuation_rate(self): + from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note + + item_code = "Test Item for Do Not Use Batchwise Valuation" + make_item( + item_code, + properties={ + "is_stock_item": 1, + "has_batch_no": 1, + "create_new_batch": 1, + "batch_number_series": "TIDNBV-.#####", + "valuation_method": "Moving Average", + }, + ) + + # 1st pr for 100 rate + pr = make_purchase_receipt( + item_code=item_code, + qty=1, + rate=100, + posting_date=add_days(today(), -2), + ) + + make_purchase_receipt( + item_code=item_code, + qty=1, + rate=200, + posting_date=add_days(today(), -1), + ) + + dn = create_delivery_note( + item_code=item_code, + qty=1, + rate=300, + posting_date=today(), + use_serial_batch_fields=1, + batch_no=get_batch_from_bundle(pr.items[0].serial_and_batch_bundle), + ) + dn.reload() + bundle = dn.items[0].serial_and_batch_bundle + + valuation_rate = frappe.db.get_value("Serial and Batch Bundle", bundle, "avg_rate") + self.assertEqual(valuation_rate, 100) + + doc = frappe.get_doc("Stock Settings") + doc.do_not_use_batchwise_valuation = 1 + doc.flags.ignore_validate = True + doc.save() + + pr.repost_future_sle_and_gle(force=True) + + valuation_rate = frappe.db.get_value("Serial and Batch Bundle", bundle, "avg_rate") + self.assertEqual(valuation_rate, 150) + + doc = frappe.get_doc("Stock Settings") + doc.do_not_use_batchwise_valuation = 0 + doc.flags.ignore_validate = True + doc.save() + def prepare_data_for_internal_transfer(): from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier 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 b1bb1ce8e32..c3fd28fcfa6 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 @@ -228,6 +228,16 @@ class SerialandBatchBundle(Document): def get_serial_nos(self): return [d.serial_no for d in self.entries if d.serial_no] + def update_valuation_rate(self, valuation_rate=None, save=False): + for row in self.entries: + row.incoming_rate = valuation_rate + row.stock_value_difference = flt(row.qty) * flt(valuation_rate) + + if save: + row.db_set( + {"incoming_rate": row.incoming_rate, "stock_value_difference": row.stock_value_difference} + ) + def set_incoming_rate_for_outward_transaction(self, row=None, save=False, allow_negative_stock=False): sle = self.get_sle_for_outward_transaction() 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 2913af4a724..32f3b4fb85b 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 @@ -653,7 +653,7 @@ class TestSerialandBatchBundle(FrappeTestCase): "is_stock_item": 1, "has_batch_no": 1, "create_new_batch": 1, - "batch_no_series": "PSNBI-TSNVL-.#####", + "batch_number_series": "PSNBI-TSNVL-.#####", "has_serial_no": 1, "serial_no_series": "SN-PSNBI-TSNVL-.#####", }, diff --git a/erpnext/stock/doctype/stock_settings/stock_settings.json b/erpnext/stock/doctype/stock_settings/stock_settings.json index b0e2c481333..3b42d41ebea 100644 --- a/erpnext/stock/doctype/stock_settings/stock_settings.json +++ b/erpnext/stock/doctype/stock_settings/stock_settings.json @@ -44,6 +44,7 @@ "auto_reserve_serial_and_batch", "serial_and_batch_item_settings_tab", "section_break_7", + "do_not_use_batchwise_valuation", "auto_create_serial_and_batch_bundle_for_outward", "pick_serial_and_batch_based_on", "column_break_mhzc", @@ -437,6 +438,14 @@ "fieldname": "do_not_update_serial_batch_on_creation_of_auto_bundle", "fieldtype": "Check", "label": "Do Not Update Serial / Batch on Creation of Auto Bundle" + }, + { + "default": "0", + "depends_on": "eval:doc.valuation_method === \"Moving Average\"", + "description": "If enabled, the system will use the moving average valuation method to calculate the valuation rate for the batched items and will not consider the individual batch-wise incoming rate.", + "fieldname": "do_not_use_batchwise_valuation", + "fieldtype": "Check", + "label": "Do Not Use Batch-wise Valuation" } ], "icon": "icon-cog", @@ -444,7 +453,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2024-03-27 13:10:45.423987", + "modified": "2024-07-04 12:45:09.811280", "modified_by": "Administrator", "module": "Stock", "name": "Stock Settings", @@ -469,4 +478,4 @@ "sort_order": "ASC", "states": [], "track_changes": 1 -} +} \ No newline at end of file diff --git a/erpnext/stock/doctype/stock_settings/stock_settings.py b/erpnext/stock/doctype/stock_settings/stock_settings.py index 24e8dccd17c..e786b1f67c6 100644 --- a/erpnext/stock/doctype/stock_settings/stock_settings.py +++ b/erpnext/stock/doctype/stock_settings/stock_settings.py @@ -40,6 +40,7 @@ class StockSettings(Document): default_warehouse: DF.Link | None disable_serial_no_and_batch_selector: DF.Check do_not_update_serial_batch_on_creation_of_auto_bundle: DF.Check + do_not_use_batchwise_valuation: DF.Check enable_stock_reservation: DF.Check item_group: DF.Link | None item_naming_by: DF.Literal["Item Code", "Naming Series"] @@ -98,6 +99,22 @@ class StockSettings(Document): self.validate_stock_reservation() self.change_precision_for_for_sales() self.change_precision_for_purchase() + self.validate_use_batch_wise_valuation() + + def validate_use_batch_wise_valuation(self): + if not self.do_not_use_batchwise_valuation: + return + + if self.valuation_method == "FIFO": + frappe.throw(_("Cannot disable batch wise valuation for FIFO valuation method.")) + + if frappe.get_all( + "Item", filters={"valuation_method": "FIFO", "is_stock_item": 1, "has_batch_no": 1}, limit=1 + ): + frappe.throw(_("Can't disable batch wise valuation for items with FIFO valuation method.")) + + if frappe.get_all("Batch", filters={"use_batchwise_valuation": 1}, limit=1): + frappe.throw(_("Can't disable batch wise valuation for active batches.")) def validate_warehouses(self): warehouse_fields = ["default_warehouse", "sample_retention_warehouse"] diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index f6091b56df5..a1792e080f2 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -530,6 +530,10 @@ class update_entries_after: self.allow_zero_rate = allow_zero_rate self.via_landed_cost_voucher = via_landed_cost_voucher self.item_code = args.get("item_code") + self.use_moving_avg_for_batch = frappe.db.get_single_value( + "Stock Settings", "do_not_use_batchwise_valuation" + ) + self.allow_negative_stock = allow_negative_stock or is_negative_stock_allowed( item_code=self.item_code ) @@ -745,7 +749,7 @@ class update_entries_after: if sle.get(dimension.get("fieldname")): has_dimensions = True - if sle.serial_and_batch_bundle: + if sle.serial_and_batch_bundle and (not self.use_moving_avg_for_batch or sle.has_serial_no): self.calculate_valuation_for_serial_batch_bundle(sle) elif sle.serial_no and not self.args.get("sle_id"): # Only run in reposting @@ -765,7 +769,12 @@ class update_entries_after: # Only run in reposting self.update_batched_values(sle) else: - if sle.voucher_type == "Stock Reconciliation" and not sle.batch_no and not has_dimensions: + if ( + sle.voucher_type == "Stock Reconciliation" + and not sle.batch_no + and not sle.has_batch_no + and not has_dimensions + ): # assert self.wh_data.valuation_rate = sle.valuation_rate self.wh_data.qty_after_transaction = sle.qty_after_transaction @@ -806,6 +815,15 @@ class update_entries_after: sle.doctype = "Stock Ledger Entry" frappe.get_doc(sle).db_update() + if ( + sle.serial_and_batch_bundle + and self.valuation_method == "Moving Average" + and self.use_moving_avg_for_batch + and (sle.batch_no or sle.has_batch_no) + ): + valuation_rate = flt(stock_value_difference) / flt(sle.actual_qty) + self.update_valuation_rate_in_serial_and_batch_bundle(sle, valuation_rate) + if not self.args.get("sle_id") or ( sle.serial_and_batch_bundle and sle.auto_created_serial_and_batch_bundle ): @@ -916,6 +934,21 @@ class update_entries_after: self.wh_data.qty_after_transaction, precision ) + def update_valuation_rate_in_serial_and_batch_bundle(self, sle, valuation_rate): + # Only execute if the item has batch_no and the valuation method is moving average + if not frappe.db.exists("Serial and Batch Bundle", sle.serial_and_batch_bundle): + return + + doc = frappe.get_cached_doc("Serial and Batch Bundle", sle.serial_and_batch_bundle) + doc.update_valuation_rate(valuation_rate, save=True) + doc.calculate_qty_and_amount(save=True) + + def get_outgoing_rate_for_batched_item(self, sle): + if self.wh_data.qty_after_transaction == 0: + return 0 + + return flt(self.wh_data.stock_value) / flt(self.wh_data.qty_after_transaction) + def validate_negative_stock(self, sle): """ validate negative stock for entries current datetime onwards diff --git a/erpnext/stock/utils.py b/erpnext/stock/utils.py index 12b0b9a7ca8..bdd2ee0483c 100644 --- a/erpnext/stock/utils.py +++ b/erpnext/stock/utils.py @@ -244,6 +244,8 @@ def get_incoming_rate(args, raise_error_if_no_rate=True): "Item", args.get("item_code"), ["has_serial_no", "has_batch_no"], as_dict=1 ) + use_moving_avg_for_batch = frappe.db.get_single_value("Stock Settings", "do_not_use_batchwise_valuation") + if isinstance(args, dict): args = frappe._dict(args) @@ -257,7 +259,12 @@ def get_incoming_rate(args, raise_error_if_no_rate=True): return sn_obj.get_incoming_rate() - elif item_details and item_details.has_batch_no and args.get("serial_and_batch_bundle"): + elif ( + item_details + and item_details.has_batch_no + and args.get("serial_and_batch_bundle") + and not use_moving_avg_for_batch + ): args.actual_qty = args.qty batch_obj = BatchNoValuation( sle=args, @@ -274,7 +281,7 @@ def get_incoming_rate(args, raise_error_if_no_rate=True): sn_obj = SerialNoValuation(sle=args, warehouse=args.get("warehouse"), item_code=args.get("item_code")) return sn_obj.get_incoming_rate() - elif args.get("batch_no") and not args.get("serial_and_batch_bundle"): + elif args.get("batch_no") and not args.get("serial_and_batch_bundle") and not use_moving_avg_for_batch: args.actual_qty = args.qty args.batch_nos = frappe._dict({args.batch_no: args})