Merge branch 'develop' into putaway
diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.js b/erpnext/buying/doctype/purchase_order/purchase_order.js
index 47483c9..20faded 100644
--- a/erpnext/buying/doctype/purchase_order/purchase_order.js
+++ b/erpnext/buying/doctype/purchase_order/purchase_order.js
@@ -347,7 +347,8 @@
 	make_purchase_receipt: function() {
 		frappe.model.open_mapped_doc({
 			method: "erpnext.buying.doctype.purchase_order.purchase_order.make_purchase_receipt",
-			frm: cur_frm
+			frm: cur_frm,
+			freeze_message: __("Creating Purchase Receipt ...")
 		})
 	},
 
diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py
index c7efb8a..bb67eb9 100644
--- a/erpnext/buying/doctype/purchase_order/purchase_order.py
+++ b/erpnext/buying/doctype/purchase_order/purchase_order.py
@@ -123,8 +123,8 @@
 		if self.is_subcontracted == "Yes":
 			for item in self.items:
 				if not item.bom:
-					frappe.throw(_("BOM is not specified for subcontracting item {0} at row {1}"\
-						.format(item.item_code, item.idx)))
+					frappe.throw(_("BOM is not specified for subcontracting item {0} at row {1}")
+						.format(item.item_code, item.idx))
 
 	def get_schedule_dates(self):
 		for d in self.get('items'):
@@ -349,7 +349,9 @@
 	frappe.local.message_log = []
 
 def set_missing_values(source, target):
+	from erpnext.stock.doctype.putaway_rule.putaway_rule import apply_putaway_rule
 	target.ignore_pricing_rule = 1
+	target.items = apply_putaway_rule(target.items, target.company)
 	target.run_method("set_missing_values")
 	target.run_method("calculate_taxes_and_totals")
 
diff --git a/erpnext/stock/doctype/putaway_rule/__init__.py b/erpnext/stock/doctype/putaway_rule/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/stock/doctype/putaway_rule/__init__.py
diff --git a/erpnext/stock/doctype/putaway_rule/putaway_rule.js b/erpnext/stock/doctype/putaway_rule/putaway_rule.js
new file mode 100644
index 0000000..e056920
--- /dev/null
+++ b/erpnext/stock/doctype/putaway_rule/putaway_rule.js
@@ -0,0 +1,43 @@
+// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('Putaway Rule', {
+	setup: function(frm) {
+		frm.set_query("warehouse", function() {
+			return {
+				"filters": {
+					"company": frm.doc.company,
+					"is_group": 0
+				}
+			};
+		});
+	},
+
+	uom: function(frm) {
+		if (frm.doc.item_code && frm.doc.uom) {
+			return frm.call({
+				method: "erpnext.stock.get_item_details.get_conversion_factor",
+				args: {
+					item_code: frm.doc.item_code,
+					uom: frm.doc.uom
+				},
+				callback: function(r) {
+					if (!r.exc) {
+						let stock_capacity = flt(frm.doc.capacity) * flt(r.message.conversion_factor);
+						frm.set_value('conversion_factor', r.message.conversion_factor);
+						frm.set_value('stock_capacity', stock_capacity);
+					}
+				}
+			});
+		}
+	},
+
+	capacity: function(frm) {
+		let stock_capacity = flt(frm.doc.capacity) * flt(frm.doc.conversion_factor);
+		frm.set_value('stock_capacity', stock_capacity);
+	}
+
+	// refresh: function(frm) {
+
+	// }
+});
diff --git a/erpnext/stock/doctype/putaway_rule/putaway_rule.json b/erpnext/stock/doctype/putaway_rule/putaway_rule.json
new file mode 100644
index 0000000..e5b6b2b
--- /dev/null
+++ b/erpnext/stock/doctype/putaway_rule/putaway_rule.json
@@ -0,0 +1,155 @@
+{
+ "actions": [],
+ "autoname": "format:{item_code}-{warehouse}",
+ "creation": "2020-11-09 11:39:46.489501",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+  "disable",
+  "item_code",
+  "item_name",
+  "warehouse",
+  "priority",
+  "col_break_capacity",
+  "company",
+  "capacity",
+  "uom",
+  "conversion_factor",
+  "stock_uom",
+  "stock_capacity"
+ ],
+ "fields": [
+  {
+   "fieldname": "item_code",
+   "fieldtype": "Link",
+   "in_standard_filter": 1,
+   "label": "Item",
+   "options": "Item",
+   "reqd": 1
+  },
+  {
+   "fetch_from": "item_code.item_name",
+   "fieldname": "item_name",
+   "fieldtype": "Data",
+   "label": "Item Name",
+   "read_only": 1
+  },
+  {
+   "fieldname": "warehouse",
+   "fieldtype": "Link",
+   "in_list_view": 1,
+   "in_standard_filter": 1,
+   "label": "Warehouse",
+   "options": "Warehouse",
+   "reqd": 1
+  },
+  {
+   "fieldname": "col_break_capacity",
+   "fieldtype": "Column Break"
+  },
+  {
+   "default": "0",
+   "fieldname": "capacity",
+   "fieldtype": "Float",
+   "in_list_view": 1,
+   "label": "Capacity",
+   "reqd": 1
+  },
+  {
+   "fetch_from": "item_code.stock_uom",
+   "fieldname": "stock_uom",
+   "fieldtype": "Link",
+   "label": "Stock UOM",
+   "options": "UOM",
+   "read_only": 1
+  },
+  {
+   "default": "1",
+   "fieldname": "priority",
+   "fieldtype": "Int",
+   "label": "Priority"
+  },
+  {
+   "fieldname": "company",
+   "fieldtype": "Link",
+   "in_standard_filter": 1,
+   "label": "Company",
+   "options": "Company",
+   "reqd": 1
+  },
+  {
+   "default": "0",
+   "depends_on": "eval:!doc.__islocal",
+   "fieldname": "disable",
+   "fieldtype": "Check",
+   "label": "Disable"
+  },
+  {
+   "fieldname": "uom",
+   "fieldtype": "Link",
+   "label": "UOM",
+   "options": "UOM"
+  },
+  {
+   "fieldname": "stock_capacity",
+   "fieldtype": "Float",
+   "label": "Capacity in Stock UOM",
+   "read_only": 1
+  },
+  {
+   "default": "1",
+   "fieldname": "conversion_factor",
+   "fieldtype": "Float",
+   "label": "Conversion Factor",
+   "read_only": 1
+  }
+ ],
+ "index_web_pages_for_search": 1,
+ "links": [],
+ "modified": "2020-11-23 19:25:50.948068",
+ "modified_by": "Administrator",
+ "module": "Stock",
+ "name": "Putaway Rule",
+ "owner": "Administrator",
+ "permissions": [
+  {
+   "create": 1,
+   "delete": 1,
+   "email": 1,
+   "export": 1,
+   "print": 1,
+   "read": 1,
+   "report": 1,
+   "role": "Stock Manager",
+   "share": 1,
+   "write": 1
+  },
+  {
+   "create": 1,
+   "delete": 1,
+   "email": 1,
+   "export": 1,
+   "print": 1,
+   "read": 1,
+   "report": 1,
+   "role": "Stock User",
+   "share": 1,
+   "write": 1
+  },
+  {
+   "email": 1,
+   "export": 1,
+   "permlevel": 1,
+   "print": 1,
+   "read": 1,
+   "report": 1,
+   "role": "Stock Manager",
+   "share": 1,
+   "write": 1
+  }
+ ],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/stock/doctype/putaway_rule/putaway_rule.py b/erpnext/stock/doctype/putaway_rule/putaway_rule.py
new file mode 100644
index 0000000..73534aa
--- /dev/null
+++ b/erpnext/stock/doctype/putaway_rule/putaway_rule.py
@@ -0,0 +1,171 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+import frappe
+import copy
+from collections import defaultdict
+from frappe import _
+from frappe.utils import flt, floor, nowdate
+from frappe.model.document import Document
+from erpnext.stock.utils import get_stock_balance
+
+class PutawayRule(Document):
+	def validate(self):
+		self.validate_duplicate_rule()
+		self.validate_warehouse_and_company()
+		self.validate_capacity()
+		self.validate_priority()
+		self.set_stock_capacity()
+
+	def validate_duplicate_rule(self):
+		existing_rule = frappe.db.exists("Putaway Rule", {"item_code": self.item_code, "warehouse": self.warehouse})
+		if existing_rule and existing_rule != self.name:
+			frappe.throw(_("Putaway Rule already exists for Item {0} in Warehouse {1}.")
+				.format(frappe.bold(self.item_code), frappe.bold(self.warehouse)),
+				title=_("Duplicate"))
+
+	def validate_priority(self):
+		if self.priority < 1:
+			frappe.throw(_("Priority cannot be lesser than 1."), title=_("Invalid Priority"))
+
+	def validate_warehouse_and_company(self):
+		company = frappe.db.get_value("Warehouse", self.warehouse, "company")
+		if company != self.company:
+			frappe.throw(_("Warehouse {0} does not belong to Company {1}.")
+				.format(frappe.bold(self.warehouse), frappe.bold(self.company)),
+				title=_("Invalid Warehouse"))
+
+	def validate_capacity(self):
+		balance_qty = get_stock_balance(self.item_code, self.warehouse, nowdate())
+		if flt(self.stock_capacity) < flt(balance_qty):
+			frappe.throw(_("Warehouse Capacity for Item '{0}' must be greater than the existing stock level of {1} qty.")
+				.format(self.item_code, frappe.bold(balance_qty)), title=_("Insufficient Capacity"))
+
+		if not self.capacity:
+			frappe.throw(_("Capacity must be greater than 0"), title=_("Invalid"))
+
+	def set_stock_capacity(self):
+		self.stock_capacity = (flt(self.conversion_factor) or 1) * flt(self.capacity)
+
+@frappe.whitelist()
+def get_ordered_putaway_rules(item_code, company):
+	"""Returns an ordered list of putaway rules to apply on an item."""
+	rules = frappe.get_all("Putaway Rule", fields=["name", "item_code", "stock_capacity", "priority", "warehouse"],
+		filters={"item_code": item_code, "company": company, "disable": 0},
+		order_by="priority asc, capacity desc")
+
+	if not rules:
+		return False, None
+
+	for rule in rules:
+		balance_qty = get_stock_balance(rule.item_code, rule.warehouse, nowdate())
+		free_space = flt(rule.stock_capacity) - flt(balance_qty)
+		if free_space > 0:
+			rule["free_space"] = free_space
+		else:
+			del rule
+
+	if not rules:
+		# After iterating through rules, if no rules are left
+		# then there is not enough space left in any rule
+		return True, None
+
+	rules = sorted(rules, key = lambda i: (i['priority'], -i['free_space']))
+	return False, rules
+
+@frappe.whitelist()
+def apply_putaway_rule(items, company):
+	""" Applies Putaway Rule on line items.
+
+		items: List of Purchase Receipt Item objects
+		company: Company in the Purchase Receipt
+	"""
+	items_not_accomodated, updated_table = [], []
+	item_wise_rules = defaultdict(list)
+
+	def add_row(item, to_allocate, warehouse):
+		new_updated_table_row = copy.deepcopy(item)
+		new_updated_table_row.name = ''
+		new_updated_table_row.idx = 1 if not updated_table else flt(updated_table[-1].idx) + 1
+		new_updated_table_row.qty = to_allocate
+		new_updated_table_row.stock_qty = flt(to_allocate) * flt(new_updated_table_row.conversion_factor)
+		new_updated_table_row.warehouse = warehouse
+		updated_table.append(new_updated_table_row)
+
+	for item in items:
+		conversion = flt(item.conversion_factor)
+		uom_must_be_whole_number = frappe.db.get_value('UOM', item.uom, 'must_be_whole_number')
+		pending_qty, pending_stock_qty, item_code = flt(item.qty), flt(item.stock_qty), item.item_code
+
+		if not pending_qty:
+			add_row(item, pending_qty, item.warehouse)
+			continue
+
+		at_capacity, rules = get_ordered_putaway_rules(item_code, company)
+
+		if not rules:
+			if at_capacity:
+				# rules available, but no free space
+				add_row(item, pending_qty, '')
+				items_not_accomodated.append([item_code, pending_qty])
+			else:
+				# no rules to apply
+				add_row(item, pending_qty, item.warehouse)
+			continue
+
+		# maintain item wise rules, to handle if item is entered twice
+		# in the table, due to different price, etc.
+		if not item_wise_rules[item_code]:
+			item_wise_rules[item_code] = rules
+
+		for rule in item_wise_rules[item_code]:
+			if pending_stock_qty > 0 and rule.free_space:
+				stock_qty_to_allocate = flt(rule.free_space) if pending_stock_qty >= flt(rule.free_space) else pending_stock_qty
+				qty_to_allocate = stock_qty_to_allocate / (conversion or 1)
+
+				if uom_must_be_whole_number:
+					qty_to_allocate = floor(qty_to_allocate)
+					stock_qty_to_allocate = qty_to_allocate * conversion
+
+				if not qty_to_allocate: break
+
+				add_row(item, qty_to_allocate, rule.warehouse)
+
+				pending_stock_qty -= stock_qty_to_allocate
+				pending_qty -= qty_to_allocate
+				rule["free_space"] -= stock_qty_to_allocate
+
+				if not pending_stock_qty: break
+
+		# if pending qty after applying all rules, add row without warehouse
+		if pending_stock_qty > 0:
+			add_row(item, pending_qty, '')
+			items_not_accomodated.append([item.item_code, pending_qty])
+
+	if items_not_accomodated:
+		msg = _("The following Items, having Putaway Rules, could not be accomodated:") + "<br><br>"
+		formatted_item_rows = ""
+
+		for entry in items_not_accomodated:
+			item_link = frappe.utils.get_link_to_form("Item", entry[0])
+			formatted_item_rows += """
+				<td>{0}</td>
+				<td>{1}</td>
+			</tr>""".format(item_link, frappe.bold(entry[1]))
+
+		msg += """
+			<table class="table">
+				<thead>
+					<td>{0}</td>
+					<td>{1}</td>
+				</thead>
+				{2}
+			</table>
+		""".format(_("Item"), _("Unassigned Qty"), formatted_item_rows)
+
+		frappe.msgprint(msg, title=_("Insufficient Capacity"), is_minimizable=True, wide=True)
+
+	return updated_table if updated_table else items
+	# TODO: check pricing rule, item tax impact
\ No newline at end of file
diff --git a/erpnext/stock/doctype/putaway_rule/putaway_rule_list.js b/erpnext/stock/doctype/putaway_rule/putaway_rule_list.js
new file mode 100644
index 0000000..e48c415
--- /dev/null
+++ b/erpnext/stock/doctype/putaway_rule/putaway_rule_list.js
@@ -0,0 +1,10 @@
+frappe.listview_settings['Putaway Rule'] = {
+	add_fields: ["disable"],
+	get_indicator: (doc) => {
+		if (doc.disable) {
+			return [__("Disabled"), "darkgrey", "disable,=,1"];
+		} else {
+			return [__("Active"), "blue", "disable,=,0"];
+		}
+	}
+};
diff --git a/erpnext/stock/doctype/putaway_rule/test_putaway_rule.py b/erpnext/stock/doctype/putaway_rule/test_putaway_rule.py
new file mode 100644
index 0000000..7b81784
--- /dev/null
+++ b/erpnext/stock/doctype/putaway_rule/test_putaway_rule.py
@@ -0,0 +1,261 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+from __future__ import unicode_literals
+import frappe
+import unittest
+from frappe.utils import add_days, nowdate
+from erpnext.stock.doctype.item.test_item import make_item
+from erpnext.stock.get_item_details import get_conversion_factor
+from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
+from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
+from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_receipt
+from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
+
+class TestPutawayRule(unittest.TestCase):
+	def setUp(self):
+		if not frappe.db.exists("Item", "_Rice"):
+			make_item("_Rice", {
+				'is_stock_item': 1,
+				'has_batch_no' : 1,
+				'create_new_batch': 1,
+				'stock_uom': 'Kg'
+			})
+
+		if not frappe.db.exists("Warehouse", {"warehouse_name": "Rack 1"}):
+			create_warehouse("Rack 1")
+		if not frappe.db.exists("Warehouse", {"warehouse_name": "Rack 2"}):
+			create_warehouse("Rack 2")
+
+		if not frappe.db.exists("UOM", "Bag"):
+			new_uom = frappe.new_doc("UOM")
+			new_uom.uom_name = "Bag"
+			new_uom.save()
+
+	def test_putaway_rules_priority(self):
+		"""Test if rule is applied by priority, irrespective of free space."""
+		warehouse_1 = frappe.db.get_value("Warehouse", {"warehouse_name": "Rack 1"})
+		warehouse_2 = frappe.db.get_value("Warehouse", {"warehouse_name": "Rack 2"})
+
+		rule_1 = create_putaway_rule(item_code="_Rice", warehouse=warehouse_1, capacity=200,
+			uom="Kg")
+		rule_2 = create_putaway_rule(item_code="_Rice", warehouse=warehouse_2, capacity=300,
+			uom="Kg", priority=2)
+
+		po = create_purchase_order(item_code="_Rice", qty=300)
+		self.assertEqual(len(po.items), 1)
+
+		pr = make_purchase_receipt(po.name)
+		self.assertEqual(len(pr.items), 2)
+		self.assertEqual(pr.items[0].qty, 200)
+		self.assertEqual(pr.items[0].warehouse, warehouse_1)
+		self.assertEqual(pr.items[1].qty, 100)
+		self.assertEqual(pr.items[1].warehouse, warehouse_2)
+
+		po.cancel()
+		rule_1.delete()
+		rule_2.delete()
+
+	def test_putaway_rules_with_same_priority(self):
+		"""Test if rule with more free space is applied,
+		among two rules with same priority and capacity."""
+		warehouse_1 = frappe.db.get_value("Warehouse", {"warehouse_name": "Rack 1"})
+		warehouse_2 = frappe.db.get_value("Warehouse", {"warehouse_name": "Rack 2"})
+
+		rule_1 = create_putaway_rule(item_code="_Rice", warehouse=warehouse_1, capacity=500,
+			uom="Kg")
+		rule_2 = create_putaway_rule(item_code="_Rice", warehouse=warehouse_2, capacity=500,
+			uom="Kg")
+
+		# out of 500 kg capacity, occupy 100 kg in warehouse_1
+		stock_receipt = make_stock_entry(item_code="_Rice", target=warehouse_1, qty=100, basic_rate=50)
+
+		po = create_purchase_order(item_code="_Rice", qty=700)
+		self.assertEqual(len(po.items), 1)
+
+		pr = make_purchase_receipt(po.name)
+		self.assertEqual(len(pr.items), 2)
+		self.assertEqual(pr.items[0].qty, 500)
+		# warehouse_2 has 500 kg free space, it is given priority
+		self.assertEqual(pr.items[0].warehouse, warehouse_2)
+		self.assertEqual(pr.items[1].qty, 200)
+		# warehouse_1 has 400 kg free space, it is given less priority
+		self.assertEqual(pr.items[1].warehouse, warehouse_1)
+
+		po.cancel()
+		stock_receipt.cancel()
+		rule_1.delete()
+		rule_2.delete()
+
+	def test_putaway_rules_with_insufficient_capacity(self):
+		"""Test if qty exceeding capacity, is handled."""
+		warehouse_1 = frappe.db.get_value("Warehouse", {"warehouse_name": "Rack 1"})
+		warehouse_2 = frappe.db.get_value("Warehouse", {"warehouse_name": "Rack 2"})
+
+		rule_1 = create_putaway_rule(item_code="_Rice", warehouse=warehouse_1, capacity=100,
+			uom="Kg")
+		rule_2 = create_putaway_rule(item_code="_Rice", warehouse=warehouse_2, capacity=200,
+			uom="Kg")
+
+		po = create_purchase_order(item_code="_Rice", qty=350)
+		self.assertEqual(len(po.items), 1)
+
+		pr = make_purchase_receipt(po.name)
+
+		self.assertEqual(len(pr.items), 3)
+		self.assertEqual(pr.items[0].qty, 200)
+		self.assertEqual(pr.items[0].warehouse, warehouse_2)
+		self.assertEqual(pr.items[1].qty, 100)
+		self.assertEqual(pr.items[1].warehouse, warehouse_1)
+		# extra qty has no warehouse assigned
+		self.assertEqual(pr.items[2].qty, 50)
+		self.assertEqual(pr.items[2].warehouse, '')
+
+		po.cancel()
+		rule_1.delete()
+		rule_2.delete()
+
+	def test_putaway_rules_multi_uom(self):
+		"""Test rules applied on uom other than stock uom."""
+		item = frappe.get_doc("Item", "_Rice")
+		if not frappe.db.get_value("UOM Conversion Detail", {"parent": "_Rice", "uom": "Bag"}):
+			item.append("uoms", {
+				"uom": "Bag",
+				"conversion_factor": 1000
+			})
+			item.save()
+
+		warehouse_1 = frappe.db.get_value("Warehouse", {"warehouse_name": "Rack 1"})
+		warehouse_2 = frappe.db.get_value("Warehouse", {"warehouse_name": "Rack 2"})
+
+		rule_1 = create_putaway_rule(item_code="_Rice", warehouse=warehouse_1, capacity=3,
+			uom="Bag")
+		self.assertEqual(rule_1.stock_capacity, 3000)
+		rule_2 = create_putaway_rule(item_code="_Rice", warehouse=warehouse_2, capacity=4,
+			uom="Bag")
+		self.assertEqual(rule_2.stock_capacity, 4000)
+
+		stock_receipt = make_stock_entry(item_code="_Rice", target=warehouse_1, qty=1000, basic_rate=50)
+
+		po = create_purchase_order(item_code="_Rice", qty=6, do_not_save=True)
+		po.items[0].uom = "Bag"
+		po.save()
+		po.submit()
+
+		self.assertEqual(po.items[0].stock_qty, 6000)
+
+		pr = make_purchase_receipt(po.name)
+		self.assertEqual(len(pr.items), 2)
+		self.assertEqual(pr.items[0].qty, 4)
+		self.assertEqual(pr.items[0].warehouse, warehouse_2)
+		self.assertEqual(pr.items[1].qty, 2)
+		self.assertEqual(pr.items[1].warehouse, warehouse_1)
+
+		po.cancel()
+		stock_receipt.cancel()
+		rule_1.delete()
+		rule_2.delete()
+
+	def test_putaway_rules_multi_uom_whole_uom(self):
+		"""Test if whole UOMs are handled."""
+		item = frappe.get_doc("Item", "_Rice")
+		if not frappe.db.get_value("UOM Conversion Detail", {"parent": "_Rice", "uom": "Bag"}):
+			item.append("uoms", {
+				"uom": "Bag",
+				"conversion_factor": 1000
+			})
+			item.save()
+
+		frappe.db.set_value("UOM", "Bag", "must_be_whole_number", 1)
+
+		warehouse_1 = frappe.db.get_value("Warehouse", {"warehouse_name": "Rack 1"})
+		warehouse_2 = frappe.db.get_value("Warehouse", {"warehouse_name": "Rack 2"})
+
+		# Putaway Rule in different UOM
+		rule_1 = create_putaway_rule(item_code="_Rice", warehouse=warehouse_1, capacity=1,
+			uom="Bag")
+		self.assertEqual(rule_1.stock_capacity, 1000)
+		# Putaway Rule in Stock UOM
+		rule_2 = create_putaway_rule(item_code="_Rice", warehouse=warehouse_2, capacity=500)
+		self.assertEqual(rule_2.stock_capacity, 500)
+		# total capacity is 1500 Kg
+
+		po = create_purchase_order(item_code="_Rice", qty=2, do_not_save=True)
+		# PO for 2 Bags (2000 Kg)
+		po.items[0].uom = "Bag"
+		po.save()
+		po.submit()
+
+		self.assertEqual(po.items[0].stock_qty, 2000)
+
+		pr = make_purchase_receipt(po.name)
+		self.assertEqual(len(pr.items), 2)
+		self.assertEqual(pr.items[0].qty, 1)
+		self.assertEqual(pr.items[0].warehouse, warehouse_1)
+		# leftover space was for 500 kg (0.5 Bag)
+		# Since Bag is a whole UOM, 1(out of 2) Bag will be unassigned
+		self.assertEqual(pr.items[1].qty, 1)
+		self.assertEqual(pr.items[1].warehouse, '')
+
+		po.cancel()
+		rule_1.delete()
+		rule_2.delete()
+
+	def test_putaway_rules_with_reoccurring_item(self):
+		"""Test rules on same item entered multiple times."""
+		warehouse_1 = frappe.db.get_value("Warehouse", {"warehouse_name": "Rack 1"})
+		warehouse_2 = frappe.db.get_value("Warehouse", {"warehouse_name": "Rack 2"})
+
+		rule_1 = create_putaway_rule(item_code="_Rice", warehouse=warehouse_1, capacity=200,
+			uom="Kg")
+		rule_2 = create_putaway_rule(item_code="_Rice", warehouse=warehouse_2, capacity=100,
+			uom="Kg", priority=2)
+		# total capacity is 300 Kg
+
+		po = create_purchase_order(item_code="_Rice", qty=200, rate=100, do_not_save=True)
+		po.append("items", {
+			"item_code":"_Rice",
+			"warehouse": "_Test Warehouse - _TC",
+			"qty": 300,
+			"rate": 120,
+			"schedule_date": add_days(nowdate(), 1),
+		})
+		po.save()
+		po.submit()
+		# PO for 500 Kg (two rows of same item, different rates)
+		self.assertEqual(len(po.items), 2)
+
+		pr = make_purchase_receipt(po.name)
+		self.assertEqual(len(pr.items), 3)
+		self.assertEqual(pr.items[0].qty, 200)
+		self.assertEqual(pr.items[0].warehouse, warehouse_1)
+		# same rules applied to second item row
+		# with previous assignment considered
+		self.assertEqual(pr.items[1].qty, 100)
+		self.assertEqual(pr.items[1].warehouse, warehouse_2)
+		# unassigned 200 Kg
+		self.assertEqual(pr.items[2].qty, 200)
+		self.assertEqual(pr.items[2].warehouse, '')
+
+		po.cancel()
+		rule_1.delete()
+		rule_2.delete()
+
+def create_putaway_rule(**args):
+	args = frappe._dict(args)
+	putaway = frappe.new_doc("Putaway Rule")
+
+	putaway.disable = args.disable or 0
+	putaway.company = args.company or "_Test Company"
+	putaway.item_code = args.item or args.item_code or "_Test Item"
+	putaway.warehouse = args.warehouse
+	putaway.priority = args.priority or 1
+	putaway.capacity = args.capacity or 1
+	putaway.stock_uom = frappe.db.get_value("Item", putaway.item_code, "stock_uom")
+	putaway.uom = args.uom or putaway.stock_uom
+	putaway.conversion_factor = get_conversion_factor(putaway.item_code, putaway.uom)['conversion_factor']
+
+	if not args.do_not_save:
+		putaway.save()
+
+	return putaway
\ No newline at end of file