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], ["Draft", None],
["Completed", "eval:self.docstatus == 1"], ["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.v15_0.remove_agriculture_roles
erpnext.patches.v14_0.update_full_name_in_contract erpnext.patches.v14_0.update_full_name_in_contract
erpnext.patches.v15_0.drop_sle_indexes 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) 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.rename_pos_closing_entry_fields #2025-06-13
erpnext.patches.v15_0.update_pegged_currencies 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 ( if (
opts.allow_child_item_selection || 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 // args contains filtered child docnames
opts.args = args; opts.args = args;

View File

@@ -1774,8 +1774,8 @@ def create_pick_list(source_name, target_doc=None):
"doctype": "Pick List Item", "doctype": "Pick List Item",
"field_map": { "field_map": {
"parent": "sales_order", "parent": "sales_order",
"name": "sales_order_item", "parent_detail_docname": "sales_order_item",
"parent_detail_docname": "product_bundle_item", "name": "product_bundle_item",
}, },
"field_no_map": ["picked_qty"], "field_no_map": ["picked_qty"],
"postprocess": update_packed_item_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.is_return && doc.status != "Closed") {
if (doc.docstatus == 1 && frappe.model.can_create("Shipment")) { if (doc.docstatus == 1 && frappe.model.can_create("Shipment")) {
this.frm.add_custom_button( this.frm.add_custom_button(

View File

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

View File

@@ -176,6 +176,19 @@ class DeliveryNote(SellingController):
"overflow_type": "delivery", "overflow_type": "delivery",
"no_allowance": 1, "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): if cint(self.is_return):
self.status_updater.extend( self.status_updater.extend(
@@ -328,18 +341,15 @@ class DeliveryNote(SellingController):
def set_serial_and_batch_bundle_from_pick_list(self): def set_serial_and_batch_bundle_from_pick_list(self):
from erpnext.stock.serial_batch_bundle import SerialBatchCreation from erpnext.stock.serial_batch_bundle import SerialBatchCreation
if not self.pick_list:
return
for item in self.items: for item in self.items:
if item.use_serial_batch_fields: if item.use_serial_batch_fields or not item.against_pick_list:
continue continue
if item.pick_list_item and not item.serial_and_batch_bundle: if item.pick_list_item and not item.serial_and_batch_bundle:
filters = { filters = {
"item_code": item.item_code, "item_code": item.item_code,
"voucher_type": "Pick List", "voucher_type": "Pick List",
"voucher_no": self.pick_list, "voucher_no": item.against_pick_list,
"voucher_detail_no": item.pick_list_item, "voucher_detail_no": item.pick_list_item,
} }
@@ -588,7 +598,9 @@ class DeliveryNote(SellingController):
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
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): def check_next_docstatus(self):
submit_rv = frappe.db.sql( submit_rv = frappe.db.sql(

View File

@@ -77,6 +77,7 @@
"against_sales_invoice", "against_sales_invoice",
"si_detail", "si_detail",
"dn_detail", "dn_detail",
"against_pick_list",
"pick_list_item", "pick_list_item",
"section_break_40", "section_break_40",
"pick_serial_and_batch", "pick_serial_and_batch",
@@ -935,6 +936,16 @@
{ {
"fieldname": "column_break_fguf", "fieldname": "column_break_fguf",
"fieldtype": "Column Break" "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, "grid_page_length": 50,
@@ -942,7 +953,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2025-03-07 12:33:40.868499", "modified": "2025-05-31 18:51:32.651562",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Delivery Note Item", "name": "Delivery Note Item",

View File

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

View File

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

View File

@@ -30,7 +30,11 @@
"amended_from", "amended_from",
"print_settings_section", "print_settings_section",
"group_same_items", "group_same_items",
"status" "status_section",
"status",
"column_break_qyam",
"delivery_status",
"per_delivered"
], ],
"fields": [ "fields": [
{ {
@@ -181,7 +185,7 @@
"in_standard_filter": 1, "in_standard_filter": 1,
"label": "Status", "label": "Status",
"no_copy": 1, "no_copy": 1,
"options": "Draft\nOpen\nCompleted\nCancelled", "options": "Draft\nOpen\nPartly Delivered\nCompleted\nCancelled",
"print_hide": 1, "print_hide": 1,
"read_only": 1, "read_only": 1,
"report_hide": 1, "report_hide": 1,
@@ -208,11 +212,42 @@
"fieldname": "ignore_pricing_rule", "fieldname": "ignore_pricing_rule",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Ignore Pricing Rule" "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, "is_submittable": 1,
"links": [], "links": [],
"modified": "2024-08-14 13:20:42.168827", "modified": "2025-05-31 19:18:30.860044",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Pick List", "name": "Pick List",
@@ -280,6 +315,7 @@
"write": 1 "write": 1
} }
], ],
"row_format": "Dynamic",
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"states": [], "states": [],

View File

@@ -7,8 +7,7 @@ from itertools import groupby
import frappe import frappe
from frappe import _, bold from frappe import _, bold
from frappe.model.document import Document from frappe.model.mapper import get_mapped_doc, map_child_doc
from frappe.model.mapper import map_child_doc
from frappe.query_builder import Case from frappe.query_builder import Case
from frappe.query_builder.custom import GROUP_CONCAT from frappe.query_builder.custom import GROUP_CONCAT
from frappe.query_builder.functions import Coalesce, Locate, Replace, Sum 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_batches_from_bundle,
get_serial_nos_from_bundle, get_serial_nos_from_bundle,
) )
from erpnext.utilities.transaction_base import TransactionBase
# TODO: Prioritize SO or WO group warehouse # TODO: Prioritize SO or WO group warehouse
class PickList(Document): class PickList(TransactionBase):
# begin: auto-generated types # begin: auto-generated types
# This code is auto-generated. Do not modify anything in this block. # This code is auto-generated. Do not modify anything in this block.
@@ -48,6 +48,7 @@ class PickList(Document):
consider_rejected_warehouses: DF.Check consider_rejected_warehouses: DF.Check
customer: DF.Link | None customer: DF.Link | None
customer_name: DF.Data | None customer_name: DF.Data | None
delivery_status: DF.Literal["Not Delivered", "Fully Delivered", "Partly Delivered"]
for_qty: DF.Float for_qty: DF.Float
group_same_items: DF.Check group_same_items: DF.Check
ignore_pricing_rule: DF.Check ignore_pricing_rule: DF.Check
@@ -55,12 +56,13 @@ class PickList(Document):
material_request: DF.Link | None material_request: DF.Link | None
naming_series: DF.Literal["STO-PICK-.YYYY.-"] naming_series: DF.Literal["STO-PICK-.YYYY.-"]
parent_warehouse: DF.Link | None parent_warehouse: DF.Link | None
per_delivered: DF.Percent
pick_manually: DF.Check pick_manually: DF.Check
prompt_qty: DF.Check prompt_qty: DF.Check
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", "Completed", "Cancelled"] status: DF.Literal["Draft", "Open", "Partly Delivered", "Completed", "Cancelled"]
work_order: DF.Link | None work_order: DF.Link | None
# end: auto-generated types # end: auto-generated types
@@ -77,6 +79,7 @@ class PickList(Document):
self.validate_for_qty() self.validate_for_qty()
self.validate_stock_qty() self.validate_stock_qty()
self.check_serial_no_status() self.check_serial_no_status()
self.validate_with_previous_doc()
def before_save(self): def before_save(self):
self.update_status() self.update_status()
@@ -152,6 +155,19 @@ class PickList(Document):
title=_("Incorrect Warehouse"), 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): def validate_sales_order_percentage(self):
# set percentage picked in SO # set percentage picked in SO
for location in self.get("locations"): for location in self.get("locations"):
@@ -329,26 +345,27 @@ class PickList(Document):
def update_status(self, status=None, update_modified=True): def update_status(self, status=None, update_modified=True):
if not status: if not status:
if self.docstatus == 0: status = self.get_status().get("status")
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"
if status: if status:
self.db_set("status", 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): def update_reference_qty(self):
packed_items = [] packed_items = []
so_items = [] so_items = []
for item in self.locations: for item in self.locations:
if item.product_bundle_item: if item.product_bundle_item:
packed_items.append(item.sales_order_item) packed_items.append(item.product_bundle_item)
elif item.sales_order_item: elif item.sales_order_item:
so_items.append(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) self.update_sales_order_item_qty(so_items)
def update_packed_items_qty(self, packed_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) self.validate_picked_qty(picked_items)
picked_qty = frappe._dict() picked_qty = frappe._dict()
for d in picked_items: 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: for packed_item in packed_items:
frappe.db.set_value( frappe.db.set_value(
@@ -577,7 +594,6 @@ class PickList(Document):
# maintain count of each item (useful to limit get query) # maintain count of each item (useful to limit get query)
self.item_count_map.setdefault(item_code, 0) self.item_count_map.setdefault(item_code, 0)
self.item_count_map[item_code] += flt(item.stock_qty, item.precision("stock_qty")) self.item_count_map[item_code] += flt(item.stock_qty, item.precision("stock_qty"))
return item_map.values() return item_map.values()
def validate_for_qty(self): def validate_for_qty(self):
@@ -741,9 +757,10 @@ class PickList(Document):
for item in self.locations: for item in self.locations:
if not item.product_bundle_item: if not item.product_bundle_item:
continue continue
product_bundles[item.product_bundle_item] = frappe.db.get_value(
product_bundles[item.sales_order_item] = frappe.db.get_value(
"Sales Order Item", "Sales Order Item",
item.product_bundle_item, item.sales_order_item,
"item_code", "item_code",
) )
return product_bundles return product_bundles
@@ -759,10 +776,9 @@ class PickList(Document):
def _compute_picked_qty_for_bundle(self, bundle_row, bundle_items) -> int: def _compute_picked_qty_for_bundle(self, bundle_row, bundle_items) -> int:
"""Compute how many full bundles can be created from picked items.""" """Compute how many full bundles can be created from picked items."""
precision = frappe.get_precision("Stock Ledger Entry", "qty_after_transaction") precision = frappe.get_precision("Stock Ledger Entry", "qty_after_transaction")
possible_bundles = [] possible_bundles = []
for item in self.locations: for item in self.locations:
if item.product_bundle_item != bundle_row: if item.sales_order_item != bundle_row:
continue continue
if qty_in_bundle := bundle_items.get(item.item_code): 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") 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") pi_item = frappe.qb.DocType("Pick List Item")
return (
query = (
frappe.qb.from_(pi_item) frappe.qb.from_(pi_item)
.select( .select(
pi_item.sales_order_item, pi_item.sales_order_item,
pi_item.product_bundle_item,
pi_item.item_code, pi_item.item_code,
pi_item.sales_order, pi_item.sales_order,
Sum(pi_item.stock_qty).as_("stock_qty"), Sum(pi_item.stock_qty).as_("stock_qty"),
Sum(pi_item.picked_qty).as_("picked_qty"), Sum(pi_item.picked_qty).as_("picked_qty"),
) )
.where((pi_item.docstatus == 1) & (pi_item.sales_order_item.isin(items))) .where(pi_item.docstatus == 1)
.groupby( .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_item,
pi_item.sales_order, pi_item.sales_order,
) ).where(pi_item.sales_order_item.isin(items))
.for_update()
).run(as_dict=True) return query.run(as_dict=True)
def validate_item_locations(pick_list): def validate_item_locations(pick_list):
@@ -1211,9 +1238,41 @@ def create_dn_wo_so(pick_list):
return delivery_note 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): 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 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 = { item_table_mapper = {
"doctype": "Delivery Note Item", "doctype": "Delivery Note Item",
"field_map": { "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, "condition": lambda doc: abs(doc.delivered_qty) < abs(doc.qty) and doc.delivered_by_supplier != 1,
} }
for customer in sales_dict: kwargs = {"skip_item_mapping": True, "ignore_pricing_rule": pick_list.ignore_pricing_rule}
for so in sales_dict[customer]: delivery_note = create_delivery_note_from_sales_order(
delivery_note = None next(iter(sales_order_list)), delivery_note, kwargs=kwargs
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 not delivery_note:
if delivery_note: return
# map all items of all sales orders of that customer
for so in sales_dict[customer]: if delivery_note:
map_pl_locations(pick_list, item_table_mapper, delivery_note, so) for so in sales_order_list:
update_packed_item_details(pick_list, delivery_note) map_pl_locations(pick_list, item_table_mapper, delivery_note, so)
update_packed_item_details(pick_list, delivery_note)
return 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) dn_item = map_child_doc(source_doc, delivery_note, item_mapper)
if dn_item: if dn_item:
dn_item.against_pick_list = pick_list.name
dn_item.pick_list_item = location.name dn_item.pick_list_item = location.name
dn_item.warehouse = location.warehouse 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.batch_no = location.batch_no
dn_item.serial_no = location.serial_no dn_item.serial_no = location.serial_no
dn_item.use_serial_batch_fields = location.use_serial_batch_fields dn_item.use_serial_batch_fields = location.use_serial_batch_fields
update_delivery_note_item(source_doc, dn_item, delivery_note) 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) set_delivery_note_missing_values(delivery_note)
delivery_note.pick_list = pick_list.name
delivery_note.company = pick_list.company delivery_note.company = pick_list.company
delivery_note.customer = frappe.get_value("Sales Order", sales_order, "customer") 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. """Add product bundles found in pick list to delivery note.
When mapping pick list items, the bundle item itself isn't part of the 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(): for so_row, item_code in product_bundles.items():
sales_order_item = frappe.get_doc("Sales Order Item", so_row) 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 = map_child_doc(sales_order_item, delivery_note, item_mapper)
dn_bundle_item.qty = pick_list._compute_picked_qty_for_bundle( dn_bundle_item.qty = pick_list._compute_picked_qty_for_bundle(
so_row, product_bundle_qty_map[item_code] 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) ).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() @frappe.whitelist()
def get_item_details(item_code, uom=None): def get_item_details(item_code, uom=None):
details = frappe.db.get_value("Item", item_code, ["stock_uom", "name"], as_dict=1) 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 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 = { const status_colors = {
Draft: "red", Draft: "red",
Open: "orange", Open: "orange",
"Partly Delivered": "orange",
Completed: "green", Completed: "green",
Cancelled: "red", 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) self.assertEqual(pick_list.locations[1].sales_order_item, sales_order.items[0].name)
def test_pick_list_for_items_with_multiple_UOM(self): 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 = make_purchase_receipt(item_code=item_code, qty=10)
purchase_receipt.submit() purchase_receipt.submit()
@@ -411,8 +417,7 @@ class TestPickList(IntegrationTestCase):
{ {
"item_code": item_code, "item_code": item_code,
"qty": 1, "qty": 1,
"conversion_factor": 5, "uom": "Box",
"stock_qty": 5,
"delivery_date": frappe.utils.today(), "delivery_date": frappe.utils.today(),
"warehouse": "_Test Warehouse - _TC", "warehouse": "_Test Warehouse - _TC",
}, },
@@ -426,6 +431,7 @@ class TestPickList(IntegrationTestCase):
], ],
} }
).insert() ).insert()
sales_order.submit() sales_order.submit()
pick_list = frappe.get_doc( pick_list = frappe.get_doc(
@@ -440,6 +446,7 @@ class TestPickList(IntegrationTestCase):
"item_code": item_code, "item_code": item_code,
"qty": 2, "qty": 2,
"stock_qty": 1, "stock_qty": 1,
"uom": "Unit",
"conversion_factor": 0.5, "conversion_factor": 0.5,
"sales_order": sales_order.name, "sales_order": sales_order.name,
"sales_order_item": sales_order.items[0].name, "sales_order_item": sales_order.items[0].name,
@@ -461,7 +468,11 @@ class TestPickList(IntegrationTestCase):
delivery_note = create_delivery_note(pick_list.name) delivery_note = create_delivery_note(pick_list.name)
pick_list.load_from_db() 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(pick_list.locations[1].qty, delivery_note.items[1].qty)
self.assertEqual(sales_order.items[0].conversion_factor, delivery_note.items[0].conversion_factor) 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( sales_order_2 = frappe.get_doc(
{ {
"doctype": "Sales Order", "doctype": "Sales Order",
"customer": "_Test Customer 1", "customer": "_Test Customer",
"company": "_Test Company", "company": "_Test Company",
"items": [ "items": [
{ {
@@ -555,6 +566,7 @@ class TestPickList(IntegrationTestCase):
"items_based_on": "Sales Order", "items_based_on": "Sales Order",
"purpose": "Delivery", "purpose": "Delivery",
"picker": "P001", "picker": "P001",
"customer": "_Test Customer",
"locations": [ "locations": [
{ {
"item_code": "_Test Item ", "item_code": "_Test Item ",
@@ -580,22 +592,25 @@ class TestPickList(IntegrationTestCase):
create_delivery_note(pick_list.name) create_delivery_note(pick_list.name)
for dn in frappe.get_all( for dn in frappe.get_all(
"Delivery Note", "Delivery Note",
filters={"pick_list": pick_list.name, "customer": "_Test Customer"}, filters={"against_pick_list": pick_list.name, "customer": "_Test Customer"},
fields={"name"}, fields={"name"},
): ):
for dn_item in frappe.get_doc("Delivery Note", dn.name).get("items"): 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.item_code, "_Test Item")
self.assertEqual(dn_item.against_sales_order, sales_order_1.name) 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) self.assertEqual(dn_item.pick_list_item, pick_list.locations[dn_item.idx - 1].name)
for dn in frappe.get_all( for dn in frappe.get_all(
"Delivery Note", "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"}, fields={"name"},
): ):
for dn_item in frappe.get_doc("Delivery Note", dn.name).get("items"): 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.item_code, "_Test Item 2")
self.assertEqual(dn_item.against_sales_order, sales_order_2.name) 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 # test DN creation without so
pick_list_1 = frappe.get_doc( pick_list_1 = frappe.get_doc(
{ {
@@ -622,7 +637,9 @@ class TestPickList(IntegrationTestCase):
pick_list_1.set_item_locations() pick_list_1.set_item_locations()
pick_list_1.submit() pick_list_1.submit()
create_delivery_note(pick_list_1.name) 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"): for dn_item in frappe.get_doc("Delivery Note", dn.name).get("items"):
if dn_item.item_code == "_Test Item": if dn_item.item_code == "_Test Item":
self.assertEqual(dn_item.qty, 1) self.assertEqual(dn_item.qty, 1)
@@ -759,7 +776,6 @@ class TestPickList(IntegrationTestCase):
quantities = [5, 2] quantities = [5, 2]
bundle, components = create_product_bundle(quantities, warehouse=warehouse) bundle, components = create_product_bundle(quantities, warehouse=warehouse)
bundle_items = dict(zip(components, quantities, strict=False)) bundle_items = dict(zip(components, quantities, strict=False))
so = make_sales_order(item_code=bundle, qty=3, rate=42) so = make_sales_order(item_code=bundle, qty=3, rate=42)
pl = create_pick_list(so.name) pl = create_pick_list(so.name)
@@ -1307,3 +1323,59 @@ class TestPickList(IntegrationTestCase):
for loc in pl.locations: for loc in pl.locations:
self.assertEqual(loc.batch_no, batch2) 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", "uom",
"conversion_factor", "conversion_factor",
"stock_uom", "stock_uom",
"delivered_qty",
"serial_no_and_batch_section", "serial_no_and_batch_section",
"pick_serial_and_batch", "pick_serial_and_batch",
"serial_and_batch_bundle", "serial_and_batch_bundle",
@@ -237,17 +238,28 @@
{ {
"fieldname": "column_break_belw", "fieldname": "column_break_belw",
"fieldtype": "Column Break" "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, "istable": 1,
"links": [], "links": [],
"modified": "2024-05-07 15:32:42.905446", "modified": "2025-05-31 19:57:43.531298",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Pick List Item", "name": "Pick List Item",
"owner": "Administrator", "owner": "Administrator",
"permissions": [], "permissions": [],
"quick_entry": 1, "quick_entry": 1,
"row_format": "Dynamic",
"sort_field": "creation", "sort_field": "creation",
"sort_order": "DESC", "sort_order": "DESC",
"states": [], "states": [],

View File

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