diff --git a/erpnext/controllers/subcontracting_controller.py b/erpnext/controllers/subcontracting_controller.py index 473ab7c86b3..11ad4f4d2ef 100644 --- a/erpnext/controllers/subcontracting_controller.py +++ b/erpnext/controllers/subcontracting_controller.py @@ -313,10 +313,10 @@ class SubcontractingController(StockController): ): for row in frappe.get_all( f"{self.subcontract_data.order_doctype} Item", - fields=["item_code", {"SUB": ["qty", "received_qty"], "as": "qty"}, "parent", "name"], + fields=["item_code", {"SUB": ["qty", "received_qty"], "as": "qty"}, "parent", "bom"], filters={"docstatus": 1, "parent": ("in", self.subcontract_orders)}, ): - self.qty_to_be_received[(row.item_code, row.parent)] += row.qty + self.qty_to_be_received[(row.item_code, row.parent, row.bom)] += row.qty def __get_transferred_items(self): se = frappe.qb.DocType("Stock Entry") @@ -922,13 +922,17 @@ class SubcontractingController(StockController): self.__set_serial_nos(item_row, rm_obj) def __get_qty_based_on_material_transfer(self, item_row, transfer_item): - key = (item_row.item_code, item_row.get(self.subcontract_data.order_field)) + key = ( + item_row.item_code, + item_row.get(self.subcontract_data.order_field), + item_row.get("bom"), + ) if self.qty_to_be_received == item_row.qty: return transfer_item.qty - if self.qty_to_be_received: - qty = (flt(item_row.qty) * flt(transfer_item.qty)) / flt(self.qty_to_be_received.get(key, 0)) + if self.qty_to_be_received.get(key): + qty = (flt(item_row.qty) * flt(transfer_item.qty)) / flt(self.qty_to_be_received.get(key)) transfer_item.item_details.required_qty = transfer_item.qty if transfer_item.serial_no or frappe.get_cached_value( @@ -977,7 +981,11 @@ class SubcontractingController(StockController): if self.qty_to_be_received: self.qty_to_be_received[ - (row.item_code, row.get(self.subcontract_data.order_field)) + ( + row.item_code, + row.get(self.subcontract_data.order_field), + row.get("bom"), + ) ] -= row.qty def __set_rate_for_serial_and_batch_bundle(self): diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py b/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py index 2a69083bc0f..29ecdd24b82 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py +++ b/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py @@ -616,6 +616,117 @@ class TestSubcontractingReceipt(IntegrationTestCase): for item in scr.supplied_items: self.assertFalse(item.available_qty_for_consumption) + def test_supplied_items_consumed_qty_for_similar_finished_goods(self): + """ + Test that supplied raw material consumption is calculated correctly + when multiple subcontracted service items use the same finished good + but different BOMs. + """ + + from erpnext.controllers.subcontracting_controller import ( + make_rm_stock_entry as make_subcontract_transfer_entry, + ) + from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom + + # Configuration: Backflush based on subcontract material transfer + set_backflush_based_on("Material Transferred for Subcontract") + + # Create Raw Materials + raw_material_1 = make_item("_RM Item 1", properties={"is_stock_item": 1}).name + + raw_material_2 = make_item("_RM Item 2", properties={"is_stock_item": 1}).name + + # Create Subcontracted Finished Good + finished_good = make_item("_Finished Good Item", properties={"is_stock_item": 1}) + finished_good.is_sub_contracted_item = 1 + finished_good.save() + + # Receive Raw Materials into Warehouse + for raw_material in (raw_material_1, raw_material_2): + make_stock_entry( + item_code=raw_material, + qty=10, + target="_Test Warehouse - _TC", + basic_rate=100, + ) + + # Create BOMs for the same Finished Good with different RMs + bom_rm_1 = make_bom( + item=finished_good.name, + quantity=1, + raw_materials=[raw_material_1], + ).name + + _bom_rm_2 = make_bom( + item=finished_good.name, + quantity=1, + raw_materials=[raw_material_2], + ).name + + # Define Subcontracted Service Items + service_items = [ + { + "warehouse": "_Test Warehouse - _TC", + "item_code": "Subcontracted Service Item 1", + "qty": 1, + "rate": 100, + "fg_item": finished_good.name, + "fg_item_qty": 10, + }, + { + "warehouse": "_Test Warehouse - _TC", + "item_code": "Subcontracted Service Item 1", + "qty": 1, + "rate": 150, + "fg_item": finished_good.name, + "fg_item_qty": 10, + }, + ] + + # Create Subcontracting Order + subcontracting_order = get_subcontracting_order( + service_items=service_items, + do_not_save=True, + ) + + # Assign BOM only to the first service item + subcontracting_order.items[0].bom = bom_rm_1 + subcontracting_order.save() + subcontracting_order.submit() + + # Prepare Raw Material Transfer Items + raw_material_transfer_items = [] + for supplied_item in subcontracting_order.supplied_items: + raw_material_transfer_items.append( + { + "item_code": supplied_item.main_item_code, + "rm_item_code": supplied_item.rm_item_code, + "qty": supplied_item.required_qty, + "warehouse": "_Test Warehouse - _TC", + "stock_uom": "Nos", + } + ) + + # Transfer Raw Materials to Subcontractor Warehouse + stock_entry = frappe.get_doc( + make_subcontract_transfer_entry( + subcontracting_order.name, + raw_material_transfer_items, + ) + ) + stock_entry.to_warehouse = "_Test Warehouse 1 - _TC" + stock_entry.save() + stock_entry.submit() + + # Create Subcontracting Receipt + subcontracting_receipt = make_subcontracting_receipt(subcontracting_order.name) + subcontracting_receipt.save() + + # Check consumed_qty for each supplied item + self.assertEqual(len(subcontracting_receipt.supplied_items), 2) + self.assertEqual(subcontracting_receipt.supplied_items[0].consumed_qty, 10) + self.assertEqual(subcontracting_receipt.supplied_items[1].consumed_qty, 10) + def test_supplied_items_cost_after_reposting(self): # Set Backflush Based On as "BOM" set_backflush_based_on("BOM")