Merge branch 'develop' of github.com:frappe/erpnext into feature-pick-list
diff --git a/erpnext/selling/doctype/sales_order/sales_order.js b/erpnext/selling/doctype/sales_order/sales_order.js
index 39dda92..136ff55 100644
--- a/erpnext/selling/doctype/sales_order/sales_order.js
+++ b/erpnext/selling/doctype/sales_order/sales_order.js
@@ -109,7 +109,9 @@
 		this._super();
 		let allow_delivery = false;
 
-		if(doc.docstatus==1) {
+		if (doc.docstatus==1) {
+			this.frm.add_custom_button(__('Pick List'), () => this.make_pick_ticket(), __('Create'));
+
 			if(this.frm.has_perm("submit")) {
 				if(doc.status === 'On Hold') {
 				   // un-hold
@@ -233,6 +235,13 @@
 		this.order_type(doc);
 	},
 
+	make_pick_ticket() {
+		frappe.model.open_mapped_doc({
+			method: "erpnext.selling.doctype.sales_order.sales_order.make_pick_ticket",
+			frm: this.frm
+		})
+	},
+
 	make_work_order() {
 		var me = this;
 		this.frm.call({
diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py
index 09dc9a9..1d636e3 100755
--- a/erpnext/selling/doctype/sales_order/sales_order.py
+++ b/erpnext/selling/doctype/sales_order/sales_order.py
@@ -996,3 +996,25 @@
 def make_inter_company_purchase_order(source_name, target_doc=None):
 	from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_inter_company_transaction
 	return make_inter_company_transaction("Sales Order", source_name, target_doc)
+
+@frappe.whitelist()
+def make_pick_ticket(source_name, target_doc=None):
+	doc = get_mapped_doc("Sales Order", source_name, {
+		"Sales Order": {
+			"doctype": "Pick List",
+			"validation": {
+				"docstatus": ["=", 1]
+			}
+		},
+		"Sales Order Item": {
+			"doctype": "Pick List Reference Item",
+			"field_map": {
+				"item_code": "item",
+				"parenttype": "reference_doctype",
+				"parent": "reference_name",
+				"name": "reference_document_item"
+			},
+		},
+	}, target_doc)
+
+	return doc
diff --git a/erpnext/stock/doctype/pick_list/__init__.py b/erpnext/stock/doctype/pick_list/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/stock/doctype/pick_list/__init__.py
diff --git a/erpnext/stock/doctype/pick_list/pick_list.js b/erpnext/stock/doctype/pick_list/pick_list.js
new file mode 100644
index 0000000..4cf4cdf
--- /dev/null
+++ b/erpnext/stock/doctype/pick_list/pick_list.js
@@ -0,0 +1,43 @@
+// Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('Pick List', {
+	setup: (frm) => {
+		frm.set_query('parent_warehouse', () => {
+			return {
+				filters: {
+					'is_group': 1,
+					'company': frm.doc.company
+				}
+			};
+		});
+	},
+	refresh: (frm) => {
+		frm.add_custom_button(__('Delivery Note'), () => frm.trigger('make_delivery_note'), __('Create'));
+		frm.add_custom_button(__('Sales Order'), function() {
+			erpnext.utils.map_current_doc({
+				method: "erpnext.selling.doctype.sales_order.sales_order.make_pick_list",
+				source_doctype: "Sales Order",
+				target: frm,
+				setters: {
+					company: frm.doc.company || undefined,
+				},
+				get_query_filters: {
+					docstatus: 1,
+				}
+			});
+		}, __("Get items from"));
+
+		if (frm.doc.reference_items && frm.doc.reference_items.length) {
+			frm.add_custom_button(__('Get Item Locations'), () => {
+				frm.call('set_item_locations');
+			});
+		}
+	},
+	make_delivery_note(frm) {
+		frappe.model.open_mapped_doc({
+			method: "erpnext.stock.doctype.pick_list.pick_list.make_delivery_note",
+			frm: frm
+		});
+	},
+});
diff --git a/erpnext/stock/doctype/pick_list/pick_list.json b/erpnext/stock/doctype/pick_list/pick_list.json
new file mode 100644
index 0000000..3f96630
--- /dev/null
+++ b/erpnext/stock/doctype/pick_list/pick_list.json
@@ -0,0 +1,78 @@
+{
+ "autoname": "PICK.####",
+ "creation": "2019-07-11 16:03:13.681045",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+  "company",
+  "column_break_4",
+  "parent_warehouse",
+  "section_break_4",
+  "reference_items",
+  "section_break_6",
+  "item_locations"
+ ],
+ "fields": [
+  {
+   "fieldname": "company",
+   "fieldtype": "Link",
+   "label": "Company",
+   "options": "Company"
+  },
+  {
+   "fieldname": "column_break_4",
+   "fieldtype": "Column Break"
+  },
+  {
+   "fieldname": "section_break_6",
+   "fieldtype": "Section Break"
+  },
+  {
+   "fieldname": "section_break_4",
+   "fieldtype": "Section Break"
+  },
+  {
+   "description": "Items under this warehouse will be suggested",
+   "fieldname": "parent_warehouse",
+   "fieldtype": "Link",
+   "label": "Parent Warehouse",
+   "options": "Warehouse"
+  },
+  {
+   "fieldname": "item_locations",
+   "fieldtype": "Table",
+   "label": "Item Locations",
+   "options": "Pick List Item"
+  },
+  {
+   "fieldname": "reference_items",
+   "fieldtype": "Table",
+   "label": "Items To Be Picked",
+   "options": "Pick List Reference Item"
+  }
+ ],
+ "modified": "2019-08-01 10:50:17.055509",
+ "modified_by": "Administrator",
+ "module": "Stock",
+ "name": "Pick List",
+ "owner": "Administrator",
+ "permissions": [
+  {
+   "create": 1,
+   "delete": 1,
+   "email": 1,
+   "export": 1,
+   "print": 1,
+   "read": 1,
+   "report": 1,
+   "role": "System Manager",
+   "share": 1,
+   "write": 1
+  }
+ ],
+ "quick_entry": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py
new file mode 100644
index 0000000..3e45bdd
--- /dev/null
+++ b/erpnext/stock/doctype/pick_list/pick_list.py
@@ -0,0 +1,150 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+import frappe
+from frappe.model.document import Document
+from frappe.model.mapper import get_mapped_doc
+
+class PickList(Document):
+	def set_item_locations(self):
+		reference_items = self.reference_items
+
+		from_warehouses = None
+		if self.parent_warehouse:
+			from_warehouses = frappe.db.get_descendants('Warehouse', self.parent_warehouse)
+
+		# Reset
+		self.delete_key('item_locations')
+		for item in reference_items:
+			data = get_items_with_warehouse_and_quantity(item, from_warehouses)
+			for item_info in data:
+				print(self.append('item_locations', item_info))
+
+		for item_doc in self.get('item_locations'):
+			if frappe.get_cached_value('Item', item_doc.item, 'has_serial_no'):
+				set_serial_nos(item_doc)
+			elif frappe.get_cached_value('Item', item_doc.item, 'has_batch_no'):
+				set_batch_no(item_doc, self)
+
+def get_items_with_warehouse_and_quantity(item_doc, from_warehouses):
+	items = []
+	item_locations = get_available_items(item_doc.item, from_warehouses)
+	remaining_qty = item_doc.qty
+
+	while remaining_qty > 0 and item_locations:
+		item_location = item_locations.pop(0)
+		qty = remaining_qty if item_location.qty >= remaining_qty else item_location.qty
+		items.append({
+			'item': item_doc.item,
+			'qty': qty,
+			'warehouse': item_location.warehouse,
+			'reference_doctype': item_doc.reference_doctype,
+			'reference_name': item_doc.reference_name,
+			'reference_document_item': item_doc.reference_document_item,
+		})
+		remaining_qty -= qty
+
+	if remaining_qty:
+		frappe.msgprint('{} qty of {} is out of stock. Skipping...'.format(remaining_qty, item_doc.item))
+		return items
+
+	return items
+
+def get_available_items(item, from_warehouses):
+	# gets all items available in different warehouses
+	# FIFO
+	filters = frappe._dict({
+		'item_code': item,
+		'actual_qty': ['>', 0]
+	})
+	if from_warehouses:
+		filters.warehouse = ['in', from_warehouses]
+
+	available_items = frappe.get_all('Bin',
+		fields=['warehouse', 'actual_qty as qty'],
+		filters=filters,
+		order_by='creation')
+
+	return available_items
+
+def set_serial_nos(item_doc):
+	serial_nos = frappe.get_all('Serial No', {
+		'item_code': item_doc.item,
+		'warehouse': item_doc.warehouse
+	}, limit=item_doc.qty, order_by='purchase_date')
+	item_doc.set('serial_no', '\n'.join([serial_no.name for serial_no in serial_nos]))
+
+	# should we assume that all serialized item available in stock will have serial no?
+
+def set_batch_no(item_doc, parent_doc):
+	batches = frappe.db.sql("""
+		SELECT
+			`batch_no`,
+			SUM(`actual_qty`) AS `qty`
+		FROM
+			`tabStock Ledger Entry`
+		WHERE
+			`item_code`=%(item_code)s
+			AND `warehouse`=%(warehouse)s
+		GROUP BY
+			`warehouse`,
+			`batch_no`,
+			`item_code`
+		HAVING `qty` > 0
+	""", {
+		'item_code': item_doc.item,
+		'warehouse': item_doc.warehouse,
+	}, as_dict=1)
+	print(batches)
+
+	required_qty = item_doc.qty
+	while required_qty > 0 and batches:
+		batch = batches.pop()
+		batch_expiry = frappe.get_value('Batch', batch.batch_no, 'expiry_date')
+		if batch_expiry and batch_expiry <= frappe.utils.getdate():
+			frappe.msgprint('Skipping expired Batch {}'.format(batch.batch_no))
+			continue
+		item_doc.batch_no = batch.batch_no
+		if batch.qty >= item_doc.qty:
+			required_qty = 0
+			break
+		else:
+			# split item if quantity of item in batch is less that required
+			# Look for another batch
+
+			# set quantity of of item equal to batch quantity
+			required_qty -= batch.qty
+			item_doc.set('qty', batch.qty)
+			item_doc = parent_doc.append('items', {
+				'item': item_doc.item,
+				'qty': required_qty,
+				'warehouse': item_doc.warehouse,
+				'reference_doctype': item_doc.reference_doctype,
+				'reference_name': item_doc.reference_name,
+				'reference_document_item': item_doc.reference_document_item,
+			})
+	if required_qty:
+		frappe.msgprint('No batches found for {} qty of {}. Skipping...'.format(required_qty, item_doc.item))
+		parent_doc.remove(item_doc)
+
+@frappe.whitelist()
+def make_delivery_note(source_name, target_doc=None):
+	target_doc = get_mapped_doc("Pick List", source_name, {
+		"Pick List": {
+			"doctype": "Delivery Note",
+			# "validation": {
+			# 	"docstatus": ["=", 1]
+			# }
+		},
+		"Pick List Item": {
+			"doctype": "Delivery Note Item",
+			"field_map": {
+				"item": "item_code",
+				"reference_docname": "against_sales_order",
+			},
+		},
+	}, target_doc)
+
+	return target_doc
\ No newline at end of file
diff --git a/erpnext/stock/doctype/pick_list/test_pick_list.py b/erpnext/stock/doctype/pick_list/test_pick_list.py
new file mode 100644
index 0000000..ed4b6b5
--- /dev/null
+++ b/erpnext/stock/doctype/pick_list/test_pick_list.py
@@ -0,0 +1,100 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+from __future__ import unicode_literals
+
+import frappe
+import unittest
+# test_dependencies = ['Item', 'Sales Invoice', 'Stock Entry', 'Batch']
+
+from erpnext.selling.doctype.sales_order.sales_order import make_pick_list
+
+class TestPickList(unittest.TestCase):
+	def test_pick_list_picks_warehouse_for_each_item(self):
+		pick_list = frappe.get_doc({
+			'doctype': 'Pick List',
+			'company': '_Test Company',
+			'reference_items': [{
+				'item': '_Test Item Home Desktop 100',
+				'reference_doctype': 'Sales Order',
+				'qty': 5,
+				'reference_name': '_T-Sales Order-1',
+			}],
+		})
+
+		pick_list.set_item_locations()
+
+		self.assertEqual(pick_list.items_locations[0].item, '_Test Item Home Desktop 100')
+		self.assertEqual(pick_list.items_locations[0].warehouse, '_Test Warehouse - _TC')
+		self.assertEqual(pick_list.items_locations[0].qty, 5)
+
+	def test_pick_list_skips_out_of_stock_item(self):
+		pick_list = frappe.get_doc({
+			'doctype': 'Pick List',
+			'company': '_Test Company',
+			'reference_items': [{
+				'item': '_Test Item Warehouse Group Wise Reorder',
+				'reference_doctype': 'Sales Order',
+				'qty': 1000,
+				'reference_name': '_T-Sales Order-1',
+			}],
+		})
+
+		pick_list.set_item_locations()
+
+		self.assertEqual(pick_list.items_locations[0].item, '_Test Item Warehouse Group Wise Reorder')
+		self.assertEqual(pick_list.items_locations[0].warehouse, '_Test Warehouse Group-C1 - _TC')
+		self.assertEqual(pick_list.items_locations[0].qty, 30)
+
+
+	def test_pick_list_skips_items_in_expired_batch(self):
+		pass
+
+	def test_pick_list_shows_serial_no_for_serialized_item(self):
+
+		stock_reconciliation = frappe.get_doc({
+			'doctype': 'Stock Reconciliation',
+			'company': '_Test Company',
+			'items': [{
+				'item_code': '_Test Serialized Item',
+				'warehouse': '_Test Warehouse - _TC',
+				'qty': 5,
+				'serial_no': '123450\n123451\n123452\n123453\n123454'
+			}]
+		})
+
+		stock_reconciliation.submit()
+
+		pick_list = frappe.get_doc({
+			'doctype': 'Pick List',
+			'company': '_Test Company',
+			'reference_items': [{
+				'item': '_Test Serialized Item',
+				'reference_doctype': 'Sales Order',
+				'qty': 1000,
+				'reference_name': '_T-Sales Order-1',
+			}],
+		})
+
+		pick_list.set_item_locations()
+		self.assertEqual(pick_list.items_locations[0].item, '_Test Serialized Item')
+		self.assertEqual(pick_list.items_locations[0].warehouse, '_Test Warehouse Group-C1 - _TC')
+		self.assertEqual(pick_list.items_locations[0].qty, 30)
+		self.assertEqual(pick_list.items_locations[0].serial_no, 30)
+
+
+	def test_pick_list_for_multiple_reference_doctypes(self):
+		pass
+
+
+## records required
+
+'''
+batch no
+items
+sales invoice
+stock entries
+	bin
+	stock ledger entry
+warehouses
+'''
\ No newline at end of file
diff --git a/erpnext/stock/doctype/pick_list_item/pick_list_item/__init__.py b/erpnext/stock/doctype/pick_list_item/pick_list_item/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/stock/doctype/pick_list_item/pick_list_item/__init__.py
diff --git a/erpnext/stock/doctype/pick_list_item/pick_list_item/pick_ticket_item.json b/erpnext/stock/doctype/pick_list_item/pick_list_item/pick_ticket_item.json
new file mode 100644
index 0000000..c83b696
--- /dev/null
+++ b/erpnext/stock/doctype/pick_list_item/pick_list_item/pick_ticket_item.json
@@ -0,0 +1,142 @@
+{
+ "creation": "2019-07-11 16:01:22.832885",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+  "item",
+  "item_name",
+  "column_break_2",
+  "description",
+  "has_batch_no",
+  "has_serial_no",
+  "section_break_5",
+  "warehouse",
+  "qty",
+  "picked_qty",
+  "serial_no",
+  "batch_no",
+  "reference_section",
+  "reference_doctype",
+  "reference_name",
+  "reference_document_item"
+ ],
+ "fields": [
+  {
+   "fieldname": "item",
+   "fieldtype": "Link",
+   "in_list_view": 1,
+   "label": "Item",
+   "options": "Item",
+   "read_only": 1
+  },
+  {
+   "fieldname": "qty",
+   "fieldtype": "Float",
+   "in_list_view": 1,
+   "label": "Qty",
+   "read_only": 1
+  },
+  {
+   "fieldname": "picked_qty",
+   "fieldtype": "Float",
+   "in_list_view": 1,
+   "label": "Picked Qty"
+  },
+  {
+   "fieldname": "warehouse",
+   "fieldtype": "Link",
+   "in_list_view": 1,
+   "label": "Warehouse",
+   "options": "Warehouse",
+   "read_only": 1
+  },
+  {
+   "fetch_from": "item.item_name",
+   "fieldname": "item_name",
+   "fieldtype": "Data",
+   "label": "Item Name",
+   "read_only": 1
+  },
+  {
+   "fetch_from": "item.description",
+   "fieldname": "description",
+   "fieldtype": "Text",
+   "label": "Description",
+   "read_only": 1
+  },
+  {
+   "fieldname": "reference_document_item",
+   "fieldtype": "Data",
+   "label": "Reference Document Item",
+   "read_only": 1
+  },
+  {
+   "fieldname": "serial_no",
+   "fieldtype": "Small Text",
+   "label": "Serial No",
+   "read_only": 1
+  },
+  {
+   "fieldname": "batch_no",
+   "fieldtype": "Link",
+   "label": "Batch No",
+   "options": "Batch",
+   "read_only": 1
+  },
+  {
+   "default": "0",
+   "fetch_from": "item.has_serial_no",
+   "fieldname": "has_serial_no",
+   "fieldtype": "Check",
+   "label": "Has Serial No",
+   "read_only": 1
+  },
+  {
+   "default": "0",
+   "fetch_from": "item.has_batch_no",
+   "fieldname": "has_batch_no",
+   "fieldtype": "Check",
+   "label": "Has Batch No",
+   "read_only": 1
+  },
+  {
+   "fieldname": "reference_section",
+   "fieldtype": "Section Break",
+   "label": "Reference"
+  },
+  {
+   "fieldname": "reference_doctype",
+   "fieldtype": "Select",
+   "label": "Reference Document Type",
+   "options": "Sales Order\nWork Order",
+   "read_only": 1
+  },
+  {
+   "fieldname": "reference_name",
+   "fieldtype": "Dynamic Link",
+   "label": "Reference Document",
+   "options": "reference_doctype",
+   "read_only": 1
+  },
+  {
+   "fieldname": "column_break_2",
+   "fieldtype": "Column Break"
+  },
+  {
+   "fieldname": "section_break_5",
+   "fieldtype": "Section Break"
+  }
+ ],
+ "istable": 1,
+ "modified": "2019-07-30 23:47:53.566473",
+ "modified_by": "Administrator",
+ "module": "Stock",
+ "name": "Pick List Item",
+ "owner": "Administrator",
+ "permissions": [],
+ "quick_entry": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/stock/doctype/pick_list_item/pick_list_item/pick_ticket_item.py b/erpnext/stock/doctype/pick_list_item/pick_list_item/pick_ticket_item.py
new file mode 100644
index 0000000..8797b8d
--- /dev/null
+++ b/erpnext/stock/doctype/pick_list_item/pick_list_item/pick_ticket_item.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+# import frappe
+from frappe.model.document import Document
+
+class PickListItem(Document):
+	pass
diff --git a/erpnext/stock/doctype/pick_list_reference_item/__init__.py b/erpnext/stock/doctype/pick_list_reference_item/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/stock/doctype/pick_list_reference_item/__init__.py
diff --git a/erpnext/stock/doctype/pick_list_reference_item/pick_list_reference_item.js b/erpnext/stock/doctype/pick_list_reference_item/pick_list_reference_item.js
new file mode 100644
index 0000000..875ce23
--- /dev/null
+++ b/erpnext/stock/doctype/pick_list_reference_item/pick_list_reference_item.js
@@ -0,0 +1,8 @@
+// Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('Pick List Reference Item', {
+	// refresh: function(frm) {
+
+	// }
+});
diff --git a/erpnext/stock/doctype/pick_list_reference_item/pick_list_reference_item.json b/erpnext/stock/doctype/pick_list_reference_item/pick_list_reference_item.json
new file mode 100644
index 0000000..23ef255
--- /dev/null
+++ b/erpnext/stock/doctype/pick_list_reference_item/pick_list_reference_item.json
@@ -0,0 +1,54 @@
+{
+ "creation": "2019-07-24 16:11:07.415562",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+  "item",
+  "qty",
+  "reference_doctype",
+  "reference_name",
+  "reference_document_item"
+ ],
+ "fields": [
+  {
+   "fieldname": "item",
+   "fieldtype": "Link",
+   "in_list_view": 1,
+   "label": "Item",
+   "options": "Item"
+  },
+  {
+   "fieldname": "reference_doctype",
+   "fieldtype": "Link",
+   "label": "Reference Document type",
+   "options": "DocType"
+  },
+  {
+   "fieldname": "reference_name",
+   "fieldtype": "Dynamic Link",
+   "label": "Reference Name",
+   "options": "reference_doctype"
+  },
+  {
+   "fieldname": "qty",
+   "fieldtype": "Float",
+   "in_list_view": 1,
+   "label": "Qty"
+  },
+  {
+   "fieldname": "reference_document_item",
+   "fieldtype": "Data",
+   "label": "Reference Document Item"
+  }
+ ],
+ "istable": 1,
+ "modified": "2019-07-30 23:43:30.901151",
+ "modified_by": "Administrator",
+ "module": "Stock",
+ "name": "Pick List Reference Item",
+ "owner": "Administrator",
+ "permissions": [],
+ "sort_field": "modified",
+ "sort_order": "DESC"
+}
\ No newline at end of file
diff --git a/erpnext/stock/doctype/pick_list_reference_item/pick_list_reference_item.py b/erpnext/stock/doctype/pick_list_reference_item/pick_list_reference_item.py
new file mode 100644
index 0000000..74f0563
--- /dev/null
+++ b/erpnext/stock/doctype/pick_list_reference_item/pick_list_reference_item.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+# import frappe
+from frappe.model.document import Document
+
+class PickListReferenceItem(Document):
+	pass