Merge pull request #35921 from rohitwaghchaure/fixed-seperate-table-for-schedule

refactor: separate table added to track scheduling in the job card.
diff --git a/.github/workflows/patch.yml b/.github/workflows/patch.yml
index e2d8957..aae2928 100644
--- a/.github/workflows/patch.yml
+++ b/.github/workflows/patch.yml
@@ -52,7 +52,7 @@
       - name: Setup Node
         uses: actions/setup-node@v2
         with:
-          node-version: 14
+          node-version: 18
           check-latest: true
 
       - name: Add to Hosts
diff --git a/.github/workflows/semantic-commits.yml b/.github/workflows/semantic-commits.yml
index 1744bc3..0e478d5 100644
--- a/.github/workflows/semantic-commits.yml
+++ b/.github/workflows/semantic-commits.yml
@@ -21,7 +21,7 @@
 
       - uses: actions/setup-node@v3
         with:
-          node-version: 14
+          node-version: 18
           check-latest: true
 
       - name: Check commit titles
diff --git a/.github/workflows/server-tests-mariadb.yml b/.github/workflows/server-tests-mariadb.yml
index 8959f7f..9b4db49 100644
--- a/.github/workflows/server-tests-mariadb.yml
+++ b/.github/workflows/server-tests-mariadb.yml
@@ -71,7 +71,7 @@
       - name: Setup Node
         uses: actions/setup-node@v2
         with:
-          node-version: 14
+          node-version: 18
           check-latest: true
 
       - name: Add to Hosts
diff --git a/.github/workflows/server-tests-postgres.yml b/.github/workflows/server-tests-postgres.yml
index df43801..a688706 100644
--- a/.github/workflows/server-tests-postgres.yml
+++ b/.github/workflows/server-tests-postgres.yml
@@ -59,7 +59,7 @@
       - name: Setup Node
         uses: actions/setup-node@v2
         with:
-          node-version: 14
+          node-version: 18
           check-latest: true
 
       - name: Add to Hosts
diff --git a/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.js b/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.js
index 8a6b021..6f0b6fc 100644
--- a/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.js
+++ b/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.js
@@ -68,6 +68,16 @@
 		frm.refresh_field("dimensions");
 		frm.trigger('setup_filters');
 	},
+	apply_restriction_on_values: function(frm) {
+		/** If restriction on values is not applied, we should set "allow_or_restrict" to "Restrict" with an empty allowed dimension table.
+		 * Hence it's not "restricted" on any value.
+		  */
+		if (!frm.doc.apply_restriction_on_values) {
+			frm.set_value("allow_or_restrict", "Restrict");
+			frm.clear_table("dimensions");
+			frm.refresh_field("dimensions");
+		}
+	}
 });
 
 frappe.ui.form.on('Allowed Dimension', {
diff --git a/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.json b/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.json
index 0f3fbc0..2bd6c12 100644
--- a/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.json
+++ b/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.json
@@ -10,6 +10,7 @@
   "disabled",
   "column_break_2",
   "company",
+  "apply_restriction_on_values",
   "allow_or_restrict",
   "section_break_4",
   "accounts",
@@ -24,94 +25,80 @@
    "fieldtype": "Select",
    "in_list_view": 1,
    "label": "Accounting Dimension",
-   "reqd": 1,
-   "show_days": 1,
-   "show_seconds": 1
+   "reqd": 1
   },
   {
    "fieldname": "column_break_2",
-   "fieldtype": "Column Break",
-   "show_days": 1,
-   "show_seconds": 1
+   "fieldtype": "Column Break"
   },
   {
    "fieldname": "section_break_4",
    "fieldtype": "Section Break",
-   "hide_border": 1,
-   "show_days": 1,
-   "show_seconds": 1
+   "hide_border": 1
   },
   {
    "fieldname": "column_break_6",
-   "fieldtype": "Column Break",
-   "show_days": 1,
-   "show_seconds": 1
+   "fieldtype": "Column Break"
   },
   {
+   "depends_on": "eval:doc.apply_restriction_on_values == 1;",
    "fieldname": "allow_or_restrict",
    "fieldtype": "Select",
    "label": "Allow Or Restrict Dimension",
    "options": "Allow\nRestrict",
-   "reqd": 1,
-   "show_days": 1,
-   "show_seconds": 1
+   "reqd": 1
   },
   {
    "fieldname": "accounts",
    "fieldtype": "Table",
    "label": "Applicable On Account",
    "options": "Applicable On Account",
-   "reqd": 1,
-   "show_days": 1,
-   "show_seconds": 1
+   "reqd": 1
   },
   {
-   "depends_on": "eval:doc.accounting_dimension",
+   "depends_on": "eval:doc.accounting_dimension && doc.apply_restriction_on_values",
    "fieldname": "dimensions",
    "fieldtype": "Table",
    "label": "Applicable Dimension",
-   "options": "Allowed Dimension",
-   "reqd": 1,
-   "show_days": 1,
-   "show_seconds": 1
+   "mandatory_depends_on": "eval:doc.apply_restriction_on_values == 1;",
+   "options": "Allowed Dimension"
   },
   {
    "default": "0",
    "fieldname": "disabled",
    "fieldtype": "Check",
-   "label": "Disabled",
-   "show_days": 1,
-   "show_seconds": 1
+   "label": "Disabled"
   },
   {
    "fieldname": "company",
    "fieldtype": "Link",
    "label": "Company",
    "options": "Company",
-   "reqd": 1,
-   "show_days": 1,
-   "show_seconds": 1
+   "reqd": 1
   },
   {
    "fieldname": "dimension_filter_help",
    "fieldtype": "HTML",
-   "label": "Dimension Filter Help",
-   "show_days": 1,
-   "show_seconds": 1
+   "label": "Dimension Filter Help"
   },
   {
    "fieldname": "section_break_10",
-   "fieldtype": "Section Break",
-   "show_days": 1,
-   "show_seconds": 1
+   "fieldtype": "Section Break"
+  },
+  {
+   "default": "1",
+   "fieldname": "apply_restriction_on_values",
+   "fieldtype": "Check",
+   "label": "Apply restriction on dimension values"
   }
  ],
  "index_web_pages_for_search": 1,
  "links": [],
- "modified": "2021-02-03 12:04:58.678402",
+ "modified": "2023-06-07 14:59:41.869117",
  "modified_by": "Administrator",
  "module": "Accounts",
  "name": "Accounting Dimension Filter",
+ "naming_rule": "Expression",
  "owner": "Administrator",
  "permissions": [
   {
@@ -154,5 +141,6 @@
  "quick_entry": 1,
  "sort_field": "modified",
  "sort_order": "DESC",
+ "states": [],
  "track_changes": 1
 }
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.py b/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.py
index 80f736f..de1b82c 100644
--- a/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.py
+++ b/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.py
@@ -8,6 +8,12 @@
 
 
 class AccountingDimensionFilter(Document):
+	def before_save(self):
+		# If restriction is not applied on values, then remove all the dimensions and set allow_or_restrict to Restrict
+		if not self.apply_restriction_on_values:
+			self.allow_or_restrict = "Restrict"
+			self.set("dimensions", [])
+
 	def validate(self):
 		self.validate_applicable_accounts()
 
@@ -44,12 +50,12 @@
 			a.applicable_on_account, d.dimension_value, p.accounting_dimension,
 			p.allow_or_restrict, a.is_mandatory
 		FROM
-			`tabApplicable On Account` a, `tabAllowed Dimension` d,
+			`tabApplicable On Account` a,
 			`tabAccounting Dimension Filter` p
+		LEFT JOIN `tabAllowed Dimension` d ON d.parent = p.name
 		WHERE
 			p.name = a.parent
 			AND p.disabled = 0
-			AND p.name = d.parent
 	""",
 		as_dict=1,
 	)
@@ -76,4 +82,5 @@
 		(dimension, account),
 		{"allowed_dimensions": [], "is_mandatory": is_mandatory, "allow_or_restrict": allow_or_restrict},
 	)
-	map_object[(dimension, account)]["allowed_dimensions"].append(filter_value)
+	if filter_value:
+		map_object[(dimension, account)]["allowed_dimensions"].append(filter_value)
diff --git a/erpnext/accounts/doctype/accounting_dimension_filter/test_accounting_dimension_filter.py b/erpnext/accounts/doctype/accounting_dimension_filter/test_accounting_dimension_filter.py
index f13f2f9..6aba2ab 100644
--- a/erpnext/accounts/doctype/accounting_dimension_filter/test_accounting_dimension_filter.py
+++ b/erpnext/accounts/doctype/accounting_dimension_filter/test_accounting_dimension_filter.py
@@ -64,6 +64,7 @@
 				"accounting_dimension": "Cost Center",
 				"allow_or_restrict": "Allow",
 				"company": "_Test Company",
+				"apply_restriction_on_values": 1,
 				"accounts": [
 					{
 						"applicable_on_account": "Sales - _TC",
@@ -85,6 +86,7 @@
 				"doctype": "Accounting Dimension Filter",
 				"accounting_dimension": "Department",
 				"allow_or_restrict": "Allow",
+				"apply_restriction_on_values": 1,
 				"company": "_Test Company",
 				"accounts": [{"applicable_on_account": "Sales - _TC", "is_mandatory": 1}],
 				"dimensions": [{"accounting_dimension": "Department", "dimension_value": "Accounts - _TC"}],
diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py
index 1f23fe1..712023f 100644
--- a/erpnext/accounts/doctype/payment_entry/payment_entry.py
+++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py
@@ -1735,7 +1735,7 @@
 		if not total_amount:
 			if party_account_currency == company_currency:
 				# for handling cases that don't have multi-currency (base field)
-				total_amount = ref_doc.get("grand_total") or ref_doc.get("base_grand_total")
+				total_amount = ref_doc.get("base_grand_total") or ref_doc.get("grand_total")
 				exchange_rate = 1
 			else:
 				total_amount = ref_doc.get("grand_total")
diff --git a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py
index 278b12f..ae2625b 100644
--- a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py
+++ b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py
@@ -11,6 +11,7 @@
 from erpnext.accounts.doctype.payment_entry.payment_entry import (
 	InvalidPaymentEntry,
 	get_payment_entry,
+	get_reference_details,
 )
 from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import (
 	make_purchase_invoice,
@@ -1037,6 +1038,29 @@
 
 		self.assertRaises(frappe.ValidationError, pe_draft.submit)
 
+	def test_details_update_on_reference_table(self):
+		so = make_sales_order(
+			customer="_Test Customer USD", currency="USD", qty=1, rate=100, do_not_submit=True
+		)
+		so.conversion_rate = 50
+		so.submit()
+		pe = get_payment_entry("Sales Order", so.name)
+		pe.references.clear()
+		pe.paid_from = "Debtors - _TC"
+		pe.paid_from_account_currency = "INR"
+		pe.source_exchange_rate = 50
+		pe.save()
+
+		ref_details = get_reference_details(so.doctype, so.name, pe.paid_from_account_currency)
+		expected_response = {
+			"total_amount": 5000.0,
+			"outstanding_amount": 5000.0,
+			"exchange_rate": 1.0,
+			"due_date": None,
+			"bill_no": None,
+		}
+		self.assertDictEqual(ref_details, expected_response)
+
 
 def create_payment_entry(**args):
 	payment_entry = frappe.new_doc("Payment Entry")
diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py
index 2e4e3b0..216d4ec 100644
--- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py
+++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py
@@ -347,7 +347,10 @@
 				payment_details = self.get_payment_details(row, dr_or_cr)
 				reconciled_entry.append(payment_details)
 
-				if payment_details.difference_amount:
+				if payment_details.difference_amount and row.reference_type not in [
+					"Sales Invoice",
+					"Purchase Invoice",
+				]:
 					self.make_difference_entry(payment_details)
 
 		if entry_list:
@@ -433,6 +436,8 @@
 		journal_entry.save()
 		journal_entry.submit()
 
+		return journal_entry
+
 	def get_payment_details(self, row, dr_or_cr):
 		return frappe._dict(
 			{
@@ -598,6 +603,16 @@
 
 
 def reconcile_dr_cr_note(dr_cr_notes, company):
+	def get_difference_row(inv):
+		if inv.difference_amount != 0 and inv.difference_account:
+			difference_row = {
+				"account": inv.difference_account,
+				inv.dr_or_cr: abs(inv.difference_amount) if inv.difference_amount > 0 else 0,
+				reconcile_dr_or_cr: abs(inv.difference_amount) if inv.difference_amount < 0 else 0,
+				"cost_center": erpnext.get_default_cost_center(company),
+			}
+			return difference_row
+
 	for inv in dr_cr_notes:
 		voucher_type = "Credit Note" if inv.voucher_type == "Sales Invoice" else "Debit Note"
 
@@ -642,5 +657,9 @@
 				],
 			}
 		)
+
+		if difference_entry := get_difference_row(inv):
+			jv.append("accounts", difference_entry)
+
 		jv.flags.ignore_mandatory = True
 		jv.submit()
diff --git a/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py
index 3be11ae..2ac7df0 100644
--- a/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py
+++ b/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py
@@ -11,10 +11,13 @@
 from erpnext import get_default_cost_center
 from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
 from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_entry
+from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
 from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
 from erpnext.accounts.party import get_party_account
 from erpnext.stock.doctype.item.test_item import create_item
 
+test_dependencies = ["Item"]
+
 
 class TestPaymentReconciliation(FrappeTestCase):
 	def setUp(self):
@@ -163,7 +166,9 @@
 	def create_payment_reconciliation(self):
 		pr = frappe.new_doc("Payment Reconciliation")
 		pr.company = self.company
-		pr.party_type = "Customer"
+		pr.party_type = (
+			self.party_type if hasattr(self, "party_type") and self.party_type else "Customer"
+		)
 		pr.party = self.customer
 		pr.receivable_payable_account = get_party_account(pr.party_type, pr.party, pr.company)
 		pr.from_invoice_date = pr.to_invoice_date = pr.from_payment_date = pr.to_payment_date = nowdate()
@@ -890,6 +895,42 @@
 		self.assertEqual(pr.allocation[0].allocated_amount, 85)
 		self.assertEqual(pr.allocation[0].difference_amount, 0)
 
+	def test_reconciliation_purchase_invoice_against_return(self):
+		pi = make_purchase_invoice(
+			supplier="_Test Supplier USD", currency="USD", conversion_rate=50
+		).submit()
+
+		pi_return = frappe.get_doc(pi.as_dict())
+		pi_return.name = None
+		pi_return.docstatus = 0
+		pi_return.is_return = 1
+		pi_return.conversion_rate = 80
+		pi_return.items[0].qty = -pi_return.items[0].qty
+		pi_return.submit()
+
+		self.company = "_Test Company"
+		self.party_type = "Supplier"
+		self.customer = "_Test Supplier USD"
+
+		pr = self.create_payment_reconciliation()
+		pr.get_unreconciled_entries()
+
+		invoices = []
+		payments = []
+		for invoice in pr.invoices:
+			if invoice.invoice_number == pi.name:
+				invoices.append(invoice.as_dict())
+				break
+		for payment in pr.payments:
+			if payment.reference_name == pi_return.name:
+				payments.append(payment.as_dict())
+				break
+
+		pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
+
+		# Should not raise frappe.exceptions.ValidationError: Total Debit must be equal to Total Credit.
+		pr.reconcile()
+
 
 def make_customer(customer_name, currency=None):
 	if not frappe.db.exists("Customer", customer_name):
diff --git a/erpnext/accounts/doctype/payment_terms_template/payment_terms_template.js b/erpnext/accounts/doctype/payment_terms_template/payment_terms_template.js
index ea18ade..6046c13 100644
--- a/erpnext/accounts/doctype/payment_terms_template/payment_terms_template.js
+++ b/erpnext/accounts/doctype/payment_terms_template/payment_terms_template.js
@@ -2,7 +2,11 @@
 // For license information, please see license.txt
 
 frappe.ui.form.on('Payment Terms Template', {
-	setup: function(frm) {
+	refresh: function(frm) {
+		frm.fields_dict.terms.grid.toggle_reqd("payment_term", frm.doc.allocate_payment_based_on_payment_terms);
+	},
 
+	allocate_payment_based_on_payment_terms: function(frm) {
+		frm.fields_dict.terms.grid.toggle_reqd("payment_term", frm.doc.allocate_payment_based_on_payment_terms);
 	}
 });
diff --git a/erpnext/accounts/doctype/payment_terms_template/payment_terms_template.py b/erpnext/accounts/doctype/payment_terms_template/payment_terms_template.py
index ea3b76c..7b04a68 100644
--- a/erpnext/accounts/doctype/payment_terms_template/payment_terms_template.py
+++ b/erpnext/accounts/doctype/payment_terms_template/payment_terms_template.py
@@ -11,7 +11,7 @@
 class PaymentTermsTemplate(Document):
 	def validate(self):
 		self.validate_invoice_portion()
-		self.check_duplicate_terms()
+		self.validate_terms()
 
 	def validate_invoice_portion(self):
 		total_portion = 0
@@ -23,9 +23,12 @@
 				_("Combined invoice portion must equal 100%"), raise_exception=1, indicator="red"
 			)
 
-	def check_duplicate_terms(self):
+	def validate_terms(self):
 		terms = []
 		for term in self.terms:
+			if self.allocate_payment_based_on_payment_terms and not term.payment_term:
+				frappe.throw(_("Row {0}: Payment Term is mandatory").format(term.idx))
+
 			term_info = (term.payment_term, term.credit_days, term.credit_months, term.due_date_based_on)
 			if term_info in terms:
 				frappe.msgprint(
diff --git a/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.js b/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.js
index e6d9fe2..a6c0102 100644
--- a/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.js
+++ b/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.js
@@ -123,22 +123,29 @@
 			row.expected_amount = row.opening_amount;
 		}
 
-		const pos_inv_promises = frm.doc.pos_transactions.map(
-			row => frappe.db.get_doc("POS Invoice", row.pos_invoice)
-		);
-
-		const pos_invoices = await Promise.all(pos_inv_promises);
-
-		for (let doc of pos_invoices) {
-			frm.doc.grand_total += flt(doc.grand_total);
-			frm.doc.net_total += flt(doc.net_total);
-			frm.doc.total_quantity += flt(doc.total_qty);
-			refresh_payments(doc, frm);
-			refresh_taxes(doc, frm);
-			refresh_fields(frm);
-			set_html_data(frm);
-		}
-
+		await Promise.all([
+			frappe.call({
+				method: 'erpnext.accounts.doctype.pos_closing_entry.pos_closing_entry.get_pos_invoices',
+				args: {
+					start: frappe.datetime.get_datetime_as_string(frm.doc.period_start_date),
+					end: frappe.datetime.get_datetime_as_string(frm.doc.period_end_date),
+					pos_profile: frm.doc.pos_profile,
+					user: frm.doc.user
+				},
+				callback: (r) => {
+					let pos_invoices = r.message;
+					for (let doc of pos_invoices) {
+						frm.doc.grand_total += flt(doc.grand_total);
+						frm.doc.net_total += flt(doc.net_total);
+						frm.doc.total_quantity += flt(doc.total_qty);
+						refresh_payments(doc, frm);
+						refresh_taxes(doc, frm);
+						refresh_fields(frm);
+						set_html_data(frm);
+					}
+				}
+			})
+		])
 		frappe.dom.unfreeze();
 	}
 });
diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.html b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.html
index 03abc93..5307ccb 100644
--- a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.html
+++ b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.html
@@ -1,6 +1,6 @@
 <div class="page-break">
 	<div id="header-html" class="hidden-pdf">
-		{% if letter_head %}
+		{% if letter_head.content %}
 		<div class="letter-head text-center">{{ letter_head.content }}</div>
 		<hr style="height:2px;border-width:0;color:black;background-color:black;">
 		{% endif %}
diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.js b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.js
index 7dd5ef3..cec48c1 100644
--- a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.js
+++ b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.js
@@ -65,6 +65,20 @@
 			frm.set_value('to_date', frappe.datetime.get_today());
 		}
 	},
+	report: function(frm){
+		let filters = {
+			'company': frm.doc.company,
+		}
+		if(frm.doc.report == 'Accounts Receivable'){
+			filters['account_type'] = 'Receivable';
+		}
+		frm.set_query("account", function() {
+			return {
+				filters: filters
+			};
+		});
+
+	},
 	customer_collection: function(frm){
 		frm.set_value('collection_name', '');
 		if(frm.doc.customer_collection){
diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.json b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.json
index e23620f..8004659 100644
--- a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.json
+++ b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.json
@@ -6,17 +6,24 @@
  "editable_grid": 1,
  "engine": "InnoDB",
  "field_order": [
+  "report",
   "section_break_11",
   "from_date",
+  "posting_date",
   "company",
   "account",
   "group_by",
   "cost_center",
+  "territory",
   "column_break_14",
   "to_date",
   "finance_book",
   "currency",
   "project",
+  "payment_terms_template",
+  "sales_partner",
+  "sales_person",
+  "based_on_payment_terms",
   "section_break_3",
   "customer_collection",
   "collection_name",
@@ -67,14 +74,14 @@
    "reqd": 1
   },
   {
-   "depends_on": "eval:doc.enable_auto_email == 0;",
+   "depends_on": "eval:(doc.enable_auto_email == 0 && doc.report == 'General Ledger');",
    "fieldname": "from_date",
    "fieldtype": "Date",
    "label": "From Date",
    "mandatory_depends_on": "eval:doc.frequency == '';"
   },
   {
-   "depends_on": "eval:doc.enable_auto_email == 0;",
+   "depends_on": "eval:(doc.enable_auto_email == 0 && doc.report == 'General Ledger');",
    "fieldname": "to_date",
    "fieldtype": "Date",
    "label": "To Date",
@@ -87,6 +94,7 @@
    "options": "PSOA Cost Center"
   },
   {
+   "depends_on": "eval: (doc.report == 'General Ledger');",
    "fieldname": "project",
    "fieldtype": "Table MultiSelect",
    "label": "Project",
@@ -104,7 +112,7 @@
   {
    "fieldname": "section_break_11",
    "fieldtype": "Section Break",
-   "label": "General Ledger Filters"
+   "label": "Report Filters"
   },
   {
    "fieldname": "column_break_14",
@@ -164,12 +172,14 @@
   },
   {
    "default": "Group by Voucher (Consolidated)",
+   "depends_on": "eval:(doc.report == 'General Ledger');",
    "fieldname": "group_by",
    "fieldtype": "Select",
    "label": "Group By",
    "options": "\nGroup by Voucher\nGroup by Voucher (Consolidated)"
   },
   {
+   "depends_on": "eval: (doc.report == 'General Ledger');",
    "fieldname": "currency",
    "fieldtype": "Link",
    "label": "Currency",
@@ -297,6 +307,7 @@
   },
   {
    "default": "0",
+   "depends_on": "eval: (doc.report == 'General Ledger');",
    "fieldname": "show_net_values_in_party_account",
    "fieldtype": "Check",
    "label": "Show Net Values in Party Account"
@@ -310,10 +321,59 @@
   {
    "fieldname": "column_break_ocfq",
    "fieldtype": "Column Break"
+  },
+  {
+   "fieldname": "report",
+   "fieldtype": "Select",
+   "label": "Report",
+   "options": "General Ledger\nAccounts Receivable",
+   "reqd": 1
+  },
+  {
+   "default": "Today",
+   "depends_on": "eval:(doc.report == 'Accounts Receivable');",
+   "fieldname": "posting_date",
+   "fieldtype": "Date",
+   "label": "Posting Date"
+  },
+  {
+   "depends_on": "eval: (doc.report == 'Accounts Receivable');",
+   "fieldname": "payment_terms_template",
+   "fieldtype": "Link",
+   "label": "Payment Terms Template",
+   "options": "Payment Terms Template"
+  },
+  {
+   "depends_on": "eval: (doc.report == 'Accounts Receivable');",
+   "fieldname": "sales_partner",
+   "fieldtype": "Link",
+   "label": "Sales Partner",
+   "options": "Sales Partner"
+  },
+  {
+   "depends_on": "eval: (doc.report == 'Accounts Receivable');",
+   "fieldname": "sales_person",
+   "fieldtype": "Link",
+   "label": "Sales Person",
+   "options": "Sales Person"
+  },
+  {
+   "depends_on": "eval: (doc.report == 'Accounts Receivable');",
+   "fieldname": "territory",
+   "fieldtype": "Link",
+   "label": "Territory",
+   "options": "Territory"
+  },
+  {
+   "default": "0",
+   "depends_on": "eval:(doc.report == 'Accounts Receivable');",
+   "fieldname": "based_on_payment_terms",
+   "fieldtype": "Check",
+   "label": "Based On Payment Terms"
   }
  ],
  "links": [],
- "modified": "2023-04-26 12:46:43.645455",
+ "modified": "2023-06-23 10:13:15.051950",
  "modified_by": "Administrator",
  "module": "Accounts",
  "name": "Process Statement Of Accounts",
diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py
index 67dbe09..08f4cf4 100644
--- a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py
+++ b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py
@@ -15,6 +15,7 @@
 
 from erpnext import get_company_currency
 from erpnext.accounts.party import get_party_account_currency
+from erpnext.accounts.report.accounts_receivable.accounts_receivable import execute as get_ar_soa
 from erpnext.accounts.report.accounts_receivable_summary.accounts_receivable_summary import (
 	execute as get_ageing,
 )
@@ -43,29 +44,10 @@
 def get_report_pdf(doc, consolidated=True):
 	statement_dict = {}
 	ageing = ""
-	base_template_path = "frappe/www/printview.html"
-	template_path = (
-		"erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.html"
-	)
 
 	for entry in doc.customers:
 		if doc.include_ageing:
-			ageing_filters = frappe._dict(
-				{
-					"company": doc.company,
-					"report_date": doc.to_date,
-					"ageing_based_on": doc.ageing_based_on,
-					"range1": 30,
-					"range2": 60,
-					"range3": 90,
-					"range4": 120,
-					"customer": entry.customer,
-				}
-			)
-			col1, ageing = get_ageing(ageing_filters)
-
-			if ageing:
-				ageing[0]["ageing_based_on"] = doc.ageing_based_on
+			ageing = set_ageing(doc, entry)
 
 		tax_id = frappe.get_doc("Customer", entry.customer).tax_id
 		presentation_currency = (
@@ -73,60 +55,25 @@
 			or doc.currency
 			or get_company_currency(doc.company)
 		)
-		if doc.letter_head:
-			from frappe.www.printview import get_letter_head
 
-			letter_head = get_letter_head(doc, 0)
+		filters = get_common_filters(doc)
 
-		filters = frappe._dict(
-			{
-				"from_date": doc.from_date,
-				"to_date": doc.to_date,
-				"company": doc.company,
-				"finance_book": doc.finance_book if doc.finance_book else None,
-				"account": [doc.account] if doc.account else None,
-				"party_type": "Customer",
-				"party": [entry.customer],
-				"party_name": [entry.customer_name] if entry.customer_name else None,
-				"presentation_currency": presentation_currency,
-				"group_by": doc.group_by,
-				"currency": doc.currency,
-				"cost_center": [cc.cost_center_name for cc in doc.cost_center],
-				"project": [p.project_name for p in doc.project],
-				"show_opening_entries": 0,
-				"include_default_book_entries": 0,
-				"tax_id": tax_id if tax_id else None,
-				"show_net_values_in_party_account": doc.show_net_values_in_party_account,
-			}
-		)
-		col, res = get_soa(filters)
+		if doc.report == "General Ledger":
+			filters.update(get_gl_filters(doc, entry, tax_id, presentation_currency))
+		else:
+			filters.update(get_ar_filters(doc, entry))
 
-		for x in [0, -2, -1]:
-			res[x]["account"] = res[x]["account"].replace("'", "")
+		if doc.report == "General Ledger":
+			col, res = get_soa(filters)
+			for x in [0, -2, -1]:
+				res[x]["account"] = res[x]["account"].replace("'", "")
+			if len(res) == 3:
+				continue
+		else:
+			ar_res = get_ar_soa(filters)
+			col, res = ar_res[0], ar_res[1]
 
-		if len(res) == 3:
-			continue
-
-		html = frappe.render_template(
-			template_path,
-			{
-				"filters": filters,
-				"data": res,
-				"ageing": ageing[0] if (doc.include_ageing and ageing) else None,
-				"letter_head": letter_head if doc.letter_head else None,
-				"terms_and_conditions": frappe.db.get_value(
-					"Terms and Conditions", doc.terms_and_conditions, "terms"
-				)
-				if doc.terms_and_conditions
-				else None,
-			},
-		)
-
-		html = frappe.render_template(
-			base_template_path,
-			{"body": html, "css": get_print_style(), "title": "Statement For " + entry.customer},
-		)
-		statement_dict[entry.customer] = html
+		statement_dict[entry.customer] = get_html(doc, filters, entry, col, res, ageing)
 
 	if not bool(statement_dict):
 		return False
@@ -140,6 +87,110 @@
 		return statement_dict
 
 
+def set_ageing(doc, entry):
+	ageing_filters = frappe._dict(
+		{
+			"company": doc.company,
+			"report_date": doc.to_date,
+			"ageing_based_on": doc.ageing_based_on,
+			"range1": 30,
+			"range2": 60,
+			"range3": 90,
+			"range4": 120,
+			"customer": entry.customer,
+		}
+	)
+	col1, ageing = get_ageing(ageing_filters)
+
+	if ageing:
+		ageing[0]["ageing_based_on"] = doc.ageing_based_on
+
+	return ageing
+
+
+def get_common_filters(doc):
+	return frappe._dict(
+		{
+			"company": doc.company,
+			"finance_book": doc.finance_book if doc.finance_book else None,
+			"account": [doc.account] if doc.account else None,
+			"cost_center": [cc.cost_center_name for cc in doc.cost_center],
+		}
+	)
+
+
+def get_gl_filters(doc, entry, tax_id, presentation_currency):
+	return {
+		"from_date": doc.from_date,
+		"to_date": doc.to_date,
+		"party_type": "Customer",
+		"party": [entry.customer],
+		"party_name": [entry.customer_name] if entry.customer_name else None,
+		"presentation_currency": presentation_currency,
+		"group_by": doc.group_by,
+		"currency": doc.currency,
+		"project": [p.project_name for p in doc.project],
+		"show_opening_entries": 0,
+		"include_default_book_entries": 0,
+		"tax_id": tax_id if tax_id else None,
+		"show_net_values_in_party_account": doc.show_net_values_in_party_account,
+	}
+
+
+def get_ar_filters(doc, entry):
+	return {
+		"report_date": doc.posting_date if doc.posting_date else None,
+		"customer_name": entry.customer,
+		"payment_terms_template": doc.payment_terms_template if doc.payment_terms_template else None,
+		"sales_partner": doc.sales_partner if doc.sales_partner else None,
+		"sales_person": doc.sales_person if doc.sales_person else None,
+		"territory": doc.territory if doc.territory else None,
+		"based_on_payment_terms": doc.based_on_payment_terms,
+		"report_name": "Accounts Receivable",
+		"ageing_based_on": doc.ageing_based_on,
+		"range1": 30,
+		"range2": 60,
+		"range3": 90,
+		"range4": 120,
+	}
+
+
+def get_html(doc, filters, entry, col, res, ageing):
+	base_template_path = "frappe/www/printview.html"
+	template_path = (
+		"erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.html"
+		if doc.report == "General Ledger"
+		else "erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts_accounts_receivable.html"
+	)
+
+	if doc.letter_head:
+		from frappe.www.printview import get_letter_head
+
+		letter_head = get_letter_head(doc, 0)
+
+	html = frappe.render_template(
+		template_path,
+		{
+			"filters": filters,
+			"data": res,
+			"report": {"report_name": doc.report, "columns": col},
+			"ageing": ageing[0] if (doc.include_ageing and ageing) else None,
+			"letter_head": letter_head if doc.letter_head else None,
+			"terms_and_conditions": frappe.db.get_value(
+				"Terms and Conditions", doc.terms_and_conditions, "terms"
+			)
+			if doc.terms_and_conditions
+			else None,
+		},
+	)
+
+	html = frappe.render_template(
+		base_template_path,
+		{"body": html, "css": get_print_style(), "title": "Statement For " + entry.customer},
+	)
+	return html
+
+
 def get_customers_based_on_territory_or_customer_group(customer_collection, collection_name):
 	fields_dict = {
 		"Customer Group": "customer_group",
diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts_accounts_receivable.html b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts_accounts_receivable.html
new file mode 100644
index 0000000..07e1896
--- /dev/null
+++ b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts_accounts_receivable.html
@@ -0,0 +1,348 @@
+<style>
+	.print-format {
+		padding: 4mm;
+		font-size: 8.0pt !important;
+	}
+	.print-format td {
+		vertical-align:middle !important;
+	}
+	</style>
+
+	<h2 class="text-center" style="margin-top:0">{{ _(report.report_name) }}</h2>
+	<h4 class="text-center">
+		{% if (filters.customer_name) %}
+			{{ filters.customer_name }}
+		{% else %}
+			{{ filters.customer ~ filters.supplier }}
+		{% endif %}
+	</h4>
+	<h6 class="text-center">
+			{% if (filters.tax_id) %}
+			{{ _("Tax Id: ") }}{{ filters.tax_id }}
+			{% endif %}
+	</h6>
+	<h5 class="text-center">
+		{{ _(filters.ageing_based_on) }}
+		{{ _("Until") }}
+		{{ frappe.format(filters.report_date, 'Date') }}
+	</h5>
+
+	<div class="clearfix">
+		<div class="pull-left">
+		{% if(filters.payment_terms) %}
+			<strong>{{ _("Payment Terms") }}:</strong> {{ filters.payment_terms }}
+		{% endif %}
+		</div>
+		<div class="pull-right">
+		{% if(filters.credit_limit) %}
+			<strong>{{ _("Credit Limit") }}:</strong> {{ frappe.utils.fmt_money(filters.credit_limit) }}
+		{% endif %}
+		</div>
+	</div>
+
+	{% if(filters.show_future_payments) %}
+		{% set balance_row = data.slice(-1).pop() %}
+		{% for i in report.columns %}
+			{% if i.fieldname == 'age' %}
+				{% set elem = i %}
+			{% endif %}
+		{% endfor %}
+		{% set start = report.columns.findIndex(elem) %}
+		{% set range1 = report.columns[start].label %}
+		{% set range2 = report.columns[start+1].label %}
+		{% set range3 = report.columns[start+2].label %}
+		{% set range4 = report.columns[start+3].label %}
+		{% set range5 = report.columns[start+4].label %}
+		{% set range6 = report.columns[start+5].label %}
+
+		{% if(balance_row) %}
+		<table class="table table-bordered table-condensed">
+			<caption class="text-right">(Amount in {{ data[0]["currency"] ~ "" }})</caption>
+				<colgroup>
+					<col style="width: 30mm;">
+					<col style="width: 18mm;">
+					<col style="width: 18mm;">
+					<col style="width: 18mm;">
+					<col style="width: 18mm;">
+					<col style="width: 18mm;">
+					<col style="width: 18mm;">
+					<col style="width: 18mm;">
+				</colgroup>
+
+			<thead>
+				<tr>
+					<th>{{ _(" ") }}</th>
+					<th>{{ _(range1) }}</th>
+					<th>{{ _(range2) }}</th>
+					<th>{{ _(range3) }}</th>
+					<th>{{ _(range4) }}</th>
+					<th>{{ _(range5) }}</th>
+					<th>{{ _(range6) }}</th>
+					<th>{{ _("Total") }}</th>
+				</tr>
+			</thead>
+			<tbody>
+				<tr>
+					<td>{{ _("Total Outstanding") }}</td>
+					<td class="text-right">
+						{{ format_number(balance_row["age"], null, 2) }}
+					</td>
+					<td class="text-right">
+						{{ frappe.utils.fmt_money(balance_row["range1"], data[data.length-1]["currency"]) }}
+					</td>
+					<td class="text-right">
+						{{ frappe.utils.fmt_money(balance_row["range2"], data[data.length-1]["currency"]) }}
+					</td>
+					<td class="text-right">
+						{{ frappe.utils.fmt_money(balance_row["range3"], data[data.length-1]["currency"]) }}
+					</td>
+					<td class="text-right">
+						{{ frappe.utils.fmt_money(balance_row["range4"], data[data.length-1]["currency"]) }}
+					</td>
+					<td class="text-right">
+						{{ frappe.utils.fmt_money(balance_row["range5"], data[data.length-1]["currency"]) }}
+					</td>
+					<td class="text-right">
+						{{ frappe.utils.fmt_money(flt(balance_row["outstanding"]), data[data.length-1]["currency"]) }}
+					</td>
+				</tr>
+					<td>{{ _("Future Payments") }}</td>
+					<td></td>
+					<td></td>
+					<td></td>
+					<td></td>
+					<td></td>
+					<td></td>
+					<td class="text-right">
+						{{ frappe.utils.fmt_money(flt(balance_row[("future_amount")]), data[data.length-1]["currency"]) }}
+					</td>
+				<tr class="cvs-footer">
+					<th class="text-left">{{ _("Cheques Required") }}</th>
+					<th></th>
+					<th></th>
+					<th></th>
+					<th></th>
+					<th></th>
+					<th></th>
+					<th class="text-right">
+						{{ frappe.utils.fmt_money(flt(balance_row["outstanding"] - balance_row[("future_amount")]), data[data.length-1]["currency"]) }}</th>
+				</tr>
+			</tbody>
+
+		</table>
+		{% endif %}
+	{% endif %}
+	<table class="table table-bordered">
+		<thead>
+			<tr>
+				{% if(report.report_name == "Accounts Receivable" or report.report_name == "Accounts Payable") %}
+					<th style="width: 10%">{{ _("Date") }}</th>
+					<th style="width: 4%">{{ _("Age (Days)") }}</th>
+
+					{% if(report.report_name == "Accounts Receivable" and filters.show_sales_person) %}
+						<th style="width: 14%">{{ _("Reference") }}</th>
+						<th style="width: 10%">{{ _("Sales Person") }}</th>
+					{% else %}
+						<th style="width: 24%">{{ _("Reference") }}</th>
+					{% endif %}
+					{% if not(filters.show_future_payments) %}
+						<th style="width: 20%">
+						{% if (filters.customer or filters.supplier or filters.customer_name) %}
+							{{ _("Remarks") }}
+						{% else %}
+							{{ _("Party") }}
+						{% endif %}
+						</th>
+					{% endif %}
+					<th style="width: 10%; text-align: right">{{ _("Invoiced Amount") }}</th>
+					{% if not(filters.show_future_payments) %}
+						<th style="width: 10%; text-align: right">{{ _("Paid Amount") }}</th>
+						<th style="width: 10%; text-align: right">
+							{% if report.report_name == "Accounts Receivable" %}
+								{{ _('Credit Note') }}
+							{% else %}
+								{{ _('Debit Note') }}
+							{% endif %}
+						</th>
+					{% endif %}
+					<th style="width: 10%; text-align: right">{{ _("Outstanding Amount") }}</th>
+					{% if(filters.show_future_payments) %}
+						{% if(report.report_name == "Accounts Receivable") %}
+							<th style="width: 12%">{{ _("Customer LPO No.") }}</th>
+						{% endif %}
+						<th style="width: 10%">{{ _("Future Payment Ref") }}</th>
+						<th style="width: 10%">{{ _("Future Payment Amount") }}</th>
+						<th style="width: 10%">{{ _("Remaining Balance") }}</th>
+					{% endif %}
+				{% else %}
+					<th style="width: 40%">
+						{% if (filters.customer or filters.supplier or filters.customer_name) %}
+							{{ _("Remarks")}}
+						{% else %}
+							{{ _("Party") }}
+						{% endif %}
+					</th>
+					<th style="width: 15%">{{ _("Total Invoiced Amount") }}</th>
+					<th style="width: 15%">{{ _("Total Paid Amount") }}</th>
+					<th style="width: 15%">
+						{% if report.report_name == "Accounts Receivable Summary" %}
+							{{ _('Credit Note Amount') }}
+						{% else %}
+							{{ _('Debit Note Amount') }}
+						{% endif %}
+					</th>
+					<th style="width: 15%">{{ _("Total Outstanding Amount") }}</th>
+				{% endif %}
+			</tr>
+		</thead>
+		<tbody>
+			{% for i in range(data|length) %}
+				<tr>
+				{% if(report.report_name == "Accounts Receivable" or report.report_name == "Accounts Payable") %}
+					{% if(data[i]["party"]) %}
+						<td>{{ (data[i]["posting_date"]) }}</td>
+						<td style="text-align: right">{{ data[i]["age"] }}</td>
+						<td>
+							{% if not(filters.show_future_payments) %}
+								{{ data[i]["voucher_type"] }}
+								<br>
+							{% endif %}
+							{{ data[i]["voucher_no"] }}
+						</td>
+
+						{% if(report.report_name == "Accounts Receivable" and filters.show_sales_person) %}
+						<td>{{ data[i]["sales_person"] }}</td>
+						{% endif %}
+
+						{% if not (filters.show_future_payments) %}
+						<td>
+							{% if(not(filters.customer or filters.supplier or filters.customer_name)) %}
+								{{ data[i]["party"] }}
+								{% if(data[i]["customer_name"] and data[i]["customer_name"] != data[i]["party"]) %}
+									<br> {{ data[i]["customer_name"] }}
+								{% elif(data[i]["supplier_name"] != data[i]["party"]) %}
+									<br> {{ data[i]["supplier_name"] }}
+								{% endif %}
+							{% endif %}
+							<div>
+							{% if data[i]["remarks"] %}
+								{{ _("Remarks") }}:
+								{{ data[i]["remarks"] }}
+							{% endif %}
+							</div>
+						</td>
+						{% endif %}
+
+						<td style="text-align: right">
+							{{ frappe.utils.fmt_money(data[i]["invoiced"], currency=data[i]["currency"]) }}</td>
+
+						{% if not(filters.show_future_payments) %}
+							<td style="text-align: right">
+								{{ frappe.utils.fmt_money(data[i]["paid"], currency=data[i]["currency"]) }}</td>
+							<td style="text-align: right">
+								{{ frappe.utils.fmt_money(data[i]["credit_note"], currency=data[i]["currency"]) }}</td>
+						{% endif %}
+						<td style="text-align: right">
+							{{ frappe.utils.fmt_money(data[i]["outstanding"], currency=data[i]["currency"]) }}</td>
+
+						{% if(filters.show_future_payments) %}
+							{% if(report.report_name == "Accounts Receivable") %}
+								<td style="text-align: right">
+									{{ data[i]["po_no"] }}</td>
+							{% endif %}
+							<td style="text-align: right">{{ data[i]["future_ref"] }}</td>
+							<td style="text-align: right">{{ frappe.utils.fmt_money(data[i]["future_amount"], currency=data[i]["currency"]) }}</td>
+							<td style="text-align: right">{{ frappe.utils.fmt_money(data[i]["remaining_balance"], currency=data[i]["currency"]) }}</td>
+						{% endif %}
+					{% else %}
+						<td></td>
+						{% if not(filters.show_future_payments) %}
+						<td></td>
+						{% endif %}
+						{% if(report.report_name == "Accounts Receivable" and filters.show_sales_person) %}
+						<td></td>
+						{% endif %}
+						<td></td>
+						<td style="text-align: right"><b>{{ _("Total") }}</b></td>
+						<td style="text-align: right">
+							{{ frappe.utils.fmt_money(data[i]["invoiced"], data[i]["currency"]) }}</td>
+
+						{% if not(filters.show_future_payments) %}
+							<td style="text-align: right">
+								{{ frappe.utils.fmt_money(data[i]["paid"], currency=data[i]["currency"]) }}</td>
+							<td style="text-align: right">{{ frappe.utils.fmt_money(data[i]["credit_note"], currency=data[i]["currency"]) }} </td>
+						{% endif %}
+						<td style="text-align: right">
+							{{ frappe.utils.fmt_money(data[i]["outstanding"], currency=data[i]["currency"]) }}</td>
+
+						{% if(filters.show_future_payments) %}
+							{% if(report.report_name == "Accounts Receivable") %}
+								<td style="text-align: right">
+									{{ data[i]["po_no"] }}</td>
+							{% endif %}
+							<td style="text-align: right">{{ data[i]["future_ref"] }}</td>
+							<td style="text-align: right">{{ frappe.utils.fmt_money(data[i]["future_amount"], currency=data[i]["currency"]) }}</td>
+							<td style="text-align: right">{{ frappe.utils.fmt_money(data[i]["remaining_balance"], currency=data[i]["currency"]) }}</td>
+						{% endif %}
+					{% endif %}
+				{% else %}
+					{% if(data[i]["party"] or "&nbsp;") %}
+						{% if not(data[i]["is_total_row"]) %}
+							<td>
+								{% if(not(filters.customer | filters.supplier)) %}
+									{{ data[i]["party"] }}
+									{% if(data[i]["customer_name"] and data[i]["customer_name"] != data[i]["party"]) %}
+										<br> {{ data[i]["customer_name"] }}
+									{% elif(data[i]["supplier_name"] != data[i]["party"]) %}
+										<br> {{ data[i]["supplier_name"] }}
+									{% endif %}
+								{% endif %}
+								<br>{{ _("Remarks") }}:
+								{{ data[i]["remarks"] }}
+							</td>
+						{% else %}
+							<td><b>{{ _("Total") }}</b></td>
+						{% endif %}
+						<td style="text-align: right">{{ frappe.utils.fmt_money(data[i]["invoiced"], currency=data[i]["currency"]) }}</td>
+						<td style="text-align: right">{{ frappe.utils.fmt_money(data[i]["paid"], currency=data[i]["currency"]) }}</td>
+						<td style="text-align: right">{{ frappe.utils.fmt_money(data[i]["credit_note"], currency=data[i]["currency"]) }}</td>
+						<td style="text-align: right">{{ frappe.utils.fmt_money(data[i]["outstanding"], currency=data[i]["currency"]) }}</td>
+					{% endif %}
+				{% endif %}
+				</tr>
+			{% endfor %}
+			<td></td>
+			<td></td>
+			<td></td>
+			<td></td>
+			<td style="text-align: right"><b>{{ frappe.utils.fmt_money(data|sum(attribute="invoiced"), currency=data[0]["currency"]) }}</b></td>
+			<td style="text-align: right"><b>{{ frappe.utils.fmt_money(data|sum(attribute="paid"), currency=data[0]["currency"]) }}</b></td>
+			<td style="text-align: right"><b>{{ frappe.utils.fmt_money(data|sum(attribute="credit_note"), currency=data[0]["currency"]) }}</b></td>
+			<td style="text-align: right"><b>{{ frappe.utils.fmt_money(data|sum(attribute="outstanding"), currency=data[0]["currency"]) }}</b></td>
+		</tbody>
+	</table>
+	<br>
+	{% if ageing %}
+	<h4 class="text-center">{{ _("Ageing Report based on ") }} {{ ageing.ageing_based_on }}
+		{{ _("up to " ) }}  {{ frappe.format(filters.report_date, 'Date')}}
+	</h4>
+	<table class="table table-bordered">
+		<thead>
+			<tr>
+				<th style="width: 25%">30 Days</th>
+				<th style="width: 25%">60 Days</th>
+				<th style="width: 25%">90 Days</th>
+				<th style="width: 25%">120 Days</th>
+			</tr>
+		</thead>
+		<tbody>
+			<tr>
+				<td>{{ frappe.utils.fmt_money(ageing.range1, currency=data[0]["currency"]) }}</td>
+				<td>{{ frappe.utils.fmt_money(ageing.range2, currency=data[0]["currency"]) }}</td>
+				<td>{{ frappe.utils.fmt_money(ageing.range3, currency=data[0]["currency"]) }}</td>
+				<td>{{ frappe.utils.fmt_money(ageing.range4, currency=data[0]["currency"]) }}</td>
+			</tr>
+		</tbody>
+	</table>
+	{% endif %}
+	<p class="text-right text-muted">{{ _("Printed On ") }}{{ frappe.utils.now() }}</p>
diff --git a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py
index c2b7ff0..58792d1 100644
--- a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py
+++ b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py
@@ -585,7 +585,9 @@
 			"supplier": ("in", parties),
 			"apply_tds": 1,
 			"docstatus": 1,
+			"tax_withholding_category": ldc.tax_withholding_category,
 			"posting_date": ("between", (ldc.valid_from, ldc.valid_upto)),
+			"company": ldc.company,
 		},
 		"sum(tax_withholding_net_total)",
 	)
@@ -615,7 +617,7 @@
 ):
 	valid = False
 
-	available_amount = flt(certificate_limit) - flt(deducted_amount) - flt(current_amount)
+	available_amount = flt(certificate_limit) - flt(deducted_amount)
 
 	if (getdate(valid_from) <= getdate(posting_date) <= getdate(valid_upto)) and available_amount > 0:
 		valid = True
diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.html b/erpnext/accounts/report/accounts_receivable/accounts_receivable.html
index f2bf942..ed3b991 100644
--- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.html
+++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.html
@@ -284,4 +284,4 @@
 			{% } %}
 		</tbody>
 	</table>
-	<p class="text-right text-muted">{{ __("Printed On ") }}{%= frappe.datetime.str_to_user(frappe.datetime.get_datetime_as_string()) %}</p>
+	<p class="text-right text-muted">{{ __("Printed On ") }}{%= frappe.datetime.str_to_user(frappe.datetime.get_datetime_as_string()) %}</p>
\ No newline at end of file
diff --git a/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py b/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py
index 924c14b..6fdb2f3 100644
--- a/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py
+++ b/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py
@@ -15,7 +15,6 @@
 	get_group_by_conditions,
 	get_tax_accounts,
 )
-from erpnext.selling.report.item_wise_sales_history.item_wise_sales_history import get_item_details
 
 
 def execute(filters=None):
@@ -40,6 +39,16 @@
 			tax_doctype="Purchase Taxes and Charges",
 		)
 
+		scrubbed_tax_fields = {}
+
+		for tax in tax_columns:
+			scrubbed_tax_fields.update(
+				{
+					tax + " Rate": frappe.scrub(tax + " Rate"),
+					tax + " Amount": frappe.scrub(tax + " Amount"),
+				}
+			)
+
 	po_pr_map = get_purchase_receipts_against_purchase_order(item_list)
 
 	data = []
@@ -50,11 +59,7 @@
 	if filters.get("group_by"):
 		grand_total = get_grand_total(filters, "Purchase Invoice")
 
-	item_details = get_item_details()
-
 	for d in item_list:
-		item_record = item_details.get(d.item_code)
-
 		purchase_receipt = None
 		if d.purchase_receipt:
 			purchase_receipt = d.purchase_receipt
@@ -67,8 +72,8 @@
 
 		row = {
 			"item_code": d.item_code,
-			"item_name": item_record.item_name if item_record else d.item_name,
-			"item_group": item_record.item_group if item_record else d.item_group,
+			"item_name": d.pi_item_name if d.pi_item_name else d.i_item_name,
+			"item_group": d.pi_item_group if d.pi_item_group else d.i_item_group,
 			"description": d.description,
 			"invoice": d.parent,
 			"posting_date": d.posting_date,
@@ -101,8 +106,8 @@
 			item_tax = itemised_tax.get(d.name, {}).get(tax, {})
 			row.update(
 				{
-					frappe.scrub(tax + " Rate"): item_tax.get("tax_rate", 0),
-					frappe.scrub(tax + " Amount"): item_tax.get("tax_amount", 0),
+					scrubbed_tax_fields[tax + " Rate"]: item_tax.get("tax_rate", 0),
+					scrubbed_tax_fields[tax + " Amount"]: item_tax.get("tax_amount", 0),
 				}
 			)
 			total_tax += flt(item_tax.get("tax_amount"))
@@ -325,15 +330,17 @@
 			`tabPurchase Invoice`.supplier, `tabPurchase Invoice`.remarks, `tabPurchase Invoice`.base_net_total,
 			`tabPurchase Invoice`.unrealized_profit_loss_account,
 			`tabPurchase Invoice Item`.`item_code`, `tabPurchase Invoice Item`.description,
-			`tabPurchase Invoice Item`.`item_name`, `tabPurchase Invoice Item`.`item_group`,
+			`tabPurchase Invoice Item`.`item_name` as pi_item_name, `tabPurchase Invoice Item`.`item_group` as pi_item_group,
+			`tabItem`.`item_name` as i_item_name, `tabItem`.`item_group` as i_item_group,
 			`tabPurchase Invoice Item`.`project`, `tabPurchase Invoice Item`.`purchase_order`,
 			`tabPurchase Invoice Item`.`purchase_receipt`, `tabPurchase Invoice Item`.`po_detail`,
 			`tabPurchase Invoice Item`.`expense_account`, `tabPurchase Invoice Item`.`stock_qty`,
 			`tabPurchase Invoice Item`.`stock_uom`, `tabPurchase Invoice Item`.`base_net_amount`,
 			`tabPurchase Invoice`.`supplier_name`, `tabPurchase Invoice`.`mode_of_payment` {0}
-		from `tabPurchase Invoice`, `tabPurchase Invoice Item`
+		from `tabPurchase Invoice`, `tabPurchase Invoice Item`, `tabItem`
 		where `tabPurchase Invoice`.name = `tabPurchase Invoice Item`.`parent` and
-		`tabPurchase Invoice`.docstatus = 1 %s
+			`tabItem`.name = `tabPurchase Invoice Item`.`item_code` and
+			`tabPurchase Invoice`.docstatus = 1 %s
 	""".format(
 			additional_query_columns
 		)
diff --git a/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py b/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py
index 0ebe13f..bd7d02e 100644
--- a/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py
+++ b/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py
@@ -11,7 +11,6 @@
 from erpnext.accounts.report.sales_register.sales_register import get_mode_of_payments
 from erpnext.selling.report.item_wise_sales_history.item_wise_sales_history import (
 	get_customer_details,
-	get_item_details,
 )
 
 
@@ -35,6 +34,16 @@
 	if item_list:
 		itemised_tax, tax_columns = get_tax_accounts(item_list, columns, company_currency)
 
+		scrubbed_tax_fields = {}
+
+		for tax in tax_columns:
+			scrubbed_tax_fields.update(
+				{
+					tax + " Rate": frappe.scrub(tax + " Rate"),
+					tax + " Amount": frappe.scrub(tax + " Amount"),
+				}
+			)
+
 	mode_of_payments = get_mode_of_payments(set(d.parent for d in item_list))
 	so_dn_map = get_delivery_notes_against_sales_order(item_list)
 
@@ -47,11 +56,9 @@
 		grand_total = get_grand_total(filters, "Sales Invoice")
 
 	customer_details = get_customer_details()
-	item_details = get_item_details()
 
 	for d in item_list:
 		customer_record = customer_details.get(d.customer)
-		item_record = item_details.get(d.item_code)
 
 		delivery_note = None
 		if d.delivery_note:
@@ -64,8 +71,8 @@
 
 		row = {
 			"item_code": d.item_code,
-			"item_name": item_record.item_name if item_record else d.item_name,
-			"item_group": item_record.item_group if item_record else d.item_group,
+			"item_name": d.si_item_name if d.si_item_name else d.i_item_name,
+			"item_group": d.si_item_group if d.si_item_group else d.i_item_group,
 			"description": d.description,
 			"invoice": d.parent,
 			"posting_date": d.posting_date,
@@ -107,8 +114,8 @@
 			item_tax = itemised_tax.get(d.name, {}).get(tax, {})
 			row.update(
 				{
-					frappe.scrub(tax + " Rate"): item_tax.get("tax_rate", 0),
-					frappe.scrub(tax + " Amount"): item_tax.get("tax_amount", 0),
+					scrubbed_tax_fields[tax + " Rate"]: item_tax.get("tax_rate", 0),
+					scrubbed_tax_fields[tax + " Amount"]: item_tax.get("tax_amount", 0),
 				}
 			)
 			if item_tax.get("is_other_charges"):
@@ -404,15 +411,18 @@
 			`tabSales Invoice Item`.project,
 			`tabSales Invoice Item`.item_code, `tabSales Invoice Item`.description,
 			`tabSales Invoice Item`.`item_name`, `tabSales Invoice Item`.`item_group`,
+			`tabSales Invoice Item`.`item_name` as si_item_name, `tabSales Invoice Item`.`item_group` as si_item_group,
+			`tabItem`.`item_name` as i_item_name, `tabItem`.`item_group` as i_item_group,
 			`tabSales Invoice Item`.sales_order, `tabSales Invoice Item`.delivery_note,
 			`tabSales Invoice Item`.income_account, `tabSales Invoice Item`.cost_center,
 			`tabSales Invoice Item`.stock_qty, `tabSales Invoice Item`.stock_uom,
 			`tabSales Invoice Item`.base_net_rate, `tabSales Invoice Item`.base_net_amount,
 			`tabSales Invoice`.customer_name, `tabSales Invoice`.customer_group, `tabSales Invoice Item`.so_detail,
 			`tabSales Invoice`.update_stock, `tabSales Invoice Item`.uom, `tabSales Invoice Item`.qty {0}
-		from `tabSales Invoice`, `tabSales Invoice Item`
-		where `tabSales Invoice`.name = `tabSales Invoice Item`.parent
-			and `tabSales Invoice`.docstatus = 1 {1}
+		from `tabSales Invoice`, `tabSales Invoice Item`, `tabItem`
+		where `tabSales Invoice`.name = `tabSales Invoice Item`.parent and
+			`tabItem`.name = `tabSales Invoice Item`.`item_code` and
+			`tabSales Invoice`.docstatus = 1 {1}
 		""".format(
 			additional_query_columns or "", conditions
 		),
diff --git a/erpnext/accounts/report/voucher_wise_balance/__init__.py b/erpnext/accounts/report/voucher_wise_balance/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/accounts/report/voucher_wise_balance/__init__.py
diff --git a/erpnext/accounts/report/voucher_wise_balance/voucher_wise_balance.js b/erpnext/accounts/report/voucher_wise_balance/voucher_wise_balance.js
new file mode 100644
index 0000000..0c148f8
--- /dev/null
+++ b/erpnext/accounts/report/voucher_wise_balance/voucher_wise_balance.js
@@ -0,0 +1,28 @@
+// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+/* eslint-disable */
+
+frappe.query_reports["Voucher-wise Balance"] = {
+	"filters": [
+        {
+            "fieldname": "company",
+            "label": __("Company"),
+            "fieldtype": "Link",
+            "options": "Company"
+        },
+        {
+            "fieldname":"from_date",
+            "label": __("From Date"),
+            "fieldtype": "Date",
+            "default": frappe.datetime.add_months(frappe.datetime.get_today(), -1),
+            "width": "60px"
+        },
+        {
+            "fieldname":"to_date",
+            "label": __("To Date"),
+            "fieldtype": "Date",
+            "default": frappe.datetime.get_today(),
+            "width": "60px"
+        },
+    ]
+};
diff --git a/erpnext/accounts/report/voucher_wise_balance/voucher_wise_balance.json b/erpnext/accounts/report/voucher_wise_balance/voucher_wise_balance.json
new file mode 100644
index 0000000..434e5a3
--- /dev/null
+++ b/erpnext/accounts/report/voucher_wise_balance/voucher_wise_balance.json
@@ -0,0 +1,33 @@
+{
+ "add_total_row": 0,
+ "columns": [],
+ "creation": "2023-06-27 16:40:15.109554",
+ "disabled": 0,
+ "docstatus": 0,
+ "doctype": "Report",
+ "filters": [],
+ "idx": 0,
+ "is_standard": "Yes",
+ "json": "{}",
+ "letter_head": "LetterHead",
+ "modified": "2023-06-27 16:40:32.493725",
+ "modified_by": "Administrator",
+ "module": "Accounts",
+ "name": "Voucher-wise Balance",
+ "owner": "Administrator",
+ "prepared_report": 0,
+ "ref_doctype": "GL Entry",
+ "report_name": "Voucher-wise Balance",
+ "report_type": "Script Report",
+ "roles": [
+  {
+   "role": "Accounts User"
+  },
+  {
+   "role": "Accounts Manager"
+  },
+  {
+   "role": "Auditor"
+  }
+ ]
+}
\ No newline at end of file
diff --git a/erpnext/accounts/report/voucher_wise_balance/voucher_wise_balance.py b/erpnext/accounts/report/voucher_wise_balance/voucher_wise_balance.py
new file mode 100644
index 0000000..5ab3611
--- /dev/null
+++ b/erpnext/accounts/report/voucher_wise_balance/voucher_wise_balance.py
@@ -0,0 +1,66 @@
+# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+import frappe
+from frappe import _
+from frappe.query_builder.functions import Sum
+
+
+def execute(filters=None):
+	columns = get_columns()
+	data = get_data(filters)
+	return columns, data
+
+
+def get_columns():
+	return [
+		{"label": _("Voucher Type"), "fieldname": "voucher_type", "width": 300},
+		{
+			"label": _("Voucher No"),
+			"fieldname": "voucher_no",
+			"fieldtype": "Dynamic Link",
+			"options": "voucher_type",
+			"width": 300,
+		},
+		{
+			"fieldname": "debit",
+			"label": _("Debit"),
+			"fieldtype": "Currency",
+			"options": "currency",
+			"width": 300,
+		},
+		{
+			"fieldname": "credit",
+			"label": _("Credit"),
+			"fieldtype": "Currency",
+			"options": "currency",
+			"width": 300,
+		},
+	]
+
+
+def get_data(filters):
+	gle = frappe.qb.DocType("GL Entry")
+	query = (
+		frappe.qb.from_(gle)
+		.select(
+			gle.voucher_type, gle.voucher_no, Sum(gle.debit).as_("debit"), Sum(gle.credit).as_("credit")
+		)
+		.groupby(gle.voucher_no)
+	)
+	query = apply_filters(query, filters, gle)
+	gl_entries = query.run(as_dict=True)
+	unmatched = [entry for entry in gl_entries if entry.debit != entry.credit]
+	return unmatched
+
+
+def apply_filters(query, filters, gle):
+	if filters.get("company"):
+		query = query.where(gle.company == filters.company)
+	if filters.get("voucher_type"):
+		query = query.where(gle.voucher_type == filters.voucher_type)
+	if filters.get("from_date"):
+		query = query.where(gle.posting_date >= filters.from_date)
+	if filters.get("to_date"):
+		query = query.where(gle.posting_date <= filters.to_date)
+	return query
diff --git a/erpnext/assets/doctype/asset_movement/asset_movement.js b/erpnext/assets/doctype/asset_movement/asset_movement.js
index 2df7db9..f9c6007 100644
--- a/erpnext/assets/doctype/asset_movement/asset_movement.js
+++ b/erpnext/assets/doctype/asset_movement/asset_movement.js
@@ -70,19 +70,21 @@
 		else if (frm.doc.purpose === 'Issue') {
 			fieldnames_to_be_altered = {
 				target_location: { read_only: 1, reqd: 0 },
-				source_location: { read_only: 1, reqd: 1 },
+				source_location: { read_only: 1, reqd: 0 },
 				from_employee: { read_only: 1, reqd: 0 },
 				to_employee: { read_only: 0, reqd: 1 }
 			};
 		}
-		Object.keys(fieldnames_to_be_altered).forEach(fieldname => {
-			let property_to_be_altered = fieldnames_to_be_altered[fieldname];
-			Object.keys(property_to_be_altered).forEach(property => {
-				let value = property_to_be_altered[property];
-				frm.set_df_property(fieldname, property, value, cdn, 'assets');
+		if (fieldnames_to_be_altered) {
+			Object.keys(fieldnames_to_be_altered).forEach(fieldname => {
+				let property_to_be_altered = fieldnames_to_be_altered[fieldname];
+				Object.keys(property_to_be_altered).forEach(property => {
+					let value = property_to_be_altered[property];
+					frm.fields_dict['assets'].grid.update_docfield_property(fieldname, property, value);
+				});
 			});
-		});
-		frm.refresh_field('assets');
+			frm.refresh_field('assets');
+		}
 	}
 });
 
diff --git a/erpnext/assets/doctype/asset_movement/asset_movement.json b/erpnext/assets/doctype/asset_movement/asset_movement.json
index bdce639..5382f9e 100644
--- a/erpnext/assets/doctype/asset_movement/asset_movement.json
+++ b/erpnext/assets/doctype/asset_movement/asset_movement.json
@@ -37,6 +37,7 @@
    "reqd": 1
   },
   {
+   "default": "Now",
    "fieldname": "transaction_date",
    "fieldtype": "Datetime",
    "in_list_view": 1,
@@ -95,10 +96,11 @@
  "index_web_pages_for_search": 1,
  "is_submittable": 1,
  "links": [],
- "modified": "2021-01-22 12:30:55.295670",
+ "modified": "2023-06-28 16:54:26.571083",
  "modified_by": "Administrator",
  "module": "Assets",
  "name": "Asset Movement",
+ "naming_rule": "Expression",
  "owner": "Administrator",
  "permissions": [
   {
@@ -148,5 +150,6 @@
   }
  ],
  "sort_field": "modified",
- "sort_order": "DESC"
+ "sort_order": "DESC",
+ "states": []
 }
\ No newline at end of file
diff --git a/erpnext/assets/doctype/asset_movement/asset_movement.py b/erpnext/assets/doctype/asset_movement/asset_movement.py
index 143f215..b58ca10 100644
--- a/erpnext/assets/doctype/asset_movement/asset_movement.py
+++ b/erpnext/assets/doctype/asset_movement/asset_movement.py
@@ -28,25 +28,20 @@
 	def validate_location(self):
 		for d in self.assets:
 			if self.purpose in ["Transfer", "Issue"]:
-				if not d.source_location:
-					d.source_location = frappe.db.get_value("Asset", d.asset, "location")
-
-				if not d.source_location:
-					frappe.throw(_("Source Location is required for the Asset {0}").format(d.asset))
-
+				current_location = frappe.db.get_value("Asset", d.asset, "location")
 				if d.source_location:
-					current_location = frappe.db.get_value("Asset", d.asset, "location")
-
 					if current_location != d.source_location:
 						frappe.throw(
 							_("Asset {0} does not belongs to the location {1}").format(d.asset, d.source_location)
 						)
+				else:
+					d.source_location = current_location
 
 			if self.purpose == "Issue":
 				if d.target_location:
 					frappe.throw(
 						_(
-							"Issuing cannot be done to a location. Please enter employee who has issued Asset {0}"
+							"Issuing cannot be done to a location. Please enter employee to issue the Asset {0} to"
 						).format(d.asset),
 						title=_("Incorrect Movement Purpose"),
 					)
@@ -107,12 +102,12 @@
 				)
 
 	def on_submit(self):
-		self.set_latest_location_in_asset()
+		self.set_latest_location_and_custodian_in_asset()
 
 	def on_cancel(self):
-		self.set_latest_location_in_asset()
+		self.set_latest_location_and_custodian_in_asset()
 
-	def set_latest_location_in_asset(self):
+	def set_latest_location_and_custodian_in_asset(self):
 		current_location, current_employee = "", ""
 		cond = "1=1"
 
diff --git a/erpnext/assets/doctype/asset_movement/test_asset_movement.py b/erpnext/assets/doctype/asset_movement/test_asset_movement.py
index 72c0575..27e7e55 100644
--- a/erpnext/assets/doctype/asset_movement/test_asset_movement.py
+++ b/erpnext/assets/doctype/asset_movement/test_asset_movement.py
@@ -47,7 +47,7 @@
 		if not frappe.db.exists("Location", "Test Location 2"):
 			frappe.get_doc({"doctype": "Location", "location_name": "Test Location 2"}).insert()
 
-		movement1 = create_asset_movement(
+		create_asset_movement(
 			purpose="Transfer",
 			company=asset.company,
 			assets=[
@@ -58,7 +58,7 @@
 		)
 		self.assertEqual(frappe.db.get_value("Asset", asset.name, "location"), "Test Location 2")
 
-		create_asset_movement(
+		movement1 = create_asset_movement(
 			purpose="Transfer",
 			company=asset.company,
 			assets=[
@@ -70,21 +70,32 @@
 		self.assertEqual(frappe.db.get_value("Asset", asset.name, "location"), "Test Location")
 
 		movement1.cancel()
-		self.assertEqual(frappe.db.get_value("Asset", asset.name, "location"), "Test Location")
+		self.assertEqual(frappe.db.get_value("Asset", asset.name, "location"), "Test Location 2")
 
 		employee = make_employee("testassetmovemp@example.com", company="_Test Company")
 		create_asset_movement(
 			purpose="Issue",
 			company=asset.company,
-			assets=[{"asset": asset.name, "source_location": "Test Location", "to_employee": employee}],
+			assets=[{"asset": asset.name, "source_location": "Test Location 2", "to_employee": employee}],
 			reference_doctype="Purchase Receipt",
 			reference_name=pr.name,
 		)
 
-		# after issuing asset should belong to an employee not at a location
+		# after issuing, asset should belong to an employee not at a location
 		self.assertEqual(frappe.db.get_value("Asset", asset.name, "location"), None)
 		self.assertEqual(frappe.db.get_value("Asset", asset.name, "custodian"), employee)
 
+		create_asset_movement(
+			purpose="Receipt",
+			company=asset.company,
+			assets=[{"asset": asset.name, "from_employee": employee, "target_location": "Test Location"}],
+			reference_doctype="Purchase Receipt",
+			reference_name=pr.name,
+		)
+
+		# after receiving, asset should belong to a location not at an employee
+		self.assertEqual(frappe.db.get_value("Asset", asset.name, "location"), "Test Location")
+
 	def test_last_movement_cancellation(self):
 		pr = make_purchase_receipt(
 			item_code="Macbook Pro", qty=1, rate=100000.0, location="Test Location"
diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.js b/erpnext/buying/doctype/purchase_order/purchase_order.js
index c6c9f1f..8fa8f30 100644
--- a/erpnext/buying/doctype/purchase_order/purchase_order.js
+++ b/erpnext/buying/doctype/purchase_order/purchase_order.js
@@ -286,7 +286,7 @@
 			source_name: this.frm.doc.supplier,
 			target: this.frm,
 			setters: {
-				company: me.frm.doc.company
+				company: this.frm.doc.company
 			},
 			get_query_filters: {
 				docstatus: ["!=", 2],
diff --git a/erpnext/buying/doctype/supplier/patches/__init__.py b/erpnext/buying/doctype/supplier/patches/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/buying/doctype/supplier/patches/__init__.py
diff --git a/erpnext/buying/doctype/supplier/patches/migrate_supplier_portal_users.py b/erpnext/buying/doctype/supplier/patches/migrate_supplier_portal_users.py
new file mode 100644
index 0000000..5834952
--- /dev/null
+++ b/erpnext/buying/doctype/supplier/patches/migrate_supplier_portal_users.py
@@ -0,0 +1,56 @@
+import os
+
+import frappe
+
+in_ci = os.environ.get("CI")
+
+
+def execute():
+	try:
+		contacts = get_portal_user_contacts()
+		add_portal_users(contacts)
+	except Exception:
+		frappe.db.rollback()
+		frappe.log_error("Failed to migrate portal users")
+
+		if in_ci:  # TODO: better way to handle this.
+			raise
+
+
+def get_portal_user_contacts():
+	contact = frappe.qb.DocType("Contact")
+	dynamic_link = frappe.qb.DocType("Dynamic Link")
+
+	return (
+		frappe.qb.from_(contact)
+		.inner_join(dynamic_link)
+		.on(contact.name == dynamic_link.parent)
+		.select(
+			(dynamic_link.link_doctype).as_("doctype"),
+			(dynamic_link.link_name).as_("parent"),
+			(contact.email_id).as_("portal_user"),
+		)
+		.where(
+			(dynamic_link.parenttype == "Contact")
+			& (dynamic_link.link_doctype.isin(["Supplier", "Customer"]))
+		)
+	).run(as_dict=True)
+
+
+def add_portal_users(contacts):
+	for contact in contacts:
+		user = frappe.db.get_value("User", {"email": contact.portal_user}, "name")
+		if not user:
+			continue
+
+		roles = frappe.get_roles(user)
+		required_role = contact.doctype
+		if required_role not in roles:
+			continue
+
+		portal_user_doc = frappe.new_doc("Portal User")
+		portal_user_doc.parenttype = contact.doctype
+		portal_user_doc.parentfield = "portal_users"
+		portal_user_doc.parent = contact.parent
+		portal_user_doc.user = user
+		portal_user_doc.insert()
diff --git a/erpnext/buying/doctype/supplier/supplier.js b/erpnext/buying/doctype/supplier/supplier.js
index 1ae6f03..a536578 100644
--- a/erpnext/buying/doctype/supplier/supplier.js
+++ b/erpnext/buying/doctype/supplier/supplier.js
@@ -42,6 +42,14 @@
 				}
 			};
 		});
+
+		frm.set_query("user", "portal_users", function(doc) {
+			return {
+				filters: {
+					"ignore_user_type": true,
+				}
+			};
+		});
 	},
 
 	refresh: function (frm) {
diff --git a/erpnext/buying/doctype/supplier/supplier.json b/erpnext/buying/doctype/supplier/supplier.json
index f009789..b3b6185 100644
--- a/erpnext/buying/doctype/supplier/supplier.json
+++ b/erpnext/buying/doctype/supplier/supplier.json
@@ -68,7 +68,10 @@
   "on_hold",
   "hold_type",
   "column_break_59",
-  "release_date"
+  "release_date",
+  "portal_users_tab",
+  "portal_users",
+  "column_break_1mqv"
  ],
  "fields": [
   {
@@ -445,6 +448,21 @@
   {
    "fieldname": "column_break_59",
    "fieldtype": "Column Break"
+  },
+  {
+   "fieldname": "portal_users_tab",
+   "fieldtype": "Tab Break",
+   "label": "Portal Users"
+  },
+  {
+   "fieldname": "portal_users",
+   "fieldtype": "Table",
+   "label": "Supplier Portal Users",
+   "options": "Portal User"
+  },
+  {
+   "fieldname": "column_break_1mqv",
+   "fieldtype": "Column Break"
   }
  ],
  "icon": "fa fa-user",
@@ -457,7 +475,7 @@
    "link_fieldname": "party"
   }
  ],
- "modified": "2023-05-09 15:34:13.408932",
+ "modified": "2023-06-26 14:20:00.961554",
  "modified_by": "Administrator",
  "module": "Buying",
  "name": "Supplier",
@@ -489,7 +507,6 @@
    "read": 1,
    "report": 1,
    "role": "Purchase Master Manager",
-   "set_user_permissions": 1,
    "share": 1,
    "write": 1
   },
diff --git a/erpnext/buying/doctype/supplier/supplier.py b/erpnext/buying/doctype/supplier/supplier.py
index 01b5c8f..31bf439 100644
--- a/erpnext/buying/doctype/supplier/supplier.py
+++ b/erpnext/buying/doctype/supplier/supplier.py
@@ -16,6 +16,7 @@
 	get_timeline_data,
 	validate_party_accounts,
 )
+from erpnext.controllers.website_list_for_contact import add_role_for_portal_user
 from erpnext.utilities.transaction_base import TransactionBase
 
 
@@ -46,12 +47,35 @@
 			self.name = set_name_from_naming_options(frappe.get_meta(self.doctype).autoname, self)
 
 	def on_update(self):
-		if not self.naming_series:
-			self.naming_series = ""
-
 		self.create_primary_contact()
 		self.create_primary_address()
 
+	def add_role_for_user(self):
+		for portal_user in self.portal_users:
+			add_role_for_portal_user(portal_user, "Supplier")
+
+	def _add_supplier_role(self, portal_user):
+		if not portal_user.is_new():
+			return
+
+		user_doc = frappe.get_doc("User", portal_user.user)
+		roles = {r.role for r in user_doc.roles}
+
+		if "Supplier" in roles:
+			return
+
+		if "System Manager" not in frappe.get_roles():
+			frappe.msgprint(
+				_("Please add 'Supplier' role to user {0}.").format(portal_user.user),
+				alert=True,
+			)
+			return
+
+		user_doc.add_roles("Supplier")
+		frappe.msgprint(
+			_("Added Supplier Role to User {0}.").format(frappe.bold(user_doc.name)), alert=True
+		)
+
 	def validate(self):
 		self.flags.is_new_doc = self.is_new()
 
@@ -62,6 +86,7 @@
 
 		validate_party_accounts(self)
 		self.validate_internal_supplier()
+		self.add_role_for_user()
 
 	@frappe.whitelist()
 	def get_supplier_group_details(self):
diff --git a/erpnext/buying/doctype/supplier/test_supplier.py b/erpnext/buying/doctype/supplier/test_supplier.py
index 7a205ac..7be1d83 100644
--- a/erpnext/buying/doctype/supplier/test_supplier.py
+++ b/erpnext/buying/doctype/supplier/test_supplier.py
@@ -7,6 +7,7 @@
 from frappe.test_runner import make_test_records
 
 from erpnext.accounts.party import get_due_date
+from erpnext.controllers.website_list_for_contact import get_customers_suppliers
 from erpnext.exceptions import PartyDisabled
 
 test_dependencies = ["Payment Term", "Payment Terms Template"]
@@ -195,6 +196,9 @@
 def create_supplier(**args):
 	args = frappe._dict(args)
 
+	if not args.supplier_name:
+		args.supplier_name = frappe.generate_hash()
+
 	if frappe.db.exists("Supplier", args.supplier_name):
 		return frappe.get_doc("Supplier", args.supplier_name)
 
@@ -209,3 +213,25 @@
 	).insert()
 
 	return doc
+
+
+class TestSupplierPortal(FrappeTestCase):
+	def test_portal_user_can_access_supplier_data(self):
+
+		supplier = create_supplier()
+
+		user = frappe.generate_hash() + "@example.com"
+		frappe.new_doc(
+			"User",
+			first_name="Supplier Portal User",
+			email=user,
+			send_welcome_email=False,
+		).insert()
+
+		supplier.append("portal_users", {"user": user})
+		supplier.save()
+
+		frappe.set_user(user)
+		_, suppliers = get_customers_suppliers("Purchase Order", user)
+
+		self.assertIn(supplier.name, suppliers)
diff --git a/erpnext/controllers/print_settings.py b/erpnext/controllers/print_settings.py
index c951154..d86607d 100644
--- a/erpnext/controllers/print_settings.py
+++ b/erpnext/controllers/print_settings.py
@@ -10,6 +10,7 @@
 	doc.child_print_templates = {
 		"items": {
 			"qty": "templates/print_formats/includes/item_table_qty.html",
+			"serial_and_batch_bundle": "templates/print_formats/includes/serial_and_batch_bundle.html",
 		}
 	}
 
diff --git a/erpnext/controllers/subcontracting_controller.py b/erpnext/controllers/subcontracting_controller.py
index 40dcd0c..57339bf 100644
--- a/erpnext/controllers/subcontracting_controller.py
+++ b/erpnext/controllers/subcontracting_controller.py
@@ -173,50 +173,52 @@
 				self.qty_to_be_received[(row.item_code, row.parent)] += row.qty
 
 	def __get_transferred_items(self):
-		fields = [
-			f"`tabStock Entry`.`{self.subcontract_data.order_field}`",
-			"`tabStock Entry`.`name` as voucher_no",
-		]
+		se = frappe.qb.DocType("Stock Entry")
+		se_detail = frappe.qb.DocType("Stock Entry Detail")
 
-		alias_dict = {
-			"item_code": "rm_item_code",
-			"subcontracted_item": "main_item_code",
-			"basic_rate": "rate",
-		}
-
-		child_table_fields = [
-			"item_code",
-			"item_name",
-			"description",
-			"qty",
-			"basic_rate",
-			"amount",
-			"serial_no",
-			"serial_and_batch_bundle",
-			"uom",
-			"subcontracted_item",
-			"stock_uom",
-			"batch_no",
-			"conversion_factor",
-			"s_warehouse",
-			"t_warehouse",
-			"item_group",
-			self.subcontract_data.rm_detail_field,
-		]
+		query = (
+			frappe.qb.from_(se)
+			.inner_join(se_detail)
+			.on(se.name == se_detail.parent)
+			.select(
+				se[self.subcontract_data.order_field],
+				se.name.as_("voucher_no"),
+				se_detail.item_code.as_("rm_item_code"),
+				se_detail.item_name,
+				se_detail.description,
+				(
+					frappe.qb.terms.Case()
+					.when(((se.purpose == "Material Transfer") & (se.is_return == 1)), -1 * se_detail.qty)
+					.else_(se_detail.qty)
+				).as_("qty"),
+				se_detail.basic_rate.as_("rate"),
+				se_detail.amount,
+				se_detail.serial_no,
+				se_detail.serial_and_batch_bundle,
+				se_detail.uom,
+				se_detail.subcontracted_item.as_("main_item_code"),
+				se_detail.stock_uom,
+				se_detail.batch_no,
+				se_detail.conversion_factor,
+				se_detail.s_warehouse,
+				se_detail.t_warehouse,
+				se_detail.item_group,
+				se_detail[self.subcontract_data.rm_detail_field],
+			)
+			.where(
+				(se.docstatus == 1)
+				& (se[self.subcontract_data.order_field].isin(self.subcontract_orders))
+				& (
+					(se.purpose == "Send to Subcontractor")
+					| ((se.purpose == "Material Transfer") & (se.is_return == 1))
+				)
+			)
+		)
 
 		if self.backflush_based_on == "BOM":
-			child_table_fields.append("original_item")
+			query = query.select(se_detail.original_item)
 
-		for field in child_table_fields:
-			fields.append(f"`tabStock Entry Detail`.`{field}` As {alias_dict.get(field, field)}")
-
-		filters = [
-			["Stock Entry", "docstatus", "=", 1],
-			["Stock Entry", "purpose", "=", "Send to Subcontractor"],
-			["Stock Entry", self.subcontract_data.order_field, "in", self.subcontract_orders],
-		]
-
-		return frappe.get_all("Stock Entry", fields=fields, filters=filters)
+		return query.run(as_dict=True)
 
 	def __set_alternative_item_details(self, row):
 		if row.get("original_item"):
diff --git a/erpnext/controllers/website_list_for_contact.py b/erpnext/controllers/website_list_for_contact.py
index 7c3c387..642722a 100644
--- a/erpnext/controllers/website_list_for_contact.py
+++ b/erpnext/controllers/website_list_for_contact.py
@@ -232,22 +232,8 @@
 	has_supplier_field = meta.has_field("supplier")
 
 	if has_common(["Supplier", "Customer"], frappe.get_roles(user)):
-		contacts = frappe.db.sql(
-			"""
-			select
-				`tabContact`.email_id,
-				`tabDynamic Link`.link_doctype,
-				`tabDynamic Link`.link_name
-			from
-				`tabContact`, `tabDynamic Link`
-			where
-				`tabContact`.name=`tabDynamic Link`.parent and `tabContact`.email_id =%s
-			""",
-			user,
-			as_dict=1,
-		)
-		customers = [c.link_name for c in contacts if c.link_doctype == "Customer"]
-		suppliers = [c.link_name for c in contacts if c.link_doctype == "Supplier"]
+		suppliers = get_parents_for_user("Supplier")
+		customers = get_parents_for_user("Customer")
 	elif frappe.has_permission(doctype, "read", user=user):
 		customer_list = frappe.get_list("Customer")
 		customers = suppliers = [customer.name for customer in customer_list]
@@ -255,6 +241,17 @@
 	return customers if has_customer_field else None, suppliers if has_supplier_field else None
 
 
+def get_parents_for_user(parenttype: str) -> list[str]:
+	portal_user = frappe.qb.DocType("Portal User")
+
+	return (
+		frappe.qb.from_(portal_user)
+		.select(portal_user.parent)
+		.where(portal_user.user == frappe.session.user)
+		.where(portal_user.parenttype == parenttype)
+	).run(pluck="name")
+
+
 def has_website_permission(doc, ptype, user, verbose=False):
 	doctype = doc.doctype
 	customers, suppliers = get_customers_suppliers(doctype, user)
@@ -282,3 +279,28 @@
 		return "party_name"
 	else:
 		return "customer"
+
+
+def add_role_for_portal_user(portal_user, role):
+	"""When a new portal user is added, give appropriate roles to user if
+	posssible, else warn user to add roles."""
+	if not portal_user.is_new():
+		return
+
+	user_doc = frappe.get_doc("User", portal_user.user)
+	roles = {r.role for r in user_doc.roles}
+
+	if role in roles:
+		return
+
+	if "System Manager" not in frappe.get_roles():
+		frappe.msgprint(
+			_("Please add {1} role to user {0}.").format(portal_user.user, role),
+			alert=True,
+		)
+		return
+
+	user_doc.add_roles(role)
+	frappe.msgprint(
+		_("Added {1} Role to User {0}.").format(frappe.bold(user_doc.name), role), alert=True
+	)
diff --git a/erpnext/crm/doctype/social_media_post/social_media_post.py b/erpnext/crm/doctype/social_media_post/social_media_post.py
index 55db29a..3654d29 100644
--- a/erpnext/crm/doctype/social_media_post/social_media_post.py
+++ b/erpnext/crm/doctype/social_media_post/social_media_post.py
@@ -74,7 +74,7 @@
 
 
 def process_scheduled_social_media_posts():
-	posts = frappe.get_list(
+	posts = frappe.get_all(
 		"Social Media Post",
 		filters={"post_status": "Scheduled", "docstatus": 1},
 		fields=["name", "scheduled_time"],
diff --git a/erpnext/manufacturing/report/exponential_smoothing_forecasting/exponential_smoothing_forecasting.js b/erpnext/manufacturing/report/exponential_smoothing_forecasting/exponential_smoothing_forecasting.js
index 123a82a..a3f0d00 100644
--- a/erpnext/manufacturing/report/exponential_smoothing_forecasting/exponential_smoothing_forecasting.js
+++ b/erpnext/manufacturing/report/exponential_smoothing_forecasting/exponential_smoothing_forecasting.js
@@ -30,7 +30,7 @@
 			"fieldname":"based_on_document",
 			"label": __("Based On Document"),
 			"fieldtype": "Select",
-			"options": ["Sales Order", "Delivery Note", "Quotation"],
+			"options": ["Sales Order", "Sales Invoice", "Delivery Note", "Quotation"],
 			"default": "Sales Order",
 			"reqd": 1
 		},
diff --git a/erpnext/manufacturing/report/exponential_smoothing_forecasting/exponential_smoothing_forecasting.py b/erpnext/manufacturing/report/exponential_smoothing_forecasting/exponential_smoothing_forecasting.py
index d3bce83..daef7f6 100644
--- a/erpnext/manufacturing/report/exponential_smoothing_forecasting/exponential_smoothing_forecasting.py
+++ b/erpnext/manufacturing/report/exponential_smoothing_forecasting/exponential_smoothing_forecasting.py
@@ -99,7 +99,9 @@
 		parent = frappe.qb.DocType(self.doctype)
 		child = frappe.qb.DocType(self.child_doctype)
 
-		date_field = "posting_date" if self.doctype == "Delivery Note" else "transaction_date"
+		date_field = (
+			"posting_date" if self.doctype in ("Delivery Note", "Sales Invoice") else "transaction_date"
+		)
 
 		query = (
 			frappe.qb.from_(parent)
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index 18bd10f..fe6346e 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -339,3 +339,5 @@
 execute:frappe.delete_doc('DocType', 'Cash Flow Mapping Template', ignore_missing=True)
 execute:frappe.delete_doc('DocType', 'Cash Flow Mapping Accounts', ignore_missing=True)
 erpnext.patches.v14_0.cleanup_workspaces
+erpnext.patches.v14_0.set_report_in_process_SOA
+erpnext.buying.doctype.supplier.patches.migrate_supplier_portal_users
diff --git a/erpnext/patches/v14_0/set_report_in_process_SOA.py b/erpnext/patches/v14_0/set_report_in_process_SOA.py
new file mode 100644
index 0000000..9eb5e3a
--- /dev/null
+++ b/erpnext/patches/v14_0/set_report_in_process_SOA.py
@@ -0,0 +1,10 @@
+# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
+# License: MIT. See LICENSE
+
+import frappe
+
+
+def execute():
+	process_soa = frappe.qb.DocType("Process Statement Of Accounts")
+	q = frappe.qb.update(process_soa).set(process_soa.report, "General Ledger")
+	q.run()
diff --git a/erpnext/patches/v15_0/create_asset_depreciation_schedules_from_assets.py b/erpnext/patches/v15_0/create_asset_depreciation_schedules_from_assets.py
index 5c46bf3..a53adf1 100644
--- a/erpnext/patches/v15_0/create_asset_depreciation_schedules_from_assets.py
+++ b/erpnext/patches/v15_0/create_asset_depreciation_schedules_from_assets.py
@@ -6,10 +6,14 @@
 
 	assets = get_details_of_draft_or_submitted_depreciable_assets()
 
-	for asset in assets:
-		finance_book_rows = get_details_of_asset_finance_books_rows(asset.name)
+	asset_finance_books_map = get_asset_finance_books_map()
 
-		for fb_row in finance_book_rows:
+	asset_depreciation_schedules_map = get_asset_depreciation_schedules_map()
+
+	for asset in assets:
+		depreciation_schedules = asset_depreciation_schedules_map[asset.name]
+
+		for fb_row in asset_finance_books_map[asset.name]:
 			asset_depr_schedule_doc = frappe.new_doc("Asset Depreciation Schedule")
 
 			asset_depr_schedule_doc.set_draft_asset_depr_schedule_details(asset, fb_row)
@@ -19,7 +23,11 @@
 			if asset.docstatus == 1:
 				asset_depr_schedule_doc.submit()
 
-			update_depreciation_schedules(asset.name, asset_depr_schedule_doc.name, fb_row.idx)
+			depreciation_schedules_of_fb_row = [
+				ds for ds in depreciation_schedules if ds["finance_book_id"] == str(fb_row.idx)
+			]
+
+			update_depreciation_schedules(depreciation_schedules_of_fb_row, asset_depr_schedule_doc.name)
 
 
 def get_details_of_draft_or_submitted_depreciable_assets():
@@ -41,12 +49,33 @@
 	return records
 
 
-def get_details_of_asset_finance_books_rows(asset_name):
+def group_records_by_asset_name(records):
+	grouped_dict = {}
+
+	for item in records:
+		key = list(item.keys())[0]
+		value = item[key]
+
+		if value not in grouped_dict:
+			grouped_dict[value] = []
+
+		del item["asset_name"]
+
+		grouped_dict[value].append(item)
+
+	return grouped_dict
+
+
+def get_asset_finance_books_map():
 	afb = frappe.qb.DocType("Asset Finance Book")
+	asset = frappe.qb.DocType("Asset")
 
 	records = (
 		frappe.qb.from_(afb)
+		.join(asset)
+		.on(afb.parent == asset.name)
 		.select(
+			asset.name.as_("asset_name"),
 			afb.finance_book,
 			afb.idx,
 			afb.depreciation_method,
@@ -55,23 +84,44 @@
 			afb.rate_of_depreciation,
 			afb.expected_value_after_useful_life,
 		)
-		.where(afb.parent == asset_name)
+		.where(asset.docstatus < 2)
+		.orderby(afb.idx)
 	).run(as_dict=True)
 
-	return records
+	asset_finance_books_map = group_records_by_asset_name(records)
+
+	return asset_finance_books_map
 
 
-def update_depreciation_schedules(asset_name, asset_depr_schedule_name, fb_row_idx):
+def get_asset_depreciation_schedules_map():
 	ds = frappe.qb.DocType("Depreciation Schedule")
+	asset = frappe.qb.DocType("Asset")
 
-	depr_schedules = (
+	records = (
 		frappe.qb.from_(ds)
-		.select(ds.name)
-		.where((ds.parent == asset_name) & (ds.finance_book_id == str(fb_row_idx)))
+		.join(asset)
+		.on(ds.parent == asset.name)
+		.select(
+			asset.name.as_("asset_name"),
+			ds.name,
+			ds.finance_book_id,
+		)
+		.where(asset.docstatus < 2)
 		.orderby(ds.idx)
 	).run(as_dict=True)
 
-	for idx, depr_schedule in enumerate(depr_schedules, start=1):
+	asset_depreciation_schedules_map = group_records_by_asset_name(records)
+
+	return asset_depreciation_schedules_map
+
+
+def update_depreciation_schedules(
+	depreciation_schedules,
+	asset_depr_schedule_name,
+):
+	ds = frappe.qb.DocType("Depreciation Schedule")
+
+	for idx, depr_schedule in enumerate(depreciation_schedules, start=1):
 		(
 			frappe.qb.update(ds)
 			.set(ds.idx, idx)
diff --git a/erpnext/projects/doctype/project/project.json b/erpnext/projects/doctype/project/project.json
index 2b3392a..f007430 100644
--- a/erpnext/projects/doctype/project/project.json
+++ b/erpnext/projects/doctype/project/project.json
@@ -364,7 +364,8 @@
    "default": "0",
    "fieldname": "collect_progress",
    "fieldtype": "Check",
-   "label": "Collect Progress"
+   "label": "Collect Progress",
+   "search_index": 1
   },
   {
    "depends_on": "collect_progress",
@@ -451,7 +452,7 @@
  "index_web_pages_for_search": 1,
  "links": [],
  "max_attachments": 4,
- "modified": "2023-04-17 21:11:11.346986",
+ "modified": "2023-06-28 18:57:11.603497",
  "modified_by": "Administrator",
  "module": "Projects",
  "name": "Project",
diff --git a/erpnext/projects/doctype/project_update/project_update.json b/erpnext/projects/doctype/project_update/project_update.json
index 497b2b7..c548111 100644
--- a/erpnext/projects/doctype/project_update/project_update.json
+++ b/erpnext/projects/doctype/project_update/project_update.json
@@ -1,355 +1,106 @@
 {
- "allow_copy": 0, 
- "allow_events_in_timeline": 0, 
- "allow_guest_to_view": 0, 
- "allow_import": 0, 
- "allow_rename": 0, 
- "autoname": "naming_series:", 
- "beta": 0, 
- "creation": "2018-01-18 09:44:47.565494", 
- "custom": 0, 
- "docstatus": 0, 
- "doctype": "DocType", 
- "document_type": "", 
- "editable_grid": 1, 
- "engine": "InnoDB", 
+ "actions": [],
+ "autoname": "naming_series:",
+ "creation": "2018-01-18 09:44:47.565494",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+  "naming_series",
+  "project",
+  "sent",
+  "column_break_2",
+  "date",
+  "time",
+  "section_break_5",
+  "users",
+  "amended_from"
+ ],
  "fields": [
   {
-   "allow_bulk_edit": 0, 
-   "allow_in_quick_entry": 0, 
-   "allow_on_submit": 0, 
-   "bold": 0, 
-   "collapsible": 0, 
-   "columns": 0, 
-   "default": "", 
-   "fieldname": "naming_series", 
-   "fieldtype": "Data", 
-   "hidden": 1, 
-   "ignore_user_permissions": 0, 
-   "ignore_xss_filter": 0, 
-   "in_filter": 0, 
-   "in_global_search": 0, 
-   "in_list_view": 0, 
-   "in_standard_filter": 0, 
-   "label": "Series", 
-   "length": 0, 
-   "no_copy": 0, 
-   "options": "PROJ-UPD-.YYYY.-", 
-   "permlevel": 0, 
-   "precision": "", 
-   "print_hide": 0, 
-   "print_hide_if_no_value": 0, 
-   "read_only": 0, 
-   "remember_last_selected_value": 0, 
-   "report_hide": 0, 
-   "reqd": 0, 
-   "search_index": 0, 
-   "set_only_once": 0, 
-   "translatable": 0, 
-   "unique": 0
-  }, 
+   "fieldname": "naming_series",
+   "fieldtype": "Data",
+   "hidden": 1,
+   "label": "Series",
+   "options": "PROJ-UPD-.YYYY.-"
+  },
   {
-   "allow_bulk_edit": 0, 
-   "allow_in_quick_entry": 0, 
-   "allow_on_submit": 0, 
-   "bold": 0, 
-   "collapsible": 0, 
-   "columns": 0, 
-   "fieldname": "project", 
-   "fieldtype": "Link", 
-   "hidden": 0, 
-   "ignore_user_permissions": 0, 
-   "ignore_xss_filter": 0, 
-   "in_filter": 0, 
-   "in_global_search": 0, 
-   "in_list_view": 1, 
-   "in_standard_filter": 0, 
-   "label": "Project", 
-   "length": 0, 
-   "no_copy": 0, 
-   "options": "Project", 
-   "permlevel": 0, 
-   "precision": "", 
-   "print_hide": 0, 
-   "print_hide_if_no_value": 0, 
-   "read_only": 0, 
-   "remember_last_selected_value": 0, 
-   "report_hide": 0, 
-   "reqd": 1, 
-   "search_index": 0, 
-   "set_only_once": 0, 
-   "translatable": 0, 
-   "unique": 0
-  }, 
+   "fieldname": "project",
+   "fieldtype": "Link",
+   "in_list_view": 1,
+   "label": "Project",
+   "options": "Project",
+   "reqd": 1
+  },
   {
-   "allow_bulk_edit": 0, 
-   "allow_in_quick_entry": 0, 
-   "allow_on_submit": 0, 
-   "bold": 0, 
-   "collapsible": 0, 
-   "columns": 0, 
-   "default": "0", 
-   "fieldname": "sent", 
-   "fieldtype": "Check", 
-   "hidden": 0, 
-   "ignore_user_permissions": 0, 
-   "ignore_xss_filter": 0, 
-   "in_filter": 0, 
-   "in_global_search": 0, 
-   "in_list_view": 0, 
-   "in_standard_filter": 0, 
-   "label": "Sent", 
-   "length": 0, 
-   "no_copy": 0, 
-   "permlevel": 0, 
-   "precision": "", 
-   "print_hide": 0, 
-   "print_hide_if_no_value": 0, 
-   "read_only": 1, 
-   "remember_last_selected_value": 0, 
-   "report_hide": 0, 
-   "reqd": 0, 
-   "search_index": 0, 
-   "set_only_once": 0, 
-   "translatable": 0, 
-   "unique": 0
-  }, 
+   "default": "0",
+   "fieldname": "sent",
+   "fieldtype": "Check",
+   "label": "Sent",
+   "read_only": 1
+  },
   {
-   "allow_bulk_edit": 0, 
-   "allow_in_quick_entry": 0, 
-   "allow_on_submit": 0, 
-   "bold": 0, 
-   "collapsible": 0, 
-   "columns": 0, 
-   "fieldname": "column_break_2", 
-   "fieldtype": "Column Break", 
-   "hidden": 0, 
-   "ignore_user_permissions": 0, 
-   "ignore_xss_filter": 0, 
-   "in_filter": 0, 
-   "in_global_search": 0, 
-   "in_list_view": 0, 
-   "in_standard_filter": 0, 
-   "length": 0, 
-   "no_copy": 0, 
-   "permlevel": 0, 
-   "precision": "", 
-   "print_hide": 0, 
-   "print_hide_if_no_value": 0, 
-   "read_only": 0, 
-   "remember_last_selected_value": 0, 
-   "report_hide": 0, 
-   "reqd": 0, 
-   "search_index": 0, 
-   "set_only_once": 0, 
-   "translatable": 0, 
-   "unique": 0
-  }, 
+   "fieldname": "column_break_2",
+   "fieldtype": "Column Break"
+  },
   {
-   "allow_bulk_edit": 0, 
-   "allow_in_quick_entry": 0, 
-   "allow_on_submit": 0, 
-   "bold": 0, 
-   "collapsible": 0, 
-   "columns": 0, 
-   "fieldname": "date", 
-   "fieldtype": "Date", 
-   "hidden": 0, 
-   "ignore_user_permissions": 0, 
-   "ignore_xss_filter": 0, 
-   "in_filter": 0, 
-   "in_global_search": 0, 
-   "in_list_view": 0, 
-   "in_standard_filter": 0, 
-   "label": "Date", 
-   "length": 0, 
-   "no_copy": 0, 
-   "permlevel": 0, 
-   "precision": "", 
-   "print_hide": 0, 
-   "print_hide_if_no_value": 0, 
-   "read_only": 1, 
-   "remember_last_selected_value": 0, 
-   "report_hide": 0, 
-   "reqd": 0, 
-   "search_index": 0, 
-   "set_only_once": 0, 
-   "translatable": 0, 
-   "unique": 0
-  }, 
+   "fieldname": "date",
+   "fieldtype": "Date",
+   "label": "Date",
+   "read_only": 1,
+   "search_index": 1
+  },
   {
-   "allow_bulk_edit": 0, 
-   "allow_in_quick_entry": 0, 
-   "allow_on_submit": 0, 
-   "bold": 0, 
-   "collapsible": 0, 
-   "columns": 0, 
-   "fieldname": "time", 
-   "fieldtype": "Time", 
-   "hidden": 0, 
-   "ignore_user_permissions": 0, 
-   "ignore_xss_filter": 0, 
-   "in_filter": 0, 
-   "in_global_search": 0, 
-   "in_list_view": 0, 
-   "in_standard_filter": 0, 
-   "label": "Time", 
-   "length": 0, 
-   "no_copy": 0, 
-   "permlevel": 0, 
-   "precision": "", 
-   "print_hide": 0, 
-   "print_hide_if_no_value": 0, 
-   "read_only": 1, 
-   "remember_last_selected_value": 0, 
-   "report_hide": 0, 
-   "reqd": 0, 
-   "search_index": 0, 
-   "set_only_once": 0, 
-   "translatable": 0, 
-   "unique": 0
-  }, 
+   "fieldname": "time",
+   "fieldtype": "Time",
+   "label": "Time",
+   "read_only": 1
+  },
   {
-   "allow_bulk_edit": 0, 
-   "allow_in_quick_entry": 0, 
-   "allow_on_submit": 0, 
-   "bold": 0, 
-   "collapsible": 0, 
-   "columns": 0, 
-   "fieldname": "section_break_5", 
-   "fieldtype": "Section Break", 
-   "hidden": 0, 
-   "ignore_user_permissions": 0, 
-   "ignore_xss_filter": 0, 
-   "in_filter": 0, 
-   "in_global_search": 0, 
-   "in_list_view": 0, 
-   "in_standard_filter": 0, 
-   "length": 0, 
-   "no_copy": 0, 
-   "permlevel": 0, 
-   "precision": "", 
-   "print_hide": 0, 
-   "print_hide_if_no_value": 0, 
-   "read_only": 0, 
-   "remember_last_selected_value": 0, 
-   "report_hide": 0, 
-   "reqd": 0, 
-   "search_index": 0, 
-   "set_only_once": 0, 
-   "translatable": 0, 
-   "unique": 0
-  }, 
+   "fieldname": "section_break_5",
+   "fieldtype": "Section Break"
+  },
   {
-   "allow_bulk_edit": 0, 
-   "allow_in_quick_entry": 0, 
-   "allow_on_submit": 0, 
-   "bold": 0, 
-   "collapsible": 0, 
-   "columns": 0, 
-   "fieldname": "users", 
-   "fieldtype": "Table", 
-   "hidden": 0, 
-   "ignore_user_permissions": 0, 
-   "ignore_xss_filter": 0, 
-   "in_filter": 0, 
-   "in_global_search": 0, 
-   "in_list_view": 0, 
-   "in_standard_filter": 0, 
-   "label": "Users", 
-   "length": 0, 
-   "no_copy": 0, 
-   "options": "Project User", 
-   "permlevel": 0, 
-   "precision": "", 
-   "print_hide": 0, 
-   "print_hide_if_no_value": 0, 
-   "read_only": 0, 
-   "remember_last_selected_value": 0, 
-   "report_hide": 0, 
-   "reqd": 0, 
-   "search_index": 0, 
-   "set_only_once": 0, 
-   "translatable": 0, 
-   "unique": 0
-  }, 
+   "fieldname": "users",
+   "fieldtype": "Table",
+   "label": "Users",
+   "options": "Project User"
+  },
   {
-   "allow_bulk_edit": 0, 
-   "allow_in_quick_entry": 0, 
-   "allow_on_submit": 0, 
-   "bold": 0, 
-   "collapsible": 0, 
-   "columns": 0, 
-   "fieldname": "amended_from", 
-   "fieldtype": "Link", 
-   "hidden": 0, 
-   "ignore_user_permissions": 0, 
-   "ignore_xss_filter": 0, 
-   "in_filter": 0, 
-   "in_global_search": 0, 
-   "in_list_view": 0, 
-   "in_standard_filter": 0, 
-   "label": "Amended From", 
-   "length": 0, 
-   "no_copy": 1, 
-   "options": "Project Update", 
-   "permlevel": 0, 
-   "print_hide": 1, 
-   "print_hide_if_no_value": 0, 
-   "read_only": 1, 
-   "remember_last_selected_value": 0, 
-   "report_hide": 0, 
-   "reqd": 0, 
-   "search_index": 0, 
-   "set_only_once": 0, 
-   "translatable": 0, 
-   "unique": 0
+   "fieldname": "amended_from",
+   "fieldtype": "Link",
+   "label": "Amended From",
+   "no_copy": 1,
+   "options": "Project Update",
+   "print_hide": 1,
+   "read_only": 1
   }
- ], 
- "has_web_view": 0, 
- "hide_heading": 0, 
- "hide_toolbar": 0, 
- "idx": 0, 
- "image_view": 0, 
- "in_create": 0, 
- "is_submittable": 1, 
- "issingle": 0, 
- "istable": 0, 
- "max_attachments": 0, 
- "modified": "2019-01-16 19:31:05.210656", 
- "modified_by": "Administrator", 
- "module": "Projects", 
- "name": "Project Update", 
- "name_case": "", 
- "owner": "Administrator", 
+ ],
+ "is_submittable": 1,
+ "links": [],
+ "modified": "2023-06-28 18:59:50.678917",
+ "modified_by": "Administrator",
+ "module": "Projects",
+ "name": "Project Update",
+ "naming_rule": "By \"Naming Series\" field",
+ "owner": "Administrator",
  "permissions": [
   {
-   "amend": 0, 
-   "cancel": 0, 
-   "create": 1, 
-   "delete": 1, 
-   "email": 1, 
-   "export": 1, 
-   "if_owner": 0, 
-   "import": 0, 
-   "permlevel": 0, 
-   "print": 1, 
-   "read": 1, 
-   "report": 1, 
-   "role": "Projects User", 
-   "set_user_permissions": 0, 
-   "share": 1, 
-   "submit": 1, 
+   "create": 1,
+   "delete": 1,
+   "email": 1,
+   "export": 1,
+   "print": 1,
+   "read": 1,
+   "report": 1,
+   "role": "Projects User",
+   "share": 1,
+   "submit": 1,
    "write": 1
   }
- ], 
- "quick_entry": 0, 
- "read_only": 0, 
- "read_only_onload": 0, 
- "show_name_in_global_search": 0, 
- "sort_field": "modified", 
- "sort_order": "DESC", 
- "track_changes": 1, 
- "track_seen": 0, 
- "track_views": 0
+ ],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "states": [],
+ "track_changes": 1
 }
\ No newline at end of file
diff --git a/erpnext/selling/doctype/customer/customer.js b/erpnext/selling/doctype/customer/customer.js
index b53f339..3a446e1 100644
--- a/erpnext/selling/doctype/customer/customer.js
+++ b/erpnext/selling/doctype/customer/customer.js
@@ -63,6 +63,14 @@
 				}
 			}
 		});
+
+		frm.set_query("user", "portal_users", function() {
+			return {
+				filters: {
+					"ignore_user_type": true,
+				}
+			};
+		});
 	},
 	customer_primary_address: function(frm){
 		if(frm.doc.customer_primary_address){
diff --git a/erpnext/selling/doctype/customer/customer.json b/erpnext/selling/doctype/customer/customer.json
index 72a1594..edfe005 100644
--- a/erpnext/selling/doctype/customer/customer.json
+++ b/erpnext/selling/doctype/customer/customer.json
@@ -81,7 +81,9 @@
   "dn_required",
   "column_break_53",
   "is_frozen",
-  "disabled"
+  "disabled",
+  "portal_users_tab",
+  "portal_users"
  ],
  "fields": [
   {
@@ -555,6 +557,17 @@
   {
    "fieldname": "column_break_54",
    "fieldtype": "Column Break"
+  },
+  {
+   "fieldname": "portal_users_tab",
+   "fieldtype": "Tab Break",
+   "label": "Portal Users"
+  },
+  {
+   "fieldname": "portal_users",
+   "fieldtype": "Table",
+   "label": "Customer Portal Users",
+   "options": "Portal User"
   }
  ],
  "icon": "fa fa-user",
@@ -568,7 +581,7 @@
    "link_fieldname": "party"
   }
  ],
- "modified": "2023-05-09 15:38:40.255193",
+ "modified": "2023-06-22 13:21:10.678382",
  "modified_by": "Administrator",
  "module": "Selling",
  "name": "Customer",
@@ -607,7 +620,6 @@
    "read": 1,
    "report": 1,
    "role": "Sales Master Manager",
-   "set_user_permissions": 1,
    "share": 1,
    "write": 1
   },
diff --git a/erpnext/selling/doctype/customer/customer.py b/erpnext/selling/doctype/customer/customer.py
index 6367e3c..555db59 100644
--- a/erpnext/selling/doctype/customer/customer.py
+++ b/erpnext/selling/doctype/customer/customer.py
@@ -22,6 +22,7 @@
 	get_timeline_data,
 	validate_party_accounts,
 )
+from erpnext.controllers.website_list_for_contact import add_role_for_portal_user
 from erpnext.utilities.transaction_base import TransactionBase
 
 
@@ -82,6 +83,7 @@
 		self.check_customer_group_change()
 		self.validate_default_bank_account()
 		self.validate_internal_customer()
+		self.add_role_for_user()
 
 		# set loyalty program tier
 		if frappe.db.exists("Customer", self.name):
@@ -170,6 +172,10 @@
 
 		self.update_customer_groups()
 
+	def add_role_for_user(self):
+		for portal_user in self.portal_users:
+			add_role_for_portal_user(portal_user, "Customer")
+
 	def update_customer_groups(self):
 		ignore_doctypes = ["Lead", "Opportunity", "POS Profile", "Tax Rule", "Pricing Rule"]
 		if frappe.flags.customer_group_changed:
diff --git a/erpnext/stock/doctype/delivery_trip/delivery_trip.json b/erpnext/stock/doctype/delivery_trip/delivery_trip.json
index 11b71c2..9d8fe46 100644
--- a/erpnext/stock/doctype/delivery_trip/delivery_trip.json
+++ b/erpnext/stock/doctype/delivery_trip/delivery_trip.json
@@ -66,8 +66,7 @@
    "fieldname": "driver",
    "fieldtype": "Link",
    "label": "Driver",
-   "options": "Driver",
-   "reqd": 1
+   "options": "Driver"
   },
   {
    "fetch_from": "driver.full_name",
@@ -189,10 +188,11 @@
  ],
  "is_submittable": 1,
  "links": [],
- "modified": "2021-04-30 21:21:36.610142",
+ "modified": "2023-06-27 11:22:27.927637",
  "modified_by": "Administrator",
  "module": "Stock",
  "name": "Delivery Trip",
+ "naming_rule": "By \"Naming Series\" field",
  "owner": "Administrator",
  "permissions": [
   {
@@ -228,5 +228,6 @@
  ],
  "sort_field": "modified",
  "sort_order": "DESC",
+ "states": [],
  "title_field": "driver_name"
 }
\ No newline at end of file
diff --git a/erpnext/stock/doctype/delivery_trip/delivery_trip.py b/erpnext/stock/doctype/delivery_trip/delivery_trip.py
index 1febbde..af2f411 100644
--- a/erpnext/stock/doctype/delivery_trip/delivery_trip.py
+++ b/erpnext/stock/doctype/delivery_trip/delivery_trip.py
@@ -24,6 +24,9 @@
 		)
 
 	def validate(self):
+		if self._action == "submit" and not self.driver:
+			frappe.throw(_("A driver must be set to submit."))
+
 		self.validate_stop_addresses()
 
 	def on_submit(self):
diff --git a/erpnext/stock/doctype/item/item.js b/erpnext/stock/doctype/item/item.js
index 6f1f981..31a3ecb 100644
--- a/erpnext/stock/doctype/item/item.js
+++ b/erpnext/stock/doctype/item/item.js
@@ -590,7 +590,7 @@
 			let selected_attributes = {};
 			me.multiple_variant_dialog.$wrapper.find('.form-column').each((i, col) => {
 				if(i===0) return;
-				let attribute_name = $(col).find('.control-label').html().trim();
+				let attribute_name = $(col).find('.column-label').html().trim();
 				selected_attributes[attribute_name] = [];
 				let checked_opts = $(col).find('.checkbox input');
 				checked_opts.each((i, opt) => {
diff --git a/erpnext/stock/doctype/item_reorder/item_reorder.json b/erpnext/stock/doctype/item_reorder/item_reorder.json
index fb4c558..a03bd45 100644
--- a/erpnext/stock/doctype/item_reorder/item_reorder.json
+++ b/erpnext/stock/doctype/item_reorder/item_reorder.json
@@ -81,7 +81,7 @@
    "print_hide_if_no_value": 0, 
    "read_only": 0, 
    "report_hide": 0, 
-   "reqd": 1, 
+   "reqd": 0, 
    "search_index": 0, 
    "set_only_once": 0, 
    "unique": 0
@@ -147,7 +147,7 @@
  "issingle": 0, 
  "istable": 1, 
  "max_attachments": 0, 
- "modified": "2016-07-28 19:15:38.270046", 
+ "modified": "2023-06-21 15:13:38.270046", 
  "modified_by": "Administrator", 
  "module": "Stock", 
  "name": "Item Reorder", 
@@ -158,4 +158,4 @@
  "read_only_onload": 0, 
  "sort_order": "ASC", 
  "track_seen": 0
-}
\ No newline at end of file
+}
diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
index 1986722..c6c84ca 100644
--- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
+++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
@@ -72,6 +72,11 @@
 		self.assertEqual(sl_entry_cancelled[1].actual_qty, -0.5)
 
 	def test_make_purchase_invoice(self):
+		from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_term
+
+		create_payment_term("_Test Payment Term 1 for Purchase Invoice")
+		create_payment_term("_Test Payment Term 2 for Purchase Invoice")
+
 		if not frappe.db.exists(
 			"Payment Terms Template", "_Test Payment Terms Template For Purchase Invoice"
 		):
@@ -83,12 +88,14 @@
 					"terms": [
 						{
 							"doctype": "Payment Terms Template Detail",
+							"payment_term": "_Test Payment Term 1 for Purchase Invoice",
 							"invoice_portion": 50.00,
 							"credit_days_based_on": "Day(s) after invoice date",
 							"credit_days": 00,
 						},
 						{
 							"doctype": "Payment Terms Template Detail",
+							"payment_term": "_Test Payment Term 2 for Purchase Invoice",
 							"invoice_portion": 50.00,
 							"credit_days_based_on": "Day(s) after invoice date",
 							"credit_days": 30,
diff --git a/erpnext/stock/doctype/warehouse/warehouse.js b/erpnext/stock/doctype/warehouse/warehouse.js
index 87a23ef..746a1cb 100644
--- a/erpnext/stock/doctype/warehouse/warehouse.js
+++ b/erpnext/stock/doctype/warehouse/warehouse.js
@@ -13,7 +13,7 @@
 			};
 		});
 
-		frm.set_query("parent_warehouse", function () {
+		frm.set_query("parent_warehouse", function (doc) {
 			return {
 				filters: {
 					is_group: 1,
diff --git a/erpnext/stock/print_format/purchase_receipt_serial_and_batch_bundle_print/purchase_receipt_serial_and_batch_bundle_print.json b/erpnext/stock/print_format/purchase_receipt_serial_and_batch_bundle_print/purchase_receipt_serial_and_batch_bundle_print.json
index 21132e0..a8ab8f6 100644
--- a/erpnext/stock/print_format/purchase_receipt_serial_and_batch_bundle_print/purchase_receipt_serial_and_batch_bundle_print.json
+++ b/erpnext/stock/print_format/purchase_receipt_serial_and_batch_bundle_print/purchase_receipt_serial_and_batch_bundle_print.json
@@ -8,14 +8,14 @@
  "docstatus": 0,
  "doctype": "Print Format",
  "font_size": 14,
- "format_data": "[{\"fieldname\": \"print_heading_template\", \"fieldtype\": \"Custom HTML\", \"options\": \"<div class=\\\"print-heading\\\">\\t\\t\\t\\t<h2><div>Purchase Receipt</div><br><small class=\\\"sub-heading\\\">{{ doc.name }}</small>\\t\\t\\t\\t</h2></div>\"}, {\"fieldtype\": \"Section Break\", \"label\": \"\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"supplier_name\", \"print_hide\": 0, \"label\": \"Supplier Name\"}, {\"fieldname\": \"supplier_delivery_note\", \"print_hide\": 0, \"label\": \"Supplier Delivery Note\"}, {\"fieldname\": \"rack\", \"print_hide\": 0, \"label\": \"Rack\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"posting_date\", \"print_hide\": 0, \"label\": \"Date\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"apply_putaway_rule\", \"print_hide\": 0, \"label\": \"Apply Putaway Rule\"}, {\"fieldtype\": \"Section Break\", \"label\": \"Accounting Dimensions\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"region\", \"print_hide\": 0, \"label\": \"Region\"}, {\"fieldname\": \"function\", \"print_hide\": 0, \"label\": \"Function\"}, {\"fieldname\": \"depot\", \"print_hide\": 0, \"label\": \"Depot\"}, {\"fieldname\": \"cost_center\", \"print_hide\": 0, \"label\": \"Cost Center\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"location\", \"print_hide\": 0, \"label\": \"Location\"}, {\"fieldname\": \"country\", \"print_hide\": 0, \"label\": \"Country\"}, {\"fieldname\": \"project\", \"print_hide\": 0, \"label\": \"Project\"}, {\"fieldtype\": \"Section Break\", \"label\": \"Items\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"scan_barcode\", \"print_hide\": 0, \"label\": \"Scan Barcode\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"set_from_warehouse\", \"print_hide\": 0, \"label\": \"Set From Warehouse\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldtype\": \"Section Break\", \"label\": \"\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"_custom_html\", \"print_hide\": 0, \"label\": \"Custom HTML\", \"fieldtype\": \"HTML\", \"options\": \"<table class=\\\"table table-bordered\\\">\\n\\t<tbody>\\n\\t\\t<tr>\\n\\t\\t\\t<th>Sr</th>\\n\\t\\t\\t<th>Item Name</th>\\n\\t\\t\\t<th>Description</th>\\n\\t\\t\\t<th class=\\\"text-right\\\">Qty</th>\\n\\t\\t\\t<th class=\\\"text-right\\\">Rate</th>\\n\\t\\t\\t<th class=\\\"text-right\\\">Amount</th>\\n\\t\\t</tr>\\n\\t\\t{%- for row in doc.items -%}\\n\\t\\t<tr>\\n\\t\\t    {% set bundle_data = get_serial_or_batch_nos(row.serial_and_batch_bundle) %}\\n\\t\\t    {% set serial_nos = [] %}\\n            {% set batches = {} %}\\n\\n\\t\\t\\t<td style=\\\"width: 4%;\\\">{{ row.idx }}</td>\\n\\t\\t\\t<td style=\\\"width: 20%;\\\">\\n\\t\\t\\t\\t{{ row.item_name }}\\n\\t\\t\\t\\t{% if row.item_code != row.item_name -%}\\n\\t\\t\\t\\t<br>Item Code: {{ row.item_code}}\\n\\t\\t\\t\\t{%- endif %}\\n\\t\\t\\t</td>\\n\\t\\t\\t<td style=\\\"width: 30%;\\\">\\n\\t\\t\\t\\t<div style=\\\"border: 0px;\\\">{{ row.description }}</div></td>\\n\\t\\t\\t<td style=\\\"width: 10%; text-align: right;\\\">{{ row.qty }} {{ row.uom or row.stock_uom }}</td>\\n\\t\\t\\t<td style=\\\"width: 18%; text-align: right;\\\">{{\\n\\t\\t\\t\\trow.get_formatted(\\\"rate\\\", doc) }}</td>\\n\\t\\t\\t<td style=\\\"width: 18%; text-align: right;\\\">{{\\n\\t\\t\\t\\trow.get_formatted(\\\"amount\\\", doc) }}</td>\\n\\t\\t\\t\\n\\t\\t</tr>\\n\\t\\t{%- endfor -%}\\n\\t</tbody>\\n</table>\\n\"}, {\"fieldtype\": \"Section Break\", \"label\": \"\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"total_qty\", \"print_hide\": 0, \"label\": \"Total Quantity\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"total\", \"print_hide\": 0, \"label\": \"Total\"}, {\"fieldtype\": \"Section Break\", \"label\": \"\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"taxes\", \"print_hide\": 0, \"label\": \"Purchase Taxes and Charges\", \"visible_columns\": [{\"fieldname\": \"category\", \"print_width\": \"\", \"print_hide\": 0}, {\"fieldname\": \"add_deduct_tax\", \"print_width\": \"\", \"print_hide\": 0}, {\"fieldname\": \"charge_type\", \"print_width\": \"\", \"print_hide\": 0}, {\"fieldname\": \"row_id\", \"print_width\": \"\", \"print_hide\": 0}, {\"fieldname\": \"included_in_print_rate\", \"print_width\": \"\", \"print_hide\": 0}, {\"fieldname\": \"included_in_paid_amount\", \"print_width\": \"\", \"print_hide\": 0}, {\"fieldname\": \"account_head\", \"print_width\": \"\", \"print_hide\": 0}, {\"fieldname\": \"description\", \"print_width\": \"300px\", \"print_hide\": 0}, {\"fieldname\": \"rate\", \"print_width\": \"\", \"print_hide\": 0}, {\"fieldname\": \"region\", \"print_width\": \"\", \"print_hide\": 0}, {\"fieldname\": \"function\", \"print_width\": \"\", \"print_hide\": 0}, {\"fieldname\": \"location\", \"print_width\": \"\", \"print_hide\": 0}, {\"fieldname\": \"cost_center\", \"print_width\": \"\", \"print_hide\": 0}, {\"fieldname\": \"depot\", \"print_width\": \"\", \"print_hide\": 0}, {\"fieldname\": \"country\", \"print_width\": \"\", \"print_hide\": 0}, {\"fieldname\": \"account_currency\", \"print_width\": \"\", \"print_hide\": 0}, {\"fieldname\": \"tax_amount\", \"print_width\": \"\", \"print_hide\": 0}, {\"fieldname\": \"total\", \"print_width\": \"\", \"print_hide\": 0}]}, {\"fieldtype\": \"Section Break\", \"label\": \"Totals\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"grand_total\", \"print_hide\": 0, \"label\": \"Grand Total\"}, {\"fieldname\": \"rounded_total\", \"print_hide\": 0, \"label\": \"Rounded Total\"}, {\"fieldname\": \"in_words\", \"print_hide\": 0, \"label\": \"In Words\"}, {\"fieldname\": \"disable_rounded_total\", \"print_hide\": 0, \"label\": \"Disable Rounded Total\"}, {\"fieldtype\": \"Section Break\", \"label\": \"Supplier Address\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"address_display\", \"print_hide\": 0, \"label\": \"Address\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"contact_display\", \"print_hide\": 0, \"label\": \"Contact\"}, {\"fieldname\": \"contact_mobile\", \"print_hide\": 0, \"label\": \"Mobile No\"}, {\"fieldtype\": \"Section Break\", \"label\": \"Company Billing Address\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"billing_address\", \"print_hide\": 0, \"label\": \"Billing Address\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"billing_address_display\", \"print_hide\": 0, \"label\": \"Billing Address\"}, {\"fieldname\": \"terms\", \"print_hide\": 0, \"label\": \"Terms and Conditions\"}, {\"fieldtype\": \"Section Break\", \"label\": \"\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"_custom_html\", \"print_hide\": 0, \"label\": \"Custom HTML\", \"fieldtype\": \"HTML\", \"options\": \"<table class=\\\"table table-bordered\\\">\\n\\t<tbody>\\n\\t\\t<tr>\\n\\t\\t\\t<th>Sr</th>\\n\\t\\t\\t<th>Item Name</th>\\n\\t\\t\\t<th>Qty</th>\\n\\t\\t\\t<th class=\\\"text-left\\\">Serial Nos</th>\\n\\t\\t\\t<th class=\\\"text-left\\\">Batch Nos (Qty)</th>\\n\\t\\t</tr>\\n\\t\\t{%- for row in doc.items -%}\\n\\t\\t<tr>\\n\\t\\t    {% set bundle_data = get_serial_or_batch_nos(row.serial_and_batch_bundle) %}\\n\\t\\t    {% set serial_nos = [] %}\\n            {% set batches = {} %}\\n            \\n            {% if bundle_data %}\\n\\t\\t\\t    {% for data in bundle_data %}\\n\\t\\t\\t        {% if data.serial_no %}\\n\\t\\t\\t            {{ serial_nos.append(data.serial_no) or \\\"\\\" }}\\n\\t\\t\\t        {% endif %}\\n\\t\\t\\t        \\n\\t\\t\\t        {% if data.batch_no %}\\n\\t\\t\\t            {{ batches.update({data.batch_no: data.qty}) or \\\"\\\" }}\\n\\t\\t\\t        {% endif %}\\n\\t\\t\\t    {% endfor %}\\n\\t\\t\\t{% endif %}\\n\\n\\t\\t\\t<td style=\\\"width: 3%;\\\">{{ row.idx }}</td>\\n\\t\\t\\t<td style=\\\"width: 20%;\\\">\\n\\t\\t\\t\\t{{ row.item_name }}\\n\\t\\t\\t\\t{% if row.item_code != row.item_name -%}\\n\\t\\t\\t\\t<br>Item Code: {{ row.item_code}}\\n\\t\\t\\t\\t{%- endif %}\\n\\t\\t\\t</td>\\n\\t\\t\\t<td style=\\\"width: 10%; text-align: right;\\\">{{ row.qty }} {{ row.uom or row.stock_uom }}</td>\\n\\t\\t\\t\\n\\t\\t\\t<td style=\\\"width: 30%; text-align: left;\\\">{{ serial_nos|join(',') }}</td>\\n\\t\\t\\t<td style=\\\"width: 30%;\\\">\\n\\t\\t\\t    {% if batches %}\\n                    {% for batch_no, qty in batches.items() %}\\n                        <p> {{batch_no}} : {{qty}} {{ row.uom or row.stock_uom }} </p>\\n                    {% endfor %}\\n                {% endif %}\\n\\t\\t\\t</td>\\n\\t\\t\\t\\n\\t\\t</tr>\\n\\t\\t{%- endfor -%}\\n\\t</tbody>\\n</table>\\n\"}]",
+ "format_data": "[{\"fieldname\": \"print_heading_template\", \"fieldtype\": \"Custom HTML\", \"options\": \"<div class=\\\"print-heading\\\">\\t\\t\\t\\t<h2><div>Purchase Receipt</div><br><small class=\\\"sub-heading\\\">{{ doc.name }}</small>\\t\\t\\t\\t</h2></div>\"}, {\"fieldtype\": \"Section Break\", \"label\": \"\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"supplier_name\", \"print_hide\": 0, \"label\": \"Supplier Name\"}, {\"fieldname\": \"supplier_delivery_note\", \"print_hide\": 0, \"label\": \"Supplier Delivery Note\"}, {\"fieldname\": \"rack\", \"print_hide\": 0, \"label\": \"Rack\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"posting_date\", \"print_hide\": 0, \"label\": \"Date\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"apply_putaway_rule\", \"print_hide\": 0, \"label\": \"Apply Putaway Rule\"}, {\"fieldtype\": \"Section Break\", \"label\": \"Accounting Dimensions\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"region\", \"print_hide\": 0, \"label\": \"Region\"}, {\"fieldname\": \"function\", \"print_hide\": 0, \"label\": \"Function\"}, {\"fieldname\": \"depot\", \"print_hide\": 0, \"label\": \"Depot\"}, {\"fieldname\": \"cost_center\", \"print_hide\": 0, \"label\": \"Cost Center\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"location\", \"print_hide\": 0, \"label\": \"Location\"}, {\"fieldname\": \"country\", \"print_hide\": 0, \"label\": \"Country\"}, {\"fieldname\": \"project\", \"print_hide\": 0, \"label\": \"Project\"}, {\"fieldtype\": \"Section Break\", \"label\": \"Items\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"scan_barcode\", \"print_hide\": 0, \"label\": \"Scan Barcode\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"set_from_warehouse\", \"print_hide\": 0, \"label\": \"Set From Warehouse\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldtype\": \"Section Break\", \"label\": \"\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"_custom_html\", \"print_hide\": 0, \"label\": \"Custom HTML\", \"fieldtype\": \"HTML\", \"options\": \"<table class=\\\"table table-bordered\\\">\\n\\t<tbody>\\n\\t\\t<tr>\\n\\t\\t\\t<th>Sr</th>\\n\\t\\t\\t<th>Item Name</th>\\n\\t\\t\\t<th>Description</th>\\n\\t\\t\\t<th class=\\\"text-right\\\">Qty</th>\\n\\t\\t\\t<th class=\\\"text-right\\\">Rate</th>\\n\\t\\t\\t<th class=\\\"text-right\\\">Amount</th>\\n\\t\\t</tr>\\n\\t\\t{%- for row in doc.items -%}\\n\\t\\t<tr>\\n\\t\\t    {% set bundle_data = get_serial_or_batch_nos(row.serial_and_batch_bundle) %}\\n\\t\\t    {% set serial_nos = [] %}\\n            {% set batches = {} %}\\n\\n\\t\\t\\t<td style=\\\"width: 4%;\\\">{{ row.idx }}</td>\\n\\t\\t\\t<td style=\\\"width: 20%;\\\">\\n\\t\\t\\t\\t{{ row.item_name }}\\n\\t\\t\\t\\t{% if row.item_code != row.item_name -%}\\n\\t\\t\\t\\t<br>Item Code: {{ row.item_code}}\\n\\t\\t\\t\\t{%- endif %}\\n\\t\\t\\t</td>\\n\\t\\t\\t<td style=\\\"width: 30%;\\\">\\n\\t\\t\\t\\t<div style=\\\"border: 0px;\\\">{{ row.description }}</div></td>\\n\\t\\t\\t<td style=\\\"width: 10%; text-align: right;\\\">{{ row.qty }} {{ row.uom or row.stock_uom }}</td>\\n\\t\\t\\t<td style=\\\"width: 18%; text-align: right;\\\">{{\\n\\t\\t\\t\\trow.get_formatted(\\\"rate\\\", doc) }}</td>\\n\\t\\t\\t<td style=\\\"width: 18%; text-align: right;\\\">{{\\n\\t\\t\\t\\trow.get_formatted(\\\"amount\\\", doc) }}</td>\\n\\t\\t\\t\\n\\t\\t</tr>\\n\\t\\t{%- endfor -%}\\n\\t</tbody>\\n</table>\\n\"}, {\"fieldtype\": \"Section Break\", \"label\": \"\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"total_qty\", \"print_hide\": 0, \"label\": \"Total Quantity\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"total\", \"print_hide\": 0, \"label\": \"Total\"}, {\"fieldtype\": \"Section Break\", \"label\": \"\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"taxes\", \"print_hide\": 0, \"label\": \"Purchase Taxes and Charges\", \"visible_columns\": [{\"fieldname\": \"category\", \"print_width\": \"\", \"print_hide\": 0}, {\"fieldname\": \"add_deduct_tax\", \"print_width\": \"\", \"print_hide\": 0}, {\"fieldname\": \"charge_type\", \"print_width\": \"\", \"print_hide\": 0}, {\"fieldname\": \"row_id\", \"print_width\": \"\", \"print_hide\": 0}, {\"fieldname\": \"included_in_print_rate\", \"print_width\": \"\", \"print_hide\": 0}, {\"fieldname\": \"included_in_paid_amount\", \"print_width\": \"\", \"print_hide\": 0}, {\"fieldname\": \"account_head\", \"print_width\": \"\", \"print_hide\": 0}, {\"fieldname\": \"description\", \"print_width\": \"300px\", \"print_hide\": 0}, {\"fieldname\": \"rate\", \"print_width\": \"\", \"print_hide\": 0}, {\"fieldname\": \"region\", \"print_width\": \"\", \"print_hide\": 0}, {\"fieldname\": \"function\", \"print_width\": \"\", \"print_hide\": 0}, {\"fieldname\": \"location\", \"print_width\": \"\", \"print_hide\": 0}, {\"fieldname\": \"cost_center\", \"print_width\": \"\", \"print_hide\": 0}, {\"fieldname\": \"depot\", \"print_width\": \"\", \"print_hide\": 0}, {\"fieldname\": \"country\", \"print_width\": \"\", \"print_hide\": 0}, {\"fieldname\": \"account_currency\", \"print_width\": \"\", \"print_hide\": 0}, {\"fieldname\": \"tax_amount\", \"print_width\": \"\", \"print_hide\": 0}, {\"fieldname\": \"total\", \"print_width\": \"\", \"print_hide\": 0}]}, {\"fieldtype\": \"Section Break\", \"label\": \"Totals\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"grand_total\", \"print_hide\": 0, \"label\": \"Grand Total\"}, {\"fieldname\": \"rounded_total\", \"print_hide\": 0, \"label\": \"Rounded Total\"}, {\"fieldname\": \"in_words\", \"print_hide\": 0, \"label\": \"In Words\"}, {\"fieldname\": \"disable_rounded_total\", \"print_hide\": 0, \"label\": \"Disable Rounded Total\"}, {\"fieldtype\": \"Section Break\", \"label\": \"Supplier Address\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"address_display\", \"print_hide\": 0, \"label\": \"Address\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"contact_display\", \"print_hide\": 0, \"label\": \"Contact\"}, {\"fieldname\": \"contact_mobile\", \"print_hide\": 0, \"label\": \"Mobile No\"}, {\"fieldtype\": \"Section Break\", \"label\": \"Company Billing Address\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"billing_address\", \"print_hide\": 0, \"label\": \"Billing Address\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"billing_address_display\", \"print_hide\": 0, \"label\": \"Billing Address\"}, {\"fieldname\": \"terms\", \"print_hide\": 0, \"label\": \"Terms and Conditions\"}, {\"fieldtype\": \"Section Break\", \"label\": \"\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"_custom_html\", \"print_hide\": 0, \"label\": \"Custom HTML\", \"fieldtype\": \"HTML\", \"options\": \"<table class=\\\"table table-bordered\\\">\\n\\t<tbody>\\n\\t\\t<tr>\\n\\t\\t\\t<th>Sr</th>\\n\\t\\t\\t<th>Item Name</th>\\n\\t\\t\\t<th>Qty</th>\\n\\t\\t\\t<th class=\\\"text-left\\\">Serial Nos</th>\\n\\t\\t\\t<th class=\\\"text-left\\\">Batch Nos (Qty)</th>\\n\\t\\t</tr>\\n\\t\\t{%- for row in doc.items -%}\\n\\t\\t<tr>\\n\\t\\t    {% set bundle_data = frappe.get_all(\\\"Serial and Batch Entry\\\", \\n\\t\\t        fields=[\\\"serial_no\\\", \\\"batch_no\\\", \\\"qty\\\"], \\n\\t\\t        filters={\\\"parent\\\": row.serial_and_batch_bundle}) %}\\n\\t\\t    {% set serial_nos = [] %}\\n            {% set batches = {} %}\\n            \\n            {% if bundle_data %}\\n\\t\\t\\t    {% for data in bundle_data %}\\n\\t\\t\\t        {% if data.serial_no %}\\n\\t\\t\\t            {{ serial_nos.append(data.serial_no) or \\\"\\\" }}\\n\\t\\t\\t        {% endif %}\\n\\t\\t\\t        \\n\\t\\t\\t        {% if data.batch_no %}\\n\\t\\t\\t            {{ batches.update({data.batch_no: data.qty}) or \\\"\\\" }}\\n\\t\\t\\t        {% endif %}\\n\\t\\t\\t    {% endfor %}\\n\\t\\t\\t{% endif %}\\n\\n\\t\\t\\t<td style=\\\"width: 3%;\\\">{{ row.idx }}</td>\\n\\t\\t\\t<td style=\\\"width: 20%;\\\">\\n\\t\\t\\t\\t{{ row.item_name }}\\n\\t\\t\\t\\t{% if row.item_code != row.item_name -%}\\n\\t\\t\\t\\t<br>Item Code: {{ row.item_code}}\\n\\t\\t\\t\\t{%- endif %}\\n\\t\\t\\t</td>\\n\\t\\t\\t<td style=\\\"width: 10%; text-align: right;\\\">{{ row.qty }} {{ row.uom or row.stock_uom }}</td>\\n\\t\\t\\t\\n\\t\\t\\t<td style=\\\"width: 30%; text-align: left;\\\">{{ serial_nos|join(',') }}</td>\\n\\t\\t\\t<td style=\\\"width: 30%;\\\">\\n\\t\\t\\t    {% if batches %}\\n                    {% for batch_no, qty in batches.items() %}\\n                        <p> {{batch_no}} : {{qty}} {{ row.uom or row.stock_uom }} </p>\\n                    {% endfor %}\\n                {% endif %}\\n\\t\\t\\t</td>\\n\\t\\t\\t\\n\\t\\t</tr>\\n\\t\\t{%- endfor -%}\\n\\t</tbody>\\n</table>\\n\"}]",
  "idx": 0,
  "line_breaks": 0,
  "margin_bottom": 15.0,
  "margin_left": 15.0,
  "margin_right": 15.0,
  "margin_top": 15.0,
- "modified": "2023-06-02 00:09:37.315002",
+ "modified": "2023-06-26 14:51:20.609682",
  "modified_by": "Administrator",
  "module": "Stock",
  "name": "Purchase Receipt Serial and Batch Bundle Print",
diff --git a/erpnext/stock/reorder_item.py b/erpnext/stock/reorder_item.py
index 136c78f..9075608 100644
--- a/erpnext/stock/reorder_item.py
+++ b/erpnext/stock/reorder_item.py
@@ -67,7 +67,7 @@
 		else:
 			projected_qty = flt(item_warehouse_projected_qty.get(item_code, {}).get(warehouse))
 
-		if (reorder_level or reorder_qty) and projected_qty < reorder_level:
+		if (reorder_level or reorder_qty) and projected_qty <= reorder_level:
 			deficiency = reorder_level - projected_qty
 			if deficiency > reorder_qty:
 				reorder_qty = deficiency
diff --git a/erpnext/stock/serial_batch_bundle.py b/erpnext/stock/serial_batch_bundle.py
index 2c18f99..d6c840f 100644
--- a/erpnext/stock/serial_batch_bundle.py
+++ b/erpnext/stock/serial_batch_bundle.py
@@ -312,7 +312,35 @@
 
 
 def get_serial_or_batch_nos(bundle):
-	return frappe.get_all("Serial and Batch Entry", fields=["*"], filters={"parent": bundle})
+	# For print format
+
+	bundle_data = frappe.get_cached_value(
+		"Serial and Batch Bundle", bundle, ["has_serial_no", "has_batch_no"], as_dict=True
+	)
+
+	fields = []
+	if bundle_data.has_serial_no:
+		fields.append("serial_no")
+
+	if bundle_data.has_batch_no:
+		fields.extend(["batch_no", "qty"])
+
+	data = frappe.get_all("Serial and Batch Entry", fields=fields, filters={"parent": bundle})
+
+	if bundle_data.has_serial_no and not bundle_data.has_batch_no:
+		return ", ".join([d.serial_no for d in data])
+
+	elif bundle_data.has_batch_no:
+		html = "<table class= 'table table-borderless' style='margin-top: 0px;margin-bottom: 0px;'>"
+		for d in data:
+			if d.serial_no:
+				html += f"<tr><td>{d.batch_no}</th><th>{d.serial_no}</th	><th>{abs(d.qty)}</th></tr>"
+			else:
+				html += f"<tr><td>{d.batch_no}</td><td>{abs(d.qty)}</td></tr>"
+
+		html += "</table>"
+
+		return html
 
 
 class SerialNoValuation(DeprecatedSerialNoValuation):
diff --git a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py
index 3919733..0b14d4d 100644
--- a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py
+++ b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py
@@ -163,9 +163,10 @@
 				elif self.per_received > 0 and self.per_received < 100:
 					status = "Partially Received"
 					for item in self.supplied_items:
-						if item.returned_qty:
-							status = "Closed"
+						if not item.returned_qty or (item.supplied_qty - item.consumed_qty - item.returned_qty) > 0:
 							break
+					else:
+						status = "Closed"
 				else:
 					total_required_qty = total_supplied_qty = 0
 					for item in self.supplied_items:
diff --git a/erpnext/templates/print_formats/includes/serial_and_batch_bundle.html b/erpnext/templates/print_formats/includes/serial_and_batch_bundle.html
new file mode 100644
index 0000000..8e62586
--- /dev/null
+++ b/erpnext/templates/print_formats/includes/serial_and_batch_bundle.html
@@ -0,0 +1,4 @@
+{% if doc.get("serial_and_batch_bundle")  %}
+	{% set bundle_print = get_serial_or_batch_nos(doc.serial_and_batch_bundle) %}
+	{{bundle_print}}
+{%- endif %}
diff --git a/erpnext/tests/test_webform.py b/erpnext/tests/test_webform.py
index 202467b..af50a05 100644
--- a/erpnext/tests/test_webform.py
+++ b/erpnext/tests/test_webform.py
@@ -3,18 +3,21 @@
 import frappe
 
 from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
+from erpnext.buying.doctype.supplier.test_supplier import create_supplier
 
 
 class TestWebsite(unittest.TestCase):
 	def test_permission_for_custom_doctype(self):
 		create_user("Supplier 1", "supplier1@gmail.com")
 		create_user("Supplier 2", "supplier2@gmail.com")
-		create_supplier_with_contact(
-			"Supplier1", "All Supplier Groups", "Supplier 1", "supplier1@gmail.com"
-		)
-		create_supplier_with_contact(
-			"Supplier2", "All Supplier Groups", "Supplier 2", "supplier2@gmail.com"
-		)
+
+		supplier1 = create_supplier(supplier_name="Supplier1")
+		supplier2 = create_supplier(supplier_name="Supplier2")
+		supplier1.append("portal_users", {"user": "supplier1@gmail.com"})
+		supplier1.save()
+		supplier2.append("portal_users", {"user": "supplier2@gmail.com"})
+		supplier2.save()
+
 		po1 = create_purchase_order(supplier="Supplier1")
 		po2 = create_purchase_order(supplier="Supplier2")
 
@@ -61,21 +64,6 @@
 	).insert(ignore_if_duplicate=True)
 
 
-def create_supplier_with_contact(name, group, contact_name, contact_email):
-	supplier = frappe.get_doc(
-		{"doctype": "Supplier", "supplier_name": name, "supplier_group": group}
-	).insert(ignore_if_duplicate=True)
-
-	if not frappe.db.exists("Contact", contact_name + "-1-" + name):
-		new_contact = frappe.new_doc("Contact")
-		new_contact.first_name = contact_name
-		new_contact.is_primary_contact = (True,)
-		new_contact.append("links", {"link_doctype": "Supplier", "link_name": supplier.name})
-		new_contact.append("email_ids", {"email_id": contact_email, "is_primary": 1})
-
-		new_contact.insert(ignore_mandatory=True)
-
-
 def create_custom_doctype():
 	frappe.get_doc(
 		{
diff --git a/erpnext/translations/de.csv b/erpnext/translations/de.csv
index 68358c6..f9ec689 100644
--- a/erpnext/translations/de.csv
+++ b/erpnext/translations/de.csv
@@ -9905,3 +9905,7 @@
 Select an item from each set to be used in the Sales Order.,"Wählen Sie aus den Alternativen jeweils einen Artikel aus, der in die Auftragsbestätigung übernommen werden soll.",
 Is Alternative,Ist Alternative,
 Alternative Items,Alternativpositionen,
+Add Template,Vorlage einfügen,
+Prepend the template to the email message,Vorlage oberhalb der Email-Nachricht einfügen,
+Clear & Add Template,Leeren und Vorlage einfügen,
+Clear the email message and add the template,Email-Feld leeren und Vorlage einfügen,
diff --git a/erpnext/translations/nl.csv b/erpnext/translations/nl.csv
index f2158b5..a13382c 100644
--- a/erpnext/translations/nl.csv
+++ b/erpnext/translations/nl.csv
@@ -875,7 +875,7 @@
 Donor Type information.,Donor Type informatie.,
 Donor information.,Donorinformatie.,
 Download JSON,JSON downloaden,
-Draft,Droogte,
+Draft,Concept,
 Drop Ship,Drop Ship,
 Drug,drug,
 Due / Reference Date cannot be after {0},Verval- / Referentiedatum kan niet na {0} zijn,
@@ -4279,7 +4279,7 @@
 Row #{0}: Item {1} is not a Serialized/Batched Item. It cannot have a Serial No/Batch No against it.,Rij # {0}: artikel {1} is geen geserialiseerd / batch artikel. Het kan geen serienummer / batchnummer hebben.,
 Please set {0},Stel {0} in,
 Please set {0},Stel {0} in,supplier
-Draft,Droogte,"docstatus,=,0"
+Draft,Concept,"docstatus,=,0"
 Cancelled,Geannuleerd,"docstatus,=,2"
 Please setup Instructor Naming System in Education > Education Settings,Stel het instructeursysteem in onder onderwijs&gt; onderwijsinstellingen,
 Please set Naming Series for {0} via Setup > Settings > Naming Series,Stel Naming Series in op {0} via Instellingen&gt; Instellingen&gt; Naming Series,
@@ -8189,7 +8189,7 @@
 Prevdoc DocType,Prevdoc DocType,
 Parent Detail docname,Bovenliggende Detail docname,
 "Generate packing slips for packages to be delivered. Used to notify package number, package contents and its weight.","Genereren van pakbonnen voor pakketten te leveren. Gebruikt voor pakket nummer, inhoud van de verpakking en het gewicht te melden.",
-Indicates that the package is a part of this delivery (Only Draft),Geeft aan dat het pakket een onderdeel is van deze levering (alleen ontwerp),
+Indicates that the package is a part of this delivery (Only Draft),Geeft aan dat het pakket een onderdeel is van deze levering (alleen concept),
 MAT-PAC-.YYYY.-,MAT-PAC-.YYYY.-,
 From Package No.,Van Pakket No,
 Identification of the package for the delivery (for print),Identificatie van het pakket voor de levering (voor afdrukken),
diff --git a/erpnext/utilities/doctype/portal_user/__init__.py b/erpnext/utilities/doctype/portal_user/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/utilities/doctype/portal_user/__init__.py
diff --git a/erpnext/utilities/doctype/portal_user/portal_user.json b/erpnext/utilities/doctype/portal_user/portal_user.json
new file mode 100644
index 0000000..361166c
--- /dev/null
+++ b/erpnext/utilities/doctype/portal_user/portal_user.json
@@ -0,0 +1,34 @@
+{
+ "actions": [],
+ "allow_rename": 1,
+ "creation": "2023-06-20 14:01:35.362233",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+  "user"
+ ],
+ "fields": [
+  {
+   "fieldname": "user",
+   "fieldtype": "Link",
+   "in_list_view": 1,
+   "label": "User",
+   "options": "User",
+   "reqd": 1,
+   "search_index": 1
+  }
+ ],
+ "index_web_pages_for_search": 1,
+ "istable": 1,
+ "links": [],
+ "modified": "2023-06-26 14:15:34.695605",
+ "modified_by": "Administrator",
+ "module": "Utilities",
+ "name": "Portal User",
+ "owner": "Administrator",
+ "permissions": [],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "states": []
+}
\ No newline at end of file
diff --git a/erpnext/utilities/doctype/portal_user/portal_user.py b/erpnext/utilities/doctype/portal_user/portal_user.py
new file mode 100644
index 0000000..2e0064d
--- /dev/null
+++ b/erpnext/utilities/doctype/portal_user/portal_user.py
@@ -0,0 +1,9 @@
+# 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 PortalUser(Document):
+	pass