Merge branch 'develop' of github.com:frappe/erpnext into feature-pick-list
diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py
index 14bf9d5..f6d4eee 100644
--- a/erpnext/controllers/accounts_controller.py
+++ b/erpnext/controllers/accounts_controller.py
@@ -263,7 +263,7 @@
if self.get("is_subcontracted"):
args["is_subcontracted"] = self.is_subcontracted
- ret = get_item_details(args, self)
+ ret = get_item_details(args, self, overwrite_warehouse=False)
for fieldname, value in ret.items():
if item.meta.get_field(fieldname) and value is not None:
diff --git a/erpnext/manufacturing/doctype/work_order/work_order.js b/erpnext/manufacturing/doctype/work_order/work_order.js
index 22613cc..d345c0b 100644
--- a/erpnext/manufacturing/doctype/work_order/work_order.js
+++ b/erpnext/manufacturing/doctype/work_order/work_order.js
@@ -161,6 +161,13 @@
frm.add_custom_button(__('Create BOM'), () => {
frm.trigger("make_bom");
});
+
+ frm.add_custom_button(__('Pick List'), () => {
+ frappe.model.open_mapped_doc({
+ method: "erpnext.manufacturing.doctype.work_order.work_order.make_pick_list",
+ frm
+ });
+ }, __('Make'));
}
},
@@ -264,6 +271,10 @@
});
},
+ make_pick_list() {
+
+ },
+
show_progress: function(frm) {
var bars = [];
var message = '';
diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py
index 2b70325..988cd22 100644
--- a/erpnext/manufacturing/doctype/work_order/work_order.py
+++ b/erpnext/manufacturing/doctype/work_order/work_order.py
@@ -19,6 +19,7 @@
from frappe.utils.csvutils import getlink
from erpnext.stock.utils import get_bin, validate_warehouse_company, get_latest_stock_qty
from erpnext.utilities.transaction_base import validate_uom_is_integer
+from frappe.model.mapper import get_mapped_doc
class OverProductionError(frappe.ValidationError): pass
class StockOverProductionError(frappe.ValidationError): pass
@@ -707,3 +708,29 @@
for d in work_order.operations:
if d.operation == operation and d.workstation == workstation:
return d
+
+@frappe.whitelist()
+def create_pick_list(source_name, target_doc=None):
+ def update_item_quantity(source, target, source_parent):
+ qty = source.required_qty - source.transferred_qty
+ target.qty = qty
+ target.stock_qty = qty
+ target.uom = frappe.get_value('Item', source.item_code, 'stock_uom')
+ target.stock_uom = target.uom
+ target.conversion_factor = 1
+
+ doc = get_mapped_doc("Work Order", source_name, {
+ "Work Order": {
+ "doctype": "Pick List",
+ "validation": {
+ "docstatus": ["=", 1]
+ }
+ },
+ "Work Order Item": {
+ "doctype": "Pick List Reference Item",
+ "postprocess": update_item_quantity,
+ "condition": lambda doc: abs(doc.transferred_qty) < abs(doc.required_qty)
+ },
+ }, target_doc)
+
+ return doc
diff --git a/erpnext/selling/doctype/sales_order/sales_order.js b/erpnext/selling/doctype/sales_order/sales_order.js
index 2e5f255..1949710 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_list(), __('Create'));
+
if(this.frm.has_perm("submit")) {
if(doc.status === 'On Hold') {
// un-hold
@@ -233,6 +235,13 @@
this.order_type(doc);
},
+ make_pick_list() {
+ frappe.model.open_mapped_doc({
+ method: "erpnext.selling.doctype.sales_order.sales_order.make_pick_list",
+ 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..dd156d0 100755
--- a/erpnext/selling/doctype/sales_order/sales_order.py
+++ b/erpnext/selling/doctype/sales_order/sales_order.py
@@ -568,7 +568,7 @@
return doc
@frappe.whitelist()
-def make_delivery_note(source_name, target_doc=None):
+def make_delivery_note(source_name, target_doc=None, skip_item_mapping=False):
def set_missing_values(source, target):
target.ignore_pricing_rule = 1
target.run_method("set_missing_values")
@@ -593,23 +593,13 @@
or item.get("buying_cost_center") \
or item_group.get("buying_cost_center")
- target_doc = get_mapped_doc("Sales Order", source_name, {
+ mapper = {
"Sales Order": {
"doctype": "Delivery Note",
"validation": {
"docstatus": ["=", 1]
}
},
- "Sales Order Item": {
- "doctype": "Delivery Note Item",
- "field_map": {
- "rate": "rate",
- "name": "so_detail",
- "parent": "against_sales_order",
- },
- "postprocess": update_item,
- "condition": lambda doc: abs(doc.delivered_qty) < abs(doc.qty) and doc.delivered_by_supplier!=1
- },
"Sales Taxes and Charges": {
"doctype": "Sales Taxes and Charges",
"add_if_empty": True
@@ -618,7 +608,21 @@
"doctype": "Sales Team",
"add_if_empty": True
}
- }, target_doc, set_missing_values)
+ }
+
+ if not skip_item_mapping:
+ mapper["Sales Order Item"] = {
+ "doctype": "Delivery Note Item",
+ "field_map": {
+ "rate": "rate",
+ "name": "so_detail",
+ "parent": "against_sales_order",
+ },
+ "postprocess": update_item,
+ "condition": lambda doc: abs(doc.delivered_qty) < abs(doc.qty) and doc.delivered_by_supplier!=1
+ }
+
+ target_doc = get_mapped_doc("Sales Order", source_name, mapper, target_doc, set_missing_values)
return target_doc
@@ -996,3 +1000,32 @@
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_list(source_name, target_doc=None):
+ def update_item_quantity(source, target, source_parent):
+ target.qty = flt(source.qty) - flt(source.delivered_qty)
+ target.stock_qty = (flt(source.qty) - flt(source.delivered_qty)) * flt(source.conversion_factor)
+
+ doc = get_mapped_doc("Sales Order", source_name, {
+ "Sales Order": {
+ "doctype": "Pick List",
+ "field_map": {
+ "doctype": "items_based_on"
+ },
+ "validation": {
+ "docstatus": ["=", 1]
+ }
+ },
+ "Sales Order Item": {
+ "doctype": "Pick List Reference Item",
+ "field_map": {
+ "parent": "sales_order",
+ "name": "sales_order_item"
+ },
+ "postprocess": update_item_quantity,
+ "condition": lambda doc: abs(doc.delivered_qty) < abs(doc.qty) and doc.delivered_by_supplier!=1
+ },
+ }, 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..bd893aa
--- /dev/null
+++ b/erpnext/stock/doctype/pick_list/pick_list.js
@@ -0,0 +1,78 @@
+// 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
+ }
+ };
+ });
+ frm.set_query('work_order', () => {
+ return {
+ query: 'erpnext.stock.doctype.pick_list.pick_list.get_pending_work_orders',
+ filters: {
+ 'company': frm.doc.company
+ }
+ };
+ });
+ },
+ refresh: (frm) => {
+ frm.trigger('add_get_items_button');
+
+ if (frm.doc.items && (frm.doc.items.length > 1 || frm.doc.items[0].item_code)) {
+ frm.add_custom_button(__('Get Item Locations'), () => {
+ frm.call('set_item_locations');
+ }).addClass('btn-primary');
+ }
+
+ frm.add_custom_button(__('Delivery Note'), () => frm.trigger('make_delivery_note'), __('Create'));
+ },
+ work_order: (frm) => {
+ frm.clear_table('items');
+ erpnext.utils.map_current_doc({
+ method: 'erpnext.manufacturing.doctype.work_order.work_order.create_pick_list',
+ target: frm,
+ source_name: frm.doc.work_order
+ });
+ },
+ items_based_on: (frm) => {
+ frm.trigger('add_get_items_button');
+ },
+ make_delivery_note(frm) {
+ frappe.model.open_mapped_doc({
+ method: 'erpnext.stock.doctype.pick_list.pick_list.make_delivery_note',
+ frm: frm
+ });
+ },
+ add_get_items_button(frm) {
+ let source_doctype = frm.doc.items_based_on;
+ if (source_doctype != 'Sales Order') return;
+ let get_query_filters = {
+ docstatus: 1,
+ per_delivered: ['<', 100],
+ status: ['!=', ''],
+ customer: frm.doc.customer
+ };
+ frm.get_items_btn = frm.add_custom_button(__('Get Items'), () => {
+ if (!frm.doc.customer) {
+ frappe.msgprint(__('Please select Customer first'));
+ return;
+ }
+ 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,
+ customer: frm.doc.customer
+ },
+ date_field: 'transaction_date',
+ get_query_filters: get_query_filters
+ });
+ });
+ }
+});
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..1a33622
--- /dev/null
+++ b/erpnext/stock/doctype/pick_list/pick_list.json
@@ -0,0 +1,109 @@
+{
+ "autoname": "PICK.####",
+ "creation": "2019-07-11 16:03:13.681045",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "items_based_on",
+ "customer",
+ "work_order",
+ "column_break_4",
+ "parent_warehouse",
+ "company",
+ "section_break_4",
+ "items",
+ "section_break_6",
+ "locations"
+ ],
+ "fields": [
+ {
+ "fieldname": "company",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Company",
+ "options": "Company",
+ "reqd": 1
+ },
+ {
+ "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"
+ },
+ {
+ "default": "Work Order",
+ "fieldname": "items_based_on",
+ "fieldtype": "Select",
+ "in_list_view": 1,
+ "label": "Items Based On",
+ "options": "Sales Order\nWork Order",
+ "reqd": 1
+ },
+ {
+ "depends_on": "eval:doc.items_based_on===\"Sales Order\"",
+ "fieldname": "customer",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Customer",
+ "options": "Customer",
+ "reqd": 1
+ },
+ {
+ "depends_on": "eval:doc.items_based_on===\"Work Order\"",
+ "fieldname": "work_order",
+ "fieldtype": "Link",
+ "label": "Work Order",
+ "options": "Work Order"
+ },
+ {
+ "fieldname": "items",
+ "fieldtype": "Table",
+ "label": "Items To Be Picked",
+ "options": "Pick List Reference Item",
+ "reqd": 1
+ },
+ {
+ "fieldname": "locations",
+ "fieldtype": "Table",
+ "label": "Item Locations",
+ "options": "Pick List Item"
+ }
+ ],
+ "modified": "2019-08-20 16:57:11.006221",
+ "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..8699ee6
--- /dev/null
+++ b/erpnext/stock/doctype/pick_list/pick_list.py
@@ -0,0 +1,264 @@
+# -*- 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 six import iteritems
+from frappe.model.mapper import get_mapped_doc, map_child_doc
+from frappe.utils import floor, flt, today
+from erpnext.selling.doctype.sales_order.sales_order import make_delivery_note as make_delivery_note_from_sales_order
+
+# TODO: Prioritize SO or WO group warehouse
+
+class PickList(Document):
+ def set_item_locations(self):
+ items = self.items
+ self.item_location_map = frappe._dict()
+
+ from_warehouses = None
+ if self.parent_warehouse:
+ from_warehouses = frappe.db.get_descendants('Warehouse', self.parent_warehouse)
+
+ # Reset
+ self.delete_key('locations')
+ for item_doc in items:
+ item_code = item_doc.item_code
+ if frappe.get_cached_value('Item', item_code, 'has_serial_no'):
+ locations = get_item_locations_based_on_serial_nos(item_doc)
+ elif frappe.get_cached_value('Item', item_code, 'has_batch_no'):
+ locations = get_item_locations_based_on_batch_nos(item_doc)
+ else:
+ if item_code not in self.item_location_map:
+ self.item_location_map[item_code] = get_available_items(item_code, from_warehouses)
+ locations = get_items_with_warehouse_and_quantity(item_doc, from_warehouses, self.item_location_map)
+
+ for row in locations:
+ row.update({
+ 'item_code': item_code,
+ 'sales_order': item_doc.sales_order,
+ 'sales_order_item': item_doc.sales_order_item,
+ 'uom': item_doc.uom,
+ 'stock_uom': item_doc.stock_uom,
+ 'conversion_factor': item_doc.conversion_factor,
+ 'stock_qty': row.get("qty", 0) * item_doc.conversion_factor,
+ 'picked_qty': row.get("qty", 0) * item_doc.conversion_factor
+ })
+ self.append('locations', row)
+
+def get_items_with_warehouse_and_quantity(item_doc, from_warehouses, item_location_map):
+ available_locations = item_location_map.get(item_doc.item_code)
+
+ locations = []
+ remaining_stock_qty = item_doc.stock_qty
+ while remaining_stock_qty > 0 and available_locations:
+ item_location = available_locations.pop(0)
+ stock_qty = remaining_stock_qty if item_location.qty >= remaining_stock_qty else item_location.qty
+ qty = stock_qty / (item_doc.conversion_factor or 1)
+
+ uom_must_be_whole_number = frappe.db.get_value("UOM", item_doc.uom, "must_be_whole_number")
+ if uom_must_be_whole_number:
+ qty = floor(qty)
+ stock_qty = qty * item_doc.conversion_factor
+
+ locations.append({
+ 'qty': qty,
+ 'warehouse': item_location.warehouse
+ })
+ remaining_stock_qty -= stock_qty
+
+ qty_diff = item_location.qty - stock_qty
+ # if extra quantity is available push current warehouse to available locations
+ if qty_diff:
+ item_location.qty = qty_diff
+ available_locations = [item_location] + available_locations
+
+ if remaining_stock_qty:
+ frappe.msgprint('{0} {1} of {2} is not available.'
+ .format(remaining_stock_qty / item_doc.conversion_factor, item_doc.uom, item_doc.item_code))
+
+ # update available locations for the item
+ item_location_map[item_doc.item_code] = available_locations
+ return locations
+
+def get_available_items(item_code, from_warehouses):
+ # gets all items available in different warehouses
+ filters = frappe._dict({
+ 'item_code': item_code,
+ '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_code,
+ 'warehouse': item_doc.warehouse
+ }, limit=item_doc.stock_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_code available in stock will have serial no?
+
+def get_item_locations_based_on_serial_nos(item_doc):
+ serial_nos = frappe.get_all('Serial No',
+ fields = ['name', 'warehouse'],
+ filters = {
+ 'item_code': item_doc.item_code,
+ 'warehouse': ['!=', '']
+ }, limit=item_doc.stock_qty, order_by='purchase_date', as_list=1)
+
+ remaining_stock_qty = flt(item_doc.stock_qty) - len(serial_nos)
+ if remaining_stock_qty:
+ frappe.msgprint('{0} {1} of {2} is not available.'
+ .format(remaining_stock_qty, item_doc.stock_uom, item_doc.item_code))
+
+ warehouse_serial_nos_map = frappe._dict()
+ for serial_no, warehouse in serial_nos:
+ warehouse_serial_nos_map.setdefault(warehouse, []).append(serial_no)
+
+ locations = []
+ for warehouse, serial_nos in iteritems(warehouse_serial_nos_map):
+ locations.append({
+ 'qty': len(serial_nos),
+ 'warehouse': warehouse,
+ 'serial_no': '\n'.join(serial_nos)
+ })
+
+ return locations
+
+def get_item_locations_based_on_batch_nos(item_doc):
+ batch_qty = frappe.db.sql("""
+ SELECT
+ sle.`warehouse`,
+ sle.`batch_no`,
+ SUM(sle.`actual_qty`) AS `qty`
+ FROM
+ `tabStock Ledger Entry` sle, `tabBatch` batch
+ WHERE
+ sle.batch_no = batch.name
+ and sle.`item_code`=%(item_code)s
+ and IFNULL(batch.expiry_date, '2200-01-01') > %(today)s
+ GROUP BY
+ `warehouse`,
+ `batch_no`,
+ `item_code`
+ HAVING `qty` > 0
+ ORDER BY IFNULL(batch.expiry_date, '2200-01-01')
+ """, {
+ 'item_code': item_doc.item_code,
+ 'today': today()
+ }, as_dict=1)
+
+ locations = []
+ required_qty = item_doc.qty
+ for d in batch_qty:
+ if d.qty > required_qty:
+ d.qty = required_qty
+ else:
+ required_qty -= d.qty
+
+ locations.append(d)
+
+ if required_qty <= 0:
+ break
+
+ if required_qty:
+ frappe.msgprint('No batches found for {} qty of {}.'.format(required_qty, item_doc.item_code))
+
+ return locations
+
+@frappe.whitelist()
+def make_delivery_note(source_name, target_doc=None):
+ pick_list = frappe.get_doc('Pick List', source_name)
+ sales_orders = [d.sales_order for d in pick_list.locations]
+ sales_orders = set(sales_orders)
+
+ delivery_note = None
+ for sales_order in sales_orders:
+ delivery_note = make_delivery_note_from_sales_order(sales_order,
+ delivery_note, skip_item_mapping=True)
+
+ for location in pick_list.locations:
+ sales_order_item = frappe.get_cached_doc('Sales Order Item', location.sales_order_item)
+ item_table_mapper = {
+ "doctype": "Delivery Note Item",
+ "field_map": {
+ "rate": "rate",
+ "name": "so_detail",
+ "parent": "against_sales_order",
+ },
+ "condition": lambda doc: abs(doc.delivered_qty) < abs(doc.qty) and doc.delivered_by_supplier!=1
+ }
+
+ dn_item = map_child_doc(sales_order_item, delivery_note, item_table_mapper)
+
+ if dn_item:
+ dn_item.warehouse = location.warehouse
+ dn_item.qty = location.qty
+
+ update_delivery_note_item(sales_order_item, dn_item, delivery_note)
+
+ set_delivery_note_missing_values(delivery_note)
+
+ return delivery_note
+
+
+def set_delivery_note_missing_values(target):
+ target.run_method("set_missing_values")
+ target.run_method("set_po_nos")
+ target.run_method("calculate_taxes_and_totals")
+
+def update_delivery_note_item(source, target, delivery_note):
+ cost_center = frappe.db.get_value("Project", delivery_note.project, "cost_center")
+ if not cost_center:
+ cost_center = frappe.db.get_value('Item Default',
+ fieldname=['buying_cost_center'],
+ filters={
+ 'parent': source.item_code,
+ 'parenttype': 'Item',
+ 'company': delivery_note.company
+ })
+
+ if not cost_center:
+ cost_center = frappe.db.get_value('Item Default',
+ fieldname=['buying_cost_center'],
+ filters={
+ 'parent': source.item_group,
+ 'parenttype': 'Item Group',
+ 'company': delivery_note.company
+ })
+
+ target.cost_center = cost_center
+
+@frappe.whitelist()
+def get_pending_work_orders(doctype, txt, searchfield, start, page_length, filters, as_dict):
+ return frappe.db.sql("""
+ SELECT
+ `name`, `company`, `planned_start_date`
+ FROM
+ `tabWork Order`
+ WHERE
+ `status` not in ('Completed', 'Stopped')
+ AND `qty` > `produced_qty`
+ AND `docstatus` = 1
+ AND `company` = %(company)s
+ AND `name` like %(txt)s
+ ORDER BY
+ if(locate(%(_txt)s, name), locate(%(_txt)s, name), 99999), name
+ LIMIT
+ %(start)s, %(page_length)s""",
+ {
+ 'txt': "%%%s%%" % txt,
+ '_txt': txt.replace('%', ''),
+ 'start': start,
+ 'page_length': frappe.utils.cint(page_length),
+ 'company': filters.get('company')
+ }, as_dict=as_dict)
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..4048e5d
--- /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',
+ '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',
+ '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',
+ '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/__init__.py b/erpnext/stock/doctype/pick_list_item/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/stock/doctype/pick_list_item/__init__.py
diff --git a/erpnext/stock/doctype/pick_list_item/pick_list_item.json b/erpnext/stock/doctype/pick_list_item/pick_list_item.json
new file mode 100644
index 0000000..9ee806a
--- /dev/null
+++ b/erpnext/stock/doctype/pick_list_item/pick_list_item.json
@@ -0,0 +1,168 @@
+{
+ "creation": "2019-07-11 16:01:22.832885",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "item_code",
+ "item_name",
+ "column_break_2",
+ "description",
+ "section_break_5",
+ "warehouse",
+ "quantity_section",
+ "qty",
+ "stock_qty",
+ "picked_qty",
+ "column_break_11",
+ "uom",
+ "stock_uom",
+ "conversion_factor",
+ "serial_no_and_batch_section",
+ "serial_no",
+ "column_break_20",
+ "batch_no",
+ "column_break_15",
+ "sales_order",
+ "sales_order_item"
+ ],
+ "fields": [
+ {
+ "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_code.item_name",
+ "fieldname": "item_name",
+ "fieldtype": "Data",
+ "label": "Item Name",
+ "read_only": 1
+ },
+ {
+ "fetch_from": "item_code.description",
+ "fieldname": "description",
+ "fieldtype": "Text",
+ "label": "Description",
+ "read_only": 1
+ },
+ {
+ "depends_on": "serial_no",
+ "fieldname": "serial_no",
+ "fieldtype": "Small Text",
+ "label": "Serial No"
+ },
+ {
+ "depends_on": "batch_no",
+ "fieldname": "batch_no",
+ "fieldtype": "Link",
+ "label": "Batch No",
+ "options": "Batch"
+ },
+ {
+ "fieldname": "column_break_2",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "section_break_5",
+ "fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "stock_uom",
+ "fieldtype": "Link",
+ "label": "Stock UOM",
+ "options": "UOM",
+ "read_only": 1
+ },
+ {
+ "fieldname": "column_break_11",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "uom",
+ "fieldtype": "Link",
+ "label": "UOM",
+ "options": "UOM",
+ "read_only": 1
+ },
+ {
+ "fieldname": "conversion_factor",
+ "fieldtype": "Float",
+ "label": "UOM Conversion Factor",
+ "read_only": 1
+ },
+ {
+ "fieldname": "stock_qty",
+ "fieldtype": "Float",
+ "label": "Qty as per Stock UOM",
+ "read_only": 1
+ },
+ {
+ "fieldname": "item_code",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Item",
+ "options": "Item",
+ "read_only": 1
+ },
+ {
+ "fieldname": "quantity_section",
+ "fieldtype": "Section Break",
+ "label": "Quantity"
+ },
+ {
+ "fieldname": "column_break_15",
+ "fieldtype": "Section Break",
+ "label": "Reference"
+ },
+ {
+ "fieldname": "sales_order",
+ "fieldtype": "Link",
+ "label": "Sales Order",
+ "options": "Sales Order",
+ "read_only": 1
+ },
+ {
+ "fieldname": "sales_order_item",
+ "fieldtype": "Data",
+ "label": "Sales Order Item",
+ "read_only": 1
+ },
+ {
+ "fieldname": "serial_no_and_batch_section",
+ "fieldtype": "Section Break",
+ "label": "Serial No and Batch"
+ },
+ {
+ "fieldname": "column_break_20",
+ "fieldtype": "Column Break"
+ }
+ ],
+ "istable": 1,
+ "modified": "2019-08-14 18:41:37.727388",
+ "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.py b/erpnext/stock/doctype/pick_list_item/pick_list_item.py
new file mode 100644
index 0000000..8797b8d
--- /dev/null
+++ b/erpnext/stock/doctype/pick_list_item/pick_list_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..0aa94fe
--- /dev/null
+++ b/erpnext/stock/doctype/pick_list_reference_item/pick_list_reference_item.json
@@ -0,0 +1,90 @@
+{
+ "creation": "2019-07-24 16:11:07.415562",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "item_code",
+ "quantity_section",
+ "qty",
+ "stock_qty",
+ "column_break_5",
+ "uom",
+ "stock_uom",
+ "conversion_factor",
+ "reference_section",
+ "sales_order",
+ "sales_order_item"
+ ],
+ "fields": [
+ {
+ "fieldname": "qty",
+ "fieldtype": "Float",
+ "in_list_view": 1,
+ "label": "Qty"
+ },
+ {
+ "fieldname": "quantity_section",
+ "fieldtype": "Section Break",
+ "label": "Quantity"
+ },
+ {
+ "fieldname": "stock_qty",
+ "fieldtype": "Float",
+ "label": "Stock Qty"
+ },
+ {
+ "fieldname": "uom",
+ "fieldtype": "Link",
+ "label": "UOM",
+ "options": "UOM"
+ },
+ {
+ "fieldname": "stock_uom",
+ "fieldtype": "Link",
+ "label": "Stock UOM",
+ "options": "UOM"
+ },
+ {
+ "fieldname": "item_code",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Item",
+ "options": "Item"
+ },
+ {
+ "fieldname": "column_break_5",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "reference_section",
+ "fieldtype": "Section Break",
+ "label": "Reference"
+ },
+ {
+ "fieldname": "sales_order",
+ "fieldtype": "Link",
+ "label": "Sales Order",
+ "options": "Sales Order"
+ },
+ {
+ "fieldname": "sales_order_item",
+ "fieldtype": "Data",
+ "label": "Sales Order Item"
+ },
+ {
+ "fieldname": "conversion_factor",
+ "fieldtype": "Float",
+ "label": "UOM Conversion Factor"
+ }
+ ],
+ "istable": 1,
+ "modified": "2019-08-14 18:38:28.867113",
+ "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
diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py
index f1d784c..41101f4 100644
--- a/erpnext/stock/get_item_details.py
+++ b/erpnext/stock/get_item_details.py
@@ -22,7 +22,7 @@
purchase_doctypes = ['Material Request', 'Supplier Quotation', 'Purchase Order', 'Purchase Receipt', 'Purchase Invoice']
@frappe.whitelist()
-def get_item_details(args, doc=None):
+def get_item_details(args, doc=None, overwrite_warehouse=True):
"""
args = {
"item_code": "",
@@ -44,11 +44,15 @@
"set_warehouse": ""
}
"""
+
args = process_args(args)
+ print('warehouse', args.warehouse, '========')
item = frappe.get_cached_doc("Item", args.item_code)
validate_item_details(args, item)
- out = get_basic_details(args, item)
+ out = get_basic_details(args, item, overwrite_warehouse)
+
+ print('warehouse2', out.warehouse, '========')
get_item_tax_template(args, item, out)
out["item_tax_rate"] = get_item_tax_map(args.company, args.get("item_tax_template") if out.get("item_tax_template") is None \
@@ -178,7 +182,7 @@
throw(_("Item {0} must be a Sub-contracted Item").format(item.name))
-def get_basic_details(args, item):
+def get_basic_details(args, item, overwrite_warehouse=True):
"""
:param args: {
"item_code": "",
@@ -225,14 +229,26 @@
item_group_defaults = get_item_group_defaults(item.name, args.company)
brand_defaults = get_brand_defaults(item.name, args.company)
- warehouse = (args.get("set_warehouse") or item_defaults.get("default_warehouse") or
- item_group_defaults.get("default_warehouse") or brand_defaults.get("default_warehouse") or args.warehouse)
+ if overwrite_warehouse or not args.warehouse:
+ warehouse = (
+ args.get("set_warehouse") or
+ item_defaults.get("default_warehouse") or
+ item_group_defaults.get("default_warehouse") or
+ brand_defaults.get("default_warehouse") or
+ args.warehouse
+ )
- if not warehouse:
- defaults = frappe.defaults.get_defaults() or {}
- if defaults.get("default_warehouse") and frappe.db.exists("Warehouse",
- {'name': defaults.default_warehouse, 'company': args.company}):
- warehouse = defaults.default_warehouse
+ if not warehouse:
+ defaults = frappe.defaults.get_defaults() or {}
+ warehouse_exists = frappe.db.exists("Warehouse", {
+ 'name': defaults.default_warehouse,
+ 'company': args.company
+ })
+ if defaults.get("default_warehouse") and warehouse_exists:
+ warehouse = defaults.default_warehouse
+
+ else:
+ warehouse = args.warehouse
if args.get('doctype') == "Material Request" and not args.get('material_request_type'):
args['material_request_type'] = frappe.db.get_value('Material Request',
@@ -784,6 +800,7 @@
@frappe.whitelist()
def get_bin_details(item_code, warehouse):
+ print(item_code, warehouse, '---------------------------')
return frappe.db.get_value("Bin", {"item_code": item_code, "warehouse": warehouse},
["projected_qty", "actual_qty", "reserved_qty"], as_dict=True, cache=True) \
or {"projected_qty": 0, "actual_qty": 0, "reserved_qty": 0}
diff --git a/erpnext/stock/print_format/__init__.py b/erpnext/stock/print_format/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/stock/print_format/__init__.py
diff --git a/erpnext/stock/print_format/pick_list/__init__.py b/erpnext/stock/print_format/pick_list/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/stock/print_format/pick_list/__init__.py
diff --git a/erpnext/stock/print_format/pick_list/pick_list.json b/erpnext/stock/print_format/pick_list/pick_list.json
new file mode 100644
index 0000000..56c819f
--- /dev/null
+++ b/erpnext/stock/print_format/pick_list/pick_list.json
@@ -0,0 +1,23 @@
+{
+ "align_labels_right": 1,
+ "creation": "2019-08-02 07:27:42.533305",
+ "custom_format": 0,
+ "disabled": 0,
+ "doc_type": "Pick List",
+ "docstatus": 0,
+ "doctype": "Print Format",
+ "font": "Default",
+ "format_data": "[{\"fieldname\": \"print_heading_template\", \"fieldtype\": \"Custom HTML\", \"options\": \"<div class=\\\"print-heading\\\">\\t\\t\\t\\t<h2>Pick List<br><small>{{ doc.name }}</small>\\t\\t\\t\\t</h2></div>\"}, {\"fieldtype\": \"Section Break\", \"label\": \"\"}, {\"fieldtype\": \"Column Break\"}, {\"print_hide\": 0, \"fieldname\": \"company\", \"label\": \"Company\"}, {\"fieldtype\": \"Column Break\"}, {\"print_hide\": 0, \"fieldname\": \"parent_warehouse\", \"label\": \"Parent Warehouse\"}, {\"fieldtype\": \"Section Break\", \"label\": \"\"}, {\"fieldtype\": \"Column Break\"}, {\"visible_columns\": [{\"print_hide\": 0, \"fieldname\": \"item\", \"print_width\": \"\"}, {\"print_hide\": 0, \"fieldname\": \"item_name\", \"print_width\": \"\"}, {\"print_hide\": 0, \"fieldname\": \"description\", \"print_width\": \"\"}, {\"print_hide\": 0, \"fieldname\": \"warehouse\", \"print_width\": \"\"}, {\"print_hide\": 0, \"fieldname\": \"qty\", \"print_width\": \"\"}, {\"print_hide\": 0, \"fieldname\": \"serial_no\", \"print_width\": \"\"}, {\"print_hide\": 0, \"fieldname\": \"batch_no\", \"print_width\": \"\"}], \"print_hide\": 0, \"fieldname\": \"item_locations\", \"label\": \"Item Locations\"}]",
+ "idx": 0,
+ "line_breaks": 1,
+ "modified": "2019-08-02 07:58:35.504361",
+ "modified_by": "Administrator",
+ "module": "Stock",
+ "name": "Pick List",
+ "owner": "Administrator",
+ "print_format_builder": 1,
+ "print_format_type": "Jinja",
+ "raw_printing": 0,
+ "show_section_headings": 1,
+ "standard": "Yes"
+}
\ No newline at end of file