From 454dd3a2f11451c4dd530c32ee4577fc8bafb21c Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Thu, 27 Mar 2025 17:33:43 +0530 Subject: [PATCH] fix: valuation rate not updating for raw materials (cherry picked from commit 5af8378471009713e4b4adf112f0343092f977e2) # Conflicts: # erpnext/manufacturing/doctype/work_order/test_work_order.py --- .../doctype/work_order/test_work_order.py | 287 ++++++++++++++++++ erpnext/stock/stock_ledger.py | 14 +- 2 files changed, 300 insertions(+), 1 deletion(-) diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py index 84f2b75432c..ca14c759088 100644 --- a/erpnext/manufacturing/doctype/work_order/test_work_order.py +++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py @@ -2602,6 +2602,293 @@ class TestWorkOrder(FrappeTestCase): status = frappe.db.get_value("Serial No", row, "status") self.assertEqual(status, "Consumed") +<<<<<<< HEAD +======= + def test_stock_reservation_for_serialized_raw_material(self): + from erpnext.stock.doctype.stock_entry.stock_entry_utils import ( + make_stock_entry as make_stock_entry_test_record, + ) + + production_item = "Test Stock Reservation FG 1" + rm_item = "Test Stock Reservation RM 1" + source_warehouse = "Stores - _TC" + + make_item(production_item, {"is_stock_item": 1}) + make_item(rm_item, {"is_stock_item": 1, "has_serial_no": 1, "serial_no_series": "TST-SER-RES-.###"}) + + bom = make_bom( + item=production_item, + source_warehouse=source_warehouse, + raw_materials=[rm_item], + operating_cost_per_bom_quantity=100, + do_not_submit=True, + ) + + for row in bom.exploded_items: + make_stock_entry_test_record( + item_code=row.item_code, + target=source_warehouse, + qty=10, + basic_rate=100, + ) + + wo = make_wo_order_test_record( + item=production_item, + qty=10, + reserve_stock=1, + source_warehouse=source_warehouse, + ) + + self.assertTrue(frappe.get_all("Stock Reservation Entry", filters={"voucher_no": wo.name})) + + wo1 = make_wo_order_test_record( + item=production_item, + qty=10, + reserve_stock=1, + source_warehouse=source_warehouse, + ) + + self.assertFalse(frappe.get_all("Stock Reservation Entry", filters={"voucher_no": wo1.name})) + + transfer_entry = frappe.get_doc(make_stock_entry(wo1.name, "Material Transfer for Manufacture", 10)) + transfer_entry.save() + + self.assertRaises(frappe.ValidationError, transfer_entry.submit) + + def test_stock_reservation_for_batched_raw_material(self): + from erpnext.stock.doctype.stock_entry.stock_entry_utils import ( + make_stock_entry as make_stock_entry_test_record, + ) + + production_item = "Test Stock Reservation FG 2" + rm_item = "Test Stock Reservation RM 2" + source_warehouse = "Stores - _TC" + + make_item(production_item, {"is_stock_item": 1}) + make_item( + rm_item, + { + "is_stock_item": 1, + "has_batch_no": 1, + "batch_number_series": "TST-BATCH-RES-.###", + "create_new_batch": 1, + }, + ) + + bom = make_bom( + item=production_item, + source_warehouse=source_warehouse, + raw_materials=[rm_item], + operating_cost_per_bom_quantity=100, + do_not_submit=True, + ) + + for row in bom.exploded_items: + make_stock_entry_test_record( + item_code=row.item_code, + target=source_warehouse, + qty=10, + basic_rate=100, + ) + + wo = make_wo_order_test_record( + item=production_item, + qty=10, + reserve_stock=1, + source_warehouse=source_warehouse, + ) + + self.assertTrue(frappe.get_all("Stock Reservation Entry", filters={"voucher_no": wo.name})) + + wo1 = make_wo_order_test_record( + item=production_item, + qty=10, + reserve_stock=1, + source_warehouse=source_warehouse, + ) + + self.assertFalse(frappe.get_all("Stock Reservation Entry", filters={"voucher_no": wo1.name})) + + transfer_entry = frappe.get_doc(make_stock_entry(wo1.name, "Material Transfer for Manufacture", 10)) + transfer_entry.save() + + self.assertRaises(frappe.ValidationError, transfer_entry.submit) + + def test_auto_stock_reservation_for_batched_raw_material(self): + from erpnext.stock.doctype.stock_entry.stock_entry_utils import ( + make_stock_entry as make_stock_entry_test_record, + ) + + frappe.db.set_single_value("Stock Settings", "auto_reserve_serial_and_batch", 1) + + production_item = "Test Stock Reservation FG 3" + rm_item = "Test Stock Reservation RM 3" + source_warehouse = "Stores - _TC" + + make_item(production_item, {"is_stock_item": 1}) + make_item( + rm_item, + { + "is_stock_item": 1, + "has_batch_no": 1, + "batch_number_series": "TST-BATCH-RES-.###", + "create_new_batch": 1, + }, + ) + + bom = make_bom( + item=production_item, + source_warehouse=source_warehouse, + raw_materials=[rm_item], + operating_cost_per_bom_quantity=100, + do_not_submit=True, + ) + + itemwise_batches = frappe._dict() + for row in bom.exploded_items: + se = make_stock_entry_test_record( + item_code=row.item_code, + target=source_warehouse, + qty=10, + basic_rate=100, + ) + + itemwise_batches[row.item_code] = get_batch_from_bundle(se.items[0].serial_and_batch_bundle) + + wo = make_wo_order_test_record( + item=production_item, + qty=10, + reserve_stock=1, + source_warehouse=source_warehouse, + ) + + self.assertTrue(frappe.get_all("Stock Reservation Entry", filters={"voucher_no": wo.name})) + + for row in frappe.get_all("Stock Reservation Entry", filters={"voucher_no": wo.name}): + reservation_entry = frappe.get_doc("Stock Reservation Entry", row.name) + self.assertTrue(reservation_entry.has_batch_no) + self.assertTrue(reservation_entry.sb_entries) + + for row in bom.exploded_items: + make_stock_entry_test_record( + item_code=row.item_code, + target=source_warehouse, + qty=10, + basic_rate=100, + ) + + transfer_entry = frappe.get_doc(make_stock_entry(wo.name, "Material Transfer for Manufacture", 10)) + transfer_entry.save() + transfer_entry.submit() + + for row in transfer_entry.items: + batch_no = get_batch_from_bundle(row.serial_and_batch_bundle) + self.assertEqual(batch_no, itemwise_batches[row.item_code]) + + def test_work_order_valuation_auto_pick(self): + fg_item = "Test FG Item For Non Transfer Item Batch" + rm_item = "Test RM Item For Non Transfer Item Batch" + + make_item(fg_item, {"is_stock_item": 1}) + make_item( + rm_item, + { + "is_stock_item": 1, + "has_batch_no": 1, + "create_new_batch": 1, + "batch_number_series": "TST-BATCH-NTI-.###", + }, + ) + + source_warehouse = "_Test Warehouse - _TC" + wip_warehouse = "Stores - _TC" + finished_goods_warehouse = create_warehouse("_Test Finished Goods Warehouse", company="_Test Company") + + batches = make_stock_in_entries_and_get_batches(rm_item, source_warehouse, wip_warehouse) + + if not frappe.db.get_value("BOM", {"item": fg_item}): + make_bom(item=fg_item, raw_materials=[rm_item]) + + wo = make_wo_order_test_record( + item=fg_item, + qty=5, + source_warehouse=source_warehouse, + wip_warehouse=wip_warehouse, + fg_warehouse=finished_goods_warehouse, + ) + + stock_entry = frappe.get_doc(make_stock_entry(wo.name, "Material Transfer for Manufacture", 5)) + stock_entry.items[0].batch_no = batches[1] + stock_entry.items[0].use_serial_batch_fields = 1 + stock_entry.submit() + stock_entry.reload() + + self.assertEqual(stock_entry.items[0].valuation_rate, 200) + + original_value = frappe.db.get_single_value( + "Stock Settings", "auto_create_serial_and_batch_bundle_for_outward" + ) + original_based_on = frappe.db.get_single_value("Stock Settings", "pick_serial_and_batch_based_on") + + frappe.db.set_single_value("Stock Settings", "auto_create_serial_and_batch_bundle_for_outward", 1) + frappe.db.set_single_value("Stock Settings", "pick_serial_and_batch_based_on", "Expiry") + + stock_entry = frappe.get_doc(make_stock_entry(wo.name, "Manufacture", 5)) + stock_entry.items[0].use_serial_batch_fields = 1 + stock_entry.submit() + stock_entry.reload() + + batch_no = get_batch_from_bundle(stock_entry.items[0].serial_and_batch_bundle) + self.assertEqual(batch_no, batches[1]) + self.assertEqual(stock_entry.items[0].valuation_rate, 200) + self.assertEqual(stock_entry.items[1].valuation_rate, 200) + + frappe.db.set_single_value( + "Stock Settings", "auto_create_serial_and_batch_bundle_for_outward", original_value + ) + frappe.db.set_single_value("Stock Settings", "pick_serial_and_batch_based_on", original_based_on) + + +def make_stock_in_entries_and_get_batches(rm_item, source_warehouse, wip_warehouse): + from erpnext.stock.doctype.stock_entry.test_stock_entry import ( + make_stock_entry as make_stock_entry_test_record, + ) + + batches = [] + for qty, rate in ((5, 100), (5, 200)): + stock_entry = make_stock_entry_test_record( + item_code=rm_item, + target=source_warehouse, + qty=qty, + basic_rate=rate, + ) + stock_entry.submit() + stock_entry.reload() + + batch_no = get_batch_from_bundle(stock_entry.items[0].serial_and_batch_bundle) + batch_doc = frappe.get_doc("Batch", batch_no) + + # keep early expiry date for the batch having rate 200 + days = 10 if rate == 100 else 1 + batch_doc.db_set("expiry_date", add_to_date(now(), days=days)) + + batches.append(batch_no) + + stock_entry = make_stock_entry_test_record( + item_code=rm_item, + target=wip_warehouse, + qty=qty, + basic_rate=rate, + ) + stock_entry.submit() + stock_entry.reload() + batch_no = get_batch_from_bundle(stock_entry.items[0].serial_and_batch_bundle) + batch_doc = frappe.get_doc("Batch", batch_no) + batch_doc.db_set("expiry_date", add_to_date(now(), days=10)) + + return batches + +>>>>>>> 5af8378471 (fix: valuation rate not updating for raw materials) def make_operation(**kwargs): kwargs = frappe._dict(kwargs) diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index aaeb90b7d30..4627c8aecfa 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -1212,9 +1212,21 @@ class update_entries_after: frappe.db.set_value("Stock Entry Detail", sle.voucher_detail_no, "basic_rate", outgoing_rate) # Update outgoing item's rate, recalculate FG Item's rate and total incoming/outgoing amount - if not sle.dependant_sle_voucher_detail_no: + if not sle.dependant_sle_voucher_detail_no or self.is_manufacture_entry_with_sabb(sle): self.recalculate_amounts_in_stock_entry(sle.voucher_no, sle.voucher_detail_no) + def is_manufacture_entry_with_sabb(self, sle): + if ( + self.args.get("sle_id") + and sle.serial_and_batch_bundle + and sle.auto_created_serial_and_batch_bundle + ): + purpose = frappe.get_cached_value("Stock Entry", sle.voucher_no, "purpose") + if purpose in ["Manufacture", "Repack"]: + return True + + return False + def recalculate_amounts_in_stock_entry(self, voucher_no, voucher_detail_no): stock_entry = frappe.get_doc("Stock Entry", voucher_no, for_update=True) stock_entry.calculate_rate_and_amount(reset_outgoing_rate=False, raise_error_if_no_rate=False)