From 39067c761427e49c3c5aff5895faaee20de3f672 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Sun, 11 Feb 2024 17:10:57 +0530 Subject: [PATCH] feat: get RM costs from consumption entry in manufacture SE (#39822) --- .../manufacturing_settings.json | 10 +++- .../manufacturing_settings.py | 1 + .../doctype/work_order/test_work_order.py | 46 ++++++++++++++ .../stock/doctype/stock_entry/stock_entry.py | 60 +++++++++++++++++-- 4 files changed, 110 insertions(+), 7 deletions(-) diff --git a/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.json b/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.json index d3ad51f72362..63e3fa3e9ff2 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 463ba9fe4bf1..9a501115b0da 100644 --- a/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.py +++ b/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.py @@ -26,6 +26,7 @@ class ManufacturingSettings(Document): 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 f6e9a0706334..efe9f53dbe4e 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 @@ def test_op_cost_and_scrap_based_on_sub_assemblies(self): "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 3419155f391b..f581fc6db9b7 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -904,14 +904,62 @@ def get_basic_rate_for_repacked_items(self, finished_item_qty, outgoing_items_co 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)