Merge pull request #35108 from s-aga-r/PACKING-SLIP-FOR-DN-PACKED-ITEMS

refactor: Packing Slip
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index 7e68ec1..3a59d3c 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -333,3 +333,4 @@
 execute:frappe.delete_doc_if_exists("Report", "Tax Detail")
 erpnext.patches.v15_0.enable_all_leads
 erpnext.patches.v14_0.update_company_in_ldc
+erpnext.patches.v14_0.set_packed_qty_in_draft_delivery_notes
diff --git a/erpnext/patches/v14_0/set_packed_qty_in_draft_delivery_notes.py b/erpnext/patches/v14_0/set_packed_qty_in_draft_delivery_notes.py
new file mode 100644
index 0000000..1aeb2e6
--- /dev/null
+++ b/erpnext/patches/v14_0/set_packed_qty_in_draft_delivery_notes.py
@@ -0,0 +1,60 @@
+# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+import frappe
+from frappe.query_builder.functions import Sum
+
+
+def execute():
+	ps = frappe.qb.DocType("Packing Slip")
+	dn = frappe.qb.DocType("Delivery Note")
+	ps_item = frappe.qb.DocType("Packing Slip Item")
+
+	ps_details = (
+		frappe.qb.from_(ps)
+		.join(ps_item)
+		.on(ps.name == ps_item.parent)
+		.join(dn)
+		.on(ps.delivery_note == dn.name)
+		.select(
+			dn.name.as_("delivery_note"),
+			ps_item.item_code.as_("item_code"),
+			Sum(ps_item.qty).as_("packed_qty"),
+		)
+		.where((ps.docstatus == 1) & (dn.docstatus == 0))
+		.groupby(dn.name, ps_item.item_code)
+	).run(as_dict=True)
+
+	if ps_details:
+		dn_list = set()
+		item_code_list = set()
+		for ps_detail in ps_details:
+			dn_list.add(ps_detail.delivery_note)
+			item_code_list.add(ps_detail.item_code)
+
+		dn_item = frappe.qb.DocType("Delivery Note Item")
+		dn_item_query = (
+			frappe.qb.from_(dn_item)
+			.select(
+				dn.parent.as_("delivery_note"),
+				dn_item.name,
+				dn_item.item_code,
+				dn_item.qty,
+			)
+			.where((dn_item.parent.isin(dn_list)) & (dn_item.item_code.isin(item_code_list)))
+		)
+
+		dn_details = frappe._dict()
+		for r in dn_item_query.run(as_dict=True):
+			dn_details.setdefault((r.delivery_note, r.item_code), frappe._dict()).setdefault(r.name, r.qty)
+
+		for ps_detail in ps_details:
+			dn_items = dn_details.get((ps_detail.delivery_note, ps_detail.item_code))
+
+			if dn_items:
+				remaining_qty = ps_detail.packed_qty
+				for name, qty in dn_items.items():
+					if remaining_qty > 0:
+						row_packed_qty = min(qty, remaining_qty)
+						frappe.db.set_value("Delivery Note Item", name, "packed_qty", row_packed_qty)
+						remaining_qty -= row_packed_qty
diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.js b/erpnext/stock/doctype/delivery_note/delivery_note.js
index ae56645..77545e0 100644
--- a/erpnext/stock/doctype/delivery_note/delivery_note.js
+++ b/erpnext/stock/doctype/delivery_note/delivery_note.js
@@ -185,11 +185,14 @@
 			}
 
 			if(doc.docstatus==0 && !doc.__islocal) {
-				this.frm.add_custom_button(__('Packing Slip'), function() {
-					frappe.model.open_mapped_doc({
-						method: "erpnext.stock.doctype.delivery_note.delivery_note.make_packing_slip",
-						frm: me.frm
-					}) }, __('Create'));
+				if (doc.__onload && doc.__onload.has_unpacked_items) {
+					this.frm.add_custom_button(__('Packing Slip'), function() {
+						frappe.model.open_mapped_doc({
+							method: "erpnext.stock.doctype.delivery_note.delivery_note.make_packing_slip",
+							frm: me.frm
+						}) }, __('Create')
+					);
+				}
 			}
 
 			if (!doc.__islocal && doc.docstatus==1) {
diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py
index c18e851..e404d0b 100644
--- a/erpnext/stock/doctype/delivery_note/delivery_note.py
+++ b/erpnext/stock/doctype/delivery_note/delivery_note.py
@@ -86,6 +86,10 @@
 				]
 			)
 
+	def onload(self):
+		if self.docstatus == 0:
+			self.set_onload("has_unpacked_items", self.has_unpacked_items())
+
 	def before_print(self, settings=None):
 		def toggle_print_hide(meta, fieldname):
 			df = meta.get_field(fieldname)
@@ -302,20 +306,21 @@
 			)
 
 	def validate_packed_qty(self):
-		"""
-		Validate that if packed qty exists, it should be equal to qty
-		"""
-		if not any(flt(d.get("packed_qty")) for d in self.get("items")):
-			return
-		has_error = False
-		for d in self.get("items"):
-			if flt(d.get("qty")) != flt(d.get("packed_qty")):
-				frappe.msgprint(
-					_("Packed quantity must equal quantity for Item {0} in row {1}").format(d.item_code, d.idx)
-				)
-				has_error = True
-		if has_error:
-			raise frappe.ValidationError
+		"""Validate that if packed qty exists, it should be equal to qty"""
+
+		if frappe.db.exists("Packing Slip", {"docstatus": 1, "delivery_note": self.name}):
+			product_bundle_list = self.get_product_bundle_list()
+			for item in self.items + self.packed_items:
+				if (
+					item.item_code not in product_bundle_list
+					and flt(item.packed_qty)
+					and flt(item.packed_qty) != flt(item.qty)
+				):
+					frappe.throw(
+						_("Row {0}: Packed Qty must be equal to {1} Qty.").format(
+							item.idx, frappe.bold(item.doctype)
+						)
+					)
 
 	def update_pick_list_status(self):
 		from erpnext.stock.doctype.pick_list.pick_list import update_pick_list_status
@@ -393,6 +398,23 @@
 				)
 			)
 
+	def has_unpacked_items(self):
+		product_bundle_list = self.get_product_bundle_list()
+
+		for item in self.items + self.packed_items:
+			if item.item_code not in product_bundle_list and flt(item.packed_qty) < flt(item.qty):
+				return True
+
+		return False
+
+	def get_product_bundle_list(self):
+		items_list = [item.item_code for item in self.items]
+		return frappe.db.get_all(
+			"Product Bundle",
+			filters={"new_item_code": ["in", items_list]},
+			pluck="name",
+		)
+
 
 def update_billed_amount_based_on_so(so_detail, update_modified=True):
 	from frappe.query_builder.functions import Sum
@@ -684,6 +706,12 @@
 
 @frappe.whitelist()
 def make_packing_slip(source_name, target_doc=None):
+	def set_missing_values(source, target):
+		target.run_method("set_missing_values")
+
+	def update_item(obj, target, source_parent):
+		target.qty = flt(obj.qty) - flt(obj.packed_qty)
+
 	doclist = get_mapped_doc(
 		"Delivery Note",
 		source_name,
@@ -698,12 +726,34 @@
 				"field_map": {
 					"item_code": "item_code",
 					"item_name": "item_name",
+					"batch_no": "batch_no",
 					"description": "description",
 					"qty": "qty",
+					"stock_uom": "stock_uom",
+					"name": "dn_detail",
 				},
+				"postprocess": update_item,
+				"condition": lambda item: (
+					not frappe.db.exists("Product Bundle", {"new_item_code": item.item_code})
+					and flt(item.packed_qty) < flt(item.qty)
+				),
+			},
+			"Packed Item": {
+				"doctype": "Packing Slip Item",
+				"field_map": {
+					"item_code": "item_code",
+					"item_name": "item_name",
+					"batch_no": "batch_no",
+					"description": "description",
+					"qty": "qty",
+					"name": "pi_detail",
+				},
+				"postprocess": update_item,
+				"condition": lambda item: (flt(item.packed_qty) < flt(item.qty)),
 			},
 		},
 		target_doc,
+		set_missing_values,
 	)
 
 	return doclist
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 e46cab0..3853bd1 100644
--- a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json
+++ b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json
@@ -84,6 +84,7 @@
   "installed_qty",
   "item_tax_rate",
   "column_break_atna",
+  "packed_qty",
   "received_qty",
   "accounting_details_section",
   "expense_account",
@@ -850,6 +851,16 @@
    "print_hide": 1,
    "read_only": 1,
    "report_hide": 1
+  },
+  {
+   "default": "0",
+   "depends_on": "eval: doc.packed_qty",
+   "fieldname": "packed_qty",
+   "fieldtype": "Float",
+   "label": "Packed Qty",
+   "no_copy": 1,
+   "non_negative": 1,
+   "read_only": 1
   }
  ],
  "idx": 1,
diff --git a/erpnext/stock/doctype/packed_item/packed_item.json b/erpnext/stock/doctype/packed_item/packed_item.json
index cb8eb30..c5fb241 100644
--- a/erpnext/stock/doctype/packed_item/packed_item.json
+++ b/erpnext/stock/doctype/packed_item/packed_item.json
@@ -27,6 +27,7 @@
   "actual_qty",
   "projected_qty",
   "ordered_qty",
+  "packed_qty",
   "column_break_16",
   "incoming_rate",
   "picked_qty",
@@ -242,13 +243,23 @@
    "label": "Picked Qty",
    "no_copy": 1,
    "read_only": 1
+  },
+  {
+   "default": "0",
+   "depends_on": "eval: doc.packed_qty",
+   "fieldname": "packed_qty",
+   "fieldtype": "Float",
+   "label": "Packed Qty",
+   "no_copy": 1,
+   "non_negative": 1,
+   "read_only": 1
   }
  ],
  "idx": 1,
  "index_web_pages_for_search": 1,
  "istable": 1,
  "links": [],
- "modified": "2022-04-27 05:23:08.683245",
+ "modified": "2023-04-28 13:16:38.460806",
  "modified_by": "Administrator",
  "module": "Stock",
  "name": "Packed Item",
diff --git a/erpnext/stock/doctype/packing_slip/packing_slip.js b/erpnext/stock/doctype/packing_slip/packing_slip.js
index 40d4685..95e5ea3 100644
--- a/erpnext/stock/doctype/packing_slip/packing_slip.js
+++ b/erpnext/stock/doctype/packing_slip/packing_slip.js
@@ -1,113 +1,46 @@
-// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-// License: GNU General Public License v3. See license.txt
+// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
 
-cur_frm.fields_dict['delivery_note'].get_query = function(doc, cdt, cdn) {
-	return{
-		filters:{ 'docstatus': 0}
-	}
-}
+frappe.ui.form.on('Packing Slip', {
+    setup: (frm) => {
+        frm.set_query('delivery_note', () => {
+            return {
+                filters: {
+                    docstatus: 0,
+                }
+            }
+        });
 
+        frm.set_query('item_code', 'items', (doc, cdt, cdn) => {
+            if (!doc.delivery_note) {
+                frappe.throw(__('Please select a Delivery Note'));
+            } else {
+                let d = locals[cdt][cdn];
+                return {
+                    query: 'erpnext.stock.doctype.packing_slip.packing_slip.item_details',
+                    filters: {
+                        delivery_note: doc.delivery_note,
+                    }
+                }
+            }
+        });
+	},
 
-cur_frm.fields_dict['items'].grid.get_field('item_code').get_query = function(doc, cdt, cdn) {
-	if(!doc.delivery_note) {
-		frappe.throw(__("Please select a Delivery Note"));
-	} else {
-		return {
-			query: "erpnext.stock.doctype.packing_slip.packing_slip.item_details",
-			filters:{ 'delivery_note': doc.delivery_note}
+	refresh: (frm) => {
+		frm.toggle_display('misc_details', frm.doc.amended_from);
+	},
+
+	delivery_note: (frm) => {
+		frm.set_value('items', null);
+
+		if (frm.doc.delivery_note) {
+			erpnext.utils.map_current_doc({
+				method: 'erpnext.stock.doctype.delivery_note.delivery_note.make_packing_slip',
+				source_name: frm.doc.delivery_note,
+				target_doc: frm,
+				freeze: true,
+				freeze_message: __('Creating Packing Slip ...'),
+			});
 		}
-	}
-}
-
-cur_frm.cscript.onload_post_render = function(doc, cdt, cdn) {
-	if(doc.delivery_note && doc.__islocal) {
-		cur_frm.cscript.get_items(doc, cdt, cdn);
-	}
-}
-
-cur_frm.cscript.get_items = function(doc, cdt, cdn) {
-	return this.frm.call({
-		doc: this.frm.doc,
-		method: "get_items",
-		callback: function(r) {
-			if(!r.exc) cur_frm.refresh();
-		}
-	});
-}
-
-cur_frm.cscript.refresh = function(doc, dt, dn) {
-	cur_frm.toggle_display("misc_details", doc.amended_from);
-}
-
-cur_frm.cscript.validate = function(doc, cdt, cdn) {
-	cur_frm.cscript.validate_case_nos(doc);
-	cur_frm.cscript.validate_calculate_item_details(doc);
-}
-
-// To Case No. cannot be less than From Case No.
-cur_frm.cscript.validate_case_nos = function(doc) {
-	doc = locals[doc.doctype][doc.name];
-	if(cint(doc.from_case_no)==0) {
-		frappe.msgprint(__("The 'From Package No.' field must neither be empty nor it's value less than 1."));
-		frappe.validated = false;
-	} else if(!cint(doc.to_case_no)) {
-		doc.to_case_no = doc.from_case_no;
-		refresh_field('to_case_no');
-	} else if(cint(doc.to_case_no) < cint(doc.from_case_no)) {
-		frappe.msgprint(__("'To Case No.' cannot be less than 'From Case No.'"));
-		frappe.validated = false;
-	}
-}
-
-
-cur_frm.cscript.validate_calculate_item_details = function(doc) {
-	doc = locals[doc.doctype][doc.name];
-	var ps_detail = doc.items || [];
-
-	cur_frm.cscript.validate_duplicate_items(doc, ps_detail);
-	cur_frm.cscript.calc_net_total_pkg(doc, ps_detail);
-}
-
-
-// Do not allow duplicate items i.e. items with same item_code
-// Also check for 0 qty
-cur_frm.cscript.validate_duplicate_items = function(doc, ps_detail) {
-	for(var i=0; i<ps_detail.length; i++) {
-		for(var j=0; j<ps_detail.length; j++) {
-			if(i!=j && ps_detail[i].item_code && ps_detail[i].item_code==ps_detail[j].item_code) {
-				frappe.msgprint(__("You have entered duplicate items. Please rectify and try again."));
-				frappe.validated = false;
-				return;
-			}
-		}
-		if(flt(ps_detail[i].qty)<=0) {
-			frappe.msgprint(__("Invalid quantity specified for item {0}. Quantity should be greater than 0.", [ps_detail[i].item_code]));
-			frappe.validated = false;
-		}
-	}
-}
-
-
-// Calculate Net Weight of Package
-cur_frm.cscript.calc_net_total_pkg = function(doc, ps_detail) {
-	var net_weight_pkg = 0;
-	doc.net_weight_uom = (ps_detail && ps_detail.length) ? ps_detail[0].weight_uom : '';
-	doc.gross_weight_uom = doc.net_weight_uom;
-
-	for(var i=0; i<ps_detail.length; i++) {
-		var item = ps_detail[i];
-		if(item.weight_uom != doc.net_weight_uom) {
-			frappe.msgprint(__("Different UOM for items will lead to incorrect (Total) Net Weight value. Make sure that Net Weight of each item is in the same UOM."));
-			frappe.validated = false;
-		}
-		net_weight_pkg += flt(item.net_weight) * flt(item.qty);
-	}
-
-	doc.net_weight_pkg = roundNumber(net_weight_pkg, 2);
-	if(!flt(doc.gross_weight_pkg)) {
-		doc.gross_weight_pkg = doc.net_weight_pkg;
-	}
-	refresh_many(['net_weight_pkg', 'net_weight_uom', 'gross_weight_uom', 'gross_weight_pkg']);
-}
-
-// TODO: validate gross weight field
+	},
+});
diff --git a/erpnext/stock/doctype/packing_slip/packing_slip.json b/erpnext/stock/doctype/packing_slip/packing_slip.json
index ec8d57c..86ed794 100644
--- a/erpnext/stock/doctype/packing_slip/packing_slip.json
+++ b/erpnext/stock/doctype/packing_slip/packing_slip.json
@@ -1,264 +1,262 @@
 {
-   "allow_import": 1,
-   "autoname": "MAT-PAC-.YYYY.-.#####",
-   "creation": "2013-04-11 15:32:24",
-   "description": "Generate packing slips for packages to be delivered. Used to notify package number, package contents and its weight.",
-   "doctype": "DocType",
-   "document_type": "Document",
-   "engine": "InnoDB",
-   "field_order": [
-    "packing_slip_details",
-    "column_break0",
-    "delivery_note",
-    "column_break1",
-    "naming_series",
-    "section_break0",
-    "column_break2",
-    "from_case_no",
-    "column_break3",
-    "to_case_no",
-    "package_item_details",
-    "get_items",
-    "items",
-    "package_weight_details",
-    "net_weight_pkg",
-    "net_weight_uom",
-    "column_break4",
-    "gross_weight_pkg",
-    "gross_weight_uom",
-    "letter_head_details",
-    "letter_head",
-    "misc_details",
-    "amended_from"
-   ],
-   "fields": [
-    {
-     "fieldname": "packing_slip_details",
-     "fieldtype": "Section Break"
-    },
-    {
-     "fieldname": "column_break0",
-     "fieldtype": "Column Break"
-    },
-    {
-     "description": "Indicates that the package is a part of this delivery (Only Draft)",
-     "fieldname": "delivery_note",
-     "fieldtype": "Link",
-     "in_global_search": 1,
-     "in_list_view": 1,
-     "label": "Delivery Note",
-     "options": "Delivery Note",
-     "reqd": 1
-    },
-    {
-     "fieldname": "column_break1",
-     "fieldtype": "Column Break"
-    },
-    {
-     "fieldname": "naming_series",
-     "fieldtype": "Select",
-     "label": "Series",
-     "options": "MAT-PAC-.YYYY.-",
-     "print_hide": 1,
-     "reqd": 1,
-     "set_only_once": 1
-    },
-    {
-     "fieldname": "section_break0",
-     "fieldtype": "Section Break"
-    },
-    {
-     "fieldname": "column_break2",
-     "fieldtype": "Column Break"
-    },
-    {
-     "description": "Identification of the package for the delivery (for print)",
-     "fieldname": "from_case_no",
-     "fieldtype": "Int",
-     "in_list_view": 1,
-     "label": "From Package No.",
-     "no_copy": 1,
-     "reqd": 1,
-     "width": "50px"
-    },
-    {
-     "fieldname": "column_break3",
-     "fieldtype": "Column Break"
-    },
-    {
-     "description": "If more than one package of the same type (for print)",
-     "fieldname": "to_case_no",
-     "fieldtype": "Int",
-     "in_list_view": 1,
-     "label": "To Package No.",
-     "no_copy": 1,
-     "width": "50px"
-    },
-    {
-     "fieldname": "package_item_details",
-     "fieldtype": "Section Break"
-    },
-    {
-     "fieldname": "get_items",
-     "fieldtype": "Button",
-     "label": "Get Items"
-    },
-    {
-     "fieldname": "items",
-     "fieldtype": "Table",
-     "label": "Items",
-     "options": "Packing Slip Item",
-     "reqd": 1
-    },
-    {
-     "fieldname": "package_weight_details",
-     "fieldtype": "Section Break",
-     "label": "Package Weight Details"
-    },
-    {
-     "description": "The net weight of this package. (calculated automatically as sum of net weight of items)",
-     "fieldname": "net_weight_pkg",
-     "fieldtype": "Float",
-     "label": "Net Weight",
-     "no_copy": 1,
-     "read_only": 1
-    },
-    {
-     "fieldname": "net_weight_uom",
-     "fieldtype": "Link",
-     "label": "Net Weight UOM",
-     "no_copy": 1,
-     "options": "UOM",
-     "read_only": 1
-    },
-    {
-     "fieldname": "column_break4",
-     "fieldtype": "Column Break"
-    },
-    {
-     "description": "The gross weight of the package. Usually net weight + packaging material weight. (for print)",
-     "fieldname": "gross_weight_pkg",
-     "fieldtype": "Float",
-     "label": "Gross Weight",
-     "no_copy": 1
-    },
-    {
-     "fieldname": "gross_weight_uom",
-     "fieldtype": "Link",
-     "label": "Gross Weight UOM",
-     "no_copy": 1,
-     "options": "UOM"
-    },
-    {
-     "fieldname": "letter_head_details",
-     "fieldtype": "Section Break",
-     "label": "Letter Head"
-    },
-    {
-     "allow_on_submit": 1,
-     "fieldname": "letter_head",
-     "fieldtype": "Link",
-     "label": "Letter Head",
-     "options": "Letter Head",
-     "print_hide": 1
-    },
-    {
-     "fieldname": "misc_details",
-     "fieldtype": "Section Break"
-    },
-    {
-     "fieldname": "amended_from",
-     "fieldtype": "Link",
-     "ignore_user_permissions": 1,
-     "label": "Amended From",
-     "no_copy": 1,
-     "options": "Packing Slip",
-     "print_hide": 1,
-     "read_only": 1
-    }
-   ],
-   "icon": "fa fa-suitcase",
-   "idx": 1,
-   "is_submittable": 1,
-   "modified": "2019-09-09 04:45:08.082862",
-   "modified_by": "Administrator",
-   "module": "Stock",
-   "name": "Packing Slip",
-   "owner": "Administrator",
-   "permissions": [
-    {
-     "amend": 1,
-     "cancel": 1,
-     "create": 1,
-     "delete": 1,
-     "email": 1,
-     "print": 1,
-     "read": 1,
-     "report": 1,
-     "role": "Stock User",
-     "share": 1,
-     "submit": 1,
-     "write": 1
-    },
-    {
-     "amend": 1,
-     "cancel": 1,
-     "create": 1,
-     "delete": 1,
-     "email": 1,
-     "print": 1,
-     "read": 1,
-     "report": 1,
-     "role": "Sales User",
-     "share": 1,
-     "submit": 1,
-     "write": 1
-    },
-    {
-     "amend": 1,
-     "cancel": 1,
-     "create": 1,
-     "delete": 1,
-     "email": 1,
-     "print": 1,
-     "read": 1,
-     "report": 1,
-     "role": "Item Manager",
-     "share": 1,
-     "submit": 1,
-     "write": 1
-    },
-    {
-     "amend": 1,
-     "cancel": 1,
-     "create": 1,
-     "delete": 1,
-     "email": 1,
-     "print": 1,
-     "read": 1,
-     "report": 1,
-     "role": "Stock Manager",
-     "share": 1,
-     "submit": 1,
-     "write": 1
-    },
-    {
-     "amend": 1,
-     "cancel": 1,
-     "create": 1,
-     "delete": 1,
-     "email": 1,
-     "print": 1,
-     "read": 1,
-     "report": 1,
-     "role": "Sales Manager",
-     "share": 1,
-     "submit": 1,
-     "write": 1
-    }
-   ],
-   "search_fields": "delivery_note",
-   "show_name_in_global_search": 1,
-   "sort_field": "modified",
-   "sort_order": "DESC"
+ "actions": [],
+ "allow_import": 1,
+ "autoname": "MAT-PAC-.YYYY.-.#####",
+ "creation": "2013-04-11 15:32:24",
+ "description": "Generate packing slips for packages to be delivered. Used to notify package number, package contents and its weight.",
+ "doctype": "DocType",
+ "document_type": "Document",
+ "engine": "InnoDB",
+ "field_order": [
+  "packing_slip_details",
+  "column_break0",
+  "delivery_note",
+  "column_break1",
+  "naming_series",
+  "section_break0",
+  "column_break2",
+  "from_case_no",
+  "column_break3",
+  "to_case_no",
+  "package_item_details",
+  "items",
+  "package_weight_details",
+  "net_weight_pkg",
+  "net_weight_uom",
+  "column_break4",
+  "gross_weight_pkg",
+  "gross_weight_uom",
+  "letter_head_details",
+  "letter_head",
+  "misc_details",
+  "amended_from"
+ ],
+ "fields": [
+  {
+   "fieldname": "packing_slip_details",
+   "fieldtype": "Section Break"
+  },
+  {
+   "fieldname": "column_break0",
+   "fieldtype": "Column Break"
+  },
+  {
+   "description": "Indicates that the package is a part of this delivery (Only Draft)",
+   "fieldname": "delivery_note",
+   "fieldtype": "Link",
+   "in_global_search": 1,
+   "in_list_view": 1,
+   "label": "Delivery Note",
+   "options": "Delivery Note",
+   "reqd": 1
+  },
+  {
+   "fieldname": "column_break1",
+   "fieldtype": "Column Break"
+  },
+  {
+   "fieldname": "naming_series",
+   "fieldtype": "Select",
+   "label": "Series",
+   "options": "MAT-PAC-.YYYY.-",
+   "print_hide": 1,
+   "reqd": 1,
+   "set_only_once": 1
+  },
+  {
+   "fieldname": "section_break0",
+   "fieldtype": "Section Break"
+  },
+  {
+   "fieldname": "column_break2",
+   "fieldtype": "Column Break"
+  },
+  {
+   "description": "Identification of the package for the delivery (for print)",
+   "fieldname": "from_case_no",
+   "fieldtype": "Int",
+   "in_list_view": 1,
+   "label": "From Package No.",
+   "no_copy": 1,
+   "reqd": 1,
+   "width": "50px"
+  },
+  {
+   "fieldname": "column_break3",
+   "fieldtype": "Column Break"
+  },
+  {
+   "description": "If more than one package of the same type (for print)",
+   "fieldname": "to_case_no",
+   "fieldtype": "Int",
+   "in_list_view": 1,
+   "label": "To Package No.",
+   "no_copy": 1,
+   "width": "50px"
+  },
+  {
+   "fieldname": "package_item_details",
+   "fieldtype": "Section Break"
+  },
+  {
+   "fieldname": "items",
+   "fieldtype": "Table",
+   "label": "Items",
+   "options": "Packing Slip Item",
+   "reqd": 1
+  },
+  {
+   "fieldname": "package_weight_details",
+   "fieldtype": "Section Break",
+   "label": "Package Weight Details"
+  },
+  {
+   "description": "The net weight of this package. (calculated automatically as sum of net weight of items)",
+   "fieldname": "net_weight_pkg",
+   "fieldtype": "Float",
+   "label": "Net Weight",
+   "no_copy": 1,
+   "read_only": 1
+  },
+  {
+   "fieldname": "net_weight_uom",
+   "fieldtype": "Link",
+   "label": "Net Weight UOM",
+   "no_copy": 1,
+   "options": "UOM",
+   "read_only": 1
+  },
+  {
+   "fieldname": "column_break4",
+   "fieldtype": "Column Break"
+  },
+  {
+   "description": "The gross weight of the package. Usually net weight + packaging material weight. (for print)",
+   "fieldname": "gross_weight_pkg",
+   "fieldtype": "Float",
+   "label": "Gross Weight",
+   "no_copy": 1
+  },
+  {
+   "fieldname": "gross_weight_uom",
+   "fieldtype": "Link",
+   "label": "Gross Weight UOM",
+   "no_copy": 1,
+   "options": "UOM"
+  },
+  {
+   "fieldname": "letter_head_details",
+   "fieldtype": "Section Break",
+   "label": "Letter Head"
+  },
+  {
+   "allow_on_submit": 1,
+   "fieldname": "letter_head",
+   "fieldtype": "Link",
+   "label": "Letter Head",
+   "options": "Letter Head",
+   "print_hide": 1
+  },
+  {
+   "fieldname": "misc_details",
+   "fieldtype": "Section Break"
+  },
+  {
+   "fieldname": "amended_from",
+   "fieldtype": "Link",
+   "ignore_user_permissions": 1,
+   "label": "Amended From",
+   "no_copy": 1,
+   "options": "Packing Slip",
+   "print_hide": 1,
+   "read_only": 1
   }
+ ],
+ "icon": "fa fa-suitcase",
+ "idx": 1,
+ "is_submittable": 1,
+ "links": [],
+ "modified": "2023-04-28 18:01:37.341619",
+ "modified_by": "Administrator",
+ "module": "Stock",
+ "name": "Packing Slip",
+ "naming_rule": "Expression (old style)",
+ "owner": "Administrator",
+ "permissions": [
+  {
+   "amend": 1,
+   "cancel": 1,
+   "create": 1,
+   "delete": 1,
+   "email": 1,
+   "print": 1,
+   "read": 1,
+   "report": 1,
+   "role": "Stock User",
+   "share": 1,
+   "submit": 1,
+   "write": 1
+  },
+  {
+   "amend": 1,
+   "cancel": 1,
+   "create": 1,
+   "delete": 1,
+   "email": 1,
+   "print": 1,
+   "read": 1,
+   "report": 1,
+   "role": "Sales User",
+   "share": 1,
+   "submit": 1,
+   "write": 1
+  },
+  {
+   "amend": 1,
+   "cancel": 1,
+   "create": 1,
+   "delete": 1,
+   "email": 1,
+   "print": 1,
+   "read": 1,
+   "report": 1,
+   "role": "Item Manager",
+   "share": 1,
+   "submit": 1,
+   "write": 1
+  },
+  {
+   "amend": 1,
+   "cancel": 1,
+   "create": 1,
+   "delete": 1,
+   "email": 1,
+   "print": 1,
+   "read": 1,
+   "report": 1,
+   "role": "Stock Manager",
+   "share": 1,
+   "submit": 1,
+   "write": 1
+  },
+  {
+   "amend": 1,
+   "cancel": 1,
+   "create": 1,
+   "delete": 1,
+   "email": 1,
+   "print": 1,
+   "read": 1,
+   "report": 1,
+   "role": "Sales Manager",
+   "share": 1,
+   "submit": 1,
+   "write": 1
+  }
+ ],
+ "search_fields": "delivery_note",
+ "show_name_in_global_search": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "states": []
+}
\ No newline at end of file
diff --git a/erpnext/stock/doctype/packing_slip/packing_slip.py b/erpnext/stock/doctype/packing_slip/packing_slip.py
index e5b9de8..6ea5938 100644
--- a/erpnext/stock/doctype/packing_slip/packing_slip.py
+++ b/erpnext/stock/doctype/packing_slip/packing_slip.py
@@ -4,193 +4,181 @@
 
 import frappe
 from frappe import _
-from frappe.model import no_value_fields
-from frappe.model.document import Document
 from frappe.utils import cint, flt
 
+from erpnext.controllers.status_updater import StatusUpdater
 
-class PackingSlip(Document):
-	def validate(self):
-		"""
-		* Validate existence of submitted Delivery Note
-		* Case nos do not overlap
-		* Check if packed qty doesn't exceed actual qty of delivery note
 
-		It is necessary to validate case nos before checking quantity
-		"""
-		self.validate_delivery_note()
-		self.validate_items_mandatory()
-		self.validate_case_nos()
-		self.validate_qty()
+class PackingSlip(StatusUpdater):
+	def __init__(self, *args, **kwargs) -> None:
+		super(PackingSlip, self).__init__(*args, **kwargs)
+		self.status_updater = [
+			{
+				"target_dt": "Delivery Note Item",
+				"join_field": "dn_detail",
+				"target_field": "packed_qty",
+				"target_parent_dt": "Delivery Note",
+				"target_ref_field": "qty",
+				"source_dt": "Packing Slip Item",
+				"source_field": "qty",
+			},
+			{
+				"target_dt": "Packed Item",
+				"join_field": "pi_detail",
+				"target_field": "packed_qty",
+				"target_parent_dt": "Delivery Note",
+				"target_ref_field": "qty",
+				"source_dt": "Packing Slip Item",
+				"source_field": "qty",
+			},
+		]
 
+	def validate(self) -> None:
 		from erpnext.utilities.transaction_base import validate_uom_is_integer
 
+		self.validate_delivery_note()
+		self.validate_case_nos()
+		self.validate_items()
+
 		validate_uom_is_integer(self, "stock_uom", "qty")
 		validate_uom_is_integer(self, "weight_uom", "net_weight")
 
-	def validate_delivery_note(self):
-		"""
-		Validates if delivery note has status as draft
-		"""
-		if cint(frappe.db.get_value("Delivery Note", self.delivery_note, "docstatus")) != 0:
-			frappe.throw(_("Delivery Note {0} must not be submitted").format(self.delivery_note))
+		self.set_missing_values()
+		self.calculate_net_total_pkg()
 
-	def validate_items_mandatory(self):
-		rows = [d.item_code for d in self.get("items")]
-		if not rows:
-			frappe.msgprint(_("No Items to pack"), raise_exception=1)
+	def on_submit(self):
+		self.update_prevdoc_status()
+
+	def on_cancel(self):
+		self.update_prevdoc_status()
+
+	def validate_delivery_note(self):
+		"""Raises an exception if the `Delivery Note` status is not Draft"""
+
+		if cint(frappe.db.get_value("Delivery Note", self.delivery_note, "docstatus")) != 0:
+			frappe.throw(
+				_("A Packing Slip can only be created for Draft Delivery Note.").format(self.delivery_note)
+			)
 
 	def validate_case_nos(self):
-		"""
-		Validate if case nos overlap. If they do, recommend next case no.
-		"""
-		if not cint(self.from_case_no):
-			frappe.msgprint(_("Please specify a valid 'From Case No.'"), raise_exception=1)
+		"""Validate if case nos overlap. If they do, recommend next case no."""
+
+		if cint(self.from_case_no) <= 0:
+			frappe.throw(
+				_("The 'From Package No.' field must neither be empty nor it's value less than 1.")
+			)
 		elif not self.to_case_no:
 			self.to_case_no = self.from_case_no
-		elif cint(self.from_case_no) > cint(self.to_case_no):
-			frappe.msgprint(_("'To Case No.' cannot be less than 'From Case No.'"), raise_exception=1)
+		elif cint(self.to_case_no) < cint(self.from_case_no):
+			frappe.throw(_("'To Package No.' cannot be less than 'From Package No.'"))
+		else:
+			ps = frappe.qb.DocType("Packing Slip")
+			res = (
+				frappe.qb.from_(ps)
+				.select(
+					ps.name,
+				)
+				.where(
+					(ps.delivery_note == self.delivery_note)
+					& (ps.docstatus == 1)
+					& (
+						(ps.from_case_no.between(self.from_case_no, self.to_case_no))
+						| (ps.to_case_no.between(self.from_case_no, self.to_case_no))
+						| ((ps.from_case_no <= self.from_case_no) & (ps.to_case_no >= self.from_case_no))
+					)
+				)
+			).run()
 
-		res = frappe.db.sql(
-			"""SELECT name FROM `tabPacking Slip`
-			WHERE delivery_note = %(delivery_note)s AND docstatus = 1 AND
-			((from_case_no BETWEEN %(from_case_no)s AND %(to_case_no)s)
-			OR (to_case_no BETWEEN %(from_case_no)s AND %(to_case_no)s)
-			OR (%(from_case_no)s BETWEEN from_case_no AND to_case_no))
-			""",
-			{
-				"delivery_note": self.delivery_note,
-				"from_case_no": self.from_case_no,
-				"to_case_no": self.to_case_no,
-			},
-		)
+			if res:
+				frappe.throw(
+					_("""Package No(s) already in use. Try from Package No {0}""").format(
+						self.get_recommended_case_no()
+					)
+				)
 
-		if res:
-			frappe.throw(
-				_("""Case No(s) already in use. Try from Case No {0}""").format(self.get_recommended_case_no())
+	def validate_items(self):
+		for item in self.items:
+			if item.qty <= 0:
+				frappe.throw(_("Row {0}: Qty must be greater than 0.").format(item.idx))
+
+			if not item.dn_detail and not item.pi_detail:
+				frappe.throw(
+					_("Row {0}: Either Delivery Note Item or Packed Item reference is mandatory.").format(
+						item.idx
+					)
+				)
+
+			remaining_qty = frappe.db.get_value(
+				"Delivery Note Item" if item.dn_detail else "Packed Item",
+				{"name": item.dn_detail or item.pi_detail, "docstatus": 0},
+				["sum(qty - packed_qty)"],
 			)
 
-	def validate_qty(self):
-		"""Check packed qty across packing slips and delivery note"""
-		# Get Delivery Note Items, Item Quantity Dict and No. of Cases for this Packing slip
-		dn_details, ps_item_qty, no_of_cases = self.get_details_for_packing()
+			if remaining_qty is None:
+				frappe.throw(
+					_("Row {0}: Please provide a valid Delivery Note Item or Packed Item reference.").format(
+						item.idx
+					)
+				)
+			elif remaining_qty <= 0:
+				frappe.throw(
+					_("Row {0}: Packing Slip is already created for Item {1}.").format(
+						item.idx, frappe.bold(item.item_code)
+					)
+				)
+			elif item.qty > remaining_qty:
+				frappe.throw(
+					_("Row {0}: Qty cannot be greater than {1} for the Item {2}.").format(
+						item.idx, frappe.bold(remaining_qty), frappe.bold(item.item_code)
+					)
+				)
 
-		for item in dn_details:
-			new_packed_qty = (flt(ps_item_qty[item["item_code"]]) * no_of_cases) + flt(item["packed_qty"])
-			if new_packed_qty > flt(item["qty"]) and no_of_cases:
-				self.recommend_new_qty(item, ps_item_qty, no_of_cases)
-
-	def get_details_for_packing(self):
-		"""
-		Returns
-		* 'Delivery Note Items' query result as a list of dict
-		* Item Quantity dict of current packing slip doc
-		* No. of Cases of this packing slip
-		"""
-
-		rows = [d.item_code for d in self.get("items")]
-
-		# also pick custom fields from delivery note
-		custom_fields = ", ".join(
-			"dni.`{0}`".format(d.fieldname)
-			for d in frappe.get_meta("Delivery Note Item").get_custom_fields()
-			if d.fieldtype not in no_value_fields
-		)
-
-		if custom_fields:
-			custom_fields = ", " + custom_fields
-
-		condition = ""
-		if rows:
-			condition = " and item_code in (%s)" % (", ".join(["%s"] * len(rows)))
-
-		# gets item code, qty per item code, latest packed qty per item code and stock uom
-		res = frappe.db.sql(
-			"""select item_code, sum(qty) as qty,
-			(select sum(psi.qty * (abs(ps.to_case_no - ps.from_case_no) + 1))
-				from `tabPacking Slip` ps, `tabPacking Slip Item` psi
-				where ps.name = psi.parent and ps.docstatus = 1
-				and ps.delivery_note = dni.parent and psi.item_code=dni.item_code) as packed_qty,
-			stock_uom, item_name, description, dni.batch_no {custom_fields}
-			from `tabDelivery Note Item` dni
-			where parent=%s {condition}
-			group by item_code""".format(
-				condition=condition, custom_fields=custom_fields
-			),
-			tuple([self.delivery_note] + rows),
-			as_dict=1,
-		)
-
-		ps_item_qty = dict([[d.item_code, d.qty] for d in self.get("items")])
-		no_of_cases = cint(self.to_case_no) - cint(self.from_case_no) + 1
-
-		return res, ps_item_qty, no_of_cases
-
-	def recommend_new_qty(self, item, ps_item_qty, no_of_cases):
-		"""
-		Recommend a new quantity and raise a validation exception
-		"""
-		item["recommended_qty"] = (flt(item["qty"]) - flt(item["packed_qty"])) / no_of_cases
-		item["specified_qty"] = flt(ps_item_qty[item["item_code"]])
-		if not item["packed_qty"]:
-			item["packed_qty"] = 0
-
-		frappe.throw(
-			_("Quantity for Item {0} must be less than {1}").format(
-				item.get("item_code"), item.get("recommended_qty")
-			)
-		)
-
-	def update_item_details(self):
-		"""
-		Fill empty columns in Packing Slip Item
-		"""
+	def set_missing_values(self):
 		if not self.from_case_no:
 			self.from_case_no = self.get_recommended_case_no()
 
-		for d in self.get("items"):
-			res = frappe.db.get_value("Item", d.item_code, ["weight_per_unit", "weight_uom"], as_dict=True)
+		for item in self.items:
+			stock_uom, weight_per_unit, weight_uom = frappe.db.get_value(
+				"Item", item.item_code, ["stock_uom", "weight_per_unit", "weight_uom"]
+			)
 
-			if res and len(res) > 0:
-				d.net_weight = res["weight_per_unit"]
-				d.weight_uom = res["weight_uom"]
+			item.stock_uom = stock_uom
+			if weight_per_unit and not item.net_weight:
+				item.net_weight = weight_per_unit
+			if weight_uom and not item.weight_uom:
+				item.weight_uom = weight_uom
 
 	def get_recommended_case_no(self):
-		"""
-		Returns the next case no. for a new packing slip for a delivery
-		note
-		"""
-		recommended_case_no = frappe.db.sql(
-			"""SELECT MAX(to_case_no) FROM `tabPacking Slip`
-			WHERE delivery_note = %s AND docstatus=1""",
-			self.delivery_note,
+		"""Returns the next case no. for a new packing slip for a delivery note"""
+
+		return (
+			cint(
+				frappe.db.get_value(
+					"Packing Slip", {"delivery_note": self.delivery_note, "docstatus": 1}, ["max(to_case_no)"]
+				)
+			)
+			+ 1
 		)
 
-		return cint(recommended_case_no[0][0]) + 1
+	def calculate_net_total_pkg(self):
+		self.net_weight_uom = self.items[0].weight_uom if self.items else None
+		self.gross_weight_uom = self.net_weight_uom
 
-	@frappe.whitelist()
-	def get_items(self):
-		self.set("items", [])
+		net_weight_pkg = 0
+		for item in self.items:
+			if item.weight_uom != self.net_weight_uom:
+				frappe.throw(
+					_(
+						"Different UOM for items will lead to incorrect (Total) Net Weight value. Make sure that Net Weight of each item is in the same UOM."
+					)
+				)
 
-		custom_fields = frappe.get_meta("Delivery Note Item").get_custom_fields()
+			net_weight_pkg += flt(item.net_weight) * flt(item.qty)
 
-		dn_details = self.get_details_for_packing()[0]
-		for item in dn_details:
-			if flt(item.qty) > flt(item.packed_qty):
-				ch = self.append("items", {})
-				ch.item_code = item.item_code
-				ch.item_name = item.item_name
-				ch.stock_uom = item.stock_uom
-				ch.description = item.description
-				ch.batch_no = item.batch_no
-				ch.qty = flt(item.qty) - flt(item.packed_qty)
+		self.net_weight_pkg = round(net_weight_pkg, 2)
 
-				# copy custom fields
-				for d in custom_fields:
-					if item.get(d.fieldname):
-						ch.set(d.fieldname, item.get(d.fieldname))
-
-		self.update_item_details()
+		if not flt(self.gross_weight_pkg):
+			self.gross_weight_pkg = self.net_weight_pkg
 
 
 @frappe.whitelist()
diff --git a/erpnext/stock/doctype/packing_slip/test_packing_slip.py b/erpnext/stock/doctype/packing_slip/test_packing_slip.py
index bc405b2..96da23d 100644
--- a/erpnext/stock/doctype/packing_slip/test_packing_slip.py
+++ b/erpnext/stock/doctype/packing_slip/test_packing_slip.py
@@ -3,9 +3,118 @@
 
 import unittest
 
-# test_records = frappe.get_test_records('Packing Slip')
+import frappe
 from frappe.tests.utils import FrappeTestCase
 
+from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle
+from erpnext.stock.doctype.delivery_note.delivery_note import make_packing_slip
+from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
+from erpnext.stock.doctype.item.test_item import make_item
 
-class TestPackingSlip(unittest.TestCase):
-	pass
+
+class TestPackingSlip(FrappeTestCase):
+	def test_packing_slip(self):
+		# Step - 1: Create a Product Bundle
+		items = create_items()
+		make_product_bundle(items[0], items[1:], 5)
+
+		# Step - 2: Create a Delivery Note (Draft) with Product Bundle
+		dn = create_delivery_note(
+			item_code=items[0],
+			qty=2,
+			do_not_save=True,
+		)
+		dn.append(
+			"items",
+			{
+				"item_code": items[1],
+				"warehouse": "_Test Warehouse - _TC",
+				"qty": 10,
+			},
+		)
+		dn.save()
+
+		# Step - 3: Make a Packing Slip from Delivery Note for 4 Qty
+		ps1 = make_packing_slip(dn.name)
+		for item in ps1.items:
+			item.qty = 4
+		ps1.save()
+		ps1.submit()
+
+		# Test - 1: `Packed Qty` should be updated to 4 in Delivery Note Items and Packed Items.
+		dn.load_from_db()
+		for item in dn.items:
+			if not frappe.db.exists("Product Bundle", {"new_item_code": item.item_code}):
+				self.assertEqual(item.packed_qty, 4)
+
+		for item in dn.packed_items:
+			self.assertEqual(item.packed_qty, 4)
+
+		# Step - 4: Make another Packing Slip from Delivery Note for 6 Qty
+		ps2 = make_packing_slip(dn.name)
+		ps2.save()
+		ps2.submit()
+
+		# Test - 2: `Packed Qty` should be updated to 10 in Delivery Note Items and Packed Items.
+		dn.load_from_db()
+		for item in dn.items:
+			if not frappe.db.exists("Product Bundle", {"new_item_code": item.item_code}):
+				self.assertEqual(item.packed_qty, 10)
+
+		for item in dn.packed_items:
+			self.assertEqual(item.packed_qty, 10)
+
+		# Step - 5: Cancel Packing Slip [1]
+		ps1.cancel()
+
+		# Test - 3: `Packed Qty` should be updated to 4 in Delivery Note Items and Packed Items.
+		dn.load_from_db()
+		for item in dn.items:
+			if not frappe.db.exists("Product Bundle", {"new_item_code": item.item_code}):
+				self.assertEqual(item.packed_qty, 6)
+
+		for item in dn.packed_items:
+			self.assertEqual(item.packed_qty, 6)
+
+		# Step - 6: Cancel Packing Slip [2]
+		ps2.cancel()
+
+		# Test - 4: `Packed Qty` should be updated to 0 in Delivery Note Items and Packed Items.
+		dn.load_from_db()
+		for item in dn.items:
+			if not frappe.db.exists("Product Bundle", {"new_item_code": item.item_code}):
+				self.assertEqual(item.packed_qty, 0)
+
+		for item in dn.packed_items:
+			self.assertEqual(item.packed_qty, 0)
+
+		# Step - 7: Make Packing Slip for more Qty than Delivery Note
+		ps3 = make_packing_slip(dn.name)
+		ps3.items[0].qty = 20
+
+		# Test - 5: Should throw an ValidationError, as Packing Slip Qty is more than Delivery Note Qty
+		self.assertRaises(frappe.exceptions.ValidationError, ps3.save)
+
+		# Step - 8: Make Packing Slip for less Qty than Delivery Note
+		ps4 = make_packing_slip(dn.name)
+		ps4.items[0].qty = 5
+		ps4.save()
+		ps4.submit()
+
+		# Test - 6: Delivery Note should throw a ValidationError on Submit, as Packed Qty and Delivery Note Qty are not the same
+		dn.load_from_db()
+		self.assertRaises(frappe.exceptions.ValidationError, dn.submit)
+
+
+def create_items():
+	items_properties = [
+		{"is_stock_item": 0},
+		{"is_stock_item": 1, "stock_uom": "Nos"},
+		{"is_stock_item": 1, "stock_uom": "Box"},
+	]
+
+	items = []
+	for properties in items_properties:
+		items.append(make_item(properties=properties).name)
+
+	return items
diff --git a/erpnext/stock/doctype/packing_slip_item/packing_slip_item.json b/erpnext/stock/doctype/packing_slip_item/packing_slip_item.json
index 4270839..4bd9035 100644
--- a/erpnext/stock/doctype/packing_slip_item/packing_slip_item.json
+++ b/erpnext/stock/doctype/packing_slip_item/packing_slip_item.json
@@ -20,7 +20,8 @@
   "stock_uom",
   "weight_uom",
   "page_break",
-  "dn_detail"
+  "dn_detail",
+  "pi_detail"
  ],
  "fields": [
   {
@@ -121,13 +122,23 @@
    "fieldtype": "Data",
    "hidden": 1,
    "in_list_view": 1,
-   "label": "DN Detail"
+   "label": "Delivery Note Item",
+   "no_copy": 1,
+   "read_only": 1
+  },
+  {
+   "fieldname": "pi_detail",
+   "fieldtype": "Data",
+   "hidden": 1,
+   "label": "Delivery Note Packed Item",
+   "no_copy": 1,
+   "read_only": 1
   }
  ],
  "idx": 1,
  "istable": 1,
  "links": [],
- "modified": "2021-12-14 01:22:00.715935",
+ "modified": "2023-04-28 15:00:14.079306",
  "modified_by": "Administrator",
  "module": "Stock",
  "name": "Packing Slip Item",
@@ -136,5 +147,6 @@
  "permissions": [],
  "sort_field": "modified",
  "sort_order": "DESC",
+ "states": [],
  "track_changes": 1
 }
\ No newline at end of file