mirror of
https://github.com/frappe/erpnext.git
synced 2026-07-20 11:22:28 +00:00
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.
(cherry picked from commit af495ed253)
# Conflicts:
# erpnext/stock/doctype/pick_list/pick_list.json
# erpnext/stock/doctype/stock_entry/stock_entry.py
This commit is contained in:
@@ -158,7 +158,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'",
|
||||
|
||||
@@ -184,7 +184,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,
|
||||
@@ -246,7 +246,11 @@
|
||||
],
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
<<<<<<< HEAD
|
||||
"modified": "2025-10-03 18:36:52.282355",
|
||||
=======
|
||||
"modified": "2026-07-06 18:17:18.000000",
|
||||
>>>>>>> af495ed253 (feat(stock): support partial transfer from pick list)
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Pick List",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -398,6 +400,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 = []
|
||||
@@ -1470,13 +1500,10 @@ def add_product_bundles_to_delivery_note(
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def create_stock_entry(pick_list):
|
||||
pick_list = frappe.get_doc(json.loads(pick_list))
|
||||
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 been already 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")
|
||||
@@ -1490,6 +1517,9 @@ def create_stock_entry(pick_list):
|
||||
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()
|
||||
@@ -1591,6 +1621,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
|
||||
@@ -1602,6 +1634,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(
|
||||
@@ -1617,6 +1651,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)
|
||||
|
||||
@@ -1625,11 +1661,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
|
||||
@@ -1637,6 +1680,7 @@ 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
|
||||
|
||||
|
||||
def get_rejected_warehouses():
|
||||
|
||||
@@ -7,6 +7,7 @@ frappe.listview_settings["Pick List"] = {
|
||||
Draft: "red",
|
||||
Open: "orange",
|
||||
"Partly Delivered": "orange",
|
||||
"Partially Transferred": "yellow",
|
||||
Completed: "green",
|
||||
Cancelled: "red",
|
||||
};
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
"conversion_factor",
|
||||
"stock_uom",
|
||||
"delivered_qty",
|
||||
"transferred_qty",
|
||||
"available_quantity_section",
|
||||
"actual_qty",
|
||||
"column_break_kyek",
|
||||
@@ -254,6 +255,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",
|
||||
@@ -284,7 +295,7 @@
|
||||
],
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2026-03-17 16:25:10.358013",
|
||||
"modified": "2026-07-06 18:17:18.000000",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Pick List Item",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -163,6 +163,15 @@ class StockEntry(StockController):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
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.purchase_order:
|
||||
self.subcontract_data = frappe._dict(
|
||||
{
|
||||
@@ -519,6 +528,11 @@ class StockEntry(StockController):
|
||||
self.validate_closed_subcontracting_order()
|
||||
self.update_subcontract_order_supplied_items()
|
||||
self.update_subcontracting_order_status()
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
self.update_pick_list_status()
|
||||
self.cancel_stock_reserve_for_wip_and_fg()
|
||||
>>>>>>> af495ed253 (feat(stock): support partial transfer from pick list)
|
||||
|
||||
if self.work_order and self.purpose == "Material Consumption for Manufacture":
|
||||
self.validate_work_order_status()
|
||||
@@ -3544,6 +3558,9 @@ class StockEntry(StockController):
|
||||
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):
|
||||
|
||||
@@ -68,6 +68,7 @@
|
||||
"col_break6",
|
||||
"material_request",
|
||||
"material_request_item",
|
||||
"pick_list_item",
|
||||
"original_item",
|
||||
"reference_section",
|
||||
"against_stock_entry",
|
||||
@@ -415,6 +416,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",
|
||||
@@ -615,7 +626,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2026-07-03 12:11:53.714931",
|
||||
"modified": "2026-07-06 18:17:18.000000",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Stock Entry Detail",
|
||||
|
||||
@@ -43,6 +43,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
|
||||
|
||||
Reference in New Issue
Block a user