fix: Pick List empty table and Serial-Batch items handling (#22426)

* chore: Pick List empty table and serial-batch items handling

* fix: Remove console statement

* chore: Added tests for batched and batched-serialised item
This commit is contained in:
Marica 2020-07-23 17:52:06 +05:30 committed by GitHub
parent 55125fbe4e
commit 3c8c346227
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 151 additions and 16 deletions

View File

@ -3,6 +3,9 @@
frappe.ui.form.on('Pick List', {
setup: (frm) => {
frm.set_indicator_formatter('item_code',
function(doc) { return (doc.stock_qty === 0) ? "red" : "green"; });
frm.custom_make_buttons = {
'Delivery Note': 'Delivery Note',
'Stock Entry': 'Stock Entry',

View File

@ -26,11 +26,12 @@ class PickList(Document):
continue
if not item.serial_no:
frappe.throw(_("Row #{0}: {1} does not have any available serial numbers in {2}".format(
frappe.bold(item.idx), frappe.bold(item.item_code), frappe.bold(item.warehouse))))
frappe.bold(item.idx), frappe.bold(item.item_code), frappe.bold(item.warehouse))),
title=_("Serial Nos Required"))
if len(item.serial_no.split('\n')) == item.picked_qty:
continue
frappe.throw(_('For item {0} at row {1}, count of serial numbers does not match with the picked quantity')
.format(frappe.bold(item.item_code), frappe.bold(item.idx)))
.format(frappe.bold(item.item_code), frappe.bold(item.idx)), title=_("Quantity Mismatch"))
def set_item_locations(self, save=False):
items = self.aggregate_item_qty()
@ -40,6 +41,9 @@ class PickList(Document):
if self.parent_warehouse:
from_warehouses = frappe.db.get_descendants('Warehouse', self.parent_warehouse)
# Create replica before resetting, to handle empty table on update after submit.
locations_replica = self.get('locations')
# reset
self.delete_key('locations')
for item_doc in items:
@ -48,7 +52,7 @@ class PickList(Document):
self.item_location_map.setdefault(item_code,
get_available_item_locations(item_code, from_warehouses, self.item_count_map.get(item_code), self.company))
locations = get_items_with_location_and_quantity(item_doc, self.item_location_map)
locations = get_items_with_location_and_quantity(item_doc, self.item_location_map, self.docstatus)
item_doc.idx = None
item_doc.name = None
@ -62,6 +66,16 @@ class PickList(Document):
location.update(row)
self.append('locations', location)
# If table is empty on update after submit, set stock_qty, picked_qty to 0 so that indicator is red
# and give feedback to the user. This is to avoid empty Pick Lists.
if not self.get('locations') and self.docstatus == 1:
for location in locations_replica:
location.stock_qty = 0
location.picked_qty = 0
self.append('locations', location)
frappe.msgprint(_("Please Restock Items and Update the Pick List to continue. To discontinue, cancel the Pick List."),
title=_("Out of Stock"), indicator="red")
if save:
self.save()
@ -97,11 +111,13 @@ def validate_item_locations(pick_list):
if not pick_list.locations:
frappe.throw(_("Add items in the Item Locations table"))
def get_items_with_location_and_quantity(item_doc, item_location_map):
def get_items_with_location_and_quantity(item_doc, item_location_map, docstatus):
available_locations = item_location_map.get(item_doc.item_code)
locations = []
remaining_stock_qty = item_doc.stock_qty
# if stock qty is zero on submitted entry, show positive remaining qty to recalculate in case of restock.
remaining_stock_qty = item_doc.qty if (docstatus == 1 and item_doc.stock_qty == 0) else item_doc.stock_qty
while remaining_stock_qty > 0 and available_locations:
item_location = available_locations.pop(0)
item_location = frappe._dict(item_location)
@ -119,13 +135,11 @@ def get_items_with_location_and_quantity(item_doc, item_location_map):
if item_location.serial_no:
serial_nos = '\n'.join(item_location.serial_no[0: cint(stock_qty)])
auto_set_serial_no = frappe.db.get_single_value("Stock Settings", "automatically_set_serial_nos_based_on_fifo")
locations.append(frappe._dict({
'qty': qty,
'stock_qty': stock_qty,
'warehouse': item_location.warehouse,
'serial_no': serial_nos if auto_set_serial_no else item_doc.serial_no,
'serial_no': serial_nos,
'batch_no': item_location.batch_no
}))
@ -137,7 +151,7 @@ def get_items_with_location_and_quantity(item_doc, item_location_map):
item_location.qty = qty_diff
if item_location.serial_no:
# set remaining serial numbers
item_location.serial_no = item_location.serial_no[-qty_diff:]
item_location.serial_no = item_location.serial_no[-int(qty_diff):]
available_locations = [item_location] + available_locations
# update available locations for the item
@ -146,9 +160,14 @@ def get_items_with_location_and_quantity(item_doc, item_location_map):
def get_available_item_locations(item_code, from_warehouses, required_qty, company, ignore_validation=False):
locations = []
if frappe.get_cached_value('Item', item_code, 'has_serial_no'):
has_serial_no = frappe.get_cached_value('Item', item_code, 'has_serial_no')
has_batch_no = frappe.get_cached_value('Item', item_code, 'has_batch_no')
if has_batch_no and has_serial_no:
locations = get_available_item_locations_for_serial_and_batched_item(item_code, from_warehouses, required_qty, company)
elif has_serial_no:
locations = get_available_item_locations_for_serialized_item(item_code, from_warehouses, required_qty, company)
elif frappe.get_cached_value('Item', item_code, 'has_batch_no'):
elif has_batch_no:
locations = get_available_item_locations_for_batched_item(item_code, from_warehouses, required_qty, company)
else:
locations = get_available_item_locations_for_other_item(item_code, from_warehouses, required_qty, company)
@ -158,8 +177,9 @@ def get_available_item_locations(item_code, from_warehouses, required_qty, compa
remaining_qty = required_qty - total_qty_available
if remaining_qty > 0 and not ignore_validation:
frappe.msgprint(_('{0} units of {1} is not available.')
.format(remaining_qty, frappe.get_desk_link('Item', item_code)))
frappe.msgprint(_('{0} units of Item {1} is not available.')
.format(remaining_qty, frappe.get_desk_link('Item', item_code)),
title=_("Insufficient Stock"))
return locations
@ -226,6 +246,34 @@ def get_available_item_locations_for_batched_item(item_code, from_warehouses, re
return batch_locations
def get_available_item_locations_for_serial_and_batched_item(item_code, from_warehouses, required_qty, company):
# Get batch nos by FIFO
locations = get_available_item_locations_for_batched_item(item_code, from_warehouses, required_qty, company)
filters = frappe._dict({
'item_code': item_code,
'company': company,
'warehouse': ['!=', ''],
'batch_no': ''
})
# Get Serial Nos by FIFO for Batch No
for location in locations:
filters.batch_no = location.batch_no
filters.warehouse = location.warehouse
location.qty = required_qty if location.qty > required_qty else location.qty # if extra qty in batch
serial_nos = frappe.get_list('Serial No',
fields=['name'],
filters=filters,
limit=location.qty,
order_by='purchase_date')
serial_nos = [sn.name for sn in serial_nos]
location.serial_no = serial_nos
return locations
def get_available_item_locations_for_other_item(item_code, from_warehouses, required_qty, company):
# gets all items available in different warehouses
warehouses = [x.get('name') for x in frappe.get_list("Warehouse", {'company': company}, "name")]

View File

@ -7,6 +7,8 @@ import frappe
import unittest
test_dependencies = ['Item', 'Sales Invoice', 'Stock Entry', 'Batch']
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
from erpnext.stock.doctype.item.test_item import create_item
from erpnext.stock.doctype.stock_reconciliation.stock_reconciliation \
import EmptyStockReconciliationItemsError
@ -49,7 +51,7 @@ class TestPickList(unittest.TestCase):
self.assertEqual(pick_list.locations[0].warehouse, '_Test Warehouse - _TC')
self.assertEqual(pick_list.locations[0].qty, 5)
def test_pick_list_splits_row_according_to_warhouse_availability(self):
def test_pick_list_splits_row_according_to_warehouse_availability(self):
try:
frappe.get_doc({
'doctype': 'Stock Reconciliation',
@ -122,7 +124,10 @@ class TestPickList(unittest.TestCase):
}]
})
stock_reconciliation.submit()
try:
stock_reconciliation.submit()
except EmptyStockReconciliationItemsError:
pass
pick_list = frappe.get_doc({
'doctype': 'Pick List',
@ -145,6 +150,85 @@ class TestPickList(unittest.TestCase):
self.assertEqual(pick_list.locations[0].qty, 5)
self.assertEqual(pick_list.locations[0].serial_no, '123450\n123451\n123452\n123453\n123454')
def test_pick_list_shows_batch_no_for_batched_item(self):
# check if oldest batch no is picked
item = frappe.db.exists("Item", {'item_name': 'Batched Item'})
if not item:
item = create_item("Batched Item")
item.has_batch_no = 1
item.create_new_batch = 1
item.batch_number_series = "B-BATCH-.##"
item.save()
else:
item = frappe.get_doc("Item", {'item_name': 'Batched Item'})
pr1 = make_purchase_receipt(item_code="Batched Item", qty=1, rate=100.0)
pr1.load_from_db()
oldest_batch_no = pr1.items[0].batch_no
pr2 = make_purchase_receipt(item_code="Batched Item", qty=2, rate=100.0)
pick_list = frappe.get_doc({
'doctype': 'Pick List',
'company': '_Test Company',
'purpose': 'Material Transfer',
'locations': [{
'item_code': 'Batched Item',
'qty': 1,
'stock_qty': 1,
'conversion_factor': 1,
}]
})
pick_list.set_item_locations()
self.assertEqual(pick_list.locations[0].batch_no, oldest_batch_no)
pr1.cancel()
pr2.cancel()
def test_pick_list_for_batched_and_serialised_item(self):
# check if oldest batch no and serial nos are picked
item = frappe.db.exists("Item", {'item_name': 'Batched and Serialised Item'})
if not item:
item = create_item("Batched and Serialised Item")
item.has_batch_no = 1
item.create_new_batch = 1
item.has_serial_no = 1
item.batch_number_series = "B-BATCH-.##"
item.serial_no_series = "S-.####"
item.save()
else:
item = frappe.get_doc("Item", {'item_name': 'Batched and Serialised Item'})
pr1 = make_purchase_receipt(item_code="Batched and Serialised Item", qty=2, rate=100.0)
pr1.load_from_db()
oldest_batch_no = pr1.items[0].batch_no
oldest_serial_nos = pr1.items[0].serial_no
pr2 = make_purchase_receipt(item_code="Batched and Serialised Item", qty=2, rate=100.0)
pick_list = frappe.get_doc({
'doctype': 'Pick List',
'company': '_Test Company',
'purpose': 'Material Transfer',
'locations': [{
'item_code': 'Batched and Serialised Item',
'qty': 2,
'stock_qty': 2,
'conversion_factor': 1,
}]
})
pick_list.set_item_locations()
self.assertEqual(pick_list.locations[0].batch_no, oldest_batch_no)
self.assertEqual(pick_list.locations[0].serial_no, oldest_serial_nos)
pr1.cancel()
pr2.cancel()
def test_pick_list_for_items_from_multiple_sales_orders(self):
try:
frappe.get_doc({

View File

@ -180,7 +180,7 @@
],
"istable": 1,
"links": [],
"modified": "2020-03-13 19:08:21.995986",
"modified": "2020-06-24 17:18:57.357120",
"modified_by": "Administrator",
"module": "Stock",
"name": "Pick List Item",