diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py index 6dd53f290c96..066dba95b1e3 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.py +++ b/erpnext/stock/doctype/pick_list/pick_list.py @@ -6,7 +6,7 @@ from itertools import groupby import frappe -from frappe import _ +from frappe import _, bold from frappe.model.document import Document from frappe.model.mapper import map_child_doc from frappe.query_builder import Case @@ -73,6 +73,9 @@ def onload(self) -> None: def validate(self): self.validate_for_qty() + if self.pick_manually and self.get("locations"): + self.validate_stock_qty() + self.check_serial_no_status() def before_save(self): self.update_status() @@ -82,6 +85,60 @@ def before_save(self): if self.get("locations"): self.validate_sales_order_percentage() + def validate_stock_qty(self): + from erpnext.stock.doctype.batch.batch import get_batch_qty + + for row in self.get("locations"): + if row.batch_no and not row.qty: + batch_qty = get_batch_qty(row.batch_no, row.warehouse, row.item_code) + + if row.qty > batch_qty: + frappe.throw( + _( + "At Row #{0}: The picked quantity {1} for the item {2} is greater than available stock {3} for the batch {4} in the warehouse {5}." + ).format(row.idx, row.item_code, batch_qty, row.batch_no, bold(row.warehouse)), + title=_("Insufficient Stock"), + ) + + continue + + bin_qty = frappe.db.get_value( + "Bin", + {"item_code": row.item_code, "warehouse": row.warehouse}, + "actual_qty", + ) + + if row.qty > bin_qty: + frappe.throw( + _( + "At Row #{0}: The picked quantity {1} for the item {2} is greater than available stock {3} in the warehouse {4}." + ).format(row.idx, row.qty, bold(row.item_code), bin_qty, bold(row.warehouse)), + title=_("Insufficient Stock"), + ) + + def check_serial_no_status(self): + from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos + + for row in self.get("locations"): + if not row.serial_no: + continue + + picked_serial_nos = get_serial_nos(row.serial_no) + validated_serial_nos = frappe.get_all( + "Serial No", + filters={"name": ("in", picked_serial_nos), "warehouse": row.warehouse}, + pluck="name", + ) + + incorrect_serial_nos = set(picked_serial_nos) - set(validated_serial_nos) + if incorrect_serial_nos: + frappe.throw( + _("The Serial No at Row #{0}: {1} is not available in warehouse {2}.").format( + row.idx, ", ".join(incorrect_serial_nos), row.warehouse + ), + title=_("Incorrect Warehouse"), + ) + def validate_sales_order_percentage(self): # set percentage picked in SO for location in self.get("locations"): diff --git a/erpnext/stock/doctype/pick_list/test_pick_list.py b/erpnext/stock/doctype/pick_list/test_pick_list.py index 116a0bd833d9..3341b1fc6de4 100644 --- a/erpnext/stock/doctype/pick_list/test_pick_list.py +++ b/erpnext/stock/doctype/pick_list/test_pick_list.py @@ -1132,3 +1132,45 @@ def test_pick_list_for_multiple_sales_orders_for_non_serialized_item(self): pl.save() self.assertEqual(pl.locations[0].qty, 80.0) + + def test_validate_picked_qty_with_manual_option(self): + warehouse = "_Test Warehouse - _TC" + non_serialized_item = make_item( + "Test Non Serialized Pick List Item For Manual Option", properties={"is_stock_item": 1} + ).name + + serialized_item = make_item( + "Test Serialized Pick List Item For Manual Option", + properties={"is_stock_item": 1, "has_serial_no": 1, "serial_no_series": "SN-HSNMSPLI-.####"}, + ).name + + batched_item = make_item( + "Test Batched Pick List Item For Manual Option", + properties={ + "is_stock_item": 1, + "has_batch_no": 1, + "batch_number_series": "SN-HBNMSPLI-.####", + "create_new_batch": 1, + }, + ).name + + make_stock_entry(item=non_serialized_item, to_warehouse=warehouse, qty=10, basic_rate=100) + make_stock_entry(item=serialized_item, to_warehouse=warehouse, qty=10, basic_rate=100) + make_stock_entry(item=batched_item, to_warehouse=warehouse, qty=10, basic_rate=100) + + so = make_sales_order( + item_code=non_serialized_item, qty=10, rate=100, do_not_save=True, warehouse=warehouse + ) + so.append("items", {"item_code": serialized_item, "qty": 10, "rate": 100, "warehouse": warehouse}) + so.append("items", {"item_code": batched_item, "qty": 10, "rate": 100, "warehouse": warehouse}) + so.set_missing_values() + so.save() + so.submit() + + pl = create_pick_list(so.name) + pl.pick_manually = 1 + + for row in pl.locations: + row.qty = row.qty + 10 + + self.assertRaises(frappe.ValidationError, pl.save)