feat: get RM costs from consumption entry in manufacture SE (#39822)

diff --git a/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.json b/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.json
index d3ad51f..63e3fa3 100644
--- a/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.json
+++ b/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.json
@@ -7,6 +7,7 @@
  "field_order": [
   "raw_materials_consumption_section",
   "material_consumption",
+  "get_rm_cost_from_consumption_entry",
   "column_break_3",
   "backflush_raw_materials_based_on",
   "capacity_planning",
@@ -202,13 +203,20 @@
    "fieldname": "set_op_cost_and_scrape_from_sub_assemblies",
    "fieldtype": "Check",
    "label": "Set Operating Cost / Scrape Items From Sub-assemblies"
+  },
+  {
+   "default": "0",
+   "depends_on": "eval: doc.material_consumption",
+   "fieldname": "get_rm_cost_from_consumption_entry",
+   "fieldtype": "Check",
+   "label": "Get Raw Materials Cost from Consumption Entry"
   }
  ],
  "icon": "icon-wrench",
  "index_web_pages_for_search": 1,
  "issingle": 1,
  "links": [],
- "modified": "2023-12-28 16:37:44.874096",
+ "modified": "2024-02-08 19:00:37.561244",
  "modified_by": "Administrator",
  "module": "Manufacturing",
  "name": "Manufacturing Settings",
diff --git a/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.py b/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.py
index 463ba9f..9a50111 100644
--- a/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.py
+++ b/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.py
@@ -26,6 +26,7 @@
 		default_scrap_warehouse: DF.Link | None
 		default_wip_warehouse: DF.Link | None
 		disable_capacity_planning: DF.Check
+		get_rm_cost_from_consumption_entry: DF.Check
 		job_card_excess_transfer: DF.Check
 		make_serial_no_batch_from_work_order: DF.Check
 		material_consumption: DF.Check
diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py
index f6e9a07..efe9f53 100644
--- a/erpnext/manufacturing/doctype/work_order/test_work_order.py
+++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py
@@ -1776,6 +1776,52 @@
 			"Manufacturing Settings", "set_op_cost_and_scrape_from_sub_assemblies", 0
 		)
 
+	@change_settings(
+		"Manufacturing Settings", {"material_consumption": 1, "get_rm_cost_from_consumption_entry": 1}
+	)
+	def test_get_rm_cost_from_consumption_entry(self):
+		from erpnext.stock.doctype.stock_entry.test_stock_entry import (
+			make_stock_entry as make_stock_entry_test_record,
+		)
+
+		rm = make_item(properties={"is_stock_item": 1}).name
+		fg = make_item(properties={"is_stock_item": 1}).name
+
+		make_stock_entry_test_record(
+			purpose="Material Receipt",
+			item_code=rm,
+			target="Stores - _TC",
+			qty=10,
+			basic_rate=100,
+		)
+		make_stock_entry_test_record(
+			purpose="Material Receipt",
+			item_code=rm,
+			target="Stores - _TC",
+			qty=10,
+			basic_rate=200,
+		)
+
+		bom = make_bom(item=fg, raw_materials=[rm], rate=150).name
+		wo = make_wo_order_test_record(
+			production_item=fg,
+			bom_no=bom,
+			qty=10,
+		)
+
+		mte = frappe.get_doc(make_stock_entry(wo.name, "Material Transfer for Manufacture", 10))
+		mte.items[0].s_warehouse = "Stores - _TC"
+		mte.insert().submit()
+
+		mce = frappe.get_doc(make_stock_entry(wo.name, "Material Consumption for Manufacture", 10))
+		mce.insert().submit()
+
+		me = frappe.get_doc(make_stock_entry(wo.name, "Manufacture", 10))
+		me.insert().submit()
+
+		valuation_rate = sum([item.valuation_rate * item.transfer_qty for item in mce.items]) / 10
+		self.assertEqual(me.items[0].valuation_rate, valuation_rate)
+
 
 def prepare_boms_for_sub_assembly_test():
 	if not frappe.db.exists("BOM", {"item": "Test Final SF Item 1"}):
diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py
index 3419155..f581fc6 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry.py
+++ b/erpnext/stock/doctype/stock_entry/stock_entry.py
@@ -904,14 +904,62 @@
 				return flt(outgoing_items_cost / total_fg_qty)
 
 	def get_basic_rate_for_manufactured_item(self, finished_item_qty, outgoing_items_cost=0) -> float:
+		settings = frappe.get_single("Manufacturing Settings")
 		scrap_items_cost = sum([flt(d.basic_amount) for d in self.get("items") if d.is_scrap_item])
 
-		# Get raw materials cost from BOM if multiple material consumption entries
-		if not outgoing_items_cost and frappe.db.get_single_value(
-			"Manufacturing Settings", "material_consumption", cache=True
-		):
-			bom_items = self.get_bom_raw_materials(finished_item_qty)
-			outgoing_items_cost = sum([flt(row.qty) * flt(row.rate) for row in bom_items.values()])
+		if settings.material_consumption:
+			if settings.get_rm_cost_from_consumption_entry and self.work_order:
+
+				# Validate only if Material Consumption Entry exists for the Work Order.
+				if frappe.db.exists(
+					"Stock Entry",
+					{
+						"docstatus": 1,
+						"work_order": self.work_order,
+						"purpose": "Material Consumption for Manufacture",
+					},
+				):
+					for item in self.items:
+						if not item.is_finished_item and not item.is_scrap_item:
+							label = frappe.get_meta(settings.doctype).get_label("get_rm_cost_from_consumption_entry")
+							frappe.throw(
+								_(
+									"Row {0}: As {1} is enabled, raw materials cannot be added to {2} entry. Use {3} entry to consume raw materials."
+								).format(
+									item.idx,
+									frappe.bold(label),
+									frappe.bold("Manufacture"),
+									frappe.bold("Material Consumption for Manufacture"),
+								)
+							)
+
+					if frappe.db.exists(
+						"Stock Entry", {"docstatus": 1, "work_order": self.work_order, "purpose": "Manufacture"}
+					):
+						frappe.throw(
+							_("Only one {0} entry can be created against the Work Order {1}").format(
+								frappe.bold("Manufacture"), frappe.bold(self.work_order)
+							)
+						)
+
+					SE = frappe.qb.DocType("Stock Entry")
+					SE_ITEM = frappe.qb.DocType("Stock Entry Detail")
+
+					outgoing_items_cost = (
+						frappe.qb.from_(SE)
+						.left_join(SE_ITEM)
+						.on(SE.name == SE_ITEM.parent)
+						.select(Sum(SE_ITEM.valuation_rate * SE_ITEM.transfer_qty))
+						.where(
+							(SE.docstatus == 1)
+							& (SE.work_order == self.work_order)
+							& (SE.purpose == "Material Consumption for Manufacture")
+						)
+					).run()[0][0] or 0
+
+			elif not outgoing_items_cost:
+				bom_items = self.get_bom_raw_materials(finished_item_qty)
+				outgoing_items_cost = sum([flt(row.qty) * flt(row.rate) for row in bom_items.values()])
 
 		return flt((outgoing_items_cost - scrap_items_cost) / finished_item_qty)