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

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

Co-authored-by: priyanshshah2442 <priyanshshah2442@gmail.com>
(cherry picked from commit 527cfe9c7d)

# Conflicts:
#	erpnext/patches.txt
#	erpnext/stock/doctype/delivery_note_item/delivery_note_item.json
#	erpnext/stock/doctype/pick_list/pick_list.py
#	erpnext/stock/doctype/pick_list_item/pick_list_item.json

* chore: resolve conflicts

* fix: setting status correctly as per v15 utility

* fix: get items from Pick List to DN even if not linked to Sales Order

---------

Co-authored-by: Smit Vora <smitvora203@gmail.com>
Co-authored-by: Priyansh Shah <108476017+priyanshshah2442@users.noreply.github.com>
This commit is contained in:
mergify[bot]
2025-06-26 18:25:53 +05:30
committed by GitHub
parent b8a773e3e1
commit 8f47505604
19 changed files with 602 additions and 154 deletions

View File

@@ -156,6 +156,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

@@ -409,4 +409,5 @@ erpnext.patches.v15_0.set_cancelled_status_to_cancelled_pos_invoice
erpnext.patches.v15_0.rename_group_by_to_categorize_by_in_custom_reports erpnext.patches.v15_0.rename_group_by_to_categorize_by_in_custom_reports
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
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

@@ -1004,7 +1004,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

@@ -1741,8 +1741,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

@@ -38,7 +38,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",
@@ -1218,15 +1217,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

@@ -174,6 +174,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(
@@ -326,18 +339,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,
} }
@@ -586,7 +596,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,13 +936,23 @@
{ {
"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
} }
], ],
"idx": 1, "idx": 1,
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2025-02-05 14:28:33.322181", "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

@@ -80,6 +80,10 @@ def make_packing_list(doc):
update_packed_item_basic_data(item_row, pi_row, bundle_item, item_data) update_packed_item_basic_data(item_row, pi_row, bundle_item, item_data)
update_packed_item_stock_data(item_row, pi_row, bundle_item, item_data, doc) update_packed_item_stock_data(item_row, pi_row, bundle_item, item_data, doc)
update_packed_item_price_data(pi_row, item_data, doc) update_packed_item_price_data(pi_row, item_data, doc)
if item_row.get("against_pick_list"):
update_packed_item_with_pick_list_info(item_row, pi_row)
update_packed_item_from_cancelled_doc(item_row, bundle_item, pi_row, doc) update_packed_item_from_cancelled_doc(item_row, bundle_item, pi_row, doc)
if set_price_from_children: # create/update bundle item wise price dict if set_price_from_children: # create/update bundle item wise price dict
@@ -228,6 +232,28 @@ def update_packed_item_stock_data(main_item_row, pi_row, packing_item, item_data
pi_row.use_serial_batch_fields = frappe.db.get_single_value("Stock Settings", "use_serial_batch_fields") pi_row.use_serial_batch_fields = frappe.db.get_single_value("Stock Settings", "use_serial_batch_fields")
def update_packed_item_with_pick_list_info(main_item_row, pi_row):
pl_row = frappe.db.get_value(
"Pick List Item",
{
"item_code": pi_row.item_code,
"sales_order": main_item_row.get("against_sales_order"),
"sales_order_item": main_item_row.get("so_detail"),
"parent": main_item_row.against_pick_list,
},
["warehouse", "batch_no", "serial_no"],
as_dict=True,
order_by="qty desc",
)
if not pl_row:
return
pi_row.warehouse = pl_row.warehouse
pi_row.batch_no = pl_row.batch_no
pi_row.serial_no = pl_row.serial_no
def update_packed_item_price_data(pi_row, item_data, doc): def update_packed_item_price_data(pi_row, item_data, doc):
"Set price as per price list or from the Item master." "Set price as per price list or from the Item master."
if pi_row.rate: if pi_row.rate:

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()
@@ -150,6 +153,18 @@ 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": [
["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"):
@@ -326,19 +341,19 @@ class PickList(Document):
doc.submit() doc.submit()
def update_status(self, status=None, update_modified=True): 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"
if status: if status:
self.db_set("status", status) self.db_set("status", status, update_modified=update_modified)
else:
self.set_status(update=True)
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 = []
@@ -346,7 +361,7 @@ class PickList(Document):
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)
@@ -357,12 +372,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(
@@ -575,7 +590,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):
@@ -739,9 +753,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
@@ -757,17 +772,16 @@ 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):
possible_bundles.append(item.picked_qty / qty_in_bundle) possible_bundles.setdefault(item.product_bundle_item, 0)
else: possible_bundles[item.product_bundle_item] += item.picked_qty / qty_in_bundle
possible_bundles.append(0)
return int(flt(min(possible_bundles), precision or 6)) return int(flt(min(possible_bundles.values()), precision or 6)) if possible_bundles else 0
def has_unreserved_stock(self): def has_unreserved_stock(self):
if self.purpose == "Delivery": if self.purpose == "Delivery":
@@ -800,24 +814,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):
@@ -1188,13 +1213,17 @@ def create_delivery_note(source_name, target_doc=None):
if not all(item.sales_order for item in pick_list.locations): if not all(item.sales_order for item in pick_list.locations):
delivery_note = create_dn_wo_so(pick_list) delivery_note = create_dn_wo_so(pick_list)
delivery_note.flags.ignore_mandatory = True
delivery_note.save()
frappe.msgprint(_("Delivery Note(s) created for the Pick List")) frappe.msgprint(_("Delivery Note(s) created for the Pick List"))
return delivery_note return delivery_note
def create_dn_wo_so(pick_list): def create_dn_wo_so(pick_list, delivery_note=None):
delivery_note = frappe.new_doc("Delivery Note") if not delivery_note:
delivery_note = frappe.new_doc("Delivery Note")
delivery_note.company = pick_list.company delivery_note.company = pick_list.company
item_table_mapper_without_so = { item_table_mapper_without_so = {
@@ -1206,14 +1235,61 @@ def create_dn_wo_so(pick_list):
}, },
} }
map_pl_locations(pick_list, item_table_mapper_without_so, delivery_note) map_pl_locations(pick_list, item_table_mapper_without_so, delivery_note)
delivery_note.insert(ignore_mandatory=True)
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)
sales_order_arg = kwargs.get("sales_order") if kwargs else None
customer_arg = kwargs.get("customer") if kwargs else None
if sales_order_arg:
sales_orders = {sales_order_arg}
else:
sales_orders = {row.sales_order for row in pick_list.locations if row.sales_order}
if customer_arg:
sales_orders = frappe.get_all(
"Sales Order",
filters={"customer": customer_arg, "name": ["in", list(sales_orders)]},
pluck="name",
)
delivery_note = create_dn_from_so(pick_list, sales_orders, delivery_note=target_doc)
if not sales_order_arg and not all(item.sales_order for item in pick_list.locations):
if isinstance(delivery_note, str):
delivery_note = frappe.get_doc(frappe.parse_json(delivery_note))
delivery_note = create_dn_wo_so(pick_list, delivery_note)
return delivery_note return delivery_note
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)
if delivery_note:
delivery_note.flags.ignore_mandatory = True
# updates packed_items on save
# save as multiple customers are possible
delivery_note.save()
return delivery_note
def create_dn_from_so(pick_list, sales_order_list, delivery_note=None):
if not sales_order_list:
return delivery_note
item_table_mapper = { item_table_mapper = {
"doctype": "Delivery Note Item", "doctype": "Delivery Note Item",
"field_map": { "field_map": {
@@ -1224,20 +1300,17 @@ 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 = None delivery_note = create_delivery_note_from_sales_order(
kwargs = {"skip_item_mapping": True, "ignore_pricing_rule": pick_list.ignore_pricing_rule} next(iter(sales_order_list)), delivery_note, kwargs=kwargs
delivery_note = create_delivery_note_from_sales_order(so, delivery_note, kwargs=kwargs) )
break
if delivery_note: if not delivery_note:
# map all items of all sales orders of that customer return
for so in sales_dict[customer]:
map_pl_locations(pick_list, item_table_mapper, delivery_note, so) for so in sales_order_list:
delivery_note.flags.ignore_mandatory = True map_pl_locations(pick_list, item_table_mapper, delivery_note, so)
delivery_note.insert()
update_packed_item_details(pick_list, delivery_note)
delivery_note.save()
return delivery_note return delivery_note
@@ -1257,24 +1330,29 @@ 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") if sales_order:
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
@@ -1284,38 +1362,17 @@ 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]
) )
dn_bundle_item.against_pick_list = pick_list.name
update_delivery_note_item(sales_order_item, dn_bundle_item, delivery_note) update_delivery_note_item(sales_order_item, dn_bundle_item, delivery_note)
def update_packed_item_details(pick_list: "PickList", delivery_note) -> None:
"""Update stock details on packed items table of delivery note."""
def _find_so_row(packed_item):
for item in delivery_note.items:
if packed_item.parent_detail_docname == item.name:
return item.so_detail
def _find_pick_list_location(bundle_row, packed_item):
if not bundle_row:
return
for loc in pick_list.locations:
if loc.product_bundle_item == bundle_row and loc.item_code == packed_item.item_code:
return loc
for packed_item in delivery_note.packed_items:
so_row = _find_so_row(packed_item)
location = _find_pick_list_location(so_row, packed_item)
if not location:
continue
packed_item.warehouse = location.warehouse
packed_item.batch_no = location.batch_no
packed_item.serial_no = location.serial_no
@frappe.whitelist() @frappe.whitelist()
def create_stock_entry(pick_list): def create_stock_entry(pick_list):
pick_list = frappe.get_doc(json.loads(pick_list)) pick_list = frappe.get_doc(json.loads(pick_list))
@@ -1362,14 +1419,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)
@@ -1490,3 +1539,50 @@ 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")
SALES_ORDER = frappe.qb.DocType("Sales Order")
query = (
frappe.qb.from_(PICK_LIST)
.join(PICK_LIST_ITEM)
.on(PICK_LIST.name == PICK_LIST_ITEM.parent)
.join(SALES_ORDER)
.on(PICK_LIST_ITEM.sales_order == SALES_ORDER.name)
.select(
PICK_LIST.name,
SALES_ORDER.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(SALES_ORDER.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

@@ -3,6 +3,7 @@ def get_data():
"fieldname": "pick_list", "fieldname": "pick_list",
"non_standard_fieldnames": { "non_standard_fieldnames": {
"Stock Reservation Entry": "from_voucher_no", "Stock Reservation Entry": "from_voucher_no",
"Delivery Note": "against_pick_list",
}, },
"internal_links": { "internal_links": {
"Sales Order": ["locations", "sales_order"], "Sales Order": ["locations", "sales_order"],

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

@@ -5,11 +5,12 @@ import frappe
from frappe import _dict from frappe import _dict
from frappe.tests.utils import FrappeTestCase from frappe.tests.utils import FrappeTestCase
from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle
from erpnext.selling.doctype.sales_order.sales_order import create_pick_list from erpnext.selling.doctype.sales_order.sales_order import create_pick_list
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
from erpnext.stock.doctype.item.test_item import create_item, make_item from erpnext.stock.doctype.item.test_item import create_item, make_item
from erpnext.stock.doctype.packed_item.test_packed_item import create_product_bundle from erpnext.stock.doctype.packed_item.test_packed_item import create_product_bundle
from erpnext.stock.doctype.pick_list.pick_list import create_delivery_note from erpnext.stock.doctype.pick_list.pick_list import create_delivery_note, create_dn_for_pick_lists
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
from erpnext.stock.doctype.serial_and_batch_bundle.test_serial_and_batch_bundle import ( from erpnext.stock.doctype.serial_and_batch_bundle.test_serial_and_batch_bundle import (
get_batch_from_bundle, get_batch_from_bundle,
@@ -398,7 +399,13 @@ class TestPickList(FrappeTestCase):
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": "Hand", "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 +418,7 @@ class TestPickList(FrappeTestCase):
{ {
"item_code": item_code, "item_code": item_code,
"qty": 1, "qty": 1,
"conversion_factor": 5, "uom": "Hand",
"stock_qty": 5,
"delivery_date": frappe.utils.today(), "delivery_date": frappe.utils.today(),
"warehouse": "_Test Warehouse - _TC", "warehouse": "_Test Warehouse - _TC",
}, },
@@ -426,6 +432,7 @@ class TestPickList(FrappeTestCase):
], ],
} }
).insert() ).insert()
sales_order.submit() sales_order.submit()
pick_list = frappe.get_doc( pick_list = frappe.get_doc(
@@ -440,6 +447,7 @@ class TestPickList(FrappeTestCase):
"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 +469,11 @@ class TestPickList(FrappeTestCase):
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)
@@ -554,10 +566,10 @@ class TestPickList(FrappeTestCase):
"company": "_Test Company", "company": "_Test Company",
"items_based_on": "Sales Order", "items_based_on": "Sales Order",
"purpose": "Delivery", "purpose": "Delivery",
"picker": "P001", "customer": "_Test Customer",
"locations": [ "locations": [
{ {
"item_code": "_Test Item ", "item_code": "_Test Item",
"qty": 1, "qty": 1,
"stock_qty": 1, "stock_qty": 1,
"conversion_factor": 1, "conversion_factor": 1,
@@ -580,32 +592,34 @@ class TestPickList(FrappeTestCase):
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.pick_list_item, pick_list.locations[dn_item.idx - 1].name) self.assertEqual(dn_item.against_pick_list, pick_list.name)
self.assertEqual(dn_item.pick_list_item, pick_list.locations[0].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[1].name)
# test DN creation without so # test DN creation without so
pick_list_1 = frappe.get_doc( pick_list_1 = frappe.get_doc(
{ {
"doctype": "Pick List", "doctype": "Pick List",
"company": "_Test Company", "company": "_Test Company",
"purpose": "Delivery", "purpose": "Delivery",
"picker": "P001",
"locations": [ "locations": [
{ {
"item_code": "_Test Item ", "item_code": "_Test Item",
"qty": 1, "qty": 1,
"stock_qty": 1, "stock_qty": 1,
"conversion_factor": 1, "conversion_factor": 1,
@@ -622,7 +636,9 @@ class TestPickList(FrappeTestCase):
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 +775,6 @@ class TestPickList(FrappeTestCase):
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 +1322,166 @@ class TestPickList(FrappeTestCase):
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()
def test_packed_item_in_pick_list(self):
warehouse_1 = "RJ Warehouse - _TC"
warehouse_2 = "_Test Warehouse 2 - _TC"
item_1 = make_item(properties={"is_stock_item": 0}).name
item_2 = make_item().name
item_3 = make_item().name
make_product_bundle(item_1, items=[item_2, item_3])
stock_entry_1 = make_stock_entry(item=item_2, to_warehouse=warehouse_1, qty=10, basic_rate=100)
stock_entry_2 = make_stock_entry(item=item_3, to_warehouse=warehouse_1, qty=4, basic_rate=100)
stock_entry_3 = make_stock_entry(item=item_3, to_warehouse=warehouse_2, qty=6, basic_rate=100)
sales_order = make_sales_order(item_code=item_1, qty=10, rate=100)
pick_list = create_pick_list(sales_order.name)
pick_list.submit()
self.assertEqual(len(pick_list.locations), 3)
delivery_note = create_delivery_note(pick_list.name)
self.assertEqual(delivery_note.items[0].qty, 10)
self.assertEqual(delivery_note.packed_items[0].warehouse, warehouse_1)
self.assertEqual(delivery_note.packed_items[1].warehouse, warehouse_2)
pick_list.cancel()
sales_order.cancel()
stock_entry_1.cancel()
stock_entry_2.cancel()
stock_entry_3.cancel()
def test_packed_item_multiple_times_in_so(self):
frappe.db.delete("Item Price")
warehouse_1 = "RJ Warehouse - _TC"
warehouse_2 = "_Test Warehouse 2 - _TC"
warehouse = "_Test Warehouse - _TC"
item_1 = make_item(properties={"is_stock_item": 0}).name
item_2 = make_item().name
item_3 = make_item().name
make_product_bundle(item_1, items=[item_2, item_3])
stock_entry_1 = make_stock_entry(item=item_2, to_warehouse=warehouse_1, qty=20, basic_rate=100)
stock_entry_2 = make_stock_entry(item=item_3, to_warehouse=warehouse_1, qty=8, basic_rate=100)
stock_entry_3 = make_stock_entry(item=item_3, to_warehouse=warehouse_2, qty=12, basic_rate=100)
sales_order = make_sales_order(
item_list=[
{"item_code": item_1, "qty": 8, "rate": 100, "warehouse": warehouse},
{"item_code": item_1, "qty": 12, "rate": 100, "warehouse": warehouse},
]
)
pick_list = create_pick_list(sales_order.name)
pick_list.submit()
self.assertEqual(len(pick_list.locations), 4)
delivery_note = create_delivery_note(pick_list.name)
self.assertEqual(delivery_note.items[0].qty, 8)
self.assertEqual(delivery_note.items[1].qty, 12)
self.assertEqual(delivery_note.packed_items[0].qty, 8)
self.assertEqual(delivery_note.packed_items[2].qty, 12)
self.assertEqual(delivery_note.packed_items[0].warehouse, warehouse_1)
self.assertEqual(delivery_note.packed_items[1].warehouse, warehouse_1)
self.assertEqual(delivery_note.packed_items[2].warehouse, warehouse_1)
self.assertEqual(delivery_note.packed_items[3].warehouse, warehouse_2)
pick_list.cancel()
sales_order.cancel()
stock_entry_1.cancel()
stock_entry_2.cancel()
stock_entry_3.cancel()
def test_pick_list_with_and_without_so(self):
warehouse = "_Test Warehouse - _TC"
item = make_item().name
sales_order = make_sales_order(item_code=item, qty=20, rate=100)
stock_entry = make_stock_entry(item=item, to_warehouse=warehouse, qty=500, basic_rate=100)
pick_list = create_pick_list(sales_order.name)
pick_list.append(
"locations",
{
"item_code": item,
"qty": 10,
"stock_qty": 10,
"warehouse": warehouse,
"picked_qty": 0,
},
)
pick_list.submit()
delivery_note = create_dn_for_pick_lists(pick_list.name)
self.assertEqual(delivery_note.items[0].against_pick_list, pick_list.name)
self.assertEqual(delivery_note.items[0].against_sales_order, sales_order.name)
self.assertEqual(delivery_note.items[0].qty, 20)
self.assertEqual(delivery_note.items[1].against_pick_list, pick_list.name)
self.assertEqual(delivery_note.items[1].qty, 10)
pick_list.cancel()
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": "modified", "sort_field": "modified",
"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