From b7699012b2c41ffb226bc870737b7b97f2d74f9b Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Mon, 23 Dec 2024 13:11:44 +0530 Subject: [PATCH] feat: Create subcontracted PO from Material Request (#44745) * feat: Create subcontracted PO from Material Request * fix: Made minor changes in logic to pass all test cases * refactor: Made changes suggested by mentor and simplified logic * test: Made changes to tests --- erpnext/controllers/status_updater.py | 7 ++ .../material_request/material_request.js | 6 ++ .../material_request/material_request.json | 4 +- .../material_request/material_request.py | 65 +++++++++++++++---- .../material_request/test_material_request.py | 50 ++++++++++++++ 5 files changed, 119 insertions(+), 13 deletions(-) diff --git a/erpnext/controllers/status_updater.py b/erpnext/controllers/status_updater.py index c953ab48a1c..a221f6e7cb8 100644 --- a/erpnext/controllers/status_updater.py +++ b/erpnext/controllers/status_updater.py @@ -421,6 +421,13 @@ class StatusUpdater(Document): if d.doctype != args["source_dt"]: continue + if ( + d.get("material_request") + and frappe.db.get_value("Material Request", d.material_request, "material_request_type") + == "Subcontracting" + ): + args.update({"source_field": "fg_item_qty"}) + self._update_modified(args, update_modified) # updates qty in the child table diff --git a/erpnext/stock/doctype/material_request/material_request.js b/erpnext/stock/doctype/material_request/material_request.js index d557116961c..7a8b83bba5c 100644 --- a/erpnext/stock/doctype/material_request/material_request.js +++ b/erpnext/stock/doctype/material_request/material_request.js @@ -122,6 +122,12 @@ frappe.ui.form.on("Material Request", { () => frm.events.make_purchase_order(frm), __("Create") ); + } else if (frm.doc.material_request_type === "Subcontracting") { + frm.add_custom_button( + __("Subcontracted Purchase Order"), + () => frm.events.make_purchase_order(frm), + __("Create") + ); } } diff --git a/erpnext/stock/doctype/material_request/material_request.json b/erpnext/stock/doctype/material_request/material_request.json index e3a5df10d94..1684d531889 100644 --- a/erpnext/stock/doctype/material_request/material_request.json +++ b/erpnext/stock/doctype/material_request/material_request.json @@ -80,7 +80,7 @@ "in_list_view": 1, "in_standard_filter": 1, "label": "Purpose", - "options": "Purchase\nMaterial Transfer\nMaterial Issue\nManufacture\nCustomer Provided", + "options": "Purchase\nMaterial Transfer\nMaterial Issue\nManufacture\nSubcontracting\nCustomer Provided", "reqd": 1 }, { @@ -357,7 +357,7 @@ "idx": 70, "is_submittable": 1, "links": [], - "modified": "2024-03-27 13:10:04.971211", + "modified": "2024-12-16 12:46:02.262167", "modified_by": "Administrator", "module": "Stock", "name": "Material Request", diff --git a/erpnext/stock/doctype/material_request/material_request.py b/erpnext/stock/doctype/material_request/material_request.py index 87dd188fd8c..66e0def290a 100644 --- a/erpnext/stock/doctype/material_request/material_request.py +++ b/erpnext/stock/doctype/material_request/material_request.py @@ -18,6 +18,9 @@ from erpnext.controllers.buying_controller import BuyingController from erpnext.manufacturing.doctype.work_order.work_order import get_item_details from erpnext.stock.doctype.item.item import get_item_defaults from erpnext.stock.stock_balance import get_indented_qty, update_bin_qty +from erpnext.subcontracting.doctype.subcontracting_bom.subcontracting_bom import ( + get_subcontracting_boms_for_finished_goods, +) form_grid_templates = {"items": "templates/form_grid/material_request_grid.html"} @@ -40,7 +43,12 @@ class MaterialRequest(BuyingController): job_card: DF.Link | None letter_head: DF.Link | None material_request_type: DF.Literal[ - "Purchase", "Material Transfer", "Material Issue", "Manufacture", "Customer Provided" + "Purchase", + "Material Transfer", + "Material Issue", + "Manufacture", + "Subcontracting", + "Customer Provided", ] naming_series: DF.Literal["MAT-MR-.YYYY.-"] per_ordered: DF.Percent @@ -385,6 +393,22 @@ def update_item(obj, target, source_parent): if getdate(target.schedule_date) < getdate(nowdate()): target.schedule_date = None + if target.fg_item: + target.fg_item_qty = obj.stock_qty + if sc_bom := get_subcontracting_boms_for_finished_goods(target.fg_item): + target.item_code = sc_bom.service_item + target.uom = sc_bom.service_item_uom + target.conversion_factor = ( + frappe.db.get_value( + "UOM Conversion Detail", + {"parent": sc_bom.service_item, "uom": sc_bom.service_item_uom}, + "conversion_factor", + ) + or 1 + ) + target.qty = target.fg_item_qty * sc_bom.conversion_factor + target.stock_qty = target.qty * target.conversion_factor + def get_list_context(context=None): from erpnext.controllers.website_list_for_contact import get_list_context @@ -416,11 +440,18 @@ def make_purchase_order(source_name, target_doc=None, args=None): if isinstance(args, str): args = json.loads(args) + is_subcontracted = ( + frappe.db.get_value("Material Request", source_name, "material_request_type") == "Subcontracting" + ) + def postprocess(source, target_doc): + target_doc.is_subcontracted = is_subcontracted if frappe.flags.args and frappe.flags.args.default_supplier: # items only for given default supplier supplier_items = [] for d in target_doc.items: + if is_subcontracted and not d.item_code: + continue default_supplier = get_item_defaults(d.item_code, target_doc.company).get("default_supplier") if frappe.flags.args.default_supplier == default_supplier: supplier_items.append(d) @@ -436,25 +467,37 @@ def make_purchase_order(source_name, target_doc=None, args=None): return qty < d.stock_qty and child_filter + def generate_field_map(): + field_map = [ + ["name", "material_request_item"], + ["parent", "material_request"], + ["sales_order", "sales_order"], + ["sales_order_item", "sales_order_item"], + ["wip_composite_asset", "wip_composite_asset"], + ] + + if is_subcontracted: + field_map.extend([["item_code", "fg_item"], ["qty", "fg_item_qty"]]) + else: + field_map.extend([["uom", "stock_uom"], ["uom", "uom"]]) + + return field_map + doclist = get_mapped_doc( "Material Request", source_name, { "Material Request": { "doctype": "Purchase Order", - "validation": {"docstatus": ["=", 1], "material_request_type": ["=", "Purchase"]}, + "validation": { + "docstatus": ["=", 1], + "material_request_type": ["in", ["Purchase", "Subcontracting"]], + }, }, "Material Request Item": { "doctype": "Purchase Order Item", - "field_map": [ - ["name", "material_request_item"], - ["parent", "material_request"], - ["uom", "stock_uom"], - ["uom", "uom"], - ["sales_order", "sales_order"], - ["sales_order_item", "sales_order_item"], - ["wip_composite_asset", "wip_composite_asset"], - ], + "field_map": generate_field_map(), + "field_no_map": ["item_code", "item_name", "qty"] if is_subcontracted else [], "postprocess": update_item, "condition": select_item, }, diff --git a/erpnext/stock/doctype/material_request/test_material_request.py b/erpnext/stock/doctype/material_request/test_material_request.py index 4c87cf573d3..309413fefe9 100644 --- a/erpnext/stock/doctype/material_request/test_material_request.py +++ b/erpnext/stock/doctype/material_request/test_material_request.py @@ -6,6 +6,7 @@ import frappe +import frappe.model from frappe.tests import IntegrationTestCase, UnitTestCase from frappe.utils import flt, today @@ -53,6 +54,55 @@ class TestMaterialRequest(IntegrationTestCase): self.assertEqual(po.doctype, "Purchase Order") self.assertEqual(len(po.get("items")), len(mr.get("items"))) + def test_make_subcontracted_purchase_order(self): + from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom + from erpnext.stock.doctype.item.test_item import create_item, make_item + from erpnext.subcontracting.doctype.subcontracting_bom.test_subcontracting_bom import ( + create_subcontracting_bom, + ) + + mr = frappe.copy_doc(self.globalTestRecords["Material Request"][0]).insert() + mr.material_request_type = "Subcontracting" + mr.submit() + + frappe.db.set_value("Item", mr.items[0].item_code, "is_sub_contracted_item", 1) + + raw_materials = ["Raw Material Item 1", "Raw Material Item 2"] + for item in raw_materials: + create_item(item) + + frappe.new_doc("UOM").update({"uom_name": "Test UOM"}).save() + service_item = make_item( + properties={"is_stock_item": 0}, uoms=[{"uom": "Test UOM", "conversion_factor": 3}] + ) + + mr.items[0].default_bom = make_bom(item=mr.items[0].item_code, raw_materials=raw_materials) + mr.reload() + + create_subcontracting_bom( + finished_good=mr.items[0].item_code, + service_item=service_item.name, + finished_good_qty=2, + service_item_qty=1, + service_item_uom="Test UOM", + ) + + po = make_purchase_order(mr.name) + po.supplier = "_Test Supplier" + po.items[0].schedule_date = today() + po.items.pop(1) + + # Test 1 - Test if items stock qty, qty and finished good qty are calculated correctly based on provided UOMs + self.assertEqual(po.items[0].stock_qty, 81) + self.assertEqual(po.items[0].qty, 27) + self.assertEqual(po.items[0].fg_item_qty, 54) + + po.submit() + mr.reload() + + # Test 2 - MR items ordered qty should be updated based on PO items qty when submitted + self.assertEqual(mr.items[0].ordered_qty, 54) + def test_make_supplier_quotation(self): mr = frappe.copy_doc(self.globalTestRecords["Material Request"][0]).insert()