Merge pull request #38805 from barredterra/remove-unused-coa

diff --git a/.github/workflows/lock.yml b/.github/workflows/lock.yml
new file mode 100644
index 0000000..e990228
--- /dev/null
+++ b/.github/workflows/lock.yml
@@ -0,0 +1,21 @@
+name: 'Lock threads'
+
+on:
+  schedule:
+    - cron: '0 0 * * *'
+  workflow_dispatch:
+
+permissions:
+  issues: write
+  pull-requests: write
+
+
+jobs:
+  lock:
+    runs-on: ubuntu-latest
+    steps:
+      - uses: dessant/lock-threads@v5
+        with:
+          github-token: ${{ github.token }}
+          issue-inactive-days: 14
+          pr-inactive-days: 14
diff --git a/erpnext/accounts/doctype/bisect_accounting_statements/__init__.py b/erpnext/accounts/doctype/bisect_accounting_statements/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/accounts/doctype/bisect_accounting_statements/__init__.py
diff --git a/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.js b/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.js
new file mode 100644
index 0000000..ece0fb3
--- /dev/null
+++ b/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.js
@@ -0,0 +1,100 @@
+// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on("Bisect Accounting Statements", {
+	onload(frm) {
+		frm.trigger("render_heatmap");
+	},
+	refresh(frm) {
+		frm.add_custom_button(__('Bisect Left'), () => {
+			frm.trigger("bisect_left");
+		});
+
+		frm.add_custom_button(__('Bisect Right'), () => {
+			frm.trigger("bisect_right");
+		});
+
+		frm.add_custom_button(__('Up'), () => {
+			frm.trigger("move_up");
+		});
+		frm.add_custom_button(__('Build Tree'), () => {
+			frm.trigger("build_tree");
+		});
+	},
+	render_heatmap(frm) {
+		let bisect_heatmap = frm.get_field("bisect_heatmap").$wrapper;
+		bisect_heatmap.addClass("bisect_heatmap_location");
+
+		// milliseconds in a day
+		let msiad=24*60*60*1000;
+		let datapoints = {};
+		let fr_dt = new Date(frm.doc.from_date).getTime();
+		let to_dt = new Date(frm.doc.to_date).getTime();
+		let bisect_start = new Date(frm.doc.current_from_date).getTime();
+		let bisect_end = new Date(frm.doc.current_to_date).getTime();
+
+		for(let x=fr_dt; x <= to_dt; x+=msiad){
+			let epoch_in_seconds = x/1000;
+			if ((bisect_start <= x) && (x <= bisect_end )) {
+				datapoints[epoch_in_seconds] = 1.0;
+			} else {
+				datapoints[epoch_in_seconds] = 0.0;
+			}
+		}
+
+		new frappe.Chart(".bisect_heatmap_location", {
+			type: "heatmap",
+			data: {
+				dataPoints: datapoints,
+				start: new Date(frm.doc.from_date),
+				end: new Date(frm.doc.to_date),
+			},
+			countLabel: 'Bisecting',
+			discreteDomains: 1,
+		});
+	},
+	bisect_left(frm) {
+		frm.call({
+			doc: frm.doc,
+			method: 'bisect_left',
+			freeze: true,
+			freeze_message: __("Bisecting Left ..."),
+			callback: (r) => {
+				frm.trigger("render_heatmap");
+			}
+		});
+	},
+	bisect_right(frm) {
+		frm.call({
+			doc: frm.doc,
+			freeze: true,
+			freeze_message: __("Bisecting Right ..."),
+			method: 'bisect_right',
+			callback: (r) => {
+				frm.trigger("render_heatmap");
+			}
+		});
+	},
+	move_up(frm) {
+		frm.call({
+			doc: frm.doc,
+			freeze: true,
+			freeze_message: __("Moving up in tree ..."),
+			method: 'move_up',
+			callback: (r) => {
+				frm.trigger("render_heatmap");
+			}
+		});
+	},
+	build_tree(frm) {
+		frm.call({
+			doc: frm.doc,
+			freeze: true,
+			freeze_message: __("Rebuilding BTree for period ..."),
+			method: 'build_tree',
+			callback: (r) => {
+				frm.trigger("render_heatmap");
+			}
+		});
+	},
+});
diff --git a/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.json b/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.json
new file mode 100644
index 0000000..e129fa6
--- /dev/null
+++ b/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.json
@@ -0,0 +1,194 @@
+{
+ "actions": [],
+ "allow_rename": 1,
+ "creation": "2023-09-15 21:28:28.054773",
+ "default_view": "List",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+  "section_break_cvfg",
+  "company",
+  "column_break_hcam",
+  "from_date",
+  "column_break_qxbi",
+  "to_date",
+  "column_break_iwny",
+  "algorithm",
+  "section_break_8ph9",
+  "current_node",
+  "section_break_ngid",
+  "bisect_heatmap",
+  "section_break_hmsy",
+  "bisecting_from",
+  "current_from_date",
+  "column_break_uqyd",
+  "bisecting_to",
+  "current_to_date",
+  "section_break_hbyo",
+  "heading_cppb",
+  "p_l_summary",
+  "column_break_aivo",
+  "balance_sheet_summary",
+  "b_s_summary",
+  "column_break_gvwx",
+  "difference_heading",
+  "difference"
+ ],
+ "fields": [
+  {
+   "fieldname": "column_break_qxbi",
+   "fieldtype": "Column Break"
+  },
+  {
+   "fieldname": "from_date",
+   "fieldtype": "Datetime",
+   "label": "From Date"
+  },
+  {
+   "fieldname": "to_date",
+   "fieldtype": "Datetime",
+   "label": "To Date"
+  },
+  {
+   "default": "BFS",
+   "fieldname": "algorithm",
+   "fieldtype": "Select",
+   "label": "Algorithm",
+   "options": "BFS\nDFS"
+  },
+  {
+   "fieldname": "column_break_iwny",
+   "fieldtype": "Column Break"
+  },
+  {
+   "fieldname": "current_node",
+   "fieldtype": "Link",
+   "label": "Current Node",
+   "options": "Bisect Nodes"
+  },
+  {
+   "fieldname": "section_break_hmsy",
+   "fieldtype": "Section Break"
+  },
+  {
+   "fieldname": "current_from_date",
+   "fieldtype": "Datetime",
+   "read_only": 1
+  },
+  {
+   "fieldname": "current_to_date",
+   "fieldtype": "Datetime",
+   "read_only": 1
+  },
+  {
+   "fieldname": "column_break_uqyd",
+   "fieldtype": "Column Break"
+  },
+  {
+   "fieldname": "section_break_hbyo",
+   "fieldtype": "Section Break"
+  },
+  {
+   "fieldname": "p_l_summary",
+   "fieldtype": "Float",
+   "read_only": 1
+  },
+  {
+   "fieldname": "b_s_summary",
+   "fieldtype": "Float",
+   "read_only": 1
+  },
+  {
+   "fieldname": "difference",
+   "fieldtype": "Float",
+   "read_only": 1
+  },
+  {
+   "fieldname": "column_break_aivo",
+   "fieldtype": "Column Break"
+  },
+  {
+   "fieldname": "column_break_gvwx",
+   "fieldtype": "Column Break"
+  },
+  {
+   "fieldname": "company",
+   "fieldtype": "Link",
+   "label": "Company",
+   "options": "Company"
+  },
+  {
+   "fieldname": "column_break_hcam",
+   "fieldtype": "Column Break"
+  },
+  {
+   "fieldname": "section_break_ngid",
+   "fieldtype": "Section Break"
+  },
+  {
+   "fieldname": "section_break_8ph9",
+   "fieldtype": "Section Break",
+   "hidden": 1
+  },
+  {
+   "fieldname": "bisect_heatmap",
+   "fieldtype": "HTML",
+   "label": "Heatmap"
+  },
+  {
+   "fieldname": "heading_cppb",
+   "fieldtype": "Heading",
+   "label": "Profit and Loss Summary"
+  },
+  {
+   "fieldname": "balance_sheet_summary",
+   "fieldtype": "Heading",
+   "label": "Balance Sheet Summary"
+  },
+  {
+   "fieldname": "difference_heading",
+   "fieldtype": "Heading",
+   "label": "Difference"
+  },
+  {
+   "fieldname": "bisecting_from",
+   "fieldtype": "Heading",
+   "label": "Bisecting From"
+  },
+  {
+   "fieldname": "bisecting_to",
+   "fieldtype": "Heading",
+   "label": "Bisecting To"
+  },
+  {
+   "fieldname": "section_break_cvfg",
+   "fieldtype": "Section Break"
+  }
+ ],
+ "hide_toolbar": 1,
+ "index_web_pages_for_search": 1,
+ "issingle": 1,
+ "links": [],
+ "modified": "2023-12-01 16:49:54.073890",
+ "modified_by": "Administrator",
+ "module": "Accounts",
+ "name": "Bisect Accounting Statements",
+ "owner": "Administrator",
+ "permissions": [
+  {
+   "create": 1,
+   "delete": 1,
+   "email": 1,
+   "print": 1,
+   "read": 1,
+   "role": "Administrator",
+   "share": 1,
+   "write": 1
+  }
+ ],
+ "read_only": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "states": []
+}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.py b/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.py
new file mode 100644
index 0000000..da273b9
--- /dev/null
+++ b/erpnext/accounts/doctype/bisect_accounting_statements/bisect_accounting_statements.py
@@ -0,0 +1,226 @@
+# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+import datetime
+from collections import deque
+from math import floor
+
+import frappe
+from dateutil.relativedelta import relativedelta
+from frappe import _
+from frappe.model.document import Document
+from frappe.utils import getdate
+from frappe.utils.data import guess_date_format
+
+
+class BisectAccountingStatements(Document):
+	# begin: auto-generated types
+	# This code is auto-generated. Do not modify anything in this block.
+
+	from typing import TYPE_CHECKING
+
+	if TYPE_CHECKING:
+		from frappe.types import DF
+
+		algorithm: DF.Literal["BFS", "DFS"]
+		b_s_summary: DF.Float
+		company: DF.Link | None
+		current_from_date: DF.Datetime | None
+		current_node: DF.Link | None
+		current_to_date: DF.Datetime | None
+		difference: DF.Float
+		from_date: DF.Datetime | None
+		p_l_summary: DF.Float
+		to_date: DF.Datetime | None
+	# end: auto-generated types
+
+	def validate(self):
+		self.validate_dates()
+
+	def validate_dates(self):
+		if getdate(self.from_date) > getdate(self.to_date):
+			frappe.throw(
+				_("From Date: {0} cannot be greater than To date: {1}").format(
+					frappe.bold(self.from_date), frappe.bold(self.to_date)
+				)
+			)
+
+	def bfs(self, from_date: datetime, to_date: datetime):
+		# Make Root node
+		node = frappe.new_doc("Bisect Nodes")
+		node.root = None
+		node.period_from_date = from_date
+		node.period_to_date = to_date
+		node.insert()
+
+		period_queue = deque([node])
+		while period_queue:
+			cur_node = period_queue.popleft()
+			delta = cur_node.period_to_date - cur_node.period_from_date
+			if delta.days == 0:
+				continue
+			else:
+				cur_floor = floor(delta.days / 2)
+				next_to_date = cur_node.period_from_date + relativedelta(days=+cur_floor)
+				left_node = frappe.new_doc("Bisect Nodes")
+				left_node.period_from_date = cur_node.period_from_date
+				left_node.period_to_date = next_to_date
+				left_node.root = cur_node.name
+				left_node.generated = False
+				left_node.insert()
+				cur_node.left_child = left_node.name
+				period_queue.append(left_node)
+
+				next_from_date = cur_node.period_from_date + relativedelta(days=+(cur_floor + 1))
+				right_node = frappe.new_doc("Bisect Nodes")
+				right_node.period_from_date = next_from_date
+				right_node.period_to_date = cur_node.period_to_date
+				right_node.root = cur_node.name
+				right_node.generated = False
+				right_node.insert()
+				cur_node.right_child = right_node.name
+				period_queue.append(right_node)
+
+				cur_node.save()
+
+	def dfs(self, from_date: datetime, to_date: datetime):
+		# Make Root node
+		node = frappe.new_doc("Bisect Nodes")
+		node.root = None
+		node.period_from_date = from_date
+		node.period_to_date = to_date
+		node.insert()
+
+		period_stack = [node]
+		while period_stack:
+			cur_node = period_stack.pop()
+			delta = cur_node.period_to_date - cur_node.period_from_date
+			if delta.days == 0:
+				continue
+			else:
+				cur_floor = floor(delta.days / 2)
+				next_to_date = cur_node.period_from_date + relativedelta(days=+cur_floor)
+				left_node = frappe.new_doc("Bisect Nodes")
+				left_node.period_from_date = cur_node.period_from_date
+				left_node.period_to_date = next_to_date
+				left_node.root = cur_node.name
+				left_node.generated = False
+				left_node.insert()
+				cur_node.left_child = left_node.name
+				period_stack.append(left_node)
+
+				next_from_date = cur_node.period_from_date + relativedelta(days=+(cur_floor + 1))
+				right_node = frappe.new_doc("Bisect Nodes")
+				right_node.period_from_date = next_from_date
+				right_node.period_to_date = cur_node.period_to_date
+				right_node.root = cur_node.name
+				right_node.generated = False
+				right_node.insert()
+				cur_node.right_child = right_node.name
+				period_stack.append(right_node)
+
+				cur_node.save()
+
+	@frappe.whitelist()
+	def build_tree(self):
+		frappe.db.delete("Bisect Nodes")
+
+		# Convert str to datetime format
+		dt_format = guess_date_format(self.from_date)
+		from_date = datetime.datetime.strptime(self.from_date, dt_format)
+		to_date = datetime.datetime.strptime(self.to_date, dt_format)
+
+		if self.algorithm == "BFS":
+			self.bfs(from_date, to_date)
+
+		if self.algorithm == "DFS":
+			self.dfs(from_date, to_date)
+
+		# set root as current node
+		root = frappe.db.get_all("Bisect Nodes", filters={"root": ["is", "not set"]})[0]
+		self.get_report_summary()
+		self.current_node = root.name
+		self.current_from_date = self.from_date
+		self.current_to_date = self.to_date
+		self.save()
+
+	def get_report_summary(self):
+		filters = {
+			"company": self.company,
+			"filter_based_on": "Date Range",
+			"period_start_date": self.current_from_date,
+			"period_end_date": self.current_to_date,
+			"periodicity": "Yearly",
+		}
+		pl_summary = frappe.get_doc("Report", "Profit and Loss Statement")
+		self.p_l_summary = pl_summary.execute_script_report(filters=filters)[5]
+		bs_summary = frappe.get_doc("Report", "Balance Sheet")
+		self.b_s_summary = bs_summary.execute_script_report(filters=filters)[5]
+		self.difference = abs(self.p_l_summary - self.b_s_summary)
+
+	def update_node(self):
+		current_node = frappe.get_doc("Bisect Nodes", self.current_node)
+		current_node.balance_sheet_summary = self.b_s_summary
+		current_node.profit_loss_summary = self.p_l_summary
+		current_node.difference = self.difference
+		current_node.generated = True
+		current_node.save()
+
+	def current_node_has_summary_info(self):
+		"Assertion method"
+		return frappe.db.get_value("Bisect Nodes", self.current_node, "generated")
+
+	def fetch_summary_info_from_current_node(self):
+		current_node = frappe.get_doc("Bisect Nodes", self.current_node)
+		self.p_l_summary = current_node.balance_sheet_summary
+		self.b_s_summary = current_node.profit_loss_summary
+		self.difference = abs(self.p_l_summary - self.b_s_summary)
+
+	def fetch_or_calculate(self):
+		if self.current_node_has_summary_info():
+			self.fetch_summary_info_from_current_node()
+		else:
+			self.get_report_summary()
+			self.update_node()
+
+	@frappe.whitelist()
+	def bisect_left(self):
+		if self.current_node is not None:
+			cur_node = frappe.get_doc("Bisect Nodes", self.current_node)
+			if cur_node.left_child is not None:
+				lft_node = frappe.get_doc("Bisect Nodes", cur_node.left_child)
+				self.current_node = cur_node.left_child
+				self.current_from_date = lft_node.period_from_date
+				self.current_to_date = lft_node.period_to_date
+				self.fetch_or_calculate()
+				self.save()
+			else:
+				frappe.msgprint(_("No more children on Left"))
+
+	@frappe.whitelist()
+	def bisect_right(self):
+		if self.current_node is not None:
+			cur_node = frappe.get_doc("Bisect Nodes", self.current_node)
+			if cur_node.right_child is not None:
+				rgt_node = frappe.get_doc("Bisect Nodes", cur_node.right_child)
+				self.current_node = cur_node.right_child
+				self.current_from_date = rgt_node.period_from_date
+				self.current_to_date = rgt_node.period_to_date
+				self.fetch_or_calculate()
+				self.save()
+			else:
+				frappe.msgprint(_("No more children on Right"))
+
+	@frappe.whitelist()
+	def move_up(self):
+		if self.current_node is not None:
+			cur_node = frappe.get_doc("Bisect Nodes", self.current_node)
+			if cur_node.root is not None:
+				root = frappe.get_doc("Bisect Nodes", cur_node.root)
+				self.current_node = cur_node.root
+				self.current_from_date = root.period_from_date
+				self.current_to_date = root.period_to_date
+				self.fetch_or_calculate()
+				self.save()
+			else:
+				frappe.msgprint(_("Reached Root"))
diff --git a/erpnext/accounts/doctype/bisect_accounting_statements/test_bisect_accounting_statements.py b/erpnext/accounts/doctype/bisect_accounting_statements/test_bisect_accounting_statements.py
new file mode 100644
index 0000000..56ecc94
--- /dev/null
+++ b/erpnext/accounts/doctype/bisect_accounting_statements/test_bisect_accounting_statements.py
@@ -0,0 +1,9 @@
+# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+
+# import frappe
+from frappe.tests.utils import FrappeTestCase
+
+
+class TestBisectAccountingStatements(FrappeTestCase):
+	pass
diff --git a/erpnext/accounts/doctype/bisect_nodes/__init__.py b/erpnext/accounts/doctype/bisect_nodes/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/accounts/doctype/bisect_nodes/__init__.py
diff --git a/erpnext/accounts/doctype/bisect_nodes/bisect_nodes.js b/erpnext/accounts/doctype/bisect_nodes/bisect_nodes.js
new file mode 100644
index 0000000..6dea25f
--- /dev/null
+++ b/erpnext/accounts/doctype/bisect_nodes/bisect_nodes.js
@@ -0,0 +1,8 @@
+// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+// frappe.ui.form.on("Bisect Nodes", {
+// 	refresh(frm) {
+
+// 	},
+// });
diff --git a/erpnext/accounts/doctype/bisect_nodes/bisect_nodes.json b/erpnext/accounts/doctype/bisect_nodes/bisect_nodes.json
new file mode 100644
index 0000000..03fad26
--- /dev/null
+++ b/erpnext/accounts/doctype/bisect_nodes/bisect_nodes.json
@@ -0,0 +1,97 @@
+{
+ "actions": [],
+ "autoname": "autoincrement",
+ "creation": "2023-09-27 14:56:38.112462",
+ "default_view": "List",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+  "root",
+  "left_child",
+  "right_child",
+  "period_from_date",
+  "period_to_date",
+  "difference",
+  "balance_sheet_summary",
+  "profit_loss_summary",
+  "generated"
+ ],
+ "fields": [
+  {
+   "fieldname": "root",
+   "fieldtype": "Link",
+   "label": "Root",
+   "options": "Bisect Nodes"
+  },
+  {
+   "fieldname": "left_child",
+   "fieldtype": "Link",
+   "label": "Left Child",
+   "options": "Bisect Nodes"
+  },
+  {
+   "fieldname": "right_child",
+   "fieldtype": "Link",
+   "label": "Right Child",
+   "options": "Bisect Nodes"
+  },
+  {
+   "fieldname": "period_from_date",
+   "fieldtype": "Datetime",
+   "label": "Period_from_date"
+  },
+  {
+   "fieldname": "period_to_date",
+   "fieldtype": "Datetime",
+   "label": "Period To Date"
+  },
+  {
+   "fieldname": "difference",
+   "fieldtype": "Float",
+   "label": "Difference"
+  },
+  {
+   "fieldname": "balance_sheet_summary",
+   "fieldtype": "Float",
+   "label": "Balance Sheet Summary"
+  },
+  {
+   "fieldname": "profit_loss_summary",
+   "fieldtype": "Float",
+   "label": "Profit and Loss Summary"
+  },
+  {
+   "default": "0",
+   "fieldname": "generated",
+   "fieldtype": "Check",
+   "label": "Generated"
+  }
+ ],
+ "index_web_pages_for_search": 1,
+ "links": [],
+ "modified": "2023-12-01 17:46:12.437996",
+ "modified_by": "Administrator",
+ "module": "Accounts",
+ "name": "Bisect Nodes",
+ "naming_rule": "Autoincrement",
+ "owner": "Administrator",
+ "permissions": [
+  {
+   "create": 1,
+   "delete": 1,
+   "email": 1,
+   "export": 1,
+   "print": 1,
+   "read": 1,
+   "report": 1,
+   "role": "Administrator",
+   "share": 1,
+   "write": 1
+  }
+ ],
+ "read_only": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "states": []
+}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/bisect_nodes/bisect_nodes.py b/erpnext/accounts/doctype/bisect_nodes/bisect_nodes.py
new file mode 100644
index 0000000..f507766
--- /dev/null
+++ b/erpnext/accounts/doctype/bisect_nodes/bisect_nodes.py
@@ -0,0 +1,29 @@
+# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+# import frappe
+from frappe.model.document import Document
+
+
+class BisectNodes(Document):
+	# begin: auto-generated types
+	# This code is auto-generated. Do not modify anything in this block.
+
+	from typing import TYPE_CHECKING
+
+	if TYPE_CHECKING:
+		from frappe.types import DF
+
+		balance_sheet_summary: DF.Float
+		difference: DF.Float
+		generated: DF.Check
+		left_child: DF.Link | None
+		name: DF.Int | None
+		period_from_date: DF.Datetime | None
+		period_to_date: DF.Datetime | None
+		profit_loss_summary: DF.Float
+		right_child: DF.Link | None
+		root: DF.Link | None
+	# end: auto-generated types
+
+	pass
diff --git a/erpnext/accounts/doctype/bisect_nodes/test_bisect_nodes.py b/erpnext/accounts/doctype/bisect_nodes/test_bisect_nodes.py
new file mode 100644
index 0000000..5399df1
--- /dev/null
+++ b/erpnext/accounts/doctype/bisect_nodes/test_bisect_nodes.py
@@ -0,0 +1,9 @@
+# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+
+# import frappe
+from frappe.tests.utils import FrappeTestCase
+
+
+class TestBisectNodes(FrappeTestCase):
+	pass
diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py
index bfb51da..e542d3c 100644
--- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py
+++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py
@@ -765,7 +765,7 @@
 	reserved_qty = (
 		frappe.qb.from_(p_inv)
 		.from_(p_item)
-		.select(Sum(p_item.qty).as_("qty"))
+		.select(Sum(p_item.stock_qty).as_("stock_qty"))
 		.where(
 			(p_inv.name == p_item.parent)
 			& (IfNull(p_inv.consolidated_invoice, "") == "")
@@ -775,7 +775,7 @@
 		)
 	).run(as_dict=True)
 
-	return reserved_qty[0].qty or 0 if reserved_qty else 0
+	return flt(reserved_qty[0].stock_qty) if reserved_qty else 0
 
 
 @frappe.whitelist()
diff --git a/erpnext/accounts/report/balance_sheet/balance_sheet.py b/erpnext/accounts/report/balance_sheet/balance_sheet.py
index b225aac..5d6ca23 100644
--- a/erpnext/accounts/report/balance_sheet/balance_sheet.py
+++ b/erpnext/accounts/report/balance_sheet/balance_sheet.py
@@ -97,11 +97,11 @@
 
 	chart = get_chart_data(filters, columns, asset, liability, equity)
 
-	report_summary = get_report_summary(
+	report_summary, primitive_summary = get_report_summary(
 		period_list, asset, liability, equity, provisional_profit_loss, currency, filters
 	)
 
-	return columns, data, message, chart, report_summary
+	return columns, data, message, chart, report_summary, primitive_summary
 
 
 def get_provisional_profit_loss(
@@ -217,7 +217,7 @@
 			"datatype": "Currency",
 			"currency": currency,
 		},
-	]
+	], (net_asset - net_liability + net_equity)
 
 
 def get_chart_data(filters, columns, asset, liability, equity):
diff --git a/erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.py b/erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.py
index 6635335..002c05c 100644
--- a/erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.py
+++ b/erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.py
@@ -66,11 +66,11 @@
 	currency = filters.presentation_currency or frappe.get_cached_value(
 		"Company", filters.company, "default_currency"
 	)
-	report_summary = get_report_summary(
+	report_summary, primitive_summary = get_report_summary(
 		period_list, filters.periodicity, income, expense, net_profit_loss, currency, filters
 	)
 
-	return columns, data, None, chart, report_summary
+	return columns, data, None, chart, report_summary, primitive_summary
 
 
 def get_report_summary(
@@ -112,7 +112,7 @@
 			"datatype": "Currency",
 			"currency": currency,
 		},
-	]
+	], net_profit
 
 
 def get_net_profit_loss(income, expense, period_list, company, currency=None, consolidated=False):
diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py
index d696cc4..23650b6 100644
--- a/erpnext/manufacturing/doctype/job_card/job_card.py
+++ b/erpnext/manufacturing/doctype/job_card/job_card.py
@@ -273,35 +273,39 @@
 
 	def has_overlap(self, production_capacity, time_logs):
 		overlap = False
-		if production_capacity == 1 and len(time_logs) > 0:
+		if production_capacity == 1 and len(time_logs) >= 1:
 			return True
+		if not len(time_logs):
+			return False
 
-		# Check overlap exists or not between the overlapping time logs with the current Job Card
-		for row in time_logs:
-			count = 1
-			for next_row in time_logs:
-				if row.name == next_row.name:
-					continue
-
-				if (
-					(
-						get_datetime(next_row.from_time) >= get_datetime(row.from_time)
-						and get_datetime(next_row.from_time) <= get_datetime(row.to_time)
-					)
-					or (
-						get_datetime(next_row.to_time) >= get_datetime(row.from_time)
-						and get_datetime(next_row.to_time) <= get_datetime(row.to_time)
-					)
-					or (
-						get_datetime(next_row.from_time) <= get_datetime(row.from_time)
-						and get_datetime(next_row.to_time) >= get_datetime(row.to_time)
-					)
-				):
-					count += 1
-
-			if count > production_capacity:
-				return True
-
+		# sorting overlapping job cards as per from_time
+		time_logs = sorted(time_logs, key=lambda x: x.get("from_time"))
+		# alloted_capacity has key number starting from 1. Key number will increment by 1 if non sequential job card found
+		# if key number reaches/crosses to production_capacity means capacity is full and overlap error generated
+		# this will store last to_time of sequential job cards
+		alloted_capacity = {1: time_logs[0]["to_time"]}
+		# flag for sequential Job card found
+		sequential_job_card_found = False
+		for i in range(1, len(time_logs)):
+			# scanning for all Existing keys
+			for key in alloted_capacity.keys():
+				# if current Job Card from time is greater than last to_time in that key means these job card are sequential
+				if alloted_capacity[key] <= time_logs[i]["from_time"]:
+					# So update key's value with last to_time
+					alloted_capacity[key] = time_logs[i]["to_time"]
+					# flag is true as we get sequential Job Card for that key
+					sequential_job_card_found = True
+					# Immediately break so that job card to time is not added with any other key except this
+					break
+			# if sequential job card not found above means it is overlapping  so increment key number to alloted_capacity
+			if not sequential_job_card_found:
+				# increment key number
+				key = key + 1
+				# for that key last to time is assigned.
+				alloted_capacity[key] = time_logs[i]["to_time"]
+		if len(alloted_capacity) >= production_capacity:
+			# if number of keys greater or equal to production caoacity means full capacity is utilized and we should throw overlap error
+			return True
 		return overlap
 
 	def get_time_logs(self, args, doctype, check_next_available_slot=False):
diff --git a/erpnext/public/js/utils.js b/erpnext/public/js/utils.js
index b0ea568..866e94f 100755
--- a/erpnext/public/js/utils.js
+++ b/erpnext/public/js/utils.js
@@ -843,7 +843,7 @@
 			freeze_message: __("Mapping {0} ...", [opts.source_doctype]),
 			callback: function(r) {
 				if(!r.exc) {
-					var doc = frappe.model.sync(r.message);
+					frappe.model.sync(r.message);
 					cur_frm.dirty();
 					cur_frm.refresh();
 				}
@@ -870,6 +870,11 @@
 			target: opts.target,
 			date_field: opts.date_field || undefined,
 			setters: opts.setters,
+			data_fields: [{
+				fieldname: 'merge_taxes',
+				fieldtype: 'Check',
+				label: __('Merge taxes from multiple documents'),
+			}],
 			get_query: opts.get_query,
 			add_filters_group: 1,
 			allow_child_item_selection: opts.allow_child_item_selection,
@@ -883,10 +888,7 @@
 					return;
 				}
 				opts.source_name = values;
-				if (opts.allow_child_item_selection) {
-					// args contains filtered child docnames
-					opts.args = args;
-				}
+				opts.args = args;
 				d.dialog.hide();
 				_map();
 			},
diff --git a/erpnext/public/js/utils/serial_no_batch_selector.js b/erpnext/public/js/utils/serial_no_batch_selector.js
index 4abc8fa..4cd1243 100644
--- a/erpnext/public/js/utils/serial_no_batch_selector.js
+++ b/erpnext/public/js/utils/serial_no_batch_selector.js
@@ -502,6 +502,7 @@
 
 	set_data(data) {
 		data.forEach(d => {
+			d.qty = Math.abs(d.qty);
 			this.dialog.fields_dict.entries.df.data.push(d);
 		});
 
diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py
index 675f8e9..132f8f2 100644
--- a/erpnext/stock/doctype/delivery_note/delivery_note.py
+++ b/erpnext/stock/doctype/delivery_note/delivery_note.py
@@ -311,11 +311,13 @@
 			)
 
 	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.pick_list_item:
+			if item.pick_list_item and not item.serial_and_batch_bundle:
 				filters = {
 					"item_code": item.item_code,
 					"voucher_type": "Pick List",
@@ -326,7 +328,17 @@
 				bundle_id = frappe.db.get_value("Serial and Batch Bundle", filters, "name")
 
 				if bundle_id:
-					item.serial_and_batch_bundle = bundle_id
+					cls_obj = SerialBatchCreation(
+						{
+							"type_of_transaction": "Outward",
+							"serial_and_batch_bundle": bundle_id,
+							"item_code": item.get("item_code"),
+						}
+					)
+
+					cls_obj.duplicate_package()
+
+					item.serial_and_batch_bundle = cls_obj.serial_and_batch_bundle
 
 	def validate_proj_cust(self):
 		"""check for does customer belong to same project as entered.."""
@@ -408,6 +420,7 @@
 		self.update_stock_ledger()
 
 		self.cancel_packing_slips()
+		self.update_pick_list_status()
 
 		self.make_gl_entries_on_cancel()
 		self.repost_future_sle_and_gle()
diff --git a/erpnext/stock/doctype/pick_list/pick_list.js b/erpnext/stock/doctype/pick_list/pick_list.js
index 7cd171e..afd6ce8 100644
--- a/erpnext/stock/doctype/pick_list/pick_list.js
+++ b/erpnext/stock/doctype/pick_list/pick_list.js
@@ -283,6 +283,7 @@
 			});
 		}
 	},
+
 	uom: (frm, cdt, cdn) => {
 		let row = frappe.get_doc(cdt, cdn);
 		if (row.uom) {
@@ -291,13 +292,50 @@
 			});
 		}
 	},
+
 	qty: (frm, cdt, cdn) => {
 		let row = frappe.get_doc(cdt, cdn);
 		frappe.model.set_value(cdt, cdn, 'stock_qty', row.qty * row.conversion_factor);
 	},
+
 	conversion_factor: (frm, cdt, cdn) => {
 		let row = frappe.get_doc(cdt, cdn);
 		frappe.model.set_value(cdt, cdn, 'stock_qty', row.qty * row.conversion_factor);
+	},
+
+	pick_serial_and_batch(frm, cdt, cdn) {
+		let item = locals[cdt][cdn];
+		let path = "assets/erpnext/js/utils/serial_no_batch_selector.js";
+
+		frappe.db.get_value("Item", item.item_code, ["has_batch_no", "has_serial_no"])
+			.then((r) => {
+				if (r.message && (r.message.has_batch_no || r.message.has_serial_no)) {
+					item.has_serial_no = r.message.has_serial_no;
+					item.has_batch_no = r.message.has_batch_no;
+					item.type_of_transaction = item.qty > 0 ? "Outward":"Inward";
+
+					item.title = item.has_serial_no ?
+						__("Select Serial No") : __("Select Batch No");
+
+					if (item.has_serial_no && item.has_batch_no) {
+						item.title = __("Select Serial and Batch");
+					}
+
+					frappe.require(path, function() {
+						new erpnext.SerialBatchPackageSelector(
+							frm, item, (r) => {
+								if (r) {
+									let qty = Math.abs(r.total_qty);
+									frappe.model.set_value(item.doctype, item.name, {
+										"serial_and_batch_bundle": r.name,
+										"qty": qty
+									});
+								}
+							}
+						);
+					});
+				}
+			});
 	}
 });
 
diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py
index 545e45f..758448a 100644
--- a/erpnext/stock/doctype/pick_list/pick_list.py
+++ b/erpnext/stock/doctype/pick_list/pick_list.py
@@ -21,6 +21,7 @@
 )
 from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import (
 	get_auto_batch_nos,
+	get_picked_serial_nos,
 )
 from erpnext.stock.get_item_details import get_conversion_factor
 from erpnext.stock.serial_batch_bundle import SerialBatchCreation
@@ -167,6 +168,9 @@
 					"Serial and Batch Bundle", row.serial_and_batch_bundle
 				).set_serial_and_batch_values(self, row)
 
+	def on_trash(self):
+		self.remove_serial_and_batch_bundle()
+
 	def remove_serial_and_batch_bundle(self):
 		for row in self.locations:
 			if row.serial_and_batch_bundle:
@@ -723,13 +727,14 @@
 def get_available_item_locations_for_serialized_item(
 	item_code, from_warehouses, required_qty, company, total_picked_qty=0
 ):
+	picked_serial_nos = get_picked_serial_nos(item_code, from_warehouses)
+
 	sn = frappe.qb.DocType("Serial No")
 	query = (
 		frappe.qb.from_(sn)
 		.select(sn.name, sn.warehouse)
 		.where((sn.item_code == item_code) & (sn.company == company))
 		.orderby(sn.creation)
-		.limit(cint(required_qty + total_picked_qty))
 	)
 
 	if from_warehouses:
@@ -742,6 +747,9 @@
 	warehouse_serial_nos_map = frappe._dict()
 	picked_qty = required_qty
 	for serial_no, warehouse in serial_nos:
+		if serial_no in picked_serial_nos:
+			continue
+
 		if picked_qty <= 0:
 			break
 
@@ -786,7 +794,8 @@
 			{
 				"item_code": item_code,
 				"warehouse": from_warehouses,
-				"qty": required_qty + total_picked_qty,
+				"qty": required_qty,
+				"is_pick_list": True,
 			}
 		)
 	)
@@ -1050,7 +1059,7 @@
 @frappe.whitelist()
 def target_document_exists(pick_list_name, purpose):
 	if purpose == "Delivery":
-		return frappe.db.exists("Delivery Note", {"pick_list": pick_list_name})
+		return frappe.db.exists("Delivery Note", {"pick_list": pick_list_name, "docstatus": 1})
 
 	return stock_entry_exists(pick_list_name)
 
diff --git a/erpnext/stock/doctype/pick_list/test_pick_list.py b/erpnext/stock/doctype/pick_list/test_pick_list.py
index 56c44bf..322b0b4 100644
--- a/erpnext/stock/doctype/pick_list/test_pick_list.py
+++ b/erpnext/stock/doctype/pick_list/test_pick_list.py
@@ -644,6 +644,122 @@
 		so.reload()
 		self.assertEqual(so.per_picked, 50)
 
+	def test_picklist_for_batch_item(self):
+		warehouse = "_Test Warehouse - _TC"
+		item = make_item(
+			properties={"is_stock_item": 1, "has_batch_no": 1, "batch_no_series": "PICKLT-.######"}
+		).name
+
+		# create batch
+		for batch_id in ["PICKLT-000001", "PICKLT-000002"]:
+			if not frappe.db.exists("Batch", batch_id):
+				frappe.get_doc(
+					{
+						"doctype": "Batch",
+						"batch_id": batch_id,
+						"item": item,
+					}
+				).insert()
+
+		make_stock_entry(
+			item=item,
+			to_warehouse=warehouse,
+			qty=50,
+			basic_rate=100,
+			batches=frappe._dict({"PICKLT-000001": 30, "PICKLT-000002": 20}),
+		)
+
+		so = make_sales_order(item_code=item, qty=25.0, rate=100)
+		pl = create_pick_list(so.name)
+		# pick half the qty
+		for loc in pl.locations:
+			self.assertEqual(loc.qty, 25.0)
+			self.assertTrue(loc.serial_and_batch_bundle)
+
+			data = frappe.get_all(
+				"Serial and Batch Entry",
+				fields=["qty", "batch_no"],
+				filters={"parent": loc.serial_and_batch_bundle},
+			)
+
+			for d in data:
+				self.assertEqual(d.batch_no, "PICKLT-000001")
+				self.assertEqual(d.qty, 25.0 * -1)
+
+		pl.save()
+		pl.submit()
+
+		so1 = make_sales_order(item_code=item, qty=10.0, rate=100)
+		pl = create_pick_list(so1.name)
+		# pick half the qty
+		for loc in pl.locations:
+			self.assertEqual(loc.qty, 10.0)
+			self.assertTrue(loc.serial_and_batch_bundle)
+
+			data = frappe.get_all(
+				"Serial and Batch Entry",
+				fields=["qty", "batch_no"],
+				filters={"parent": loc.serial_and_batch_bundle},
+			)
+
+			for d in data:
+				self.assertTrue(d.batch_no in ["PICKLT-000001", "PICKLT-000002"])
+				if d.batch_no == "PICKLT-000001":
+					self.assertEqual(d.qty, 5.0 * -1)
+				elif d.batch_no == "PICKLT-000002":
+					self.assertEqual(d.qty, 5.0 * -1)
+
+		pl.save()
+		pl.submit()
+		pl.cancel()
+
+	def test_picklist_for_serial_item(self):
+		warehouse = "_Test Warehouse - _TC"
+		item = make_item(
+			properties={"is_stock_item": 1, "has_serial_no": 1, "serial_no_series": "SN-PICKLT-.######"}
+		).name
+
+		make_stock_entry(item=item, to_warehouse=warehouse, qty=50, basic_rate=100)
+
+		so = make_sales_order(item_code=item, qty=25.0, rate=100)
+		pl = create_pick_list(so.name)
+		picked_serial_nos = []
+		# pick half the qty
+		for loc in pl.locations:
+			self.assertEqual(loc.qty, 25.0)
+			self.assertTrue(loc.serial_and_batch_bundle)
+
+			data = frappe.get_all(
+				"Serial and Batch Entry", fields=["serial_no"], filters={"parent": loc.serial_and_batch_bundle}
+			)
+
+			picked_serial_nos = [d.serial_no for d in data]
+			self.assertEqual(len(picked_serial_nos), 25)
+
+		pl.save()
+		pl.submit()
+
+		so1 = make_sales_order(item_code=item, qty=10.0, rate=100)
+		pl = create_pick_list(so1.name)
+		# pick half the qty
+		for loc in pl.locations:
+			self.assertEqual(loc.qty, 10.0)
+			self.assertTrue(loc.serial_and_batch_bundle)
+
+			data = frappe.get_all(
+				"Serial and Batch Entry",
+				fields=["qty", "batch_no"],
+				filters={"parent": loc.serial_and_batch_bundle},
+			)
+
+			self.assertEqual(len(data), 10)
+			for d in data:
+				self.assertTrue(d.serial_no not in picked_serial_nos)
+
+		pl.save()
+		pl.submit()
+		pl.cancel()
+
 	def test_picklist_with_bundles(self):
 		warehouse = "_Test Warehouse - _TC"
 
@@ -732,7 +848,7 @@
 
 		dn.cancel()
 		pl.reload()
-		self.assertEqual(pl.status, "Completed")
+		self.assertEqual(pl.status, "Open")
 
 		pl.cancel()
 		pl.reload()
diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
index 10d9eaa..c7e36e9 100644
--- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
+++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
@@ -1124,8 +1124,39 @@
 	)
 
 
+def merge_taxes(source_taxes, target_doc):
+	from erpnext.accounts.doctype.pos_invoice_merge_log.pos_invoice_merge_log import (
+		update_item_wise_tax_detail,
+	)
+
+	existing_taxes = target_doc.get("taxes") or []
+	idx = 1
+	for tax in source_taxes:
+		found = False
+		for t in existing_taxes:
+			if t.account_head == tax.account_head and t.cost_center == tax.cost_center:
+				t.tax_amount = flt(t.tax_amount) + flt(tax.tax_amount_after_discount_amount)
+				t.base_tax_amount = flt(t.base_tax_amount) + flt(tax.base_tax_amount_after_discount_amount)
+				update_item_wise_tax_detail(t, tax)
+				found = True
+
+		if not found:
+			tax.charge_type = "Actual"
+			tax.idx = idx
+			idx += 1
+			tax.included_in_print_rate = 0
+			tax.dont_recompute_tax = 1
+			tax.row_id = ""
+			tax.tax_amount = tax.tax_amount_after_discount_amount
+			tax.base_tax_amount = tax.base_tax_amount_after_discount_amount
+			tax.item_wise_tax_detail = tax.item_wise_tax_detail
+			existing_taxes.append(tax)
+
+	target_doc.set("taxes", existing_taxes)
+
+
 @frappe.whitelist()
-def make_purchase_invoice(source_name, target_doc=None):
+def make_purchase_invoice(source_name, target_doc=None, args=None):
 	from erpnext.accounts.party import get_payment_terms_template
 
 	doc = frappe.get_doc("Purchase Receipt", source_name)
@@ -1142,6 +1173,10 @@
 		)
 		doc.run_method("onload")
 		doc.run_method("set_missing_values")
+
+		if args and args.get("merge_taxes"):
+			merge_taxes(source.get("taxes") or [], doc)
+
 		doc.run_method("calculate_taxes_and_totals")
 		doc.set_payment_schedule()
 
@@ -1205,7 +1240,11 @@
 				if not doc.get("is_return")
 				else get_pending_qty(d)[0] > 0,
 			},
-			"Purchase Taxes and Charges": {"doctype": "Purchase Taxes and Charges", "add_if_empty": True},
+			"Purchase Taxes and Charges": {
+				"doctype": "Purchase Taxes and Charges",
+				"add_if_empty": True,
+				"ignore": args.get("merge_taxes") if args else 0,
+			},
 		},
 		target_doc,
 		set_missing_values,
diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py
index afb53fb..dd38e11 100644
--- a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py
+++ b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py
@@ -936,7 +936,7 @@
 		if index == 0:
 			has_serial_no = row[0] == "Serial No"
 			has_batch_no = row[0] == "Batch No"
-			if not has_batch_no:
+			if not has_batch_no and len(row) > 1:
 				has_batch_no = row[1] == "Batch No"
 
 			continue
@@ -1611,10 +1611,17 @@
 	stock_ledgers_batches = get_stock_ledgers_batches(kwargs)
 	pos_invoice_batches = get_reserved_batches_for_pos(kwargs)
 	sre_reserved_batches = get_reserved_batches_for_sre(kwargs)
+	picked_batches = frappe._dict()
+	if kwargs.get("is_pick_list"):
+		picked_batches = get_picked_batches(kwargs)
 
-	if stock_ledgers_batches or pos_invoice_batches or sre_reserved_batches:
+	if stock_ledgers_batches or pos_invoice_batches or sre_reserved_batches or picked_batches:
 		update_available_batches(
-			available_batches, stock_ledgers_batches, pos_invoice_batches, sre_reserved_batches
+			available_batches,
+			stock_ledgers_batches,
+			pos_invoice_batches,
+			sre_reserved_batches,
+			picked_batches,
 		)
 
 	if not kwargs.consider_negative_batches:
@@ -1771,6 +1778,102 @@
 	return group_by_voucher
 
 
+def get_picked_batches(kwargs) -> dict[str, dict]:
+	picked_batches = frappe._dict()
+
+	table = frappe.qb.DocType("Serial and Batch Bundle")
+	child_table = frappe.qb.DocType("Serial and Batch Entry")
+	pick_list_table = frappe.qb.DocType("Pick List")
+
+	query = (
+		frappe.qb.from_(table)
+		.inner_join(child_table)
+		.on(table.name == child_table.parent)
+		.inner_join(pick_list_table)
+		.on(table.voucher_no == pick_list_table.name)
+		.select(
+			child_table.batch_no,
+			child_table.warehouse,
+			Sum(child_table.qty).as_("qty"),
+		)
+		.where(
+			(table.docstatus != 2)
+			& (pick_list_table.status != "Completed")
+			& (table.type_of_transaction == "Outward")
+			& (table.is_cancelled == 0)
+			& (table.voucher_type == "Pick List")
+			& (table.voucher_no.isnotnull())
+		)
+	)
+
+	if kwargs.get("item_code"):
+		query = query.where(table.item_code == kwargs.get("item_code"))
+
+	if kwargs.get("warehouse"):
+		if isinstance(kwargs.warehouse, list):
+			query = query.where(table.warehouse.isin(kwargs.warehouse))
+		else:
+			query = query.where(table.warehouse == kwargs.get("warehouse"))
+
+	data = query.run(as_dict=True)
+	for row in data:
+		if not row.qty:
+			continue
+
+		key = (row.batch_no, row.warehouse)
+		if key not in picked_batches:
+			picked_batches[key] = frappe._dict(
+				{
+					"qty": row.qty,
+					"warehouse": row.warehouse,
+				}
+			)
+		else:
+			picked_batches[key].qty += row.qty
+
+	return picked_batches
+
+
+def get_picked_serial_nos(item_code, warehouse=None) -> list[str]:
+	table = frappe.qb.DocType("Serial and Batch Bundle")
+	child_table = frappe.qb.DocType("Serial and Batch Entry")
+	pick_list_table = frappe.qb.DocType("Pick List")
+
+	query = (
+		frappe.qb.from_(table)
+		.inner_join(child_table)
+		.on(table.name == child_table.parent)
+		.inner_join(pick_list_table)
+		.on(table.voucher_no == pick_list_table.name)
+		.select(
+			child_table.serial_no,
+		)
+		.where(
+			(table.docstatus != 2)
+			& (pick_list_table.status != "Completed")
+			& (table.type_of_transaction == "Outward")
+			& (table.is_cancelled == 0)
+			& (table.voucher_type == "Pick List")
+			& (table.voucher_no.isnotnull())
+		)
+	)
+
+	if item_code:
+		query = query.where(table.item_code == item_code)
+
+	if warehouse:
+		if isinstance(warehouse, list):
+			query = query.where(table.warehouse.isin(warehouse))
+		else:
+			query = query.where(table.warehouse == warehouse)
+
+	data = query.run(as_dict=True)
+	if not data:
+		return []
+
+	return [row.serial_no for row in data if row.serial_no]
+
+
 def get_ledgers_from_serial_batch_bundle(**kwargs) -> List[frappe._dict]:
 	bundle_table = frappe.qb.DocType("Serial and Batch Bundle")
 	serial_batch_table = frappe.qb.DocType("Serial and Batch Entry")