From 8f475056048aac4ad5e694b3229d39caac22619a Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 26 Jun 2025 18:25:53 +0530 Subject: [PATCH] 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 (cherry picked from commit 527cfe9c7d6bd7f4bb38f1299d29b7d045365435) # 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 Co-authored-by: Priyansh Shah <108476017+priyanshshah2442@users.noreply.github.com> --- erpnext/controllers/status_updater.py | 11 + erpnext/patches.txt | 1 + .../patches/v15_0/update_pick_list_fields.py | 28 ++ erpnext/public/js/utils.js | 2 +- .../doctype/sales_order/sales_order.py | 4 +- .../doctype/delivery_note/delivery_note.js | 49 +++ .../doctype/delivery_note/delivery_note.json | 10 - .../doctype/delivery_note/delivery_note.py | 24 +- .../delivery_note_item.json | 13 +- .../delivery_note_item/delivery_note_item.py | 1 + .../stock/doctype/packed_item/packed_item.py | 26 ++ erpnext/stock/doctype/pick_list/pick_list.js | 44 ++- .../stock/doctype/pick_list/pick_list.json | 42 ++- erpnext/stock/doctype/pick_list/pick_list.py | 278 ++++++++++++------ .../doctype/pick_list/pick_list_dashboard.py | 1 + .../stock/doctype/pick_list/pick_list_list.js | 1 + .../stock/doctype/pick_list/test_pick_list.py | 206 ++++++++++++- .../pick_list_item/pick_list_item.json | 14 +- .../doctype/pick_list_item/pick_list_item.py | 1 + 19 files changed, 602 insertions(+), 154 deletions(-) create mode 100644 erpnext/patches/v15_0/update_pick_list_fields.py diff --git a/erpnext/controllers/status_updater.py b/erpnext/controllers/status_updater.py index cd92a102234..f16553ad908 100644 --- a/erpnext/controllers/status_updater.py +++ b/erpnext/controllers/status_updater.py @@ -156,6 +156,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"], + ], } diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 13be80338d8..34140dc2c84 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -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.v14_0.update_full_name_in_contract erpnext.patches.v15_0.drop_sle_indexes +erpnext.patches.v15_0.update_pick_list_fields erpnext.patches.v15_0.update_pegged_currencies diff --git a/erpnext/patches/v15_0/update_pick_list_fields.py b/erpnext/patches/v15_0/update_pick_list_fields.py new file mode 100644 index 00000000000..9a7a1f5f463 --- /dev/null +++ b/erpnext/patches/v15_0/update_pick_list_fields.py @@ -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() diff --git a/erpnext/public/js/utils.js b/erpnext/public/js/utils.js index e883d94c6a2..19a3f38d1e2 100755 --- a/erpnext/public/js/utils.js +++ b/erpnext/public/js/utils.js @@ -1004,7 +1004,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; diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index 66ffbba9e63..2f2f745bedb 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -1741,8 +1741,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, diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.js b/erpnext/stock/doctype/delivery_note/delivery_note.js index 1f6816e3fed..440e104abb6 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.js +++ b/erpnext/stock/doctype/delivery_note/delivery_note.js @@ -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( diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.json b/erpnext/stock/doctype/delivery_note/delivery_note.json index 4a0580f0e94..e55b7f229dc 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.json +++ b/erpnext/stock/doctype/delivery_note/delivery_note.json @@ -38,7 +38,6 @@ "ignore_pricing_rule", "items_section", "scan_barcode", - "pick_list", "col_break_warehouse", "set_warehouse", "set_target_warehouse", @@ -1218,15 +1217,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", diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py index 7b9ddb7a129..12182ae990c 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note.py @@ -174,6 +174,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( @@ -326,18 +339,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, } @@ -586,7 +596,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( diff --git a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json index e951aaf1e18..c8fcdb4c5a7 100644 --- a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json +++ b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json @@ -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,13 +936,23 @@ { "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 } ], "idx": 1, "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2025-02-05 14:28:33.322181", + "modified": "2025-05-31 18:51:32.651562", "modified_by": "Administrator", "module": "Stock", "name": "Delivery Note Item", diff --git a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.py b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.py index 7fb0e24be0b..62a7691009e 100644 --- a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.py +++ b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.py @@ -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 diff --git a/erpnext/stock/doctype/packed_item/packed_item.py b/erpnext/stock/doctype/packed_item/packed_item.py index e1a1155292d..eaeb04d568e 100644 --- a/erpnext/stock/doctype/packed_item/packed_item.py +++ b/erpnext/stock/doctype/packed_item/packed_item.py @@ -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_stock_data(item_row, pi_row, bundle_item, 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) 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") +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): "Set price as per price list or from the Item master." if pi_row.rate: diff --git a/erpnext/stock/doctype/pick_list/pick_list.js b/erpnext/stock/doctype/pick_list/pick_list.js index 6a6bb226a9e..dea83560494 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.js +++ b/erpnext/stock/doctype/pick_list/pick_list.js @@ -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) { diff --git a/erpnext/stock/doctype/pick_list/pick_list.json b/erpnext/stock/doctype/pick_list/pick_list.json index a5a46ff9187..e6449476971 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.json +++ b/erpnext/stock/doctype/pick_list/pick_list.json @@ -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,6 +315,7 @@ "write": 1 } ], + "row_format": "Dynamic", "sort_field": "modified", "sort_order": "DESC", "states": [], diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py index 31bff657fd1..aae1d4786d0 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.py +++ b/erpnext/stock/doctype/pick_list/pick_list.py @@ -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() @@ -150,6 +153,18 @@ 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": [ + ["company", "="], + ], + }, + } + ) + def validate_sales_order_percentage(self): # set percentage picked in SO for location in self.get("locations"): @@ -326,19 +341,19 @@ class PickList(Document): doc.submit() 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: - 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): packed_items = [] @@ -346,7 +361,7 @@ class PickList(Document): 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) @@ -357,12 +372,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( @@ -575,7 +590,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): @@ -739,9 +753,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 @@ -757,17 +772,16 @@ 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 = [] + 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): - possible_bundles.append(item.picked_qty / qty_in_bundle) - else: - possible_bundles.append(0) - return int(flt(min(possible_bundles), precision or 6)) + possible_bundles.setdefault(item.product_bundle_item, 0) + possible_bundles[item.product_bundle_item] += item.picked_qty / qty_in_bundle + + return int(flt(min(possible_bundles.values()), precision or 6)) if possible_bundles else 0 def has_unreserved_stock(self): if self.purpose == "Delivery": @@ -800,24 +814,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): @@ -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): 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")) return delivery_note -def create_dn_wo_so(pick_list): - delivery_note = frappe.new_doc("Delivery Note") +def create_dn_wo_so(pick_list, delivery_note=None): + if not delivery_note: + delivery_note = frappe.new_doc("Delivery Note") + delivery_note.company = pick_list.company 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) - 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 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) + 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 = { "doctype": "Delivery Note Item", "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, } - 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) - delivery_note.flags.ignore_mandatory = True - delivery_note.insert() - update_packed_item_details(pick_list, delivery_note) - delivery_note.save() + 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 + + for so in sales_order_list: + map_pl_locations(pick_list, item_table_mapper, delivery_note, so) 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) 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") + 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. 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(): 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] ) + dn_bundle_item.against_pick_list = pick_list.name 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() def create_stock_entry(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) -@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) @@ -1490,3 +1539,50 @@ 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") + 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(), ",", "
").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) diff --git a/erpnext/stock/doctype/pick_list/pick_list_dashboard.py b/erpnext/stock/doctype/pick_list/pick_list_dashboard.py index 29571a54007..8900385c265 100644 --- a/erpnext/stock/doctype/pick_list/pick_list_dashboard.py +++ b/erpnext/stock/doctype/pick_list/pick_list_dashboard.py @@ -3,6 +3,7 @@ def get_data(): "fieldname": "pick_list", "non_standard_fieldnames": { "Stock Reservation Entry": "from_voucher_no", + "Delivery Note": "against_pick_list", }, "internal_links": { "Sales Order": ["locations", "sales_order"], diff --git a/erpnext/stock/doctype/pick_list/pick_list_list.js b/erpnext/stock/doctype/pick_list/pick_list_list.js index eca6eece785..a675c95f973 100644 --- a/erpnext/stock/doctype/pick_list/pick_list_list.js +++ b/erpnext/stock/doctype/pick_list/pick_list_list.js @@ -6,6 +6,7 @@ frappe.listview_settings["Pick List"] = { const status_colors = { Draft: "red", Open: "orange", + "Partly Delivered": "orange", Completed: "green", Cancelled: "red", }; diff --git a/erpnext/stock/doctype/pick_list/test_pick_list.py b/erpnext/stock/doctype/pick_list/test_pick_list.py index c3043bbf1b5..b190e30227f 100644 --- a/erpnext/stock/doctype/pick_list/test_pick_list.py +++ b/erpnext/stock/doctype/pick_list/test_pick_list.py @@ -5,11 +5,12 @@ import frappe from frappe import _dict 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.test_sales_order import make_sales_order 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.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.serial_and_batch_bundle.test_serial_and_batch_bundle import ( 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) 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.submit() @@ -411,8 +418,7 @@ class TestPickList(FrappeTestCase): { "item_code": item_code, "qty": 1, - "conversion_factor": 5, - "stock_qty": 5, + "uom": "Hand", "delivery_date": frappe.utils.today(), "warehouse": "_Test Warehouse - _TC", }, @@ -426,6 +432,7 @@ class TestPickList(FrappeTestCase): ], } ).insert() + sales_order.submit() pick_list = frappe.get_doc( @@ -440,6 +447,7 @@ class TestPickList(FrappeTestCase): "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 +469,11 @@ class TestPickList(FrappeTestCase): 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) @@ -554,10 +566,10 @@ class TestPickList(FrappeTestCase): "company": "_Test Company", "items_based_on": "Sales Order", "purpose": "Delivery", - "picker": "P001", + "customer": "_Test Customer", "locations": [ { - "item_code": "_Test Item ", + "item_code": "_Test Item", "qty": 1, "stock_qty": 1, "conversion_factor": 1, @@ -580,32 +592,34 @@ class TestPickList(FrappeTestCase): 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.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( "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[1].name) # test DN creation without so pick_list_1 = frappe.get_doc( { "doctype": "Pick List", "company": "_Test Company", "purpose": "Delivery", - "picker": "P001", "locations": [ { - "item_code": "_Test Item ", + "item_code": "_Test Item", "qty": 1, "stock_qty": 1, "conversion_factor": 1, @@ -622,7 +636,9 @@ class TestPickList(FrappeTestCase): 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 +775,6 @@ class TestPickList(FrappeTestCase): 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 +1322,166 @@ class TestPickList(FrappeTestCase): 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() + + 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() diff --git a/erpnext/stock/doctype/pick_list_item/pick_list_item.json b/erpnext/stock/doctype/pick_list_item/pick_list_item.json index d33252aa3ff..e7af1c6a005 100644 --- a/erpnext/stock/doctype/pick_list_item/pick_list_item.json +++ b/erpnext/stock/doctype/pick_list_item/pick_list_item.json @@ -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,17 +238,28 @@ { "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": "modified", "sort_order": "DESC", "states": [], diff --git a/erpnext/stock/doctype/pick_list_item/pick_list_item.py b/erpnext/stock/doctype/pick_list_item/pick_list_item.py index f3f6298a305..af23a424949 100644 --- a/erpnext/stock/doctype/pick_list_item/pick_list_item.py +++ b/erpnext/stock/doctype/pick_list_item/pick_list_item.py @@ -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