feat(stock): support partial transfer from pick list

Creating a Stock Entry from a Pick List blocked any further entry
(stock_entry_exists) and flipped the pick list to Completed as soon as
one entry existed, so picked stock could not be transferred in parts.

Track transferred_qty per Pick List Item (summed from submitted Stock
Entry rows via a new pick_list_item link, mirroring delivered_qty), add
a Partially Transferred status, and map each new Stock Entry from the
remaining qty so transfers can continue until fully transferred.
This commit is contained in:
Sudharsanan11
2026-07-01 15:44:26 +05:30
parent 94a0c102a3
commit 27d5165755
10 changed files with 94 additions and 11 deletions

View File

@@ -167,7 +167,8 @@ status_map = {
"Pick List": [
["Draft", None],
["Open", "eval:self.docstatus == 1"],
["Completed", "stock_entry_exists"],
["Completed", "is_fully_transferred"],
["Partially Transferred", "is_partially_transferred"],
[
"Partly Delivered",
"eval:self.purpose == 'Delivery' and self.delivery_status == 'Partly Delivered'",

View File

@@ -285,9 +285,6 @@ def create_stock_entry(pick_list: str | dict):
pick_list = frappe.get_doc(frappe.parse_json(pick_list))
validate_item_locations(pick_list)
if stock_entry_exists(pick_list.get("name")):
return frappe.msgprint(_("Stock Entry has already been created against this Pick List"))
stock_entry = frappe.new_doc("Stock Entry")
stock_entry.pick_list = pick_list.get("name")
stock_entry.purpose = pick_list.get("purpose")
@@ -301,6 +298,9 @@ def create_stock_entry(pick_list: str | dict):
else:
stock_entry = update_stock_entry_items_with_no_reference(pick_list, stock_entry)
if not stock_entry.get("items"):
return frappe.msgprint(_("All picked items have already been transferred against this Pick List"))
stock_entry.set_missing_values()
return stock_entry.as_dict()
@@ -366,6 +366,8 @@ def update_stock_entry_based_on_work_order(pick_list, stock_entry):
stock_entry.project = work_order.project
for location in pick_list.locations:
if get_pending_transfer_stock_qty(location) <= 0:
continue
item = frappe._dict()
update_common_item_properties(item, location)
item.t_warehouse = wip_warehouse
@@ -377,6 +379,8 @@ def update_stock_entry_based_on_work_order(pick_list, stock_entry):
def update_stock_entry_based_on_material_request(pick_list, stock_entry):
for location in pick_list.locations:
if get_pending_transfer_stock_qty(location) <= 0:
continue
target_warehouse = None
if location.material_request_item:
target_warehouse = frappe.get_value(
@@ -392,6 +396,8 @@ def update_stock_entry_based_on_material_request(pick_list, stock_entry):
def update_stock_entry_items_with_no_reference(pick_list, stock_entry):
for location in pick_list.locations:
if get_pending_transfer_stock_qty(location) <= 0:
continue
item = frappe._dict()
update_common_item_properties(item, location)
@@ -400,11 +406,18 @@ def update_stock_entry_items_with_no_reference(pick_list, stock_entry):
return stock_entry
def get_pending_transfer_stock_qty(location):
"""Stock qty of this pick list row still to be moved into a Stock Entry."""
return flt(location.picked_qty) - flt(location.transferred_qty)
def update_common_item_properties(item, location):
pending_stock_qty = get_pending_transfer_stock_qty(location)
item.item_code = location.item_code
item.item_name = location.item_name
item.s_warehouse = location.warehouse
item.transfer_qty = location.picked_qty
item.qty = flt(location.picked_qty / (location.conversion_factor or 1), location.precision("qty"))
item.transfer_qty = pending_stock_qty
item.qty = flt(pending_stock_qty / (location.conversion_factor or 1), location.precision("qty"))
item.uom = location.uom
item.conversion_factor = location.conversion_factor
item.stock_uom = location.stock_uom
@@ -412,3 +425,4 @@ def update_common_item_properties(item, location):
item.serial_no = location.serial_no
item.batch_no = location.batch_no
item.material_request_item = location.material_request_item
item.pick_list_item = location.name

View File

@@ -190,7 +190,7 @@
"in_standard_filter": 1,
"label": "Status",
"no_copy": 1,
"options": "Draft\nOpen\nPartly Delivered\nCompleted\nCancelled",
"options": "Draft\nOpen\nPartly Delivered\nPartially Transferred\nCompleted\nCancelled",
"print_hide": 1,
"read_only": 1,
"report_hide": 1,
@@ -278,7 +278,7 @@
],
"is_submittable": 1,
"links": [],
"modified": "2026-02-06 18:14:18.361039",
"modified": "2026-07-01 14:27:50.617011",
"modified_by": "Administrator",
"module": "Stock",
"name": "Pick List",

View File

@@ -71,7 +71,9 @@ class PickList(TransactionBase):
purpose: DF.Literal["Material Transfer for Manufacture", "Material Transfer", "Delivery"]
scan_barcode: DF.Data | None
scan_mode: DF.Check
status: DF.Literal["Draft", "Open", "Partly Delivered", "Completed", "Cancelled"]
status: DF.Literal[
"Draft", "Open", "Partly Delivered", "Partially Transferred", "Completed", "Cancelled"
]
work_order: DF.Link | None
# end: auto-generated types
@@ -417,6 +419,34 @@ class PickList(TransactionBase):
return stock_entry_exists(self.name)
def get_transfer_status(self):
"""Return the pick list's transfer progress based on how much of the picked qty has been
moved into submitted Stock Entries (tracked on Pick List Item.transferred_qty).
Only applies to purposes that move stock via Stock Entry; the Delivery purpose is tracked
via delivery_status instead. Returns "Completed", "Partially Transferred" or None."""
if self.purpose == "Delivery":
return None
total_picked = sum(flt(row.picked_qty) for row in self.locations)
if not total_picked:
return None
total_transferred = sum(flt(row.transferred_qty) for row in self.locations)
if total_transferred <= 0:
return None
if total_transferred >= total_picked:
return "Completed"
return "Partially Transferred"
def is_fully_transferred(self):
return self.get_transfer_status() == "Completed"
def is_partially_transferred(self):
return self.get_transfer_status() == "Partially Transferred"
def update_reference_qty(self):
packed_items = []
so_items = []

View File

@@ -7,6 +7,7 @@ frappe.listview_settings["Pick List"] = {
Draft: "red",
Open: "orange",
"Partly Delivered": "orange",
"Partially Transferred": "yellow",
Completed: "green",
Cancelled: "red",
};

View File

@@ -22,6 +22,7 @@
"conversion_factor",
"stock_uom",
"delivered_qty",
"transferred_qty",
"available_quantity_section",
"actual_qty",
"column_break_kyek",
@@ -255,6 +256,16 @@
"read_only": 1,
"report_hide": 1
},
{
"default": "0",
"fieldname": "transferred_qty",
"fieldtype": "Float",
"label": "Transferred Qty (in Stock UOM)",
"no_copy": 1,
"print_hide": 1,
"read_only": 1,
"report_hide": 1
},
{
"fieldname": "available_quantity_section",
"fieldtype": "Section Break",
@@ -285,7 +296,7 @@
],
"istable": 1,
"links": [],
"modified": "2026-03-17 16:25:10.358013",
"modified": "2026-07-01 14:27:50.617011",
"modified_by": "Administrator",
"module": "Stock",
"name": "Pick List Item",

View File

@@ -39,6 +39,7 @@ class PickListItem(Document):
stock_qty: DF.Float
stock_reserved_qty: DF.Float
stock_uom: DF.Link | None
transferred_qty: DF.Float
uom: DF.Link | None
use_serial_batch_fields: DF.Check
warehouse: DF.Link | None

View File

@@ -165,6 +165,15 @@ class StockEntry(StockController, SubcontractingInwardController):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._configure_purpose_class()
self.status_updater = [
{
"source_dt": "Stock Entry Detail",
"target_dt": "Pick List Item",
"join_field": "pick_list_item",
"target_field": "transferred_qty",
"source_field": "transfer_qty",
}
]
if self.subcontracting_inward_order:
self.subcontract_data = frappe._dict(
@@ -350,6 +359,7 @@ class StockEntry(StockController, SubcontractingInwardController):
self.delink_asset_repair_sabb()
self.validate_closed_subcontracting_order()
self.update_subcontracting_order_status()
self.update_pick_list_status()
self.cancel_stock_reserve_for_wip_and_fg()
if self.work_order and self.purpose == "Material Consumption for Manufacture":
@@ -1485,6 +1495,9 @@ class StockEntry(StockController, SubcontractingInwardController):
def update_pick_list_status(self):
from erpnext.stock.doctype.pick_list.pick_list import update_pick_list_status
if self.pick_list:
self.update_qty()
update_pick_list_status(self.pick_list)
def set_missing_values(self):

View File

@@ -72,6 +72,7 @@
"col_break6",
"material_request",
"material_request_item",
"pick_list_item",
"original_item",
"reference_section",
"against_stock_entry",
@@ -424,6 +425,16 @@
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "pick_list_item",
"fieldtype": "Link",
"hidden": 1,
"label": "Pick List Item",
"no_copy": 1,
"options": "Pick List Item",
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "original_item",
"fieldtype": "Link",
@@ -679,7 +690,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2026-06-30 12:18:34.132425",
"modified": "2026-07-01 14:27:50.617011",
"modified_by": "Administrator",
"module": "Stock",
"name": "Stock Entry Detail",

View File

@@ -58,6 +58,7 @@ class StockEntryDetail(Document):
parent: DF.Data
parentfield: DF.Data
parenttype: DF.Data
pick_list_item: DF.Link | None
po_detail: DF.Data | None
project: DF.Link | None
putaway_rule: DF.Link | None