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