mirror of
https://github.com/frappe/erpnext.git
synced 2026-08-14 07:01:56 +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": [
|
"Pick List": [
|
||||||
["Draft", None],
|
["Draft", None],
|
||||||
["Open", "eval:self.docstatus == 1"],
|
["Open", "eval:self.docstatus == 1"],
|
||||||
["Completed", "stock_entry_exists"],
|
["Completed", "is_fully_transferred"],
|
||||||
|
["Partially Transferred", "is_partially_transferred"],
|
||||||
[
|
[
|
||||||
"Partly Delivered",
|
"Partly Delivered",
|
||||||
"eval:self.purpose == 'Delivery' and self.delivery_status == 'Partly Delivered'",
|
"eval:self.purpose == 'Delivery' and self.delivery_status == 'Partly Delivered'",
|
||||||
|
|||||||
@@ -184,7 +184,7 @@
|
|||||||
"in_standard_filter": 1,
|
"in_standard_filter": 1,
|
||||||
"label": "Status",
|
"label": "Status",
|
||||||
"no_copy": 1,
|
"no_copy": 1,
|
||||||
"options": "Draft\nOpen\nPartly Delivered\nCompleted\nCancelled",
|
"options": "Draft\nOpen\nPartly Delivered\nPartially Transferred\nCompleted\nCancelled",
|
||||||
"print_hide": 1,
|
"print_hide": 1,
|
||||||
"read_only": 1,
|
"read_only": 1,
|
||||||
"report_hide": 1,
|
"report_hide": 1,
|
||||||
@@ -246,7 +246,11 @@
|
|||||||
],
|
],
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
|
<<<<<<< HEAD
|
||||||
"modified": "2025-10-03 18:36:52.282355",
|
"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",
|
"modified_by": "Administrator",
|
||||||
"module": "Stock",
|
"module": "Stock",
|
||||||
"name": "Pick List",
|
"name": "Pick List",
|
||||||
|
|||||||
@@ -71,7 +71,9 @@ class PickList(TransactionBase):
|
|||||||
purpose: DF.Literal["Material Transfer for Manufacture", "Material Transfer", "Delivery"]
|
purpose: DF.Literal["Material Transfer for Manufacture", "Material Transfer", "Delivery"]
|
||||||
scan_barcode: DF.Data | None
|
scan_barcode: DF.Data | None
|
||||||
scan_mode: DF.Check
|
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
|
work_order: DF.Link | None
|
||||||
# end: auto-generated types
|
# end: auto-generated types
|
||||||
|
|
||||||
@@ -398,6 +400,34 @@ class PickList(TransactionBase):
|
|||||||
|
|
||||||
return stock_entry_exists(self.name)
|
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):
|
def update_reference_qty(self):
|
||||||
packed_items = []
|
packed_items = []
|
||||||
so_items = []
|
so_items = []
|
||||||
@@ -1470,13 +1500,10 @@ def add_product_bundles_to_delivery_note(
|
|||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def create_stock_entry(pick_list):
|
def create_stock_entry(pick_list: str | dict):
|
||||||
pick_list = frappe.get_doc(json.loads(pick_list))
|
pick_list = frappe.get_doc(frappe.parse_json(pick_list))
|
||||||
validate_item_locations(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 = frappe.new_doc("Stock Entry")
|
||||||
stock_entry.pick_list = pick_list.get("name")
|
stock_entry.pick_list = pick_list.get("name")
|
||||||
stock_entry.purpose = pick_list.get("purpose")
|
stock_entry.purpose = pick_list.get("purpose")
|
||||||
@@ -1490,6 +1517,9 @@ def create_stock_entry(pick_list):
|
|||||||
else:
|
else:
|
||||||
stock_entry = update_stock_entry_items_with_no_reference(pick_list, stock_entry)
|
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()
|
stock_entry.set_missing_values()
|
||||||
|
|
||||||
return stock_entry.as_dict()
|
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
|
stock_entry.project = work_order.project
|
||||||
|
|
||||||
for location in pick_list.locations:
|
for location in pick_list.locations:
|
||||||
|
if get_pending_transfer_stock_qty(location) <= 0:
|
||||||
|
continue
|
||||||
item = frappe._dict()
|
item = frappe._dict()
|
||||||
update_common_item_properties(item, location)
|
update_common_item_properties(item, location)
|
||||||
item.t_warehouse = wip_warehouse
|
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):
|
def update_stock_entry_based_on_material_request(pick_list, stock_entry):
|
||||||
for location in pick_list.locations:
|
for location in pick_list.locations:
|
||||||
|
if get_pending_transfer_stock_qty(location) <= 0:
|
||||||
|
continue
|
||||||
target_warehouse = None
|
target_warehouse = None
|
||||||
if location.material_request_item:
|
if location.material_request_item:
|
||||||
target_warehouse = frappe.get_value(
|
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):
|
def update_stock_entry_items_with_no_reference(pick_list, stock_entry):
|
||||||
for location in pick_list.locations:
|
for location in pick_list.locations:
|
||||||
|
if get_pending_transfer_stock_qty(location) <= 0:
|
||||||
|
continue
|
||||||
item = frappe._dict()
|
item = frappe._dict()
|
||||||
update_common_item_properties(item, location)
|
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
|
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):
|
def update_common_item_properties(item, location):
|
||||||
|
pending_stock_qty = get_pending_transfer_stock_qty(location)
|
||||||
item.item_code = location.item_code
|
item.item_code = location.item_code
|
||||||
|
item.item_name = location.item_name
|
||||||
item.s_warehouse = location.warehouse
|
item.s_warehouse = location.warehouse
|
||||||
item.transfer_qty = location.picked_qty
|
item.transfer_qty = pending_stock_qty
|
||||||
item.qty = flt(location.picked_qty / (location.conversion_factor or 1), location.precision("qty"))
|
item.qty = flt(pending_stock_qty / (location.conversion_factor or 1), location.precision("qty"))
|
||||||
item.uom = location.uom
|
item.uom = location.uom
|
||||||
item.conversion_factor = location.conversion_factor
|
item.conversion_factor = location.conversion_factor
|
||||||
item.stock_uom = location.stock_uom
|
item.stock_uom = location.stock_uom
|
||||||
@@ -1637,6 +1680,7 @@ def update_common_item_properties(item, location):
|
|||||||
item.serial_no = location.serial_no
|
item.serial_no = location.serial_no
|
||||||
item.batch_no = location.batch_no
|
item.batch_no = location.batch_no
|
||||||
item.material_request_item = location.material_request_item
|
item.material_request_item = location.material_request_item
|
||||||
|
item.pick_list_item = location.name
|
||||||
|
|
||||||
|
|
||||||
def get_rejected_warehouses():
|
def get_rejected_warehouses():
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ frappe.listview_settings["Pick List"] = {
|
|||||||
Draft: "red",
|
Draft: "red",
|
||||||
Open: "orange",
|
Open: "orange",
|
||||||
"Partly Delivered": "orange",
|
"Partly Delivered": "orange",
|
||||||
|
"Partially Transferred": "yellow",
|
||||||
Completed: "green",
|
Completed: "green",
|
||||||
Cancelled: "red",
|
Cancelled: "red",
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -22,6 +22,7 @@
|
|||||||
"conversion_factor",
|
"conversion_factor",
|
||||||
"stock_uom",
|
"stock_uom",
|
||||||
"delivered_qty",
|
"delivered_qty",
|
||||||
|
"transferred_qty",
|
||||||
"available_quantity_section",
|
"available_quantity_section",
|
||||||
"actual_qty",
|
"actual_qty",
|
||||||
"column_break_kyek",
|
"column_break_kyek",
|
||||||
@@ -254,6 +255,16 @@
|
|||||||
"read_only": 1,
|
"read_only": 1,
|
||||||
"report_hide": 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",
|
"fieldname": "available_quantity_section",
|
||||||
"fieldtype": "Section Break",
|
"fieldtype": "Section Break",
|
||||||
@@ -284,7 +295,7 @@
|
|||||||
],
|
],
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2026-03-17 16:25:10.358013",
|
"modified": "2026-07-06 18:17:18.000000",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Stock",
|
"module": "Stock",
|
||||||
"name": "Pick List Item",
|
"name": "Pick List Item",
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ class PickListItem(Document):
|
|||||||
stock_qty: DF.Float
|
stock_qty: DF.Float
|
||||||
stock_reserved_qty: DF.Float
|
stock_reserved_qty: DF.Float
|
||||||
stock_uom: DF.Link | None
|
stock_uom: DF.Link | None
|
||||||
|
transferred_qty: DF.Float
|
||||||
uom: DF.Link | None
|
uom: DF.Link | None
|
||||||
use_serial_batch_fields: DF.Check
|
use_serial_batch_fields: DF.Check
|
||||||
warehouse: DF.Link | None
|
warehouse: DF.Link | None
|
||||||
|
|||||||
@@ -163,6 +163,15 @@ class StockEntry(StockController):
|
|||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*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:
|
if self.purchase_order:
|
||||||
self.subcontract_data = frappe._dict(
|
self.subcontract_data = frappe._dict(
|
||||||
{
|
{
|
||||||
@@ -519,6 +528,11 @@ class StockEntry(StockController):
|
|||||||
self.validate_closed_subcontracting_order()
|
self.validate_closed_subcontracting_order()
|
||||||
self.update_subcontract_order_supplied_items()
|
self.update_subcontract_order_supplied_items()
|
||||||
self.update_subcontracting_order_status()
|
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":
|
if self.work_order and self.purpose == "Material Consumption for Manufacture":
|
||||||
self.validate_work_order_status()
|
self.validate_work_order_status()
|
||||||
@@ -3544,6 +3558,9 @@ class StockEntry(StockController):
|
|||||||
def update_pick_list_status(self):
|
def update_pick_list_status(self):
|
||||||
from erpnext.stock.doctype.pick_list.pick_list import update_pick_list_status
|
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)
|
update_pick_list_status(self.pick_list)
|
||||||
|
|
||||||
def set_missing_values(self):
|
def set_missing_values(self):
|
||||||
|
|||||||
@@ -68,6 +68,7 @@
|
|||||||
"col_break6",
|
"col_break6",
|
||||||
"material_request",
|
"material_request",
|
||||||
"material_request_item",
|
"material_request_item",
|
||||||
|
"pick_list_item",
|
||||||
"original_item",
|
"original_item",
|
||||||
"reference_section",
|
"reference_section",
|
||||||
"against_stock_entry",
|
"against_stock_entry",
|
||||||
@@ -415,6 +416,16 @@
|
|||||||
"print_hide": 1,
|
"print_hide": 1,
|
||||||
"read_only": 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",
|
"fieldname": "original_item",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
@@ -615,7 +626,7 @@
|
|||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2026-07-03 12:11:53.714931",
|
"modified": "2026-07-06 18:17:18.000000",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Stock",
|
"module": "Stock",
|
||||||
"name": "Stock Entry Detail",
|
"name": "Stock Entry Detail",
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ class StockEntryDetail(Document):
|
|||||||
parent: DF.Data
|
parent: DF.Data
|
||||||
parentfield: DF.Data
|
parentfield: DF.Data
|
||||||
parenttype: DF.Data
|
parenttype: DF.Data
|
||||||
|
pick_list_item: DF.Link | None
|
||||||
po_detail: DF.Data | None
|
po_detail: DF.Data | None
|
||||||
project: DF.Link | None
|
project: DF.Link | None
|
||||||
putaway_rule: DF.Link | None
|
putaway_rule: DF.Link | None
|
||||||
|
|||||||
Reference in New Issue
Block a user