fix: better integration of Pick List with Delivery Note (#47831)

Co-authored-by: priyanshshah2442 <priyanshshah2442@gmail.com>
This commit is contained in:
Smit Vora
2025-06-19 15:42:37 +05:30
committed by GitHub
parent 1170c5c7d3
commit 527cfe9c7d
17 changed files with 435 additions and 112 deletions

View File

@@ -164,6 +164,17 @@ status_map = {
["Draft", None],
["Completed", "eval:self.docstatus == 1"],
],
"Pick List": [
["Draft", None],
["Open", "eval:self.docstatus == 1"],
["Completed", "stock_entry_exists"],
[
"Partly Delivered",
"eval:self.purpose == 'Delivery' and self.delivery_status == 'Partly Delivered'",
],
["Completed", "eval:self.purpose == 'Delivery' and self.delivery_status == 'Fully Delivered'"],
["Cancelled", "eval:self.docstatus == 2"],
],
}

View File

@@ -419,6 +419,7 @@ erpnext.patches.v15_0.rename_group_by_to_categorize_by_in_custom_reports
erpnext.patches.v15_0.remove_agriculture_roles
erpnext.patches.v14_0.update_full_name_in_contract
erpnext.patches.v15_0.drop_sle_indexes
erpnext.patches.v15_0.update_pick_list_fields
execute:frappe.db.set_single_value("Accounts Settings", "confirm_before_resetting_posting_date", 1)
erpnext.patches.v15_0.rename_pos_closing_entry_fields #2025-06-13
erpnext.patches.v15_0.update_pegged_currencies

View File

@@ -0,0 +1,28 @@
import frappe
from frappe.query_builder.functions import IfNull
def execute():
update_delivery_note()
update_pick_list_items()
def update_delivery_note():
DN = frappe.qb.DocType("Delivery Note")
DNI = frappe.qb.DocType("Delivery Note Item")
frappe.qb.update(DNI).join(DN).on(DN.name == DNI.parent).set(DNI.against_pick_list, DN.pick_list).where(
IfNull(DN.pick_list, "") != ""
).run()
def update_pick_list_items():
PL = frappe.qb.DocType("Pick List")
PLI = frappe.qb.DocType("Pick List Item")
pick_lists = frappe.qb.from_(PL).select(PL.name).where(PL.status == "Completed").run(pluck="name")
if not pick_lists:
return
frappe.qb.update(PLI).set(PLI.delivered_qty, PLI.picked_qty).where(PLI.parent.isin(pick_lists)).run()

View File

@@ -1031,7 +1031,7 @@ erpnext.utils.map_current_doc = function (opts) {
if (
opts.allow_child_item_selection ||
["Purchase Receipt", "Delivery Note"].includes(opts.source_doctype)
["Purchase Receipt", "Delivery Note", "Pick List"].includes(opts.source_doctype)
) {
// args contains filtered child docnames
opts.args = args;

View File

@@ -1774,8 +1774,8 @@ def create_pick_list(source_name, target_doc=None):
"doctype": "Pick List Item",
"field_map": {
"parent": "sales_order",
"name": "sales_order_item",
"parent_detail_docname": "product_bundle_item",
"parent_detail_docname": "sales_order_item",
"name": "product_bundle_item",
},
"field_no_map": ["picked_qty"],
"postprocess": update_packed_item_qty,

View File

@@ -188,6 +188,55 @@ erpnext.stock.DeliveryNoteController = class DeliveryNoteController extends (
);
}
if (
!doc.is_return &&
doc.status != "Closed" &&
this.frm.has_perm("write") &&
frappe.model.can_read("Pick List") &&
this.frm.doc.docstatus === 0
) {
this.frm.add_custom_button(
__("Pick List"),
function () {
if (!me.frm.doc.customer) {
frappe.throw({
title: __("Mandatory"),
message: __("Please Select a Customer"),
});
}
erpnext.utils.map_current_doc({
method: "erpnext.stock.doctype.pick_list.pick_list.create_dn_for_pick_lists",
source_doctype: "Pick List",
target: me.frm,
setters: [
{
fieldname: "customer",
default: me.frm.doc.customer,
label: __("Customer"),
fieldtype: "Link",
options: "Customer",
reqd: 1,
read_only: 1,
},
{
fieldname: "sales_order",
label: __("Sales Order"),
fieldtype: "Link",
options: "Sales Order",
link_filters: `[["Sales Order","customer","=","${me.frm.doc.customer}"],["Sales Order","docstatus","=","1"],["Sales Order","delivery_status","not in",["Closed","Fully Delivered"]]]`,
},
],
get_query_filters: {
company: me.frm.doc.company,
},
get_query_method: "erpnext.stock.doctype.pick_list.pick_list.get_pick_list_query",
size: "extra-large",
});
},
__("Get Items From")
);
}
if (!doc.is_return && doc.status != "Closed") {
if (doc.docstatus == 1 && frappe.model.can_create("Shipment")) {
this.frm.add_custom_button(

View File

@@ -37,7 +37,6 @@
"ignore_pricing_rule",
"items_section",
"scan_barcode",
"pick_list",
"col_break_warehouse",
"set_warehouse",
"set_target_warehouse",
@@ -1196,15 +1195,6 @@
"options": "Sales Team",
"print_hide": 1
},
{
"fieldname": "pick_list",
"fieldtype": "Link",
"hidden": 1,
"label": "Pick List",
"options": "Pick List",
"read_only": 1,
"search_index": 1
},
{
"default": "0",
"fetch_from": "customer.is_internal_customer",

View File

@@ -176,6 +176,19 @@ class DeliveryNote(SellingController):
"overflow_type": "delivery",
"no_allowance": 1,
},
{
"source_dt": "Delivery Note Item",
"target_dt": "Pick List Item",
"join_field": "pick_list_item",
"target_field": "delivered_qty",
"target_parent_dt": "Pick List",
"target_parent_field": "per_delivered",
"target_ref_field": "picked_qty",
"source_field": "stock_qty",
"percent_join_field": "against_pick_list",
"status_field": "delivery_status",
"keyword": "Delivered",
},
]
if cint(self.is_return):
self.status_updater.extend(
@@ -328,18 +341,15 @@ class DeliveryNote(SellingController):
def set_serial_and_batch_bundle_from_pick_list(self):
from erpnext.stock.serial_batch_bundle import SerialBatchCreation
if not self.pick_list:
return
for item in self.items:
if item.use_serial_batch_fields:
if item.use_serial_batch_fields or not item.against_pick_list:
continue
if item.pick_list_item and not item.serial_and_batch_bundle:
filters = {
"item_code": item.item_code,
"voucher_type": "Pick List",
"voucher_no": self.pick_list,
"voucher_no": item.against_pick_list,
"voucher_detail_no": item.pick_list_item,
}
@@ -588,7 +598,9 @@ class DeliveryNote(SellingController):
def update_pick_list_status(self):
from erpnext.stock.doctype.pick_list.pick_list import update_pick_list_status
update_pick_list_status(self.pick_list)
pick_lists = {row.against_pick_list for row in self.items if row.against_pick_list}
for pick_list in pick_lists:
update_pick_list_status(pick_list)
def check_next_docstatus(self):
submit_rv = frappe.db.sql(

View File

@@ -77,6 +77,7 @@
"against_sales_invoice",
"si_detail",
"dn_detail",
"against_pick_list",
"pick_list_item",
"section_break_40",
"pick_serial_and_batch",
@@ -935,6 +936,16 @@
{
"fieldname": "column_break_fguf",
"fieldtype": "Column Break"
},
{
"fieldname": "against_pick_list",
"fieldtype": "Link",
"label": "Against Pick List",
"no_copy": 1,
"options": "Pick List",
"print_hide": 1,
"read_only": 1,
"search_index": 1
}
],
"grid_page_length": 50,
@@ -942,7 +953,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2025-03-07 12:33:40.868499",
"modified": "2025-05-31 18:51:32.651562",
"modified_by": "Administrator",
"module": "Stock",
"name": "Delivery Note Item",
@@ -953,4 +964,4 @@
"sort_field": "creation",
"sort_order": "DESC",
"states": []
}
}

View File

@@ -16,6 +16,7 @@ class DeliveryNoteItem(Document):
actual_batch_qty: DF.Float
actual_qty: DF.Float
against_pick_list: DF.Link | None
against_sales_invoice: DF.Link | None
against_sales_order: DF.Link | None
allow_zero_valuation_rate: DF.Check

View File

@@ -98,34 +98,28 @@ frappe.ui.form.on("Pick List", {
refresh: (frm) => {
frm.trigger("add_get_items_button");
if (frm.doc.docstatus === 1) {
frappe
.xcall("erpnext.stock.doctype.pick_list.pick_list.target_document_exists", {
pick_list_name: frm.doc.name,
purpose: frm.doc.purpose,
})
.then((target_document_exists) => {
frm.set_df_property("locations", "allow_on_submit", target_document_exists ? 0 : 1);
const status_completed = frm.doc.status === "Completed";
frm.set_df_property("locations", "allow_on_submit", status_completed ? 0 : 1);
if (target_document_exists) return;
if (!status_completed) {
frm.add_custom_button(__("Update Current Stock"), () =>
frm.trigger("update_pick_list_stock")
);
frm.add_custom_button(__("Update Current Stock"), () =>
frm.trigger("update_pick_list_stock")
if (frm.doc.purpose === "Delivery") {
frm.add_custom_button(
__("Create Delivery Note"),
() => frm.trigger("create_delivery_note"),
__("Create")
);
if (frm.doc.purpose === "Delivery") {
frm.add_custom_button(
__("Delivery Note"),
() => frm.trigger("create_delivery_note"),
__("Create")
);
} else {
frm.add_custom_button(
__("Stock Entry"),
() => frm.trigger("create_stock_entry"),
__("Create")
);
}
});
} else {
frm.add_custom_button(
__("Create Stock Entry"),
() => frm.trigger("create_stock_entry"),
__("Create")
);
}
}
if (frm.doc.purpose === "Delivery" && frm.doc.status === "Open") {
if (frm.doc.__onload && frm.doc.__onload.has_unreserved_stock) {

View File

@@ -30,7 +30,11 @@
"amended_from",
"print_settings_section",
"group_same_items",
"status"
"status_section",
"status",
"column_break_qyam",
"delivery_status",
"per_delivered"
],
"fields": [
{
@@ -181,7 +185,7 @@
"in_standard_filter": 1,
"label": "Status",
"no_copy": 1,
"options": "Draft\nOpen\nCompleted\nCancelled",
"options": "Draft\nOpen\nPartly Delivered\nCompleted\nCancelled",
"print_hide": 1,
"read_only": 1,
"report_hide": 1,
@@ -208,11 +212,42 @@
"fieldname": "ignore_pricing_rule",
"fieldtype": "Check",
"label": "Ignore Pricing Rule"
},
{
"collapsible": 1,
"fieldname": "status_section",
"fieldtype": "Section Break",
"label": "Status",
"print_hide": 1
},
{
"fieldname": "delivery_status",
"fieldtype": "Select",
"hidden": 1,
"in_standard_filter": 1,
"label": "Delivery Status",
"no_copy": 1,
"options": "Not Delivered\nFully Delivered\nPartly Delivered",
"print_hide": 1
},
{
"depends_on": "eval:!doc.__islocal && doc.purpose === \"Delivery\"",
"description": "% of materials delivered against this Pick List",
"fieldname": "per_delivered",
"fieldtype": "Percent",
"label": "% Delivered",
"no_copy": 1,
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "column_break_qyam",
"fieldtype": "Column Break"
}
],
"is_submittable": 1,
"links": [],
"modified": "2024-08-14 13:20:42.168827",
"modified": "2025-05-31 19:18:30.860044",
"modified_by": "Administrator",
"module": "Stock",
"name": "Pick List",
@@ -280,8 +315,9 @@
"write": 1
}
],
"row_format": "Dynamic",
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}
}

View File

@@ -7,8 +7,7 @@ from itertools import groupby
import frappe
from frappe import _, bold
from frappe.model.document import Document
from frappe.model.mapper import map_child_doc
from frappe.model.mapper import get_mapped_doc, map_child_doc
from frappe.query_builder import Case
from frappe.query_builder.custom import GROUP_CONCAT
from frappe.query_builder.functions import Coalesce, Locate, Replace, Sum
@@ -28,11 +27,12 @@ from erpnext.stock.serial_batch_bundle import (
get_batches_from_bundle,
get_serial_nos_from_bundle,
)
from erpnext.utilities.transaction_base import TransactionBase
# TODO: Prioritize SO or WO group warehouse
class PickList(Document):
class PickList(TransactionBase):
# begin: auto-generated types
# This code is auto-generated. Do not modify anything in this block.
@@ -48,6 +48,7 @@ class PickList(Document):
consider_rejected_warehouses: DF.Check
customer: DF.Link | None
customer_name: DF.Data | None
delivery_status: DF.Literal["Not Delivered", "Fully Delivered", "Partly Delivered"]
for_qty: DF.Float
group_same_items: DF.Check
ignore_pricing_rule: DF.Check
@@ -55,12 +56,13 @@ class PickList(Document):
material_request: DF.Link | None
naming_series: DF.Literal["STO-PICK-.YYYY.-"]
parent_warehouse: DF.Link | None
per_delivered: DF.Percent
pick_manually: DF.Check
prompt_qty: DF.Check
purpose: DF.Literal["Material Transfer for Manufacture", "Material Transfer", "Delivery"]
scan_barcode: DF.Data | None
scan_mode: DF.Check
status: DF.Literal["Draft", "Open", "Completed", "Cancelled"]
status: DF.Literal["Draft", "Open", "Partly Delivered", "Completed", "Cancelled"]
work_order: DF.Link | None
# end: auto-generated types
@@ -77,6 +79,7 @@ class PickList(Document):
self.validate_for_qty()
self.validate_stock_qty()
self.check_serial_no_status()
self.validate_with_previous_doc()
def before_save(self):
self.update_status()
@@ -152,6 +155,19 @@ class PickList(Document):
title=_("Incorrect Warehouse"),
)
def validate_with_previous_doc(self):
super().validate_with_previous_doc(
{
"Sales Order": {
"ref_dn_field": "sales_order",
"compare_fields": [
["customer", "="],
["company", "="],
],
},
}
)
def validate_sales_order_percentage(self):
# set percentage picked in SO
for location in self.get("locations"):
@@ -329,26 +345,27 @@ class PickList(Document):
def update_status(self, status=None, update_modified=True):
if not status:
if self.docstatus == 0:
status = "Draft"
elif self.docstatus == 1:
if target_document_exists(self.name, self.purpose):
status = "Completed"
else:
status = "Open"
elif self.docstatus == 2:
status = "Cancelled"
status = self.get_status().get("status")
if status:
self.db_set("status", status)
def stock_entry_exists(self):
if self.docstatus != 1:
return False
if self.purpose == "Delivery":
return False
return stock_entry_exists(self.name)
def update_reference_qty(self):
packed_items = []
so_items = []
for item in self.locations:
if item.product_bundle_item:
packed_items.append(item.sales_order_item)
packed_items.append(item.product_bundle_item)
elif item.sales_order_item:
so_items.append(item.sales_order_item)
@@ -359,12 +376,12 @@ class PickList(Document):
self.update_sales_order_item_qty(so_items)
def update_packed_items_qty(self, packed_items):
picked_items = get_picked_items_qty(packed_items)
picked_items = get_picked_items_qty(packed_items, contains_packed_items=True)
self.validate_picked_qty(picked_items)
picked_qty = frappe._dict()
for d in picked_items:
picked_qty[d.sales_order_item] = d.picked_qty
picked_qty[d.product_bundle_item] = d.picked_qty
for packed_item in packed_items:
frappe.db.set_value(
@@ -577,7 +594,6 @@ class PickList(Document):
# maintain count of each item (useful to limit get query)
self.item_count_map.setdefault(item_code, 0)
self.item_count_map[item_code] += flt(item.stock_qty, item.precision("stock_qty"))
return item_map.values()
def validate_for_qty(self):
@@ -741,9 +757,10 @@ class PickList(Document):
for item in self.locations:
if not item.product_bundle_item:
continue
product_bundles[item.product_bundle_item] = frappe.db.get_value(
product_bundles[item.sales_order_item] = frappe.db.get_value(
"Sales Order Item",
item.product_bundle_item,
item.sales_order_item,
"item_code",
)
return product_bundles
@@ -759,10 +776,9 @@ class PickList(Document):
def _compute_picked_qty_for_bundle(self, bundle_row, bundle_items) -> int:
"""Compute how many full bundles can be created from picked items."""
precision = frappe.get_precision("Stock Ledger Entry", "qty_after_transaction")
possible_bundles = []
for item in self.locations:
if item.product_bundle_item != bundle_row:
if item.sales_order_item != bundle_row:
continue
if qty_in_bundle := bundle_items.get(item.item_code):
@@ -802,24 +818,35 @@ def update_pick_list_status(pick_list):
doc.run_method("update_status")
def get_picked_items_qty(items) -> list[dict]:
def get_picked_items_qty(items, contains_packed_items=False) -> list[dict]:
pi_item = frappe.qb.DocType("Pick List Item")
return (
query = (
frappe.qb.from_(pi_item)
.select(
pi_item.sales_order_item,
pi_item.product_bundle_item,
pi_item.item_code,
pi_item.sales_order,
Sum(pi_item.stock_qty).as_("stock_qty"),
Sum(pi_item.picked_qty).as_("picked_qty"),
)
.where((pi_item.docstatus == 1) & (pi_item.sales_order_item.isin(items)))
.groupby(
.where(pi_item.docstatus == 1)
.for_update()
)
if contains_packed_items:
query = query.groupby(
pi_item.product_bundle_item,
pi_item.sales_order,
).where(pi_item.product_bundle_item.isin(items))
else:
query = query.groupby(
pi_item.sales_order_item,
pi_item.sales_order,
)
.for_update()
).run(as_dict=True)
).where(pi_item.sales_order_item.isin(items))
return query.run(as_dict=True)
def validate_item_locations(pick_list):
@@ -1211,9 +1238,41 @@ def create_dn_wo_so(pick_list):
return delivery_note
@frappe.whitelist()
def create_dn_for_pick_lists(source_name, target_doc=None, kwargs=None):
"""Get Items from Multiple Pick Lists and create a Delivery Note for filtered customer"""
pick_list = frappe.get_doc("Pick List", source_name)
validate_item_locations(pick_list)
if kwargs and (order := kwargs.get("sales_order")):
sales_orders = {order}
else:
sales_orders = {row.sales_order for row in pick_list.locations if row.sales_order}
if kwargs and (customer := kwargs.get("customer")):
sales_orders = frappe.get_all(
"Sales Order",
filters={"customer": customer, "name": ["in", list(sales_orders)]},
pluck="name",
)
if not sales_orders:
return
return create_dn_from_so(pick_list, sales_orders, delivery_note=target_doc)
def create_dn_with_so(sales_dict, pick_list):
"""Create Delivery Note for each customer (based on SO) in a Pick List."""
delivery_note = None
for customer in sales_dict:
delivery_note = create_dn_from_so(pick_list, sales_dict[customer], None)
return delivery_note
def create_dn_from_so(pick_list, sales_order_list, delivery_note=None):
item_table_mapper = {
"doctype": "Delivery Note Item",
"field_map": {
@@ -1224,17 +1283,19 @@ def create_dn_with_so(sales_dict, pick_list):
"condition": lambda doc: abs(doc.delivered_qty) < abs(doc.qty) and doc.delivered_by_supplier != 1,
}
for customer in sales_dict:
for so in sales_dict[customer]:
delivery_note = None
kwargs = {"skip_item_mapping": True, "ignore_pricing_rule": pick_list.ignore_pricing_rule}
delivery_note = create_delivery_note_from_sales_order(so, delivery_note, kwargs=kwargs)
break
if delivery_note:
# map all items of all sales orders of that customer
for so in sales_dict[customer]:
map_pl_locations(pick_list, item_table_mapper, delivery_note, so)
update_packed_item_details(pick_list, delivery_note)
kwargs = {"skip_item_mapping": True, "ignore_pricing_rule": pick_list.ignore_pricing_rule}
delivery_note = create_delivery_note_from_sales_order(
next(iter(sales_order_list)), delivery_note, kwargs=kwargs
)
if not delivery_note:
return
if delivery_note:
for so in sales_order_list:
map_pl_locations(pick_list, item_table_mapper, delivery_note, so)
update_packed_item_details(pick_list, delivery_note)
return delivery_note
@@ -1254,24 +1315,28 @@ def map_pl_locations(pick_list, item_mapper, delivery_note, sales_order=None):
dn_item = map_child_doc(source_doc, delivery_note, item_mapper)
if dn_item:
dn_item.against_pick_list = pick_list.name
dn_item.pick_list_item = location.name
dn_item.warehouse = location.warehouse
dn_item.qty = flt(location.picked_qty) / (flt(location.conversion_factor) or 1)
dn_item.qty = flt(location.picked_qty - location.delivered_qty) / (
flt(dn_item.conversion_factor) or 1
)
dn_item.batch_no = location.batch_no
dn_item.serial_no = location.serial_no
dn_item.use_serial_batch_fields = location.use_serial_batch_fields
update_delivery_note_item(source_doc, dn_item, delivery_note)
add_product_bundles_to_delivery_note(pick_list, delivery_note, item_mapper)
add_product_bundles_to_delivery_note(pick_list, delivery_note, item_mapper, sales_order)
set_delivery_note_missing_values(delivery_note)
delivery_note.pick_list = pick_list.name
delivery_note.company = pick_list.company
delivery_note.customer = frappe.get_value("Sales Order", sales_order, "customer")
def add_product_bundles_to_delivery_note(pick_list: "PickList", delivery_note, item_mapper) -> None:
def add_product_bundles_to_delivery_note(
pick_list: "PickList", delivery_note, item_mapper, sales_order=None
) -> None:
"""Add product bundles found in pick list to delivery note.
When mapping pick list items, the bundle item itself isn't part of the
@@ -1281,6 +1346,9 @@ def add_product_bundles_to_delivery_note(pick_list: "PickList", delivery_note, i
for so_row, item_code in product_bundles.items():
sales_order_item = frappe.get_doc("Sales Order Item", so_row)
if sales_order and sales_order_item.parent != sales_order:
continue
dn_bundle_item = map_child_doc(sales_order_item, delivery_note, item_mapper)
dn_bundle_item.qty = pick_list._compute_picked_qty_for_bundle(
so_row, product_bundle_qty_map[item_code]
@@ -1359,14 +1427,6 @@ def get_pending_work_orders(doctype, txt, searchfield, start, page_length, filte
).run(as_dict=as_dict)
@frappe.whitelist()
def target_document_exists(pick_list_name, purpose):
if purpose == "Delivery":
return frappe.db.exists("Delivery Note", {"pick_list": pick_list_name, "docstatus": 1})
return stock_entry_exists(pick_list_name)
@frappe.whitelist()
def get_item_details(item_code, uom=None):
details = frappe.db.get_value("Item", item_code, ["stock_uom", "name"], as_dict=1)
@@ -1487,3 +1547,47 @@ def get_rejected_warehouses():
)
return frappe.local.rejected_warehouses
@frappe.whitelist()
def get_pick_list_query(doctype, txt, searchfield, start, page_len, filters):
frappe.has_permission("Pick List", throw=True)
if not filters.get("company"):
frappe.throw(_("Please select a Company"))
PICK_LIST = frappe.qb.DocType("Pick List")
PICK_LIST_ITEM = frappe.qb.DocType("Pick List Item")
query = (
frappe.qb.from_(PICK_LIST)
.join(PICK_LIST_ITEM)
.on(PICK_LIST.name == PICK_LIST_ITEM.parent)
.select(
PICK_LIST.name,
PICK_LIST.customer,
Replace(GROUP_CONCAT(PICK_LIST_ITEM.sales_order).distinct(), ",", "<br>").as_("sales_order"),
)
.where(PICK_LIST.docstatus == 1)
.where(PICK_LIST.status.isin(["Open", "Partly Delivered"]))
.where(PICK_LIST.company == filters.get("company"))
.where(PICK_LIST.customer == filters.get("customer"))
.groupby(PICK_LIST.name)
)
if filters.get("sales_order"):
query = query.where(PICK_LIST_ITEM.sales_order == filters.get("sales_order"))
if txt:
meta = frappe.get_meta("Pick List")
search_fields = meta.get_search_fields()
txt = f"%{txt}%"
txt_condition = PICK_LIST[search_fields[-1]].like(txt)
for field in search_fields[:-1]:
txt_condition |= PICK_LIST[field].like(txt)
query = query.where(txt_condition)
return query.run(as_dict=True)

View File

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

View File

@@ -398,7 +398,13 @@ class TestPickList(IntegrationTestCase):
self.assertEqual(pick_list.locations[1].sales_order_item, sales_order.items[0].name)
def test_pick_list_for_items_with_multiple_UOM(self):
item_code = make_item().name
item_code = make_item(
uoms=[
{"uom": "Nos", "conversion_factor": 1},
{"uom": "Box", "conversion_factor": 5},
{"uom": "Unit", "conversion_factor": 0.5},
]
).name
purchase_receipt = make_purchase_receipt(item_code=item_code, qty=10)
purchase_receipt.submit()
@@ -411,8 +417,7 @@ class TestPickList(IntegrationTestCase):
{
"item_code": item_code,
"qty": 1,
"conversion_factor": 5,
"stock_qty": 5,
"uom": "Box",
"delivery_date": frappe.utils.today(),
"warehouse": "_Test Warehouse - _TC",
},
@@ -426,6 +431,7 @@ class TestPickList(IntegrationTestCase):
],
}
).insert()
sales_order.submit()
pick_list = frappe.get_doc(
@@ -440,6 +446,7 @@ class TestPickList(IntegrationTestCase):
"item_code": item_code,
"qty": 2,
"stock_qty": 1,
"uom": "Unit",
"conversion_factor": 0.5,
"sales_order": sales_order.name,
"sales_order_item": sales_order.items[0].name,
@@ -461,7 +468,11 @@ class TestPickList(IntegrationTestCase):
delivery_note = create_delivery_note(pick_list.name)
pick_list.load_from_db()
self.assertEqual(pick_list.locations[0].qty, delivery_note.items[0].qty)
# pick list stk_qty / dn conversion_factor = dn qty (1/5 = 0.2)
self.assertEqual(
pick_list.locations[0].picked_qty,
delivery_note.items[0].qty * delivery_note.items[0].conversion_factor,
)
self.assertEqual(pick_list.locations[1].qty, delivery_note.items[1].qty)
self.assertEqual(sales_order.items[0].conversion_factor, delivery_note.items[0].conversion_factor)
@@ -535,7 +546,7 @@ class TestPickList(IntegrationTestCase):
sales_order_2 = frappe.get_doc(
{
"doctype": "Sales Order",
"customer": "_Test Customer 1",
"customer": "_Test Customer",
"company": "_Test Company",
"items": [
{
@@ -555,6 +566,7 @@ class TestPickList(IntegrationTestCase):
"items_based_on": "Sales Order",
"purpose": "Delivery",
"picker": "P001",
"customer": "_Test Customer",
"locations": [
{
"item_code": "_Test Item ",
@@ -580,22 +592,25 @@ class TestPickList(IntegrationTestCase):
create_delivery_note(pick_list.name)
for dn in frappe.get_all(
"Delivery Note",
filters={"pick_list": pick_list.name, "customer": "_Test Customer"},
filters={"against_pick_list": pick_list.name, "customer": "_Test Customer"},
fields={"name"},
):
for dn_item in frappe.get_doc("Delivery Note", dn.name).get("items"):
self.assertEqual(dn_item.item_code, "_Test Item")
self.assertEqual(dn_item.against_sales_order, sales_order_1.name)
self.assertEqual(dn_item.against_pick_list, pick_list.name)
self.assertEqual(dn_item.pick_list_item, pick_list.locations[dn_item.idx - 1].name)
for dn in frappe.get_all(
"Delivery Note",
filters={"pick_list": pick_list.name, "customer": "_Test Customer 1"},
filters={"against_pick_list": pick_list.name, "customer": "_Test Customer 1"},
fields={"name"},
):
for dn_item in frappe.get_doc("Delivery Note", dn.name).get("items"):
self.assertEqual(dn_item.item_code, "_Test Item 2")
self.assertEqual(dn_item.against_sales_order, sales_order_2.name)
self.assertEqual(dn_item.against_pick_list, pick_list.name)
self.assertEqual(dn_item.pick_list_item, pick_list.locations[dn_item.idx - 1].name)
# test DN creation without so
pick_list_1 = frappe.get_doc(
{
@@ -622,7 +637,9 @@ class TestPickList(IntegrationTestCase):
pick_list_1.set_item_locations()
pick_list_1.submit()
create_delivery_note(pick_list_1.name)
for dn in frappe.get_all("Delivery Note", filters={"pick_list": pick_list_1.name}, fields={"name"}):
for dn in frappe.get_all(
"Delivery Note", filters={"against_pick_list": pick_list_1.name}, fields={"name"}
):
for dn_item in frappe.get_doc("Delivery Note", dn.name).get("items"):
if dn_item.item_code == "_Test Item":
self.assertEqual(dn_item.qty, 1)
@@ -759,7 +776,6 @@ class TestPickList(IntegrationTestCase):
quantities = [5, 2]
bundle, components = create_product_bundle(quantities, warehouse=warehouse)
bundle_items = dict(zip(components, quantities, strict=False))
so = make_sales_order(item_code=bundle, qty=3, rate=42)
pl = create_pick_list(so.name)
@@ -1307,3 +1323,59 @@ class TestPickList(IntegrationTestCase):
for loc in pl.locations:
self.assertEqual(loc.batch_no, batch2)
def test_multiple_pick_lists_delivery_note(self):
from erpnext.stock.doctype.pick_list.pick_list import create_dn_for_pick_lists
item_code = make_item().name
warehouse = "_Test Warehouse - _TC"
stock_entry = make_stock_entry(item=item_code, to_warehouse=warehouse, qty=500, basic_rate=100)
def create_pick_list(qty):
pick_list = frappe.get_doc(
{
"doctype": "Pick List",
"company": "_Test Company",
"customer": "_Test Customer",
"purpose": "Delivery",
"locations": [
{
"item_code": item_code,
"warehouse": warehouse,
"qty": qty,
"stock_qty": qty,
"picked_qty": 0,
"sales_order": sales_order.name,
"sales_order_item": sales_order.items[0].name,
},
],
}
)
pick_list.submit()
return pick_list
sales_order = make_sales_order(item_code=item_code, qty=50, rate=100)
pick_list_1 = create_pick_list(10)
pick_list_2 = create_pick_list(20)
delivery_note = create_dn_for_pick_lists(pick_list_1.name)
delivery_note = create_dn_for_pick_lists(pick_list_2.name, delivery_note)
delivery_note.items[0].qty = 5
delivery_note.submit()
sales_order.reload()
pick_list_1.reload()
pick_list_2.reload()
self.assertEqual(sales_order.items[0].picked_qty, 30)
self.assertEqual(pick_list_1.locations[0].delivered_qty, delivery_note.items[0].qty)
self.assertEqual(pick_list_1.status, "Partly Delivered")
self.assertEqual(pick_list_2.status, "Completed")
pick_list_1.cancel()
pick_list_2.cancel()
delivery_note.cancel()
sales_order.reload()
sales_order.cancel()
stock_entry.cancel()

View File

@@ -21,6 +21,7 @@
"uom",
"conversion_factor",
"stock_uom",
"delivered_qty",
"serial_no_and_batch_section",
"pick_serial_and_batch",
"serial_and_batch_bundle",
@@ -237,19 +238,30 @@
{
"fieldname": "column_break_belw",
"fieldtype": "Column Break"
},
{
"default": "0",
"fieldname": "delivered_qty",
"fieldtype": "Float",
"label": "Delivered Qty (in Stock UOM)",
"no_copy": 1,
"print_hide": 1,
"read_only": 1,
"report_hide": 1
}
],
"istable": 1,
"links": [],
"modified": "2024-05-07 15:32:42.905446",
"modified": "2025-05-31 19:57:43.531298",
"modified_by": "Administrator",
"module": "Stock",
"name": "Pick List Item",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}
}

View File

@@ -17,6 +17,7 @@ class PickListItem(Document):
batch_no: DF.Link | None
conversion_factor: DF.Float
delivered_qty: DF.Float
description: DF.Text | None
item_code: DF.Link
item_group: DF.Data | None