[enhance] automatic batch selection in Delivery Note and Stock Entry
diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py
index 703fe06..3649cc1 100644
--- a/erpnext/controllers/stock_controller.py
+++ b/erpnext/controllers/stock_controller.py
@@ -177,17 +177,18 @@
 				stock_ledger.setdefault(sle.voucher_detail_no, []).append(sle)
 		return stock_ledger
 
-	def make_batches(self):
+	def make_batches(self, warehouse_field):
 		'''Create batches if required. Called before submit'''
 		for d in self.items:
-			has_batch_no, create_new_batch = frappe.db.get_value('Item', d.item_code, ['has_batch_no', 'create_new_batch'])
-			if has_batch_no and not d.batch_no and create_new_batch:
-				d.batch_no = frappe.get_doc(dict(
-					doctype='Batch',
-					item=d.item_code,
-					supplier=getattr(self, 'supplier', None),
-					reference_doctype=self.doctype,
-					reference_name=self.name)).insert().name
+			if d.get(warehouse_field) and not d.batch_no:
+				has_batch_no, create_new_batch = frappe.db.get_value('Item', d.item_code, ['has_batch_no', 'create_new_batch'])
+				if has_batch_no and create_new_batch:
+					d.batch_no = frappe.get_doc(dict(
+						doctype='Batch',
+						item=d.item_code,
+						supplier=getattr(self, 'supplier', None),
+						reference_doctype=self.doctype,
+						reference_name=self.name)).insert().name
 
 	def make_adjustment_entry(self, expected_gle, voucher_obj):
 		from erpnext.accounts.utils import get_stock_and_account_difference
diff --git a/erpnext/stock/doctype/batch/batch.py b/erpnext/stock/doctype/batch/batch.py
index 193aceb..8ef8e91 100644
--- a/erpnext/stock/doctype/batch/batch.py
+++ b/erpnext/stock/doctype/batch/batch.py
@@ -6,6 +6,8 @@
 from frappe import _
 from frappe.model.document import Document
 
+class UnableToSelectBatchError(frappe.ValidationError): pass
+
 class Batch(Document):
 	def autoname(self):
 		'''Generate random ID for batch if not specified'''
@@ -34,8 +36,15 @@
 			frappe.throw(_("The selected item cannot have Batch"))
 
 @frappe.whitelist()
-def get_batch_qty(batch_no, warehouse=None):
-	'''Returns batch actual qty if warehouse is passed, or returns dict of qty by warehouse if warehouse is None'''
+def get_batch_qty(batch_no=None, warehouse=None, item_code=None):
+	'''Returns batch actual qty if warehouse is passed,
+		or returns dict of qty by warehouse if warehouse is None
+
+	The user must pass either batch_no or batch_no + warehouse or item_code + warehouse
+
+	:param batch_no: Optional - give qty for this batch no
+	:param warehouse: Optional - give qty for this warehouse
+	:param item_code: Optional - give qty for this item'''
 	frappe.has_permission('Batch', throw=True)
 	out = 0
 	if batch_no and warehouse:
@@ -48,6 +57,11 @@
 			from `tabStock Ledger Entry`
 			where batch_no=%s
 			group by warehouse''', batch_no, as_dict=1)
+	if not batch_no and item_code and warehouse:
+		out = frappe.db.sql('''select batch_no, sum(actual_qty) as qty
+			from `tabStock Ledger Entry`
+			where item_code = %s and warehouse=%s
+			group by batch_no''', (item_code, warehouse), as_dict=1)
 	return out
 
 @frappe.whitelist()
@@ -76,3 +90,30 @@
 	stock_entry.submit()
 
 	return batch.name
+
+def set_batch_nos(doc, warehouse_field, throw = False):
+	'''Automatically select `batch_no` for outgoing items in item table'''
+	for d in doc.items:
+		has_batch_no = frappe.db.get_value('Item', d.item_code, 'has_batch_no')
+		warehouse = d.get(warehouse_field, None)
+		if has_batch_no and not d.batch_no and warehouse:
+			d.batch_no = get_batch_no(d.item_code, warehouse, d.qty, throw)
+
+def get_batch_no(item_code, warehouse, qty, throw=False):
+	'''get the smallest batch with for the given item_code, warehouse and qty'''
+	batches = sorted(
+		get_batch_qty(item_code = item_code, warehouse = warehouse),
+		lambda a, b: 1 if a.qty > b.qty else -1)
+
+	batch_no = None
+	for b in batches:
+		if b.qty >= qty:
+			batch_no = b.batch_no
+			# found!
+			break
+
+	if not batch_no:
+		frappe.msgprint(_('Please select a Batch for Item {0}. Unable to find a single batch that fulfills this requirement').format(frappe.bold(item_code)))
+		if throw: raise UnableToSelectBatchError
+
+	return batch_no
\ No newline at end of file
diff --git a/erpnext/stock/doctype/batch/test_batch.py b/erpnext/stock/doctype/batch/test_batch.py
index 29023bb..e63e949 100644
--- a/erpnext/stock/doctype/batch/test_batch.py
+++ b/erpnext/stock/doctype/batch/test_batch.py
@@ -6,7 +6,7 @@
 from frappe.exceptions import ValidationError
 import unittest
 
-from erpnext.stock.doctype.batch.batch import get_batch_qty
+from erpnext.stock.doctype.batch.batch import get_batch_qty, UnableToSelectBatchError
 
 class TestBatch(unittest.TestCase):
 	def test_item_has_batch_enabled(self):
@@ -21,7 +21,7 @@
 		if not frappe.db.exists('ITEM-BATCH-1'):
 			make_item('ITEM-BATCH-1', dict(has_batch_no = 1, create_new_batch = 1))
 
-	def test_purchase_receipt(self):
+	def test_purchase_receipt(self, batch_qty = 100):
 		'''Test automated batch creation from Purchase Receipt'''
 		self.make_batch_item()
 
@@ -31,7 +31,7 @@
 			items = [
 				dict(
 					item_code = 'ITEM-BATCH-1',
-					qty = 100,
+					qty = batch_qty,
 					rate = 10
 				)
 			]
@@ -39,11 +39,12 @@
 		receipt.submit()
 
 		self.assertTrue(receipt.items[0].batch_no)
-		self.assertEquals(get_batch_qty(receipt.items[0].batch_no, receipt.items[0].warehouse), 100)
+		self.assertEquals(get_batch_qty(receipt.items[0].batch_no,
+			receipt.items[0].warehouse), batch_qty)
 
 		return receipt
 
-	def test_stock_entry(self):
+	def test_stock_entry_incoming(self):
 		'''Test batch creation via Stock Entry (Production Order)'''
 
 		self.make_batch_item()
@@ -67,6 +68,78 @@
 		self.assertTrue(stock_entry.items[0].batch_no)
 		self.assertEquals(get_batch_qty(stock_entry.items[0].batch_no, stock_entry.items[0].t_warehouse), 90)
 
+	def test_delivery_note(self):
+		'''Test automatic batch selection for outgoing items'''
+		batch_qty = 15
+		receipt = self.test_purchase_receipt(batch_qty)
+
+		delivery_note = frappe.get_doc(dict(
+			doctype = 'Delivery Note',
+			customer = '_Test Customer',
+			company = receipt.company,
+			items = [
+				dict(
+					item_code = 'ITEM-BATCH-1',
+					qty = batch_qty,
+					rate = 10,
+					warehouse = receipt.items[0].warehouse
+				)
+			]
+		)).insert()
+		delivery_note.submit()
+
+		# shipped with same batch
+		self.assertEquals(delivery_note.items[0].batch_no, receipt.items[0].batch_no)
+
+		# balance is 0
+		self.assertEquals(get_batch_qty(receipt.items[0].batch_no,
+			receipt.items[0].warehouse), 0)
+
+	def test_delivery_note_fail(self):
+		'''Test automatic batch selection for outgoing items'''
+		receipt = self.test_purchase_receipt(100)
+		delivery_note = frappe.get_doc(dict(
+			doctype = 'Delivery Note',
+			customer = '_Test Customer',
+			company = receipt.company,
+			items = [
+				dict(
+					item_code = 'ITEM-BATCH-1',
+					qty = 5000,
+					rate = 10,
+					warehouse = receipt.items[0].warehouse
+				)
+			]
+		))
+		self.assertRaises(UnableToSelectBatchError, delivery_note.insert)
+
+	def test_stock_entry_outgoing(self):
+		'''Test automatic batch selection for outgoing stock entry'''
+
+		batch_qty = 16
+		receipt = self.test_purchase_receipt(batch_qty)
+
+		stock_entry = frappe.get_doc(dict(
+			doctype = 'Stock Entry',
+			purpose = 'Material Issue',
+			company = receipt.company,
+			items = [
+				dict(
+					item_code = 'ITEM-BATCH-1',
+					qty = batch_qty,
+					s_warehouse = receipt.items[0].warehouse,
+				)
+			]
+		)).insert()
+		stock_entry.submit()
+
+		# assert same batch is selected
+		self.assertEqual(stock_entry.items[0].batch_no, receipt.items[0].batch_no)
+
+		# balance is 0
+		self.assertEquals(get_batch_qty(receipt.items[0].batch_no,
+			receipt.items[0].warehouse), 0)
+
 	def test_batch_split(self):
 		'''Test batch splitting'''
 		receipt = self.test_purchase_receipt()
diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py
index 5e8f5c9..a2a0115 100644
--- a/erpnext/stock/doctype/delivery_note/delivery_note.py
+++ b/erpnext/stock/doctype/delivery_note/delivery_note.py
@@ -11,7 +11,7 @@
 from frappe.model.mapper import get_mapped_doc
 from erpnext.controllers.selling_controller import SellingController
 from frappe.desk.notifications import clear_doctype_notifications
-
+from erpnext.stock.doctype.batch.batch import set_batch_nos
 
 form_grid_templates = {
 	"items": "templates/form_grid/item_grid.html"
@@ -106,6 +106,9 @@
 		self.validate_uom_is_integer("uom", "qty")
 		self.validate_with_previous_doc()
 
+		if self._action != 'submit':
+			set_batch_nos(self, 'warehouse', True)
+
 		from erpnext.stock.doctype.packed_item.packed_item import make_packing_list
 		make_packing_list(self)
 
diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
index 5d90338..055b9c4 100644
--- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
+++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
@@ -51,7 +51,7 @@
 		super(PurchaseReceipt, self).validate()
 
 		if self._action=="submit":
-			self.make_batches()
+			self.make_batches('warehouse')
 		else:
 			self.set_status()
 
diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py
index dcdd50d..169bfd9 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry.py
+++ b/erpnext/stock/doctype/stock_entry/stock_entry.py
@@ -9,6 +9,7 @@
 from erpnext.stock.utils import get_incoming_rate
 from erpnext.stock.stock_ledger import get_previous_sle, NegativeStockError
 from erpnext.stock.get_item_details import get_bin_details, get_default_cost_center, get_conversion_factor
+from erpnext.stock.doctype.batch.batch import get_batch_no, set_batch_nos
 from erpnext.manufacturing.doctype.bom.bom import validate_bom_no
 import json
 
@@ -49,7 +50,9 @@
 		self.validate_batch()
 
 		if self._action == 'submit':
-			self.make_batches()
+			self.make_batches('t_warehouse')
+		else:
+			set_batch_nos(self, 's_warehouse', True)
 
 		self.set_actual_qty()
 		self.calculate_rate_and_amount(update_finished_item_rate=False)
@@ -89,8 +92,10 @@
 			if item.item_code not in stock_items:
 				frappe.throw(_("{0} is not a stock Item").format(item.item_code))
 
-			item_details = self.get_item_details(frappe._dict({"item_code": item.item_code,
-				"company": self.company, "project": self.project, "uom": item.uom}), for_update=True)
+			item_details = self.get_item_details(frappe._dict(
+				{"item_code": item.item_code, "company": self.company,
+				"project": self.project, "uom": item.uom, 's_warehouse': item.s_warehouse}),
+				for_update=True)
 
 			for f in ("uom", "stock_uom", "description", "item_name", "expense_account",
 				"cost_center", "conversion_factor"):
@@ -465,7 +470,9 @@
 
 	def get_item_details(self, args=None, for_update=False):
 		item = frappe.db.sql("""select stock_uom, description, image, item_name,
-			expense_account, buying_cost_center, item_group from `tabItem`
+				expense_account, buying_cost_center, item_group, has_serial_no,
+				has_batch_no
+			from `tabItem`
 			where name = %s
 				and disabled=0
 				and (end_of_life is null or end_of_life='0000-00-00' or end_of_life > %s)""",
@@ -475,7 +482,7 @@
 
 		item = item[0]
 
-		ret = {
+		ret = frappe._dict({
 			'uom'			      	: item.stock_uom,
 			'stock_uom'			  	: item.stock_uom,
 			'description'		  	: item.description,
@@ -489,8 +496,10 @@
 			'batch_no'				: '',
 			'actual_qty'			: 0,
 			'basic_rate'			: 0,
-			'serial_no'				: ''
-		}
+			'serial_no'				: '',
+			'has_serial_no'			: item.has_serial_no,
+			'has_batch_no'			: item.has_batch_no
+		})
 		for d in [["Account", "expense_account", "default_expense_account"],
 			["Cost Center", "cost_center", "cost_center"]]:
 				company = frappe.db.get_value(d[0], ret.get(d[1]), "company")
@@ -510,6 +519,11 @@
 		stock_and_rate = args.get('warehouse') and get_warehouse_details(args) or {}
 		ret.update(stock_and_rate)
 
+		# automatically select batch for outgoing item
+		if (args.get('s_warehouse', None) and args.get('qty') and
+			ret.get('has_batch_no') and not args.get('batch_no')):
+			args.batch_no = get_batch_no(args['item_code'], args['s_warehouse'], args['qty'])
+
 		return ret
 
 	def get_items(self):
diff --git a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json
index 2f7779c..43209e8 100644
--- a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json
+++ b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json
@@ -813,7 +813,7 @@
    "collapsible": 0, 
    "columns": 0, 
    "fieldname": "serial_no", 
-   "fieldtype": "Text", 
+   "fieldtype": "Small Text", 
    "hidden": 0, 
    "ignore_user_permissions": 0, 
    "ignore_xss_filter": 0, 
@@ -1040,11 +1040,11 @@
    "unique": 0
   }, 
   {
-   "allow_on_submit": 0, 
+   "allow_on_submit": 1, 
    "bold": 0, 
    "collapsible": 0, 
    "columns": 0, 
-   "fieldname": "allow_zero_valuation_rate", 
+   "fieldname": "is_sample_item", 
    "fieldtype": "Check", 
    "hidden": 0, 
    "ignore_user_permissions": 0, 
@@ -1053,7 +1053,7 @@
    "in_global_search": 0, 
    "in_list_view": 0, 
    "in_standard_filter": 0, 
-   "label": "Allow Zero Valuation Rate", 
+   "label": "Is Sample Item", 
    "length": 0, 
    "no_copy": 1, 
    "permlevel": 0, 
@@ -1225,7 +1225,7 @@
  "issingle": 0, 
  "istable": 1, 
  "max_attachments": 0, 
- "modified": "2017-04-19 11:54:31.645381", 
+ "modified": "2017-04-21 02:56:48.306626", 
  "modified_by": "Administrator", 
  "module": "Stock", 
  "name": "Stock Entry Detail", 
diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py
index 787e4b5..a6459c5 100644
--- a/erpnext/stock/get_item_details.py
+++ b/erpnext/stock/get_item_details.py
@@ -9,6 +9,7 @@
 from erpnext.accounts.doctype.pricing_rule.pricing_rule import get_pricing_rule_for_item, set_transaction_type
 from erpnext.setup.utils import get_exchange_rate
 from frappe.model.meta import get_field_precision
+from erpnext.stock.doctype.batch.batch import get_batch_no
 
 @frappe.whitelist()
 def get_item_details(args):
@@ -74,7 +75,12 @@
 	out.update(get_pricing_rule_for_item(args))
 
 	if args.get("doctype") in ("Sales Invoice", "Delivery Note") and out.stock_qty > 0:
-		out.serial_no = get_serial_no(out)
+		if out.has_serial_no:
+			out.serial_no = get_serial_no(out)
+
+		if out.has_batch_no:
+			out.batch_no = get_batch_no(out.item_code, out.warehouse, out.qty)
+
 
 	if args.transaction_date and item.lead_time_days:
 		out.schedule_date = out.lead_time_date = add_days(args.transaction_date,
@@ -154,6 +160,8 @@
 		"income_account": get_default_income_account(args, item),
 		"expense_account": get_default_expense_account(args, item),
 		"cost_center": get_default_cost_center(args, item),
+		'has_serial_no': item.has_serial_no,
+		'has_batch_no': item.has_batch_no,
 		"batch_no": None,
 		"item_tax_rate": json.dumps(dict(([d.tax_type, d.tax_rate] for d in
 			item.get("taxes")))),