[Feature] Create Raw Material Request from Sales Order (#15452)

* Add new button to Sales Order form - Request for Raw Materials

* Modify get_work_order_items function

* Commonify functions in Production Plan to make it compatible with new feature

* Create and submit Material Request from Sales Order

* Link Sales Order with Material Request

* Minor

* Rename label

* Fix Codacy

* Modify as per review suggestions

- Move dialog to a new function
- Move checkboxes below other fields

* Minor changes

* Check for permissions

* Add common checkboxes for all items

* Fix codacy

* fix: Travis

* fix: Use variable to store query result

* fix: Add comment before fetching exploded items

* refactor: Break into multiple functions

* test: Add test case
diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.js b/erpnext/manufacturing/doctype/production_plan/production_plan.js
index e74a375..dbbf3d3 100644
--- a/erpnext/manufacturing/doctype/production_plan/production_plan.js
+++ b/erpnext/manufacturing/doctype/production_plan/production_plan.js
@@ -104,13 +104,26 @@
 			}
 		});
 	},
-	
+
 	get_items_for_mr: function(frm) {
 		frappe.call({
-			method: "get_items_for_material_requests",
+			method: "erpnext.manufacturing.doctype.production_plan.production_plan.get_items_for_material_requests",
 			freeze: true,
-			doc: frm.doc,
-			callback: function() {
+			args: {doc: frm.doc},
+			callback: function(r) {
+				if(r.message) {
+					frm.set_value('mr_items', []);
+					$.each(r.message, function(i, d) {
+						var item = frm.add_child('mr_items');
+						item.actual_qty = d.actual_qty;
+						item.item_code = d.item_code;
+						item.item_name = d.item_name;
+						item.min_order_qty = d.min_order_qty;
+						item.quantity = d.quantity;
+						item.sales_order = d.sales_order;
+						item.warehouse = d.warehouse;
+					});
+				}
 				refresh_field('mr_items');
 			}
 		});
diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py
index 12f2f04..d6b62b3 100644
--- a/erpnext/manufacturing/doctype/production_plan/production_plan.py
+++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py
@@ -10,6 +10,7 @@
 from frappe.utils import cstr, flt, cint, nowdate, add_days, comma_and, now_datetime
 from erpnext.manufacturing.doctype.work_order.work_order import get_item_details
 from six import string_types
+from erpnext.setup.doctype.item_group.item_group import get_item_group_defaults
 
 class ProductionPlan(Document):
 	def validate(self):
@@ -281,102 +282,6 @@
 
 		return item_dict
 
-	def get_items_for_material_requests(self):
-		self.mr_items = []
-
-		for data in self.po_items:
-			bom_wise_item_details = {}
-			if not data.planned_qty:
-				frappe.throw(_("For row {0}: Enter planned qty").format(data.idx))
-
-			if data.include_exploded_items and data.bom_no and self.include_subcontracted_items:
-				for d in frappe.db.sql("""select bei.item_code, item.default_bom as bom,
-						ifnull(sum(bei.stock_qty/ifnull(bom.quantity, 1)), 0) as qty, item.item_name,
-						bei.description, bei.stock_uom, item.min_order_qty, bei.source_warehouse,
-						item.default_material_request_type, item.min_order_qty, item_default.default_warehouse
-					from
-						`tabBOM Explosion Item` bei
-						JOIN `tabBOM` bom ON bom.name = bei.parent
-						JOIN `tabItem` item ON item.name = bei.item_code
-						LEFT JOIN `tabItem Default` item_default
-							ON item_default.parent = item.name and item_default.company=%s
-					where
-						bei.docstatus < 2
-						and bom.name=%s and item.is_stock_item in (1, {0})
-					group by bei.item_code, bei.stock_uom""".format(0 if self.include_non_stock_items else 1),
-					(self.company, data.bom_no), as_dict=1):
-						bom_wise_item_details.setdefault(d.item_code, d)
-			else:
-				bom_wise_item_details = self.get_subitems(data, bom_wise_item_details, data.bom_no, 1)
-
-			for item, item_details in bom_wise_item_details.items():
-				if item_details.qty > 0:
-					self.add_item_in_material_request_items(item, item_details, data)
-
-	def get_subitems(self, data, bom_wise_item_details, bom_no, parent_qty):
-		items = frappe.db.sql("""
-			SELECT
-				bom_item.item_code, default_material_request_type, item.item_name,
-				ifnull(%(parent_qty)s * sum(bom_item.stock_qty/ifnull(bom.quantity, 1)), 0) as qty,
-				item.is_sub_contracted_item as is_sub_contracted, bom_item.source_warehouse,
-				item.default_bom as default_bom, bom_item.description as description,
-				bom_item.stock_uom as stock_uom, item.min_order_qty as min_order_qty,
-				item_default.default_warehouse
-			FROM
-				`tabBOM Item` bom_item
-				JOIN `tabBOM` bom ON bom.name = bom_item.parent
-				JOIN tabItem item ON bom_item.item_code = item.name
-				LEFT JOIN `tabItem Default` item_default
-					ON item.name = item_default.parent and item_default.company = %(company)s
-			where
-				bom.name = %(bom)s
-				and bom_item.docstatus < 2
-				and item.is_stock_item in (1, {0})
-			group by bom_item.item_code""".format(0 if self.include_non_stock_items else 1),{
-				'bom': bom_no,
-				'parent_qty': parent_qty,
-				'company': self.company
-			}, as_dict=1)
-
-		for d in items:
-			if not data.include_exploded_items or not d.default_bom:
-				if d.item_code in bom_wise_item_details:
-					bom_wise_item_details[d.item_code].qty = bom_wise_item_details[d.item_code].qty + d.qty
-				else:
-					bom_wise_item_details[d.item_code] = d
-
-			if data.include_exploded_items and d.default_bom:
-				if ((d.default_material_request_type in ["Manufacture", "Purchase"] and
-					not d.is_sub_contracted) or (d.is_sub_contracted and self.include_subcontracted_items)):
-					if d.qty > 0:
-						self.get_subitems(data, bom_wise_item_details, d.default_bom, d.qty)
-
-		return bom_wise_item_details
-
-	def add_item_in_material_request_items(self, item, row, data):
-		total_qty = row.qty * data.planned_qty
-		projected_qty, actual_qty = get_bin_details(row)
-
-		requested_qty = 0
-		if self.ignore_existing_ordered_qty:
-			requested_qty = total_qty
-		else:
-			requested_qty = total_qty - projected_qty
-
-		if requested_qty > 0 and requested_qty < row.min_order_qty:
-			requested_qty = row.min_order_qty
-
-		if requested_qty > 0:
-			self.append('mr_items', {
-				'item_code': item,
-				'item_name': row.item_name,
-				'quantity': requested_qty,
-				'warehouse': row.source_warehouse or row.default_warehouse,
-				'actual_qty': actual_qty,
-				'min_order_qty': row.min_order_qty,
-				'sales_order': data.sales_order
-			})
-
 	def make_work_order(self):
 		wo_list = []
 		self.validate_data()
@@ -466,6 +371,87 @@
 		else :
 			msgprint(_("No material request created"))
 
+def get_exploded_items(bom_wise_item_details, company, bom_no, include_non_stock_items):
+	for d in frappe.db.sql("""select bei.item_code, item.default_bom as bom,
+			ifnull(sum(bei.stock_qty/ifnull(bom.quantity, 1)), 0) as qty, item.item_name,
+			bei.description, bei.stock_uom, item.min_order_qty, bei.source_warehouse,
+			item.default_material_request_type, item.min_order_qty, item_default.default_warehouse
+		from
+			`tabBOM Explosion Item` bei
+			JOIN `tabBOM` bom ON bom.name = bei.parent
+			JOIN `tabItem` item ON item.name = bei.item_code
+			LEFT JOIN `tabItem Default` item_default
+				ON item_default.parent = item.name and item_default.company=%s
+		where
+			bei.docstatus < 2
+			and bom.name=%s and item.is_stock_item in (1, {0})
+		group by bei.item_code, bei.stock_uom""".format(0 if include_non_stock_items else 1),
+		(company, bom_no), as_dict=1):
+			bom_wise_item_details.setdefault(d.get('item_code'), d)
+	return bom_wise_item_details
+
+def get_subitems(doc, data, bom_wise_item_details, bom_no, company, include_non_stock_items, include_subcontracted_items, parent_qty):
+	items = frappe.db.sql("""
+		SELECT
+			bom_item.item_code, default_material_request_type, item.item_name,
+			ifnull(%(parent_qty)s * sum(bom_item.stock_qty/ifnull(bom.quantity, 1)), 0) as qty,
+			item.is_sub_contracted_item as is_sub_contracted, bom_item.source_warehouse,
+			item.default_bom as default_bom, bom_item.description as description,
+			bom_item.stock_uom as stock_uom, item.min_order_qty as min_order_qty,
+			item_default.default_warehouse
+		FROM
+			`tabBOM Item` bom_item
+			JOIN `tabBOM` bom ON bom.name = bom_item.parent
+			JOIN tabItem item ON bom_item.item_code = item.name
+			LEFT JOIN `tabItem Default` item_default
+				ON item.name = item_default.parent and item_default.company = %(company)s
+		where
+			bom.name = %(bom)s
+			and bom_item.docstatus < 2
+			and item.is_stock_item in (1, {0})
+		group by bom_item.item_code""".format(0 if include_non_stock_items else 1),{
+			'bom': bom_no,
+			'parent_qty': parent_qty,
+			'company': company
+		}, as_dict=1)
+
+	for d in items:
+		if not data.get('include_exploded_items') or not d.default_bom:
+			if d.item_code in bom_wise_item_details:
+				bom_wise_item_details[d.item_code].qty = bom_wise_item_details[d.item_code].qty + d.qty
+			else:
+				bom_wise_item_details[d.item_code] = d
+
+		if data.get('include_exploded_items') and d.default_bom:
+			if ((d.default_material_request_type in ["Manufacture", "Purchase"] and
+				not d.is_sub_contracted) or (d.is_sub_contracted and include_subcontracted_items)):
+				if d.qty > 0:
+					get_subitems(doc, data, bom_wise_item_details, d.default_bom, company, include_non_stock_items, include_subcontracted_items, d.qty)
+	return bom_wise_item_details
+
+def add_item_in_material_request_items(doc, planned_qty, ignore_existing_ordered_qty, item, row, data, warehouse, company):
+	total_qty = row.qty * planned_qty
+	projected_qty, actual_qty = get_bin_details(row)
+
+	requested_qty = 0
+	if ignore_existing_ordered_qty:
+		requested_qty = total_qty
+	else:
+		requested_qty = total_qty - projected_qty
+	if requested_qty > 0 and requested_qty < row.min_order_qty:
+		requested_qty = row.min_order_qty
+	item_group_defaults = get_item_group_defaults(item, company)
+	if requested_qty > 0:
+		doc.setdefault('mr_items', []).append({
+			'item_code': item,
+			'item_name': row.item_name,
+			'quantity': requested_qty,
+			'warehouse': warehouse or row.source_warehouse or row.default_warehouse or item_group_defaults.get("default_warehouse"),
+			'actual_qty': actual_qty,
+			'min_order_qty': row.min_order_qty,
+			'sales_order': data.get('sales_order')
+		})
+
 def get_sales_orders(self):
 	so_filter = item_filter = ""
 	if self.from_date:
@@ -520,3 +506,46 @@
 	""".format(conditions=conditions), { "item_code": row.item_code }, as_list=1)
 
 	return item_projected_qty and item_projected_qty[0] or (0,0)
+
+@frappe.whitelist()
+def get_items_for_material_requests(doc, company=None):
+	if isinstance(doc, string_types):
+		doc = frappe._dict(json.loads(doc))
+
+	doc['mr_items'] = []
+	po_items = doc['po_items'] if doc.get('po_items') else doc['items']
+
+	for data in po_items:
+		warehouse = None
+		bom_wise_item_details = {}
+
+		if data.get('required_qty'):
+			planned_qty = data.get('required_qty')
+			bom_no = data.get('bom')
+			ignore_existing_ordered_qty = data.get('ignore_existing_ordered_qty')
+			include_non_stock_items = 1
+			warehouse = data.get('for_warehouse')
+			if data.get('include_exploded_items'):
+				include_subcontracted_items = 1
+			else:
+				include_subcontracted_items = 0
+		else:
+			planned_qty = data.get('planned_qty')
+			bom_no = data.get('bom_no')
+			include_subcontracted_items = doc['include_subcontracted_items']
+			company = doc['company']
+			include_non_stock_items = doc['include_non_stock_items']
+			ignore_existing_ordered_qty = doc['ignore_existing_ordered_qty']
+		if not planned_qty:
+			frappe.throw(_("For row {0}: Enter Planned Qty").format(data.get('idx')))
+
+		if data.get('include_exploded_items') and bom_no and include_subcontracted_items:
+			# fetch exploded items from BOM
+			bom_wise_item_details = get_exploded_items(bom_wise_item_details, company, bom_no, include_non_stock_items)
+		else:
+			bom_wise_item_details = get_subitems(doc, data, bom_wise_item_details, bom_no, company, include_non_stock_items, include_subcontracted_items, 1)
+		for item, item_details in bom_wise_item_details.items():
+			if item_details.qty > 0:
+				add_item_in_material_request_items(doc, planned_qty, ignore_existing_ordered_qty, item, item_details, data, warehouse, company)
+
+	return doc['mr_items']
diff --git a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py
index 7cf4268..a33d42b 100644
--- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py
+++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py
@@ -10,6 +10,7 @@
 from erpnext.manufacturing.doctype.production_plan.production_plan import get_sales_orders
 from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import create_stock_reconciliation
 from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
+from erpnext.manufacturing.doctype.production_plan.production_plan import get_items_for_material_requests
 
 class TestProductionPlan(unittest.TestCase):
 	def setUp(self):
@@ -160,7 +161,9 @@
 			'planned_start_date': args.planned_start_date or now_datetime()
 		}]
 	})
-	pln.get_items_for_material_requests()
+	mr_items = get_items_for_material_requests(pln.as_dict())
+	for d in mr_items:
+		pln.append('mr_items', d)
 	
 	if not args.do_not_save:
 		pln.insert()
diff --git a/erpnext/selling/doctype/sales_order/sales_order.js b/erpnext/selling/doctype/sales_order/sales_order.js
index 5275e2e..54d7654 100644
--- a/erpnext/selling/doctype/sales_order/sales_order.js
+++ b/erpnext/selling/doctype/sales_order/sales_order.js
@@ -150,6 +150,8 @@
 					&& flt(doc.per_delivered, 6) < 100) {
 					this.frm.add_custom_button(__('Material Request'),
 						function() { me.make_material_request() }, __("Make"));
+					this.frm.add_custom_button(__('Request for Raw Materials'),
+						function() { me.make_raw_material_request() }, __("Make"));
 				}
 
 				// make purchase order
@@ -313,6 +315,86 @@
 		})
 	},
 
+	make_raw_material_request: function() {
+		var me = this;
+		this.frm.call({
+			doc: this.frm.doc,
+			method: 'get_work_order_items',
+			args: {
+				for_raw_material_request: 1
+			},
+			callback: function(r) {
+				if(!r.message) {
+					frappe.msgprint({
+						message: __('No Items with Bill of Materials.'),
+						indicator: 'orange'
+					});
+					return;
+				}
+				else {
+					me.make_raw_material_request_dialog(r);
+				}
+			}
+		});
+	},
+
+	make_raw_material_request_dialog: function(r) {
+		var fields = [
+			{fieldtype:'Check', fieldname:'include_exploded_items',
+				label: __('Include Exploded Items')},
+			{fieldtype:'Check', fieldname:'ignore_existing_ordered_qty',
+				label: __('Ignore Existing Ordered Qty')},
+			{
+				fieldtype:'Table', fieldname: 'items',
+				description: __('Select BOM, Qty and For Warehouse'),
+				fields: [
+					{fieldtype:'Read Only', fieldname:'item_code',
+						label: __('Item Code'), in_list_view:1},
+					{fieldtype:'Link', fieldname:'bom', options: 'BOM', reqd: 1,
+						label: __('BOM'), in_list_view:1, get_query: function(doc) {
+							return {filters: {item: doc.item_code}};
+						}
+					},
+					{fieldtype:'Float', fieldname:'required_qty', reqd: 1,
+						label: __('Qty'), in_list_view:1},
+					{fieldtype:'Link', fieldname:'for_warehouse', options: 'Warehouse',
+						label: __('For Warehouse')}
+				],
+				data: r.message,
+				get_data: function() {
+					return r.message
+				}
+			}
+		]
+		var d = new frappe.ui.Dialog({
+			title: __("Select from Items having BOM"),
+			fields: fields,
+			primary_action: function() {
+				var data = d.get_values();
+				me.frm.call({
+					method: 'erpnext.selling.doctype.sales_order.sales_order.make_raw_material_request',
+					args: {
+						items: data,
+						company: me.frm.doc.company,
+						sales_order: me.frm.docname,
+						project: me.frm.project
+					},
+					freeze: true,
+					callback: function(r) {
+						if(r.message) {
+							frappe.msgprint(__('Material Request {0} submitted.',
+							['<a href="#Form/Material Request/'+r.message.name+'">' + r.message.name+ '</a>']));
+						}
+						d.hide();
+						me.frm.reload_doc();
+					}
+				});
+			},
+			primary_action_label: __('Make')
+		});
+		d.show();
+	},
+
 	make_delivery_note_based_on_delivery_date: function() {
 		var me = this;
 
@@ -423,7 +505,7 @@
 							filters: {'parent': me.frm.doc.name}
 						}
 					}},
-				
+
 				{"fieldtype": "Button", "label": __("Make Purchase Order"), "fieldname": "make_purchase_order", "cssClass": "btn-primary"},
 			]
 		});
diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py
index a7b4a3e..5f435ce 100755
--- a/erpnext/selling/doctype/sales_order/sales_order.py
+++ b/erpnext/selling/doctype/sales_order/sales_order.py
@@ -5,8 +5,9 @@
 import frappe
 import json
 import frappe.utils
-from frappe.utils import cstr, flt, getdate, comma_and, cint, nowdate
+from frappe.utils import cstr, flt, getdate, comma_and, cint, nowdate, add_days
 from frappe import _
+from six import string_types
 from frappe.model.utils import get_fetch_values
 from frappe.model.mapper import get_mapped_doc
 from erpnext.stock.stock_balance import update_bin_qty, get_reserved_qty
@@ -17,6 +18,7 @@
 from erpnext.selling.doctype.customer.customer import check_credit_limit
 from erpnext.stock.doctype.item.item import get_item_defaults
 from erpnext.setup.doctype.item_group.item_group import get_item_group_defaults
+from erpnext.manufacturing.doctype.production_plan.production_plan import get_items_for_material_requests
 
 form_grid_templates = {
 	"items": "templates/form_grid/item_grid.html"
@@ -366,7 +368,7 @@
 			self.indicator_color = "green"
 			self.indicator_title = _("Paid")
 
-	def get_work_order_items(self):
+	def get_work_order_items(self, for_raw_material_request=0):
 		'''Returns items with BOM that already do not have a linked work order'''
 		items = []
 
@@ -375,8 +377,13 @@
 				bom = get_default_bom_item(i.item_code)
 				if bom:
 					stock_qty = i.qty if i.doctype == 'Packed Item' else i.stock_qty
-					pending_qty= stock_qty - flt(frappe.db.sql('''select sum(qty) from `tabWork Order`
+					if not for_raw_material_request:
+						total_work_order_qty = flt(frappe.db.sql('''select sum(qty) from `tabWork Order`
 							where production_item=%s and sales_order=%s and sales_order_item = %s and docstatus<2''', (i.item_code, self.name, i.name))[0][0])
+						pending_qty = stock_qty - total_work_order_qty
+					else:
+						pending_qty = stock_qty
+
 					if pending_qty:
 						items.append(dict(
 							name= i.name,
@@ -384,6 +391,7 @@
 							bom = bom,
 							warehouse = i.warehouse,
 							pending_qty = pending_qty,
+							required_qty = pending_qty if for_raw_material_request else 0,
 							sales_order_item = i.name
 						))
 		return items
@@ -846,7 +854,7 @@
 				or supplier_name like %(txt)s)
 			and name in (select supplier from `tabSales Order Item` where parent = %(parent)s)
 			and name not in (select supplier from `tabPurchase Order` po inner join `tabPurchase Order Item` poi
-			     on po.name=poi.parent where po.docstatus<2 and poi.sales_order=%(parent)s)			
+			     on po.name=poi.parent where po.docstatus<2 and poi.sales_order=%(parent)s)
 		order by
 			if(locate(%(_txt)s, name), locate(%(_txt)s, name), 99999),
 			if(locate(%(_txt)s, supplier_name), locate(%(_txt)s, supplier_name), 99999),
@@ -902,3 +910,44 @@
 	bom = bom[0].name if bom else None
 
 	return bom
+
+@frappe.whitelist()
+def make_raw_material_request(items, company, sales_order, project=None):
+	if not frappe.has_permission("Sales Order", "write"):
+		frappe.throw(_("Not permitted"), frappe.PermissionError)
+
+	if isinstance(items, string_types):
+		items = frappe._dict(json.loads(items))
+
+	for item in items.get('items'):
+		item["include_exploded_items"] = items.get('include_exploded_items')
+		item["ignore_existing_ordered_qty"] = items.get('ignore_existing_ordered_qty')
+
+	raw_materials = get_items_for_material_requests(items, company)
+	if not raw_materials:
+		frappe.msgprint(_("Material Request not created, as quantity for Raw Materials already available."))
+
+	material_request = frappe.new_doc('Material Request')
+	material_request.update(dict(
+		doctype = 'Material Request',
+		transaction_date = nowdate(),
+		company = company,
+		requested_by = frappe.session.user,
+		material_request_type = 'Purchase'
+	))
+	for item in raw_materials:
+		item_doc = frappe.get_cached_doc('Item', item.get('item_code'))
+		schedule_date = add_days(nowdate(), cint(item_doc.lead_time_days))
+		material_request.append('items', {
+		'item_code': item.get('item_code'),
+		'qty': item.get('quantity'),
+		'schedule_date': schedule_date,
+		'warehouse': item.get('warehouse'),
+		'sales_order': sales_order,
+		'project': project
+		})
+	material_request.insert()
+	material_request.flags.ignore_permissions = 1
+	material_request.run_method("set_missing_values")
+	material_request.submit()
+	return material_request
\ No newline at end of file
diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py
index 538ea55..df92cb8 100644
--- a/erpnext/selling/doctype/sales_order/test_sales_order.py
+++ b/erpnext/selling/doctype/sales_order/test_sales_order.py
@@ -11,8 +11,7 @@
 from erpnext.selling.doctype.sales_order.sales_order import make_work_orders
 from erpnext.controllers.accounts_controller import update_child_qty_rate
 import json
-
-
+from erpnext.selling.doctype.sales_order.sales_order import make_raw_material_request
 class TestSalesOrder(unittest.TestCase):
 	def tearDown(self):
 		frappe.set_user("Administrator")
@@ -327,9 +326,8 @@
 		self.assertRaises(frappe.CancelledLinkError, dn.submit)
 
 	def test_service_type_product_bundle(self):
-		from erpnext.stock.doctype.item.test_item import make_item
 		from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle
-
+		from erpnext.stock.doctype.item.test_item import make_item
 		make_item("_Test Service Product Bundle", {"is_stock_item": 0})
 		make_item("_Test Service Product Bundle Item 1", {"is_stock_item": 0})
 		make_item("_Test Service Product Bundle Item 2", {"is_stock_item": 0})
@@ -343,9 +341,8 @@
 		self.assertTrue("_Test Service Product Bundle Item 2" in [d.item_code for d in so.packed_items])
 
 	def test_mix_type_product_bundle(self):
-		from erpnext.stock.doctype.item.test_item import make_item
 		from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle
-
+		from erpnext.stock.doctype.item.test_item import make_item
 		make_item("_Test Mix Product Bundle", {"is_stock_item": 0})
 		make_item("_Test Mix Product Bundle Item 1", {"is_stock_item": 1})
 		make_item("_Test Mix Product Bundle Item 2", {"is_stock_item": 0})
@@ -388,11 +385,10 @@
 
 	def test_drop_shipping(self):
 		from erpnext.selling.doctype.sales_order.sales_order import make_purchase_order_for_drop_shipment
-		from erpnext.stock.doctype.item.test_item import make_item
 		from erpnext.buying.doctype.purchase_order.purchase_order import update_status
 
 		make_stock_entry(target="_Test Warehouse - _TC", qty=10, rate=100)
-
+		from erpnext.stock.doctype.item.test_item import make_item
 		po_item = make_item("_Test Item for Drop Shipping", {"is_stock_item": 1, "delivered_by_supplier": 1})
 
 		dn_item = make_item("_Test Regular Item", {"is_stock_item": 1})
@@ -585,8 +581,8 @@
 			self.assertEquals(wo_qty[0][0], so_item_name.get(item))
 
 	def test_serial_no_based_delivery(self):
-		from erpnext.stock.doctype.item.test_item import make_item
 		frappe.set_value("Stock Settings", None, "automatically_set_serial_nos_based_on_fifo", 1)
+		from erpnext.stock.doctype.item.test_item import make_item
 		item = make_item("_Reserved_Serialized_Item", {"is_stock_item": 1,
 					"maintain_stock": 1,
 					"has_serial_no": 1,
@@ -685,6 +681,55 @@
 		se.cancel()
 		self.assertFalse(frappe.db.exists("Serial No", {"sales_order": so.name}))
 
+	def test_request_for_raw_materials(self):
+		from erpnext.stock.doctype.item.test_item import make_item
+		item = make_item("_Test Finished Item", {"is_stock_item": 1,
+			"maintain_stock": 1,
+			"valuation_rate": 500,
+			"item_defaults": [
+				{
+					"default_warehouse": "_Test Warehouse - _TC",
+					"company": "_Test Company"
+				}]
+			})
+		make_item("_Test Raw Item A", {"maintain_stock": 1,
+					"valuation_rate": 100,
+					"item_defaults": [
+						{
+							"default_warehouse": "_Test Warehouse - _TC",
+							"company": "_Test Company"
+						}]
+					})
+		make_item("_Test Raw Item B", {"maintain_stock": 1,
+					"valuation_rate": 200,
+					"item_defaults": [
+						{
+							"default_warehouse": "_Test Warehouse - _TC",
+							"company": "_Test Company"
+						}]
+					})
+		from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom
+		make_bom(item=item.item_code, rate=1000,
+			raw_materials = ['_Test Raw Item A', '_Test Raw Item B'])
+
+		so = make_sales_order(**{
+			"item_list": [{
+				"item_code": item.item_code,
+				"qty": 1,
+				"rate":1000
+			}]
+		})
+		so.submit()
+		mr_dict = frappe._dict()
+		items = so.get_work_order_items(1)
+		mr_dict['items'] = items
+		mr_dict['include_exploded_items'] = 0
+		mr_dict['ignore_existing_ordered_qty'] = 1
+		make_raw_material_request(mr_dict, so.company, so.name)
+		mr = frappe.db.sql("""select name from `tabMaterial Request` ORDER BY creation DESC LIMIT 1""", as_dict=1)[0]
+		mr_doc = frappe.get_doc('Material Request',mr.get('name'))
+		self.assertEqual(mr_doc.items[0].sales_order, so.name)
+
 def make_sales_order(**args):
 	so = frappe.new_doc("Sales Order")
 	args = frappe._dict(args)
@@ -714,7 +759,7 @@
 		})
 
 	so.delivery_date = add_days(so.transaction_date, 10)
-
+ 
 	if not args.do_not_save:
 		so.insert()
 		if not args.do_not_submit: