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)